diff --git a/.idea/.idea.presence/.idea/.gitignore b/.idea/.idea.presence/.idea/.gitignore new file mode 100644 index 0000000..6c406d2 --- /dev/null +++ b/.idea/.idea.presence/.idea/.gitignore @@ -0,0 +1,13 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Rider ignored files +/contentModel.xml +/projectSettingsUpdater.xml +/.idea.presence.iml +/modules.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/.idea.presence/.idea/avalonia.xml b/.idea/.idea.presence/.idea/avalonia.xml new file mode 100644 index 0000000..553b849 --- /dev/null +++ b/.idea/.idea.presence/.idea/avalonia.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.idea/.idea.presence/.idea/indexLayout.xml b/.idea/.idea.presence/.idea/indexLayout.xml new file mode 100644 index 0000000..7b08163 --- /dev/null +++ b/.idea/.idea.presence/.idea/indexLayout.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/.idea.presence/.idea/vcs.xml b/.idea/.idea.presence/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/.idea.presence/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/Desktop.UI/App.axaml b/Desktop.UI/App.axaml new file mode 100644 index 0000000..6ccc6b1 --- /dev/null +++ b/Desktop.UI/App.axaml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/Desktop.UI/App.axaml.cs b/Desktop.UI/App.axaml.cs new file mode 100644 index 0000000..3e3e6eb --- /dev/null +++ b/Desktop.UI/App.axaml.cs @@ -0,0 +1,39 @@ +using Avalonia; +using Avalonia.Controls.ApplicationLifetimes; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using System.Linq; +using Avalonia.Markup.Xaml; +using Desktop.UI.DI; +using Desktop.UI.ViewModels; +using Desktop.UI.Views; +using Microsoft.Extensions.DependencyInjection; +using presence_client.ApiClients; + +namespace Desktop.UI; + +public partial class App : Application +{ + public override void Initialize() + { + AvaloniaXamlLoader.Load(this); + } + + public override void OnFrameworkInitializationCompleted() + { + + var serviceCollection = new ServiceCollection(); + serviceCollection.AddCommonService(); + var services = serviceCollection.BuildServiceProvider(); + + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) + { + desktop.MainWindow = new MainWindow() + { + DataContext = new MainWindowViewModel(services), + }; + } + + base.OnFrameworkInitializationCompleted(); + } +} \ No newline at end of file diff --git a/Desktop.UI/Assets/avalonia-logo.ico b/Desktop.UI/Assets/avalonia-logo.ico new file mode 100644 index 0000000..da8d49f Binary files /dev/null and b/Desktop.UI/Assets/avalonia-logo.ico differ diff --git a/Desktop.UI/DI/ServiceColletionExtensions.cs b/Desktop.UI/DI/ServiceColletionExtensions.cs new file mode 100644 index 0000000..aa4ecd6 --- /dev/null +++ b/Desktop.UI/DI/ServiceColletionExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.DependencyInjection; +using Desktop.UI.ViewModels; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Runtime.CompilerServices; +using System.Text; +using System.Threading.Tasks; +using presence_client.ApiClients; + +namespace Desktop.UI.DI; + +public static class ServiceColletionExtensions +{ + public static void AddCommonService(this IServiceCollection collection) + { + collection + .AddSingleton() + .AddSingleton() + .AddSingleton() + .AddTransient(); + } +} \ No newline at end of file diff --git a/Desktop.UI/Desktop.UI.csproj b/Desktop.UI/Desktop.UI.csproj new file mode 100644 index 0000000..efa93bb --- /dev/null +++ b/Desktop.UI/Desktop.UI.csproj @@ -0,0 +1,34 @@ + + + WinExe + net8.0 + enable + true + app.manifest + true + + + + + + + + + + + + + + + + None + All + + + + + + + + + diff --git a/Desktop.UI/Program.cs b/Desktop.UI/Program.cs new file mode 100644 index 0000000..343f1f6 --- /dev/null +++ b/Desktop.UI/Program.cs @@ -0,0 +1,21 @@ +using Avalonia; +using System; + +namespace Desktop.UI; + +sealed class Program +{ + // Initialization code. Don't use any Avalonia, third-party APIs or any + // SynchronizationContext-reliant code before AppMain is called: things aren't initialized + // yet and stuff might break. + [STAThread] + public static void Main(string[] args) => BuildAvaloniaApp() + .StartWithClassicDesktopLifetime(args); + + // Avalonia configuration, don't remove; also used by visual designer. + public static AppBuilder BuildAvaloniaApp() + => AppBuilder.Configure() + .UsePlatformDetect() + .WithInterFont() + .LogToTrace(); +} \ No newline at end of file diff --git a/Desktop.UI/ViewLocator.cs b/Desktop.UI/ViewLocator.cs new file mode 100644 index 0000000..f77ae9e --- /dev/null +++ b/Desktop.UI/ViewLocator.cs @@ -0,0 +1,17 @@ +using System; +using Avalonia.Controls; +using Avalonia.Controls.Templates; +using Desktop.UI.ViewModels; +using Desktop.UI.Views; +using ReactiveUI; + +namespace Desktop.UI; + +public class ViewLocator : IViewLocator +{ + public IViewFor? ResolveView(T? viewModel, string? contract = null) => viewModel switch + { + GroupWindowViewModel groupWindowViewModel => new GroupWindow{DataContext = groupWindowViewModel}, + _ => throw new ArgumentOutOfRangeException(nameof(viewModel)) + }; +} \ No newline at end of file diff --git a/Desktop.UI/ViewModels/GroupWindowViewModel.cs b/Desktop.UI/ViewModels/GroupWindowViewModel.cs new file mode 100644 index 0000000..8c6611d --- /dev/null +++ b/Desktop.UI/ViewModels/GroupWindowViewModel.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using System.Threading.Tasks; +using Demo.Domain.Models; +using presence_client.ApiClients; +using presence_client.DTO; +using ReactiveUI; + +namespace Desktop.UI.ViewModels; + +public class GroupWindowViewModel : ReactiveObject, IRoutableViewModel +{ + private readonly GroupApiClient _groupApi; + private readonly UserApiClient _userApi; + private readonly PresenceApiClient _presenceApi; + + public string UrlPathSegment => "group"; + public IScreen HostScreen { get; } + + public ObservableCollection Groups { get; } = new(); + public ObservableCollection FilteredAndSortedStudents { get; } = new(); + public ObservableCollection SelectedStudents { get; } = new(); + + public ReactiveCommand DeleteAllStudentsCommand { get; } + public ReactiveCommand DeleteSelectedStudentsCommand { get; } + public ReactiveCommand DeleteStudentCommand { get; } + public ReactiveCommand EditStudentCommand { get; } + + public List SortOptions { get; } = new() { "Без сортировки", "По фамилии ↑", "По фамилии ↓" }; + + private Group? _selectedGroup; + public Group? SelectedGroup + { + get => _selectedGroup; + set => this.RaiseAndSetIfChanged(ref _selectedGroup, value); + } + + private string _selectedSortOption = "Без сортировки"; + public string SelectedSortOption + { + get => _selectedSortOption; + set => this.RaiseAndSetIfChanged(ref _selectedSortOption, value); + } + + public GroupWindowViewModel(GroupApiClient groupApi, UserApiClient userApi, PresenceApiClient presenceApi, IScreen screen) + { + _groupApi = groupApi; + _userApi = userApi; + _presenceApi = presenceApi; + HostScreen = screen; + + DeleteAllStudentsCommand = ReactiveCommand.CreateFromTask(DeleteAllStudents); + DeleteSelectedStudentsCommand = ReactiveCommand.CreateFromTask(DeleteSelectedStudents); + DeleteStudentCommand = ReactiveCommand.CreateFromTask(DeleteStudent); + EditStudentCommand = ReactiveCommand.Create(EditStudent); // пока-заглушка + + this.WhenAnyValue(x => x.SelectedGroup, x => x.SelectedSortOption) + .Throttle(TimeSpan.FromMilliseconds(200)) + .ObserveOn(RxApp.MainThreadScheduler) + .Subscribe(async _ => await LoadStudents()); + + LoadGroups(); + } + + private async void LoadGroups() + { + var groups = await _groupApi.ClientGetGroups(); + Groups.Clear(); + foreach (var group in groups) + Groups.Add(group); + } + + private async Task LoadStudents() + { + if (SelectedGroup == null) + { + FilteredAndSortedStudents.Clear(); + return; + } + + var response = await _presenceApi.ClientGetPresence(new PresenceQuery { GroupName = SelectedGroup.Name }); + var students = response.Select(p => new User { + FIO = p.FIO, + Guid = Guid.NewGuid(), // или другое значение + Group = SelectedGroup! // ← добавьте это + }).DistinctBy(u => u.FIO).ToList(); + + switch (SelectedSortOption) + { + case "По фамилии ↑": + students = students.OrderBy(s => s.FIO).ToList(); + break; + case "По фамилии ↓": + students = students.OrderByDescending(s => s.FIO).ToList(); + break; + } + + FilteredAndSortedStudents.Clear(); + foreach (var student in students) + FilteredAndSortedStudents.Add(student); + } + + private async Task DeleteAllStudents() + { + if (SelectedGroup != null) + { + await _presenceApi.ClientDeletePresenceByGroup(SelectedGroup.Name); + await LoadStudents(); + } + } + + private async Task DeleteSelectedStudents() + { + var guids = SelectedStudents.Select(u => u.Guid).ToList(); + await _userApi.ClientDeleteUsers(guids); + await LoadStudents(); + } + + private async Task DeleteStudent(User user) + { + await _userApi.ClientDeleteUser(user.Guid); + await LoadStudents(); + } + + private void EditStudent(User user) + { + // открытие окна редактирования + } +} \ No newline at end of file diff --git a/Desktop.UI/ViewModels/MainWindowViewModel.cs b/Desktop.UI/ViewModels/MainWindowViewModel.cs new file mode 100644 index 0000000..0d6f931 --- /dev/null +++ b/Desktop.UI/ViewModels/MainWindowViewModel.cs @@ -0,0 +1,17 @@ +using System; +using Microsoft.Extensions.DependencyInjection; +using ReactiveUI; + +namespace Desktop.UI.ViewModels; + +public class MainWindowViewModel : ViewModelBase, IScreen +{ + + public RoutingState Router { get; } = new RoutingState(); + + public MainWindowViewModel(IServiceProvider serviceProvider) + { + var groupViewModel = serviceProvider.GetRequiredService(); + Router.Navigate.Execute(groupViewModel); + } +} \ No newline at end of file diff --git a/Desktop.UI/ViewModels/ViewModelBase.cs b/Desktop.UI/ViewModels/ViewModelBase.cs new file mode 100644 index 0000000..acbc489 --- /dev/null +++ b/Desktop.UI/ViewModels/ViewModelBase.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; +using ReactiveUI; + +namespace Desktop.UI.ViewModels; + +public class ViewModelBase : ReactiveObject +{ +} \ No newline at end of file diff --git a/Desktop.UI/Views/GroupWindow.axaml b/Desktop.UI/Views/GroupWindow.axaml new file mode 100644 index 0000000..6cc6e72 --- /dev/null +++ b/Desktop.UI/Views/GroupWindow.axaml @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + +