Add company detail page.

This commit is contained in:
2025-11-15 05:32:46 +00:00
parent d5fbb55425
commit 80791a8cc9
18 changed files with 1348 additions and 20 deletions

View File

@@ -1,6 +1,7 @@
<Application x:Class="Marechai.App.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Marechai.App.Presentation.Converters">
<Application.Resources>
<ResourceDictionary>
@@ -12,6 +13,9 @@
</ResourceDictionary.MergedDictionaries>
<!-- Add resources here -->
<local:ObjectToVisibilityConverter x:Key="ObjectToVisibilityConverter" />
<local:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
<local:ZeroToVisibilityConverter x:Key="ZeroToVisibilityConverter" />
</ResourceDictionary>
</Application.Resources>

View File

@@ -10,6 +10,7 @@ using Uno.Extensions.Http;
using Uno.Extensions.Localization;
using Uno.Extensions.Navigation;
using Uno.UI;
using CompanyDetailViewModel = Marechai.App.Presentation.ViewModels.CompanyDetailViewModel;
using ComputersListViewModel = Marechai.App.Presentation.ViewModels.ComputersListViewModel;
using ComputersViewModel = Marechai.App.Presentation.ViewModels.ComputersViewModel;
using MachineViewViewModel = Marechai.App.Presentation.ViewModels.MachineViewViewModel;
@@ -113,6 +114,8 @@ public partial class App : Application
services.AddSingleton<ConsolesViewModel>();
services.AddSingleton<CompaniesService>();
services.AddSingleton<CompaniesViewModel>();
services.AddSingleton<CompanyDetailService>();
services.AddSingleton<CompanyDetailViewModel>();
services.AddSingleton<MachineViewViewModel>();
services
@@ -148,6 +151,7 @@ public partial class App : Application
new ViewMap<ConsolesPage, ConsolesViewModel>(),
new ViewMap<ConsolesListPage, ConsolesListViewModel>(),
new ViewMap<CompaniesPage, CompaniesViewModel>(),
new ViewMap<CompanyDetailPage, CompanyDetailViewModel>(),
new ViewMap<MachineViewPage, MachineViewViewModel>(),
new DataViewMap<SecondPage, SecondViewModel, Entity>());
@@ -183,7 +187,13 @@ public partial class App : Application
ConsolesListViewModel>())
]),
new RouteMap("companies",
views.FindByViewModel<CompaniesViewModel>()),
views.FindByViewModel<CompaniesViewModel>(),
Nested:
[
new RouteMap("detail",
views.FindByViewModel<
CompanyDetailViewModel>())
]),
new RouteMap("Second",
views.FindByViewModel<SecondViewModel>())
])

View File

@@ -0,0 +1,51 @@
using System;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Data;
namespace Marechai.App.Presentation.Converters;
/// <summary>
/// Converts null object to Collapsed visibility
/// </summary>
public class ObjectToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language) =>
value != null ? Visibility.Visible : Visibility.Collapsed;
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}
/// <summary>
/// Converts empty/null string to Collapsed visibility
/// </summary>
public class StringToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is string str && !string.IsNullOrWhiteSpace(str)) return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}
/// <summary>
/// Converts zero count to Collapsed visibility, otherwise Visible
/// </summary>
public class ZeroToVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, string language)
{
if(value is int count && count > 0) return Visibility.Visible;
if(value is long longCount && longCount > 0) return Visibility.Visible;
return Visibility.Collapsed;
}
public object ConvertBack(object value, Type targetType, object parameter, string language) =>
throw new NotImplementedException();
}

View File

@@ -0,0 +1,35 @@
/******************************************************************************
// MARECHAI: Master repository of computing history artifacts information
// ----------------------------------------------------------------------------
//
// Author(s) : Natalia Portillo <claunia@claunia.com>
//
// --[ License ] --------------------------------------------------------------
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
//
// ----------------------------------------------------------------------------
// Copyright © 2003-2026 Natalia Portillo
*******************************************************************************/
namespace Marechai.App.Presentation.Models;
/// <summary>
/// Navigation parameter for the CompanyDetailPage containing both the company ID and the navigation source.
/// </summary>
public class CompanyDetailNavigationParameter
{
public required int CompanyId { get; init; }
public object? NavigationSource { get; init; }
}

View File

@@ -7,6 +7,7 @@ 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;
@@ -14,13 +15,12 @@ namespace Marechai.App.Presentation.ViewModels;
public partial class CompaniesViewModel : ObservableObject
{
private readonly List<CompanyListItem> _allCompanies = [];
private readonly CompaniesService _companiesService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<CompaniesViewModel> _logger;
private readonly INavigator _navigator;
private readonly List<CompanyListItem> _allCompanies = [];
[ObservableProperty]
private ObservableCollection<CompanyListItem> _companiesList = [];
@@ -146,8 +146,14 @@ public partial class CompaniesViewModel : ObservableObject
_logger.LogInformation("Navigating to company: {CompanyName} (ID: {CompanyId})", company.Name, company.Id);
// TODO: Implement company detail view
// For now, just log the navigation
// Navigate to company detail view with navigation parameter
var navParam = new CompanyDetailNavigationParameter
{
CompanyId = company.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<CompanyDetailViewModel>(this, data: navParam);
}
/// <summary>

View File

@@ -0,0 +1,373 @@
#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 Marechai.Data;
using Uno.Extensions.Navigation;
namespace Marechai.App.Presentation.ViewModels;
public partial class CompanyDetailViewModel : ObservableObject
{
private readonly CompanyDetailService _companyDetailService;
private readonly IStringLocalizer _localizer;
private readonly ILogger<CompanyDetailViewModel> _logger;
private readonly INavigator _navigator;
[ObservableProperty]
private CompanyDto? _company;
[ObservableProperty]
private int _companyId;
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _computers = [];
[ObservableProperty]
private string _computersFilterText = string.Empty;
[ObservableProperty]
private string _consoelsFilterText = string.Empty;
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _consoles = [];
[ObservableProperty]
private string _errorMessage = string.Empty;
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _filteredComputers = [];
[ObservableProperty]
private ObservableCollection<CompanyDetailMachine> _filteredConsoles = [];
[ObservableProperty]
private bool _hasError;
[ObservableProperty]
private bool _isDataLoaded;
[ObservableProperty]
private bool _isLoading;
[ObservableProperty]
private CompanyDto? _soldToCompany;
public CompanyDetailViewModel(CompanyDetailService companyDetailService, IStringLocalizer localizer,
ILogger<CompanyDetailViewModel> logger, INavigator navigator)
{
_companyDetailService = companyDetailService;
_localizer = localizer;
_logger = logger;
_navigator = navigator;
LoadData = new AsyncRelayCommand(LoadDataAsync);
GoBackCommand = new AsyncRelayCommand(GoBackAsync);
NavigateToMachineCommand = new AsyncRelayCommand<CompanyDetailMachine>(NavigateToMachineAsync);
}
/// <summary>
/// Gets the display text for the company's status
/// </summary>
public string CompanyStatusDisplay => Company != null ? GetStatusMessage(Company) : string.Empty;
/// <summary>
/// Gets the display text for the company's founded date
/// </summary>
public string CompanyFoundedDateDisplay => Company != null ? GetFoundedDateDisplay(Company) : string.Empty;
public IAsyncRelayCommand LoadData { get; }
public ICommand GoBackCommand { get; }
public IAsyncRelayCommand<CompanyDetailMachine> NavigateToMachineCommand { get; }
public string Title { get; } = "Company Details";
partial void OnCompanyChanged(CompanyDto? oldValue, CompanyDto? newValue)
{
// Notify that computed properties have changed
OnPropertyChanged(nameof(CompanyStatusDisplay));
OnPropertyChanged(nameof(CompanyFoundedDateDisplay));
}
partial void OnComputersFilterTextChanged(string value)
{
FilterComputers(value);
}
partial void OnConsoelsFilterTextChanged(string value)
{
FilterConsoles(value);
}
private void FilterComputers(string filterText)
{
ObservableCollection<CompanyDetailMachine> filtered = string.IsNullOrWhiteSpace(filterText)
? new ObservableCollection<
CompanyDetailMachine>(Computers)
: new
ObservableCollection<
CompanyDetailMachine>(Computers.Where(c =>
c.Name.Contains(filterText,
StringComparison
.OrdinalIgnoreCase)));
FilteredComputers = filtered;
}
private void FilterConsoles(string filterText)
{
ObservableCollection<CompanyDetailMachine> filtered = string.IsNullOrWhiteSpace(filterText)
? new ObservableCollection<
CompanyDetailMachine>(Consoles)
: new
ObservableCollection<
CompanyDetailMachine>(Consoles.Where(c =>
c.Name.Contains(filterText,
StringComparison
.OrdinalIgnoreCase)));
FilteredConsoles = filtered;
}
private async Task NavigateToMachineAsync(CompanyDetailMachine? machine)
{
if(machine == null) return;
var navParam = new MachineViewNavigationParameter
{
MachineId = machine.Id,
NavigationSource = this
};
await _navigator.NavigateViewModelAsync<MachineViewViewModel>(this, data: navParam);
}
/// <summary>
/// Gets the formatted founding date with unknown handling
/// </summary>
public string GetFoundedDateDisplay(CompanyDto company)
{
if(company.Founded is null) return string.Empty;
DateTime date = company.Founded.Value.DateTime;
if(company.FoundedMonthIsUnknown ?? false) return $"{date.Year}.";
if(company.FoundedDayIsUnknown ?? false) return $"{date:Y}.";
return $"{date:D}.";
}
/// <summary>
/// Gets the formatted sold/event date with unknown handling
/// </summary>
public string GetEventDateDisplay(CompanyDto? company, bool monthUnknown = false, bool dayUnknown = false)
{
if(company?.Sold is null) return _localizer["unknown date"].Value;
DateTime date = company.Sold.Value.DateTime;
if(monthUnknown || (company.SoldMonthIsUnknown ?? false)) return $"{date.Year}";
if(dayUnknown || (company.SoldDayIsUnknown ?? false)) return $"{date:Y}";
return $"{date:D}";
}
/// <summary>
/// Gets the status message for the company
/// </summary>
public string GetStatusMessage(CompanyDto company)
{
return company.Status switch
{
1 => _localizer["Company is active."].Value,
2 => GetSoldStatusMessage(company),
3 => GetMergedStatusMessage(company),
4 => GetBankruptcyMessage(company),
5 => GetDefunctMessage(company),
6 => GetRenamedStatusMessage(company),
_ => _localizer["Current company status is unknown."].Value
};
}
private string GetSoldStatusMessage(CompanyDto company)
{
if(SoldToCompany != null)
{
return string.Format(_localizer["Company sold to {0} on {1}."].Value,
SoldToCompany.Name,
GetEventDateDisplay(company));
}
if(company.Sold != null)
{
return string.Format(_localizer["Company sold on {0} to an unknown company."].Value,
GetEventDateDisplay(company));
}
return SoldToCompany != null
? string.Format(_localizer["Company sold to {0} on an unknown date."].Value, SoldToCompany.Name)
: _localizer["Company was sold to an unknown company on an unknown date."].Value;
}
private string GetMergedStatusMessage(CompanyDto company)
{
if(SoldToCompany != null)
{
return string.Format(_localizer["Company merged on {0} to form {1}."].Value,
GetEventDateDisplay(company),
SoldToCompany.Name);
}
if(company.Sold != null)
{
return string.Format(_localizer["Company merged on {0} to form an unknown company."].Value,
GetEventDateDisplay(company));
}
return SoldToCompany != null
? string.Format(_localizer["Company merged on an unknown date to form {0}."].Value,
SoldToCompany.Name)
: _localizer["Company merged to form an unknown company on an unknown date."].Value;
}
private string GetBankruptcyMessage(CompanyDto company) => company.Sold != null
? string.Format(_localizer
["Company declared bankruptcy on {0}."]
.Value,
GetEventDateDisplay(company))
: _localizer
["Company declared bankruptcy on an unknown date."]
.Value;
private string GetDefunctMessage(CompanyDto company) => company.Sold != null
? string.Format(_localizer
["Company ceased operations on {0}."]
.Value,
GetEventDateDisplay(company))
: _localizer
["Company ceased operations on an unknown date."]
.Value;
private string GetRenamedStatusMessage(CompanyDto company)
{
if(SoldToCompany != null)
{
return string.Format(_localizer["Company renamed to {0} on {1}."].Value,
SoldToCompany.Name,
GetEventDateDisplay(company));
}
if(company.Sold != null)
{
return string.Format(_localizer["Company was renamed on {0} to an unknown name."].Value,
GetEventDateDisplay(company));
}
return SoldToCompany != null
? string.Format(_localizer["Company renamed to {0} on an unknown date."].Value, SoldToCompany.Name)
: _localizer["Company renamed to an unknown name on an unknown date."].Value;
}
/// <summary>
/// Loads company details from the API
/// </summary>
private async Task LoadDataAsync()
{
try
{
IsLoading = true;
ErrorMessage = string.Empty;
HasError = false;
IsDataLoaded = false;
if(CompanyId <= 0)
{
ErrorMessage = _localizer["Invalid company ID."].Value;
HasError = true;
return;
}
// Load company details
Company = await _companyDetailService.GetCompanyByIdAsync(CompanyId);
if(Company is null)
{
ErrorMessage = _localizer["Company not found."].Value;
HasError = true;
return;
}
// Load sold-to company if applicable
if(Company.SoldToId != null)
{
int soldToId = UntypedNodeExtractor.ExtractInt(Company.SoldToId);
if(soldToId > 0) SoldToCompany = await _companyDetailService.GetSoldToCompanyAsync(soldToId);
}
// Load computers and consoles made by this company
List<MachineDto> machines = await _companyDetailService.GetComputersByCompanyAsync(CompanyId);
Computers.Clear();
Consoles.Clear();
FilteredComputers.Clear();
FilteredConsoles.Clear();
foreach(MachineDto machine in machines)
{
int machineId = UntypedNodeExtractor.ExtractInt(machine.Id);
var machineItem = new CompanyDetailMachine
{
Id = machineId,
Name = machine.Name ?? string.Empty
};
// Categorize by machine type enum
if(machine.Type == (int)MachineType.Computer)
Computers.Add(machineItem);
else if(machine.Type == (int)MachineType.Console) Consoles.Add(machineItem);
}
// Initialize filtered lists
FilteredComputers = new ObservableCollection<CompanyDetailMachine>(Computers);
FilteredConsoles = new ObservableCollection<CompanyDetailMachine>(Consoles);
IsDataLoaded = true;
}
catch(Exception ex)
{
_logger.LogError("Error loading company details: {Exception}", ex.Message);
ErrorMessage = _localizer["Failed to load company details. Please try again later."].Value;
HasError = true;
}
finally
{
IsLoading = false;
}
}
/// <summary>
/// Handles back navigation
/// </summary>
private async Task GoBackAsync()
{
await _navigator.NavigateViewModelAsync<CompaniesViewModel>(this);
}
}
/// <summary>
/// Data model for a machine in the company detail view
/// </summary>
public class CompanyDetailMachine
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}

