From 7ee042bdec46a7e1b531aa0112dd9de7a9cc974c Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sat, 15 Nov 2025 04:13:24 +0000 Subject: [PATCH] Add consoles pages. --- Marechai.App/App.xaml.cs | 19 +- .../ViewModels/ComputersViewModel.cs | 6 +- .../ViewModels/ConsolesListViewModel.cs | 249 +++++++++++++++ .../ViewModels/ConsolesViewModel.cs | 207 ++++++++++++ .../ViewModels/MachineViewViewModel.cs | 16 + .../Presentation/Views/ConsolesListPage.xaml | 261 +++++++++++++++ .../Presentation/Views/ConsolesPage.xaml | 297 ++++++++++++++++++ .../Services/ConsolesListFilterContext.cs | 29 ++ Marechai.App/Services/ConsolesService.cs | 205 ++++++++++++ 9 files changed, 1285 insertions(+), 4 deletions(-) create mode 100644 Marechai.App/Presentation/ViewModels/ConsolesListViewModel.cs create mode 100644 Marechai.App/Presentation/ViewModels/ConsolesViewModel.cs create mode 100644 Marechai.App/Presentation/Views/ConsolesListPage.xaml create mode 100644 Marechai.App/Presentation/Views/ConsolesPage.xaml create mode 100644 Marechai.App/Services/ConsolesListFilterContext.cs create mode 100644 Marechai.App/Services/ConsolesService.cs diff --git a/Marechai.App/App.xaml.cs b/Marechai.App/App.xaml.cs index 83eb47ab..ec5b0a15 100644 --- a/Marechai.App/App.xaml.cs +++ b/Marechai.App/App.xaml.cs @@ -109,13 +109,20 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services .AddSingleton(); + services + .AddSingleton(); + services.AddTransient(); + services.AddTransient(); }) .UseNavigation(RegisterRoutes)); @@ -136,6 +143,8 @@ public partial class App : Application new ViewMap(), new ViewMap(), new ViewMap(), + new ViewMap(), + new ViewMap(), new ViewMap(), new DataViewMap()); @@ -155,13 +164,21 @@ public partial class App : Application views.FindByViewModel(), Nested: [ - new RouteMap("list", + new RouteMap("list-computers", views.FindByViewModel< ComputersListViewModel>()), new RouteMap("view", views.FindByViewModel< MachineViewViewModel>()) ]), + new RouteMap("consoles", + views.FindByViewModel(), + Nested: + [ + new RouteMap("list-consoles", + views.FindByViewModel< + ConsolesListViewModel>()) + ]), new RouteMap("Second", views.FindByViewModel()) ]) diff --git a/Marechai.App/Presentation/ViewModels/ComputersViewModel.cs b/Marechai.App/Presentation/ViewModels/ComputersViewModel.cs index 22b86d1b..1783afd4 100644 --- a/Marechai.App/Presentation/ViewModels/ComputersViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/ComputersViewModel.cs @@ -155,7 +155,7 @@ public partial class ComputersViewModel : ObservableObject _logger.LogInformation("Navigating to computers by letter: {Letter}", letter); _filterContext.FilterType = ComputerListFilterType.Letter; _filterContext.FilterValue = letter.ToString(); - await _navigator.NavigateViewModelAsync(this); + await _navigator.NavigateRouteAsync(this, "list-computers"); } catch(Exception ex) { @@ -175,7 +175,7 @@ public partial class ComputersViewModel : ObservableObject _logger.LogInformation("Navigating to computers by year: {Year}", year); _filterContext.FilterType = ComputerListFilterType.Year; _filterContext.FilterValue = year.ToString(); - await _navigator.NavigateViewModelAsync(this); + await _navigator.NavigateRouteAsync(this, "list-computers"); } catch(Exception ex) { @@ -195,7 +195,7 @@ public partial class ComputersViewModel : ObservableObject _logger.LogInformation("Navigating to all computers"); _filterContext.FilterType = ComputerListFilterType.All; _filterContext.FilterValue = string.Empty; - await _navigator.NavigateViewModelAsync(this); + await _navigator.NavigateRouteAsync(this, "list-computers"); } catch(Exception ex) { diff --git a/Marechai.App/Presentation/ViewModels/ConsolesListViewModel.cs b/Marechai.App/Presentation/ViewModels/ConsolesListViewModel.cs new file mode 100644 index 00000000..7825decf --- /dev/null +++ b/Marechai.App/Presentation/ViewModels/ConsolesListViewModel.cs @@ -0,0 +1,249 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using System.Windows.Input; +using Marechai.App.Helpers; +using Marechai.App.Presentation.Models; +using Marechai.App.Services; +using Uno.Extensions.Navigation; + +namespace Marechai.App.Presentation.ViewModels; + +/// +/// ViewModel for displaying a filtered list of consoles +/// +public partial class ConsolesListViewModel : ObservableObject +{ + private readonly ConsolesService _consolesService; + private readonly IConsolesListFilterContext _filterContext; + private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; + private readonly INavigator _navigator; + + [ObservableProperty] + private ObservableCollection _consolesList = []; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private string _filterDescription = string.Empty; + + [ObservableProperty] + private bool _hasError; + + [ObservableProperty] + private bool _isDataLoaded; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private string _pageTitle = string.Empty; + + public ConsolesListViewModel(ConsolesService consolesService, IStringLocalizer localizer, + ILogger logger, INavigator navigator, + IConsolesListFilterContext filterContext) + { + _consolesService = consolesService; + _localizer = localizer; + _logger = logger; + _navigator = navigator; + _filterContext = filterContext; + LoadData = new AsyncRelayCommand(LoadDataAsync); + GoBackCommand = new AsyncRelayCommand(GoBackAsync); + NavigateToConsoleCommand = new AsyncRelayCommand(NavigateToConsoleAsync); + } + + public IAsyncRelayCommand LoadData { get; } + public ICommand GoBackCommand { get; } + public IAsyncRelayCommand NavigateToConsoleCommand { get; } + + /// + /// Gets or sets the filter type + /// + public ConsoleListFilterType FilterType + { + get => _filterContext.FilterType; + set => _filterContext.FilterType = value; + } + + /// + /// Gets or sets the filter value + /// + public string FilterValue + { + get => _filterContext.FilterValue; + set => _filterContext.FilterValue = value; + } + + /// + /// Loads consoles based on the current filter + /// + private async Task LoadDataAsync() + { + try + { + IsLoading = true; + ErrorMessage = string.Empty; + HasError = false; + IsDataLoaded = false; + ConsolesList.Clear(); + + _logger.LogInformation("LoadDataAsync called. FilterType={FilterType}, FilterValue={FilterValue}", + FilterType, + FilterValue); + + // Update title and filter description based on filter type + UpdateFilterDescription(); + + // Load consoles from the API based on the current filter + await LoadConsolesFromApiAsync(); + + _logger.LogInformation("LoadConsolesFromApiAsync completed. ConsolesList.Count={Count}", + ConsolesList.Count); + + if(ConsolesList.Count == 0) + { + ErrorMessage = _localizer["No consoles found for this filter"].Value; + HasError = true; + + _logger.LogWarning("No consoles found for filter: {FilterType} {FilterValue}", FilterType, FilterValue); + } + else + IsDataLoaded = true; + } + catch(Exception ex) + { + _logger.LogError(ex, "Error loading consoles: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to load consoles. Please try again later."].Value; + HasError = true; + } + finally + { + IsLoading = false; + } + } + + /// + /// Updates the title and filter description based on the current filter + /// + private void UpdateFilterDescription() + { + switch(FilterType) + { + case ConsoleListFilterType.All: + PageTitle = _localizer["All Consoles"]; + FilterDescription = _localizer["Browsing all consoles in the database"]; + + break; + + case ConsoleListFilterType.Letter: + if(!string.IsNullOrEmpty(FilterValue) && FilterValue.Length == 1) + { + PageTitle = $"{_localizer["Consoles Starting with"]} {FilterValue}"; + FilterDescription = $"{_localizer["Showing consoles that start with"]} {FilterValue}"; + } + + break; + + case ConsoleListFilterType.Year: + if(!string.IsNullOrEmpty(FilterValue) && int.TryParse(FilterValue, out int year)) + { + PageTitle = $"{_localizer["Consoles from"]} {year}"; + FilterDescription = $"{_localizer["Showing consoles released in"]} {year}"; + } + + break; + } + } + + /// + /// Loads consoles from the API based on the current filter + /// + private async Task LoadConsolesFromApiAsync() + { + try + { + List consoles = FilterType switch + { + ConsoleListFilterType.Letter when FilterValue.Length == 1 => + await _consolesService.GetConsolesByLetterAsync(FilterValue[0]), + + ConsoleListFilterType.Year when int.TryParse(FilterValue, out int year) => + await _consolesService.GetConsolesByYearAsync(year), + + _ => await _consolesService.GetAllConsolesAsync() + }; + + // Add consoles to the list sorted by name + foreach(MachineDto console in consoles.OrderBy(c => c.Name)) + { + int year = console.Introduced?.Year ?? 0; + int id = UntypedNodeExtractor.ExtractInt(console.Id); + + _logger.LogInformation("Console: {Name}, Introduced: {Introduced}, Year: {Year}, Company: {Company}, ID: {Id}", + console.Name, + console.Introduced, + year, + console.Company, + id); + + ConsolesList.Add(new ConsoleListItem + { + Id = id, + Name = console.Name ?? string.Empty, + Year = year, + Manufacturer = console.Company ?? string.Empty + }); + } + } + catch(Exception ex) + { + _logger.LogError(ex, "Error loading consoles from API"); + } + } + + /// + /// Navigates back to the consoles main view + /// + private async Task GoBackAsync() + { + await _navigator.NavigateViewModelAsync(this); + } + + /// + /// Navigates to the console detail view + /// + private async Task NavigateToConsoleAsync(ConsoleListItem? console) + { + if(console is null) return; + + _logger.LogInformation("Navigating to console detail: {ConsoleName} (ID: {ConsoleId})", + console.Name, + console.Id); + + var navParam = new MachineViewNavigationParameter + { + MachineId = console.Id, + NavigationSource = this + }; + + await _navigator.NavigateViewModelAsync(this, data: navParam); + } +} + +/// +/// Data model for a console in the list +/// +public class ConsoleListItem +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public int Year { get; set; } + public string Manufacturer { get; set; } = string.Empty; +} \ No newline at end of file diff --git a/Marechai.App/Presentation/ViewModels/ConsolesViewModel.cs b/Marechai.App/Presentation/ViewModels/ConsolesViewModel.cs new file mode 100644 index 00000000..924ab1c0 --- /dev/null +++ b/Marechai.App/Presentation/ViewModels/ConsolesViewModel.cs @@ -0,0 +1,207 @@ +using System; +using System.Collections.ObjectModel; +using System.Threading.Tasks; +using System.Windows.Input; +using Marechai.App.Services; +using Uno.Extensions.Navigation; + +namespace Marechai.App.Presentation.ViewModels; + +public partial class ConsolesViewModel : ObservableObject +{ + private readonly ConsolesService _consolesService; + private readonly IConsolesListFilterContext _filterContext; + private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; + private readonly INavigator _navigator; + + [ObservableProperty] + private int _consoleCount; + + [ObservableProperty] + private string _consoleCountText = string.Empty; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private bool _hasError; + + [ObservableProperty] + private bool _isDataLoaded; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private ObservableCollection _lettersList = []; + + [ObservableProperty] + private int _maximumYear; + + [ObservableProperty] + private int _minimumYear; + + [ObservableProperty] + private string _yearsGridTitle = string.Empty; + + [ObservableProperty] + private ObservableCollection _yearsList = []; + + public ConsolesViewModel(ConsolesService consolesService, IStringLocalizer localizer, + ILogger logger, INavigator navigator, + IConsolesListFilterContext filterContext) + { + _consolesService = consolesService; + _localizer = localizer; + _logger = logger; + _navigator = navigator; + _filterContext = filterContext; + LoadData = new AsyncRelayCommand(LoadDataAsync); + GoBackCommand = new AsyncRelayCommand(GoBackAsync); + NavigateByLetterCommand = new AsyncRelayCommand(NavigateByLetterAsync); + NavigateByYearCommand = new AsyncRelayCommand(NavigateByYearAsync); + NavigateAllConsolesCommand = new AsyncRelayCommand(NavigateAllConsolesAsync); + + InitializeLetters(); + } + + public IAsyncRelayCommand LoadData { get; } + public ICommand GoBackCommand { get; } + public IAsyncRelayCommand NavigateByLetterCommand { get; } + public IAsyncRelayCommand NavigateByYearCommand { get; } + public IAsyncRelayCommand NavigateAllConsolesCommand { get; } + public string Title { get; } = "Consoles"; + + /// + /// Initializes the alphabet list (A-Z) + /// + private void InitializeLetters() + { + LettersList.Clear(); + + for(var c = 'A'; c <= 'Z'; c++) LettersList.Add(c); + } + + /// + /// Loads consoles count, minimum and maximum years from the API + /// + private async Task LoadDataAsync() + { + try + { + IsLoading = true; + ErrorMessage = string.Empty; + HasError = false; + IsDataLoaded = false; + YearsList.Clear(); + + // Load all data in parallel for better performance + Task countTask = _consolesService.GetConsolesCountAsync(); + Task minYearTask = _consolesService.GetMinimumYearAsync(); + Task maxYearTask = _consolesService.GetMaximumYearAsync(); + await Task.WhenAll(countTask, minYearTask, maxYearTask); + + ConsoleCount = countTask.Result; + MinimumYear = minYearTask.Result; + MaximumYear = maxYearTask.Result; + + // Update display text + ConsoleCountText = _localizer["Consoles in the database"]; + + // Generate years list + if(MinimumYear > 0 && MaximumYear > 0) + { + for(int year = MinimumYear; year <= MaximumYear; year++) YearsList.Add(year); + + YearsGridTitle = $"Browse by Year ({MinimumYear} - {MaximumYear})"; + } + + if(ConsoleCount == 0) + { + ErrorMessage = _localizer["No consoles found"].Value; + HasError = true; + } + else + IsDataLoaded = true; + } + catch(Exception ex) + { + _logger.LogError("Error loading consoles data: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to load consoles data. Please try again later."].Value; + HasError = true; + } + finally + { + IsLoading = false; + } + } + + /// + /// Handles back navigation + /// + private async Task GoBackAsync() + { + await _navigator.NavigateViewModelAsync(this); + } + + /// + /// Navigates to consoles filtered by letter + /// + private async Task NavigateByLetterAsync(char letter) + { + try + { + _logger.LogInformation("Navigating to consoles by letter: {Letter}", letter); + _filterContext.FilterType = ConsoleListFilterType.Letter; + _filterContext.FilterValue = letter.ToString(); + await _navigator.NavigateRouteAsync(this, "list-consoles"); + } + catch(Exception ex) + { + _logger.LogError("Error navigating to letter consoles: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to navigate. Please try again."].Value; + HasError = true; + } + } + + /// + /// Navigates to consoles filtered by year + /// + private async Task NavigateByYearAsync(int year) + { + try + { + _logger.LogInformation("Navigating to consoles by year: {Year}", year); + _filterContext.FilterType = ConsoleListFilterType.Year; + _filterContext.FilterValue = year.ToString(); + await _navigator.NavigateRouteAsync(this, "list-consoles"); + } + catch(Exception ex) + { + _logger.LogError("Error navigating to year consoles: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to navigate. Please try again."].Value; + HasError = true; + } + } + + /// + /// Navigates to all consoles view + /// + private async Task NavigateAllConsolesAsync() + { + try + { + _logger.LogInformation("Navigating to all consoles"); + _filterContext.FilterType = ConsoleListFilterType.All; + _filterContext.FilterValue = string.Empty; + await _navigator.NavigateRouteAsync(this, "list-consoles"); + } + catch(Exception ex) + { + _logger.LogError("Error navigating to all consoles: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to navigate. Please try again."].Value; + HasError = true; + } + } +} \ No newline at end of file diff --git a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs index 43ad13e8..706c9493 100644 --- a/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/MachineViewViewModel.cs @@ -127,6 +127,22 @@ public partial class MachineViewViewModel : ObservableObject return; } + // If we came from ConsolesListViewModel, navigate back to consoles list + if(_navigationSource is ConsolesListViewModel) + { + await _navigator.NavigateViewModelAsync(this); + + return; + } + + // If we came from ComputersListViewModel, navigate back to computers list + if(_navigationSource is ComputersListViewModel) + { + await _navigator.NavigateViewModelAsync(this); + + return; + } + // Otherwise, try to go back in the navigation stack await _navigator.GoBack(this); } diff --git a/Marechai.App/Presentation/Views/ConsolesListPage.xaml b/Marechai.App/Presentation/Views/ConsolesListPage.xaml new file mode 100644 index 00000000..de1dc013 --- /dev/null +++ b/Marechai.App/Presentation/Views/ConsolesListPage.xaml @@ -0,0 +1,261 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Marechai.App/Presentation/Views/ConsolesPage.xaml b/Marechai.App/Presentation/Views/ConsolesPage.xaml new file mode 100644 index 00000000..861ec13e --- /dev/null +++ b/Marechai.App/Presentation/Views/ConsolesPage.xaml @@ -0,0 +1,297 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +