From e2f78871d398ca24eb7698466a69a9636caa06dd Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Mon, 23 Mar 2026 10:32:29 +0000 Subject: [PATCH] Add PeopleByCompany DbSet and controller for managing company personnel - Created migration to add PeopleByCompany DbSet to the database context. - Updated MarechaiContext to include PeopleByCompany DbSet. - Modified MarechaiContextModelSnapshot to reflect the new PeopleByCompany table. - Implemented PeopleByCompanyController with CRUD operations for managing personnel by company. --- .../Admin/AdminCompaniesViewModel.cs | 254 + .../ViewModels/CompanyDetailViewModel.cs | 41 + .../Views/Admin/AdminCompaniesPage.xaml | 95 + .../Views/Admin/AdminCompaniesPage.xaml.cs | 7 + .../Presentation/Views/CompanyDetailPage.xaml | 29 + Marechai.App/Resources/Resources.de.resx | 7 + Marechai.App/Resources/Resources.es.resx | 7 + Marechai.App/Resources/Resources.fr.resx | 7 + Marechai.App/Resources/Resources.la.resx | 7 + Marechai.App/Resources/Resources.pt-BR.resx | 7 + Marechai.App/Resources/Resources.resx | 7 + Marechai.App/Services/Client/ApiClient.cs | 8 +- .../Item/CompanyItemRequestBuilder.cs | 6 + .../Item/People/PeopleRequestBuilder.cs | 92 + .../Client/Models/PersonByCompanyDto.cs | 129 + .../Item/PeopleByCompanyItemRequestBuilder.cs | 148 + .../PeopleByCompanyRequestBuilder.cs | 124 + Marechai.App/Services/Client/kiota-lock.json | 4 +- Marechai.App/Services/CompanyDetailService.cs | 82 + Marechai.App/Services/LocalizedStrings.cs | 7 + Marechai.Data/Dtos/PersonByCompanyDto.cs | 55 + ...101511_AddPeopleByCompanyDbSet.Designer.cs | 8423 +++++++++++++++++ .../20260323101511_AddPeopleByCompanyDbSet.cs | 22 + .../MarechaiContextModelSnapshot.cs | 2 +- Marechai.Database/Models/MarechaiContext.cs | 3 + .../Controllers/PeopleByCompanyController.cs | 144 + 26 files changed, 9713 insertions(+), 4 deletions(-) create mode 100644 Marechai.App/Services/Client/Companies/Item/People/PeopleRequestBuilder.cs create mode 100644 Marechai.App/Services/Client/Models/PersonByCompanyDto.cs create mode 100644 Marechai.App/Services/Client/PeopleByCompany/Item/PeopleByCompanyItemRequestBuilder.cs create mode 100644 Marechai.App/Services/Client/PeopleByCompany/PeopleByCompanyRequestBuilder.cs create mode 100644 Marechai.Data/Dtos/PersonByCompanyDto.cs create mode 100644 Marechai.Database/Migrations/20260323101511_AddPeopleByCompanyDbSet.Designer.cs create mode 100644 Marechai.Database/Migrations/20260323101511_AddPeopleByCompanyDbSet.cs create mode 100644 Marechai.Server/Controllers/PeopleByCompanyController.cs diff --git a/Marechai.App/Presentation/ViewModels/Admin/AdminCompaniesViewModel.cs b/Marechai.App/Presentation/ViewModels/Admin/AdminCompaniesViewModel.cs index a791b405..9d70fcbc 100644 --- a/Marechai.App/Presentation/ViewModels/Admin/AdminCompaniesViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/Admin/AdminCompaniesViewModel.cs @@ -150,6 +150,42 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware [ObservableProperty] private List _statusItems = []; + // --- People junction state --- + [ObservableProperty] + private ObservableCollection _companyPeople = []; + + [ObservableProperty] + private ObservableCollection _companyPeopleDisplays = []; + + [ObservableProperty] + private ObservableCollection _availablePeople = []; + + [ObservableProperty] + private PersonDto? _selectedAvailablePerson; + + [ObservableProperty] + private string _personSearchText = string.Empty; + + [ObservableProperty] + private string _newPersonPosition = string.Empty; + + [ObservableProperty] + private DateTimeOffset? _newPersonStart; + + [ObservableProperty] + private DateTimeOffset? _newPersonEnd; + + [ObservableProperty] + private bool _newPersonOngoing; + + [ObservableProperty] + private bool _isEditingExisting; + + // When non-null, we are editing an existing person-company record + private long? _editingPersonCompanyId; + + private List? _allPeopleList; + public AdminCompaniesViewModel(ApiClient apiClient, IJwtService jwtService, ITokenService tokenService, @@ -187,6 +223,11 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware DeleteTranslationCommand = new AsyncRelayCommand(DeleteTranslationAsync); EditTranslationCommand = new RelayCommand(EditTranslation); OpenLogosCommand = new RelayCommand(OpenLogos); + AddPersonCommand = new AsyncRelayCommand(AddPersonAsync); + RemovePersonCommand = new AsyncRelayCommand(RemovePersonByDisplayAsync); + EditPersonCommand = new RelayCommand(EditPerson); + SavePersonEditCommand = new AsyncRelayCommand(SavePersonEditAsync); + CancelPersonEditCommand = new RelayCommand(CancelPersonEdit); InitializeLanguages(); @@ -206,6 +247,11 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware public IAsyncRelayCommand DeleteTranslationCommand { get; } public IRelayCommand EditTranslationCommand { get; } public IRelayCommand OpenLogosCommand { get; } + public IAsyncRelayCommand AddPersonCommand { get; } + public IAsyncRelayCommand RemovePersonCommand { get; } + public IRelayCommand EditPersonCommand { get; } + public IAsyncRelayCommand SavePersonEditCommand { get; } + public IRelayCommand CancelPersonEditCommand { get; } // --- IRegionAware --- public bool IsNavigationTarget(NavigationContext navigationContext) => true; @@ -294,6 +340,7 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware _editingCompanyId = null; EditPanelTitle = _localizer["AddCompanyDialog_Title"]; ClearForm(); + IsEditingExisting = false; IsEditingDescription = false; IsEditing = true; } @@ -306,8 +353,12 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware _editingCompanyId = company.Id; EditPanelTitle = _localizer["EditCompanyDialog_Title"]; PopulateForm(company); + IsEditingExisting = true; IsEditingDescription = false; IsEditing = true; + + if(company.Id != null) + _ = LoadCompanyPeopleAsync(company.Id.Value); } // --- Delete company --- @@ -611,6 +662,18 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware _logger.LogError(ex, "Error loading companies for search"); } } + + if(_allPeopleList == null) + { + try + { + _allPeopleList = await _apiClient.People.GetAsync(); + } + catch(Exception ex) + { + _logger.LogError(ex, "Error loading people"); + } + } } // --- Helpers --- @@ -637,6 +700,9 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware SoldToSearchText = string.Empty; HasError = false; ErrorMessage = string.Empty; + CompanyPeople.Clear(); + CompanyPeopleDisplays.Clear(); + ClearPersonForm(); } private void PopulateForm(CompanyDto company) @@ -700,4 +766,192 @@ public partial class AdminCompaniesViewModel : ObservableObject, IRegionAware _ => _localizer["StatusUnknown"] }; } + + // --- People junction methods --- + + private async Task LoadCompanyPeopleAsync(int companyId) + { + CompanyPeople.Clear(); + CompanyPeopleDisplays.Clear(); + + try + { + List? items = await _apiClient.Companies[companyId].People.GetAsync(); + + if(items != null) + { + foreach(PersonByCompanyDto p in items) + { + CompanyPeople.Add(p); + CompanyPeopleDisplays.Add(FormatPersonDisplay(p)); + } + } + } + catch(Exception ex) + { + _logger.LogError(ex, "Error loading people for company {Id}", companyId); + } + } + + private static string FormatPersonDisplay(PersonByCompanyDto p) + { + string name = p.DisplayName ?? p.Alias ?? $"{p.Name} {p.Surname}".Trim(); + string display = name; + + if(!string.IsNullOrWhiteSpace(p.Position)) + display += $" — {p.Position}"; + + if(p.Ongoing == true) + display += " (Ongoing)"; + else if(p.Start != null || p.End != null) + { + string start = p.Start?.ToString("yyyy") ?? "?"; + string end = p.End?.ToString("yyyy") ?? "?"; + display += $" ({start}–{end})"; + } + + return display; + } + + private async Task AddPersonAsync() + { + if(_editingCompanyId == null || SelectedAvailablePerson?.Id == null) return; + + try + { + var dto = new PersonByCompanyDto + { + PersonId = SelectedAvailablePerson.Id, + CompanyId = _editingCompanyId, + Position = string.IsNullOrWhiteSpace(NewPersonPosition) ? null : NewPersonPosition, + Start = NewPersonStart, + End = NewPersonEnd, + Ongoing = NewPersonOngoing + }; + + await _apiClient.PeopleByCompany.PostAsync(dto); + + ClearPersonForm(); + await LoadCompanyPeopleAsync(_editingCompanyId.Value); + } + catch(Exception ex) + { + _logger.LogError(ex, "Error adding person to company"); + } + } + + private void EditPerson(string? display) + { + if(display == null) return; + + int idx = CompanyPeopleDisplays.IndexOf(display); + + if(idx < 0 || idx >= CompanyPeople.Count) return; + + PersonByCompanyDto p = CompanyPeople[idx]; + + _editingPersonCompanyId = p.Id; + + // Find and select the person in available people + if(p.PersonId != null && _allPeopleList != null) + { + PersonDto? person = _allPeopleList.FirstOrDefault(pp => pp.Id == p.PersonId); + + if(person != null) + { + PersonSearchText = person.DisplayName ?? person.Alias ?? $"{person.Name} {person.Surname}".Trim(); + UpdatePeopleSuggestions(PersonSearchText); + SelectedAvailablePerson = AvailablePeople.FirstOrDefault(pp => pp.Id == person.Id); + } + } + + NewPersonPosition = p.Position ?? string.Empty; + NewPersonStart = p.Start; + NewPersonEnd = p.End; + NewPersonOngoing = p.Ongoing ?? false; + } + + private async Task SavePersonEditAsync() + { + if(_editingPersonCompanyId == null || _editingCompanyId == null) return; + + try + { + var dto = new PersonByCompanyDto + { + Position = string.IsNullOrWhiteSpace(NewPersonPosition) ? null : NewPersonPosition, + Start = NewPersonStart, + End = NewPersonEnd, + Ongoing = NewPersonOngoing + }; + + await _apiClient.PeopleByCompany[_editingPersonCompanyId.Value].PutAsync(dto); + + CancelPersonEdit(); + await LoadCompanyPeopleAsync(_editingCompanyId.Value); + } + catch(Exception ex) + { + _logger.LogError(ex, "Error updating person-company association"); + } + } + + private void CancelPersonEdit() + { + _editingPersonCompanyId = null; + ClearPersonForm(); + } + + private async Task RemovePersonByDisplayAsync(string? display) + { + if(display == null || _editingCompanyId == null) return; + + int idx = CompanyPeopleDisplays.IndexOf(display); + + if(idx < 0 || idx >= CompanyPeople.Count || CompanyPeople[idx].Id == null) return; + + try + { + await _apiClient.PeopleByCompany[CompanyPeople[idx].Id!.Value].DeleteAsync(); + await LoadCompanyPeopleAsync(_editingCompanyId.Value); + } + catch(Exception ex) + { + _logger.LogError(ex, "Error removing person from company"); + } + } + + public void UpdatePeopleSuggestions(string query) + { + AvailablePeople.Clear(); + + if(_allPeopleList == null) return; + + IEnumerable source = _allPeopleList; + + if(!string.IsNullOrWhiteSpace(query)) + source = source.Where(p => + (p.Name != null && + p.Name.Contains(query, StringComparison.OrdinalIgnoreCase)) || + (p.Surname != null && + p.Surname.Contains(query, StringComparison.OrdinalIgnoreCase)) || + (p.DisplayName != null && + p.DisplayName.Contains(query, StringComparison.OrdinalIgnoreCase)) || + (p.Alias != null && + p.Alias.Contains(query, StringComparison.OrdinalIgnoreCase))); + + foreach(PersonDto match in source.Take(50)) + AvailablePeople.Add(match); + } + + private void ClearPersonForm() + { + SelectedAvailablePerson = null; + PersonSearchText = string.Empty; + NewPersonPosition = string.Empty; + NewPersonStart = null; + NewPersonEnd = null; + NewPersonOngoing = false; + _editingPersonCompanyId = null; + } } diff --git a/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs b/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs index cc972274..3cce63f8 100644 --- a/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs +++ b/Marechai.App/Presentation/ViewModels/CompanyDetailViewModel.cs @@ -78,6 +78,11 @@ public partial class CompanyDetailViewModel : ObservableObject, IRegionAware [ObservableProperty] private string _descriptionHtml = string.Empty; + [ObservableProperty] + private ObservableCollection _people = []; + + public bool ShowPeople => People.Count > 0; + public CompanyDetailViewModel(CompanyDetailService companyDetailService, FlagCache flagCache, CompanyLogoCache logoCache, IStringLocalizer localizer, ILogger logger, IRegionManager regionManager, @@ -508,6 +513,42 @@ public partial class CompanyDetailViewModel : ObservableObject, IRegionAware FilteredComputers = new ObservableCollection(Computers); FilteredConsoles = new ObservableCollection(Consoles); + // Load people associated with this company + try + { + People.Clear(); + + List people = await _companyDetailService.GetPeopleByCompanyAsync(CompanyId); + + foreach(PersonByCompanyDto person in people) + { + string name = person.DisplayName ?? person.Alias ?? + $"{person.Name} {person.Surname}".Trim(); + + string display = name; + + if(!string.IsNullOrWhiteSpace(person.Position)) + display += $" — {person.Position}"; + + if(person.Ongoing == true) + display += $" ({_localizer["OngoingText"].Value})"; + else if(person.Start != null || person.End != null) + { + string start = person.Start?.ToString("yyyy") ?? "?"; + string end = person.End?.ToString("yyyy") ?? "?"; + display += $" ({start}–{end})"; + } + + People.Add(display); + } + + OnPropertyChanged(nameof(ShowPeople)); + } + catch(Exception ex) + { + _logger.LogError("Failed to load people for company: {Exception}", ex.Message); + } + // Load localized description try { diff --git a/Marechai.App/Presentation/Views/Admin/AdminCompaniesPage.xaml b/Marechai.App/Presentation/Views/Admin/AdminCompaniesPage.xaml index f5f625df..4ae3ac7a 100644 --- a/Marechai.App/Presentation/Views/Admin/AdminCompaniesPage.xaml +++ b/Marechai.App/Presentation/Views/Admin/AdminCompaniesPage.xaml @@ -298,6 +298,101 @@ Text="{Binding Twitter, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}" /> + + + + + + + + + + + + + + + + + + + + + +