View File

@@ -31,6 +31,7 @@ using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Humanizer;
using Marechai.App.Helpers;
using Marechai.App.Presentation.Models;
using Marechai.App.Services;
using Marechai.Data;
using Microsoft.UI.Xaml;
@@ -127,6 +128,19 @@ public partial class MachineViewViewModel : ObservableObject
return;
}
// If we came from CompanyDetailViewModel, navigate back to company details
if(_navigationSource is CompanyDetailViewModel companyVm)
{
var navParam = new CompanyDetailNavigationParameter
{
CompanyId = companyVm.CompanyId
};
await _navigator.NavigateViewModelAsync<CompanyDetailViewModel>(this, data: navParam);
return;
}
// If we came from ConsolesListViewModel, navigate back to consoles list
if(_navigationSource is ConsolesListViewModel)
{

View File

@@ -92,7 +92,23 @@
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="8">
<ItemsRepeater ItemsSource="{Binding CompaniesList}">
<ItemsRepeater.ItemTemplate></ItemsRepeater.ItemTemplate>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button HorizontalAlignment="Stretch"
Command="{Binding DataContext.NavigateToCompanyCommand, ElementName=PageRoot}"
CommandParameter="{Binding}">
<StackPanel Spacing="4"
Padding="8">
<TextBlock Text="{Binding Name}"
FontSize="16"
FontWeight="SemiBold" />
<TextBlock Text="{Binding FoundationDateDisplay}"
FontSize="12"
Opacity="0.6" />
</StackPanel>
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</StackPanel>

View File

@@ -0,0 +1,59 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class CompaniesPage : Page
{
public CompaniesPage()
{
InitializeComponent();
DataContextChanged += CompaniesPage_DataContextChanged;
Loaded += CompaniesPage_Loaded;
}
private void CompaniesPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is not CompaniesViewModel viewModel) return;
// Trigger data loading
_ = viewModel.LoadData.ExecuteAsync(null);
}
private void CompaniesPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is CompaniesViewModel viewModel)
{
// Trigger data loading when data context changes
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(DataContext is CompaniesViewModel viewModel)
{
// Trigger data loading when navigating to the page
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
private void OnSearchTextChanged(AutoSuggestBox sender, AutoSuggestBoxTextChangedEventArgs args)
{
if(args.Reason == AutoSuggestionBoxTextChangeReason.UserInput)
{
// The two-way binding will automatically update SearchQuery in ViewModel,
// which will trigger OnSearchQueryChanged and filter the list
}
}
private void OnSearchQuerySubmitted(AutoSuggestBox sender, AutoSuggestBoxQuerySubmittedEventArgs args)
{
// The two-way binding will automatically update SearchQuery in ViewModel,
// which will trigger OnSearchQueryChanged and filter the list
}
}

View File

@@ -0,0 +1,273 @@
<?xml version="1.0" encoding="utf-8"?>
<Page x:Class="Marechai.App.Presentation.Views.CompanyDetailPage"
x:Name="PageRoot"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:utu="using:Uno.Toolkit.UI"
NavigationCacheMode="Required"
Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
<Grid utu:SafeArea.Insets="VisibleBounds">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Header -->
<utu:NavigationBar Grid.Row="0"
Content="{Binding Path=Title}">
<utu:NavigationBar.MainCommand>
<AppBarButton Icon="Back"
Label="Back"
Command="{Binding GoBackCommand}"
AutomationProperties.Name="Go back" />
</utu:NavigationBar.MainCommand>
</utu:NavigationBar>
<!-- Content -->
<ScrollViewer Grid.Row="1">
<StackPanel Padding="16"
Spacing="16">
<!-- Loading State -->
<StackPanel Visibility="{Binding IsLoading}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
Padding="32"
Spacing="16">
<ProgressRing IsActive="True"
IsIndeterminate="True"
Height="48"
Width="48" />
<TextBlock Text="Loading..."
TextAlignment="Center"
FontSize="14" />
</StackPanel>
<!-- Error State -->
<StackPanel Visibility="{Binding HasError}"
Padding="16"
Background="{ThemeResource SystemErrorBackgroundColor}"
CornerRadius="8"
Spacing="8">
<TextBlock Text="Error"
FontWeight="Bold"
Foreground="{ThemeResource SystemErrorTextForegroundColor}" />
<TextBlock Text="{Binding ErrorMessage}"
Foreground="{ThemeResource SystemErrorTextForegroundColor}"
TextWrapping="Wrap" />
</StackPanel>
<!-- Company Details -->
<StackPanel Visibility="{Binding IsDataLoaded}"
Spacing="16">
<!-- Company Name -->
<TextBlock Text="{Binding Company.Name}"
FontSize="28"
FontWeight="Bold"
TextWrapping="Wrap" />
<!-- Company Status -->
<StackPanel Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Status"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding CompanyStatusDisplay}"
FontSize="14"
TextWrapping="Wrap" />
</StackPanel>
<!-- Founded Date -->
<StackPanel Visibility="{Binding Company.Founded, Converter={StaticResource ObjectToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Founded"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding CompanyFoundedDateDisplay}"
FontSize="14" />
</StackPanel>
<!-- Legal Name -->
<StackPanel Visibility="{Binding Company.LegalName, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Legal Name"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Company.LegalName}"
FontSize="14" />
</StackPanel>
<!-- Country -->
<StackPanel Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Country"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Company.Country}"
FontSize="14" />
</StackPanel>
<!-- Address -->
<StackPanel Visibility="{Binding Company.Address, Converter={StaticResource StringToVisibilityConverter}}"
Background="{ThemeResource CardBackgroundFillColorDefaultBrush}"
CornerRadius="8"
Padding="12"
Spacing="8">
<TextBlock Text="Address"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock TextWrapping="Wrap">
<Run Text="{Binding Company.Address}" />
<Run Text="{Binding Company.City}" />
<Run Text="{Binding Company.PostalCode}" />
<Run Text="{Binding Company.Province}" />
</TextBlock>
</StackPanel>
<!-- Links Section -->
<StackPanel Spacing="8">
<TextBlock Text="Links"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<!-- Website -->
<HyperlinkButton Visibility="{Binding Company.Website, Converter={StaticResource StringToVisibilityConverter}}"
NavigateUri="{Binding Company.Website}">
<TextBlock Text="Website" />
</HyperlinkButton>
<!-- Twitter -->
<HyperlinkButton Visibility="{Binding Company.Twitter, Converter={StaticResource StringToVisibilityConverter}}"
Click="OnTwitterClick">
<TextBlock Text="Twitter" />
</HyperlinkButton>
<!-- Facebook -->
<HyperlinkButton Visibility="{Binding Company.Facebook, Converter={StaticResource StringToVisibilityConverter}}"
Click="OnFacebookClick">
<TextBlock Text="Facebook" />
</HyperlinkButton>
</StackPanel>
<!-- Computers Section -->
<StackPanel Visibility="{Binding Computers.Count, Converter={StaticResource ZeroToVisibilityConverter}}"
Spacing="8">
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="Computers"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Computers.Count}"
FontSize="14"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- Filter Box -->
<AutoSuggestBox PlaceholderText="Filter computers..."
Text="{Binding ComputersFilterText, Mode=TwoWay}"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}" />
<!-- Scrollable Computers List -->
<ScrollViewer Height="200"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
CornerRadius="8">
<ItemsRepeater ItemsSource="{Binding FilteredComputers}"
Margin="0">
<ItemsRepeater.Layout>
<StackLayout Spacing="4" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.NavigateToMachineCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Padding="12,8"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<TextBlock Text="{Binding Name}"
FontSize="12"
TextWrapping="Wrap" />
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</StackPanel>
<!-- Consoles Section -->
<StackPanel Visibility="{Binding Consoles.Count, Converter={StaticResource ZeroToVisibilityConverter}}"
Spacing="8">
<StackPanel Orientation="Horizontal"
Spacing="8">
<TextBlock Text="Consoles"
FontSize="14"
FontWeight="SemiBold"
Foreground="{ThemeResource SystemBaseMediumColor}" />
<TextBlock Text="{Binding Consoles.Count}"
FontSize="14"
FontWeight="Bold"
Foreground="{ThemeResource SystemAccentColor}" />
</StackPanel>
<!-- Filter Box -->
<AutoSuggestBox PlaceholderText="Filter consoles..."
Text="{Binding ConsoelsFilterText, Mode=TwoWay}"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}" />
<!-- Scrollable Consoles List -->
<ScrollViewer Height="200"
BorderThickness="1"
BorderBrush="{ThemeResource ControlElevationBorderBrush}"
CornerRadius="8">
<ItemsRepeater ItemsSource="{Binding FilteredConsoles}"
Margin="0">
<ItemsRepeater.Layout>
<StackLayout Spacing="4" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<Button Command="{Binding DataContext.NavigateToMachineCommand, ElementName=PageRoot}"
CommandParameter="{Binding}"
Padding="12,8"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Left">
<TextBlock Text="{Binding Name}"
FontSize="12"
TextWrapping="Wrap" />
</Button>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</Page>

