From d5fbb55425ea36167e2981a12ea6b14337aa0d37 Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sat, 15 Nov 2025 04:48:11 +0000 Subject: [PATCH] Add companies list. --- Marechai.App/App.xaml.cs | 5 + .../Converters/FoundationDateConverter.cs | 20 ++ .../ViewModels/CompaniesViewModel.cs | 189 ++++++++++++++++++ .../Presentation/Views/CompaniesPage.xaml | 104 ++++++++++ Marechai.App/Services/CompaniesService.cs | 77 +++++++ 5 files changed, 395 insertions(+) create mode 100644 Marechai.App/Presentation/Converters/FoundationDateConverter.cs create mode 100644 Marechai.App/Presentation/ViewModels/CompaniesViewModel.cs create mode 100644 Marechai.App/Presentation/Views/CompaniesPage.xaml create mode 100644 Marechai.App/Services/CompaniesService.cs diff --git a/Marechai.App/App.xaml.cs b/Marechai.App/App.xaml.cs index ec5b0a15..d2c04866 100644 --- a/Marechai.App/App.xaml.cs +++ b/Marechai.App/App.xaml.cs @@ -111,6 +111,8 @@ public partial class App : Application services.AddSingleton(); services.AddSingleton(); services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); services.AddSingleton(); services @@ -145,6 +147,7 @@ public partial class App : Application new ViewMap(), new ViewMap(), new ViewMap(), + new ViewMap(), new ViewMap(), new DataViewMap()); @@ -179,6 +182,8 @@ public partial class App : Application views.FindByViewModel< ConsolesListViewModel>()) ]), + new RouteMap("companies", + views.FindByViewModel()), new RouteMap("Second", views.FindByViewModel()) ]) diff --git a/Marechai.App/Presentation/Converters/FoundationDateConverter.cs b/Marechai.App/Presentation/Converters/FoundationDateConverter.cs new file mode 100644 index 00000000..071e76a8 --- /dev/null +++ b/Marechai.App/Presentation/Converters/FoundationDateConverter.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.UI.Xaml.Data; + +namespace Marechai.App.Presentation.Converters; + +/// +/// Converts DateTime to formatted foundation date string, returns empty if null +/// +public class FoundationDateConverter : IValueConverter +{ + public object Convert(object value, Type targetType, object parameter, string language) + { + if(value is DateTime dateTime) return dateTime.ToString("MMMM d, yyyy"); + + return string.Empty; + } + + public object ConvertBack(object value, Type targetType, object parameter, string language) => + throw new NotImplementedException(); +} \ No newline at end of file diff --git a/Marechai.App/Presentation/ViewModels/CompaniesViewModel.cs b/Marechai.App/Presentation/ViewModels/CompaniesViewModel.cs new file mode 100644 index 00000000..80b41897 --- /dev/null +++ b/Marechai.App/Presentation/ViewModels/CompaniesViewModel.cs @@ -0,0 +1,189 @@ +#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.Services; +using Uno.Extensions.Navigation; + +namespace Marechai.App.Presentation.ViewModels; + +public partial class CompaniesViewModel : ObservableObject +{ + private readonly CompaniesService _companiesService; + private readonly IStringLocalizer _localizer; + private readonly ILogger _logger; + private readonly INavigator _navigator; + + private readonly List _allCompanies = []; + + [ObservableProperty] + private ObservableCollection _companiesList = []; + + [ObservableProperty] + private int _companyCount; + + [ObservableProperty] + private string _companyCountText = string.Empty; + + [ObservableProperty] + private string _errorMessage = string.Empty; + + [ObservableProperty] + private bool _hasError; + + [ObservableProperty] + private bool _isDataLoaded; + + [ObservableProperty] + private bool _isLoading; + + [ObservableProperty] + private string _searchQuery = string.Empty; + + public CompaniesViewModel(CompaniesService companiesService, IStringLocalizer localizer, + ILogger logger, INavigator navigator) + { + _companiesService = companiesService; + _localizer = localizer; + _logger = logger; + _navigator = navigator; + LoadData = new AsyncRelayCommand(LoadDataAsync); + GoBackCommand = new AsyncRelayCommand(GoBackAsync); + NavigateToCompanyCommand = new AsyncRelayCommand(NavigateToCompanyAsync); + } + + public IAsyncRelayCommand LoadData { get; } + public ICommand GoBackCommand { get; } + public IAsyncRelayCommand NavigateToCompanyCommand { get; } + public string Title { get; } = "Companies"; + + partial void OnSearchQueryChanged(string value) + { + // Automatically filter when SearchQuery changes + UpdateFilter(value); + } + + /// + /// Loads companies count and list from the API + /// + private async Task LoadDataAsync() + { + try + { + IsLoading = true; + ErrorMessage = string.Empty; + HasError = false; + IsDataLoaded = false; + CompaniesList.Clear(); + _allCompanies.Clear(); + + // Load companies + List companies = await _companiesService.GetAllCompaniesAsync(); + + // Set count + CompanyCount = companies.Count; + CompanyCountText = _localizer["Companies in the database"]; + + // Build the full list in memory + foreach(CompanyDto company in companies) + { + // Extract id from UntypedNode + int companyId = UntypedNodeExtractor.ExtractInt(company.Id); + + // Convert DateTimeOffset? to DateTime? + DateTime? foundedDate = company.Founded?.DateTime; + + _allCompanies.Add(new CompanyListItem + { + Id = companyId, + Name = company.Name ?? string.Empty, + FoundationDate = foundedDate + }); + } + + // Apply current filter (will show all if SearchQuery is empty) + UpdateFilter(SearchQuery); + + if(CompaniesList.Count == 0) + { + ErrorMessage = _localizer["No companies found"].Value; + HasError = true; + } + else + IsDataLoaded = true; + } + catch(Exception ex) + { + _logger.LogError("Error loading companies data: {Exception}", ex.Message); + ErrorMessage = _localizer["Failed to load companies data. Please try again later."].Value; + HasError = true; + } + finally + { + IsLoading = false; + } + } + + /// + /// Handles back navigation + /// + private async Task GoBackAsync() + { + await _navigator.NavigateViewModelAsync(this); + } + + /// + /// Navigates to company detail view + /// + private async Task NavigateToCompanyAsync(CompanyListItem? company) + { + if(company is null) return; + + _logger.LogInformation("Navigating to company: {CompanyName} (ID: {CompanyId})", company.Name, company.Id); + + // TODO: Implement company detail view + // For now, just log the navigation + } + + /// + /// Updates the filtered list based on search query + /// + private void UpdateFilter(string? query) + { + string lowerQuery = string.IsNullOrWhiteSpace(query) ? string.Empty : query.Trim().ToLowerInvariant(); + + CompaniesList.Clear(); + + if(string.IsNullOrEmpty(lowerQuery)) + { + // No filter, show all companies + foreach(CompanyListItem company in _allCompanies) CompaniesList.Add(company); + } + else + { + // Filter companies by name (case-insensitive) + var filtered = _allCompanies.Where(c => c.Name.Contains(lowerQuery, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + foreach(CompanyListItem company in filtered) CompaniesList.Add(company); + } + } +} + +/// +/// Data model for a company in the list +/// +public class CompanyListItem +{ + public int Id { get; set; } + public string Name { get; set; } = string.Empty; + public DateTime? FoundationDate { get; set; } + + public string FoundationDateDisplay => + FoundationDate.HasValue ? FoundationDate.Value.ToString("MMMM d, yyyy") : string.Empty; +} \ No newline at end of file diff --git a/Marechai.App/Presentation/Views/CompaniesPage.xaml b/Marechai.App/Presentation/Views/CompaniesPage.xaml new file mode 100644 index 00000000..801b4dee --- /dev/null +++ b/Marechai.App/Presentation/Views/CompaniesPage.xaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Marechai.App/Services/CompaniesService.cs b/Marechai.App/Services/CompaniesService.cs new file mode 100644 index 00000000..2574379c --- /dev/null +++ b/Marechai.App/Services/CompaniesService.cs @@ -0,0 +1,77 @@ +#nullable enable + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace Marechai.App.Services; + +/// +/// Service for fetching companies data from the API +/// +public class CompaniesService +{ + private readonly ApiClient _apiClient; + private readonly ILogger _logger; + + public CompaniesService(ApiClient apiClient, ILogger logger) + { + _apiClient = apiClient; + _logger = logger; + } + + /// + /// Gets all companies + /// + public async Task> GetAllCompaniesAsync() + { + try + { + _logger.LogInformation("Fetching all companies from API"); + + List? companies = await _apiClient.Companies.GetAsync(); + + if(companies == null) return []; + + _logger.LogInformation("Successfully fetched {Count} total companies", companies.Count); + + return companies; + } + catch(Exception ex) + { + _logger.LogError(ex, "Error fetching all companies from API"); + + return []; + } + } + + /// + /// Gets a single company by ID + /// + public async Task GetCompanyByIdAsync(int companyId) + { + try + { + _logger.LogInformation("Fetching company {CompanyId} from API", companyId); + + CompanyDto? company = await _apiClient.Companies[companyId].GetAsync(); + + if(company == null) + { + _logger.LogWarning("Company {CompanyId} not found", companyId); + + return null; + } + + _logger.LogInformation("Successfully fetched company {CompanyId}: {CompanyName}", companyId, company.Name); + + return company; + } + catch(Exception ex) + { + _logger.LogError(ex, "Error fetching company {CompanyId} from API", companyId); + + return null; + } + } +} \ No newline at end of file