View File

@@ -0,0 +1,77 @@
#nullable enable
using System;
using Windows.System;
using Marechai.App.Presentation.Models;
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class CompanyDetailPage : Page
{
private object? _navigationSource;
private int? _pendingCompanyId;
public CompanyDetailPage()
{
InitializeComponent();
DataContextChanged += CompanyDetailPage_DataContextChanged;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
int? companyId = null;
// Handle both int and CompanyDetailNavigationParameter
if(e.Parameter is int intId)
companyId = intId;
else if(e.Parameter is CompanyDetailNavigationParameter navParam)
{
companyId = navParam.CompanyId;
_navigationSource = navParam.NavigationSource;
}
if(companyId.HasValue)
{
_pendingCompanyId = companyId;
if(DataContext is CompanyDetailViewModel viewModel)
{
viewModel.CompanyId = companyId.Value;
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
}
private void CompanyDetailPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is CompanyDetailViewModel viewModel && _pendingCompanyId.HasValue)
{
viewModel.CompanyId = _pendingCompanyId.Value;
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
private async void OnTwitterClick(object sender, RoutedEventArgs e)
{
if(DataContext is CompanyDetailViewModel viewModel && viewModel.Company?.Twitter is not null)
{
var uri = new Uri($"https://www.twitter.com/{viewModel.Company.Twitter}");
await Launcher.LaunchUriAsync(uri);
}
}
private async void OnFacebookClick(object sender, RoutedEventArgs e)
{
if(DataContext is CompanyDetailViewModel viewModel && viewModel.Company?.Facebook is not null)
{
var uri = new Uri($"https://www.facebook.com/{viewModel.Company.Facebook}");
await Launcher.LaunchUriAsync(uri);
}
}
}

View File

@@ -0,0 +1,37 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Marechai.App.Presentation.Views;
/// <summary>
/// Professional list view for displaying consoles filtered by letter, year, or all.
/// Features responsive layout, modern styling, and smooth navigation.
/// </summary>
public sealed partial class ConsolesListPage : Page
{
public ConsolesListPage()
{
InitializeComponent();
Loaded += ConsolesListPage_Loaded;
DataContextChanged += ConsolesListPage_DataContextChanged;
}
private void ConsolesListPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(DataContext is ConsolesListViewModel vm)
{
// Load data when DataContext is set
vm.LoadData.Execute(null);
}
}
private void ConsolesListPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is ConsolesListViewModel vm)
{
// Load data when page is loaded (fallback)
vm.LoadData.Execute(null);
}
}
}

View File

@@ -0,0 +1,44 @@
using Marechai.App.Presentation.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Navigation;
namespace Marechai.App.Presentation.Views;
public sealed partial class ConsolesPage : Page
{
public ConsolesPage()
{
InitializeComponent();
DataContextChanged += ConsolesPage_DataContextChanged;
Loaded += ConsolesPage_Loaded;
}
private void ConsolesPage_Loaded(object sender, RoutedEventArgs e)
{
if(DataContext is not ConsolesViewModel viewModel) return;
// Trigger data loading
_ = viewModel.LoadData.ExecuteAsync(null);
}
private void ConsolesPage_DataContextChanged(FrameworkElement sender, DataContextChangedEventArgs args)
{
if(args.NewValue is ConsolesViewModel viewModel)
{
// Trigger data loading when data context changes
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
protected override void OnNavigatedTo(NavigationEventArgs e)
{
base.OnNavigatedTo(e);
if(DataContext is ConsolesViewModel viewModel)
{
// Trigger data loading when navigating to the page
_ = viewModel.LoadData.ExecuteAsync(null);
}
}
}

View File

@@ -0,0 +1,101 @@
#nullable enable
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Marechai.App.Models;
namespace Marechai.App.Services;
/// <summary>
/// Service for fetching company details from the API
/// </summary>
public class CompanyDetailService
{
private readonly ApiClient _apiClient;
private readonly ILogger<CompanyDetailService> _logger;
public CompanyDetailService(ApiClient apiClient, ILogger<CompanyDetailService> logger)
{
_apiClient = apiClient;
_logger = logger;
}
/// <summary>
/// Gets a single company by ID with full details
/// </summary>
public async Task<CompanyDto?> 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;
}
}
/// <summary>
/// Gets machines (computers) made by a company
/// </summary>
public async Task<List<MachineDto>> GetComputersByCompanyAsync(int companyId)
{
try
{
_logger.LogInformation("Fetching computers for company {CompanyId}", companyId);
List<MachineDto>? machines = await _apiClient.Companies[companyId].Machines.GetAsync();
if(machines == null) return [];
_logger.LogInformation("Successfully fetched {Count} computers for company {CompanyId}", machines.Count, companyId);
return machines;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching computers for company {CompanyId}", companyId);
return [];
}
}
/// <summary>
/// Gets the sold-to company (when company was sold, merged, or renamed)
/// </summary>
public async Task<CompanyDto?> GetSoldToCompanyAsync(int? companyId)
{
if(companyId is null or <= 0) return null;
try
{
_logger.LogInformation("Fetching sold-to company {CompanyId}", companyId);
CompanyDto? company = await _apiClient.Companies[companyId.Value].GetAsync();
return company;
}
catch(Exception ex)
{
_logger.LogError(ex, "Error fetching sold-to company {CompanyId}", companyId);
return null;
}
}
}

View File

@@ -109,4 +109,61 @@
<data name="Logout" xml:space="preserve">
<value>Logout</value>
</data>
<data name="Company is active." xml:space="preserve">
<value>Company is active.</value>
</data>
<data name="Company sold to {0} on {1}." xml:space="preserve">
<value>Company sold to {0} on {1}.</value>
</data>
<data name="Company sold on {0} to an unknown company." xml:space="preserve">
<value>Company sold on {0} to an unknown company.</value>
</data>
<data name="Company sold to {0} on an unknown date." xml:space="preserve">
<value>Company sold to {0} on an unknown date.</value>
</data>
<data name="Company was sold to an unknown company on an unknown date." xml:space="preserve">
<value>Company was sold to an unknown company on an unknown date.</value>
</data>
<data name="Company merged on {0} to form {1}." xml:space="preserve">
<value>Company merged on {0} to form {1}.</value>
</data>
<data name="Company merged on {0} to form an unknown company." xml:space="preserve">
<value>Company merged on {0} to form an unknown company.</value>
</data>
<data name="Company merged on an unknown date to form {0}." xml:space="preserve">
<value>Company merged on an unknown date to form {0}.</value>
</data>
<data name="Company merged to form an unknown company on an unknown date." xml:space="preserve">
<value>Company merged to form an unknown company on an unknown date.</value>
</data>
<data name="Current company status is unknown." xml:space="preserve">
<value>Current company status is unknown.</value>
</data>
<data name="unknown date" xml:space="preserve">
<value>unknown date</value>
</data>
<data name="Company declared bankruptcy on {0}." xml:space="preserve">
<value>Company declared bankruptcy on {0}.</value>
</data>
<data name="Company declared bankruptcy on an unknown date." xml:space="preserve">
<value>Company declared bankruptcy on an unknown date.</value>
</data>
<data name="Company ceased operations on {0}." xml:space="preserve">
<value>Company ceased operations on {0}.</value>
</data>
<data name="Company ceased operations on an unknown date." xml:space="preserve">
<value>Company ceased operations on an unknown date.</value>
</data>
<data name="Company renamed to {0} on {1}." xml:space="preserve">
<value>Company renamed to {0} on {1}.</value>
</data>
<data name="Company was renamed on {0} to an unknown name." xml:space="preserve">
<value>Company was renamed on {0} to an unknown name.</value>
</data>
<data name="Company renamed to {0} on an unknown date." xml:space="preserve">
<value>Company renamed to {0} on an unknown date.</value>
</data>
<data name="Company was renamed on an unknown date to an unknown name." xml:space="preserve">
<value>Company was renamed on an unknown date to an unknown name.</value>
</data>
</root>

View File

@@ -109,4 +109,61 @@
<data name="Logout" xml:space="preserve">
<value>Cerrar sesión</value>
</data>
<data name="Company is active." xml:space="preserve">
<value>La empresa está activa.</value>
</data>
<data name="Company sold to {0} on {1}." xml:space="preserve">
<value>La empresa fue vendida a {0} el {1}.</value>
</data>
<data name="Company sold on {0} to an unknown company." xml:space="preserve">
<value>La empresa fue vendida el {0} a una empresa desconocida.</value>
</data>
<data name="Company sold to {0} on an unknown date." xml:space="preserve">
<value>La empresa fue vendida a {0} en una fecha desconocida.</value>
</data>
<data name="Company was sold to an unknown company on an unknown date." xml:space="preserve">
<value>La empresa fue vendida a una empresa desconocida en una fecha desconocida.</value>
</data>
<data name="Company merged on {0} to form {1}." xml:space="preserve">
<value>La empresa se fusionó el {0} para formar {1}.</value>
</data>
<data name="Company merged on {0} to form an unknown company." xml:space="preserve">
<value>La empresa se fusionó el {0} para formar una empresa desconocida.</value>
</data>
<data name="Company merged on an unknown date to form {0}." xml:space="preserve">
<value>La empresa se fusionó en una fecha desconocida para formar {0}.</value>
</data>
<data name="Company merged to form an unknown company on an unknown date." xml:space="preserve">
<value>La empresa se fusionó para formar una empresa desconocida en una fecha desconocida.</value>
</data>
<data name="Current company status is unknown." xml:space="preserve">
<value>El estado actual de la empresa es desconocido.</value>
</data>
<data name="unknown date" xml:space="preserve">
<value>fecha desconocida</value>
</data>
<data name="Company declared bankruptcy on {0}." xml:space="preserve">
<value>La empresa declaró quiebra el {0}.</value>
</data>
<data name="Company declared bankruptcy on an unknown date." xml:space="preserve">
<value>La empresa declaró quiebra en una fecha desconocida.</value>
</data>
<data name="Company ceased operations on {0}." xml:space="preserve">
<value>La empresa cesó operaciones el {0}.</value>
</data>
<data name="Company ceased operations on an unknown date." xml:space="preserve">
<value>La empresa cesó operaciones en una fecha desconocida.</value>
</data>
<data name="Company renamed to {0} on {1}." xml:space="preserve">
<value>La empresa fue renombrada a {0} el {1}.</value>
</data>
<data name="Company was renamed on {0} to an unknown name." xml:space="preserve">
<value>La empresa fue renombrada el {0} a un nombre desconocido.</value>
</data>
<data name="Company renamed to {0} on an unknown date." xml:space="preserve">
<value>La empresa fue renombrada a {0} en una fecha desconocida.</value>
</data>
<data name="Company was renamed on an unknown date to an unknown name." xml:space="preserve">
<value>La empresa fue renombrada en una fecha desconocida a un nombre desconocido.</value>
</data>
</root>

View File

@@ -109,4 +109,61 @@
<data name="Logout" xml:space="preserve">
<value>Déconnexion</value>
</data>
<data name="Company is active." xml:space="preserve">
<value>L'entreprise est active.</value>
</data>
<data name="Company sold to {0} on {1}." xml:space="preserve">
<value>L'entreprise a été vendue à {0} le {1}.</value>
</data>
<data name="Company sold on {0} to an unknown company." xml:space="preserve">
<value>L'entreprise a été vendue le {0} à une entreprise inconnue.</value>
</data>
<data name="Company sold to {0} on an unknown date." xml:space="preserve">
<value>L'entreprise a été vendue à {0} à une date inconnue.</value>
</data>
<data name="Company was sold to an unknown company on an unknown date." xml:space="preserve">
<value>L'entreprise a été vendue à une entreprise inconnue à une date inconnue.</value>
</data>
<data name="Company merged on {0} to form {1}." xml:space="preserve">
<value>L'entreprise a fusionné le {0} pour former {1}.</value>
</data>
<data name="Company merged on {0} to form an unknown company." xml:space="preserve">
<value>L'entreprise a fusionné le {0} pour former une entreprise inconnue.</value>
</data>
<data name="Company merged on an unknown date to form {0}." xml:space="preserve">
<value>L'entreprise a fusionné à une date inconnue pour former {0}.</value>
</data>
<data name="Company merged to form an unknown company on an unknown date." xml:space="preserve">
<value>L'entreprise a fusionné pour former une entreprise inconnue à une date inconnue.</value>
</data>
<data name="Current company status is unknown." xml:space="preserve">
<value>L'état actuel de l'entreprise est inconnu.</value>
</data>
<data name="unknown date" xml:space="preserve">
<value>date inconnue</value>
</data>
<data name="Company declared bankruptcy on {0}." xml:space="preserve">
<value>L'entreprise a déclaré faillite le {0}.</value>
</data>
<data name="Company declared bankruptcy on an unknown date." xml:space="preserve">
<value>L'entreprise a déclaré faillite à une date inconnue.</value>
</data>
<data name="Company ceased operations on {0}." xml:space="preserve">
<value>L'entreprise a cessé ses activités le {0}.</value>
</data>
<data name="Company ceased operations on an unknown date." xml:space="preserve">
<value>L'entreprise a cessé ses activités à une date inconnue.</value>
</data>
<data name="Company renamed to {0} on {1}." xml:space="preserve">
<value>L'entreprise a été renommée en {0} le {1}.</value>
</data>
<data name="Company was renamed on {0} to an unknown name." xml:space="preserve">
<value>L'entreprise a été renommée le {0} en un nom inconnu.</value>
</data>
<data name="Company renamed to {0} on an unknown date." xml:space="preserve">
<value>L'entreprise a été renommée en {0} à une date inconnue.</value>
</data>
<data name="Company was renamed on an unknown date to an unknown name." xml:space="preserve">
<value>L'entreprise a été renommée à une date inconnue en un nom inconnu.</value>
</data>
</root>

View File

@@ -109,4 +109,61 @@
<data name="Logout" xml:space="preserve">
<value>Fazer Logout</value>
</data>
<data name="Company is active." xml:space="preserve">
<value>A empresa está ativa.</value>
</data>
<data name="Company sold to {0} on {1}." xml:space="preserve">
<value>A empresa foi vendida para {0} em {1}.</value>
</data>
<data name="Company sold on {0} to an unknown company." xml:space="preserve">
<value>A empresa foi vendida em {0} para uma empresa desconhecida.</value>
</data>
<data name="Company sold to {0} on an unknown date." xml:space="preserve">
<value>A empresa foi vendida para {0} em uma data desconhecida.</value>
</data>
<data name="Company was sold to an unknown company on an unknown date." xml:space="preserve">
<value>A empresa foi vendida para uma empresa desconhecida em uma data desconhecida.</value>
</data>
<data name="Company merged on {0} to form {1}." xml:space="preserve">
<value>A empresa se fundiu em {0} para formar {1}.</value>
</data>
<data name="Company merged on {0} to form an unknown company." xml:space="preserve">
<value>A empresa se fundiu em {0} para formar uma empresa desconhecida.</value>
</data>
<data name="Company merged on an unknown date to form {0}." xml:space="preserve">
<value>A empresa se fundiu em uma data desconhecida para formar {0}.</value>
</data>
<data name="Company merged to form an unknown company on an unknown date." xml:space="preserve">
<value>A empresa se fundiu para formar uma empresa desconhecida em uma data desconhecida.</value>
</data>
<data name="Current company status is unknown." xml:space="preserve">
<value>O status atual da empresa é desconhecido.</value>
</data>
<data name="unknown date" xml:space="preserve">
<value>data desconhecida</value>
</data>
<data name="Company declared bankruptcy on {0}." xml:space="preserve">
<value>A empresa declarou falência em {0}.</value>
</data>
<data name="Company declared bankruptcy on an unknown date." xml:space="preserve">
<value>A empresa declarou falência em uma data desconhecida.</value>
</data>
<data name="Company ceased operations on {0}." xml:space="preserve">
<value>A empresa cessou operações em {0}.</value>
</data>
<data name="Company ceased operations on an unknown date." xml:space="preserve">
<value>A empresa cessou operações em uma data desconhecida.</value>
</data>
<data name="Company renamed to {0} on {1}." xml:space="preserve">
<value>A empresa foi renomeada para {0} em {1}.</value>
</data>
<data name="Company was renamed on {0} to an unknown name." xml:space="preserve">
<value>A empresa foi renomeada em {0} para um nome desconhecido.</value>
</data>
<data name="Company renamed to {0} on an unknown date." xml:space="preserve">
<value>A empresa foi renomeada para {0} em uma data desconhecida.</value>
</data>
<data name="Company was renamed on an unknown date to an unknown name." xml:space="preserve">
<value>A empresa foi renomeada em uma data desconhecida para um nome desconhecido.</value>
</data>
</root>