This commit is contained in:
NikitaOnianov 2024-12-23 14:26:41 +03:00
parent ecc361ddb8
commit f9a98b253d
327 changed files with 10017 additions and 399 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

BIN
.vs/presence/v17/.suo Normal file

Binary file not shown.

View File

@ -0,0 +1,56 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|c:\\users\\vivobook 15x\\desktop\\\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\presence_desktop\\presence.desktop\\views\\groupview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}",
"RelativeMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|solutionrelative:presence.desktop\\views\\groupview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}"
},
{
"AbsoluteMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|c:\\users\\vivobook 15x\\desktop\\\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\presence_desktop\\presence.desktop\\views\\presenceview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}",
"RelativeMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|solutionrelative:presence.desktop\\views\\presenceview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "GroupView.axaml",
"DocumentMoniker": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\GroupView.axaml",
"RelativeDocumentMoniker": "Presence.Desktop\\Views\\GroupView.axaml",
"ToolTip": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\GroupView.axaml*",
"RelativeToolTip": "Presence.Desktop\\Views\\GroupView.axaml*",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2024-12-23T11:11:10.505Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "PresenceView.axaml",
"DocumentMoniker": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\PresenceView.axaml",
"RelativeDocumentMoniker": "Presence.Desktop\\Views\\PresenceView.axaml",
"ToolTip": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\PresenceView.axaml",
"RelativeToolTip": "Presence.Desktop\\Views\\PresenceView.axaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2024-12-23T11:11:08.188Z",
"EditorCaption": ""
}
]
}
]
}
]
}

View File

@ -0,0 +1,56 @@
{
"Version": 1,
"WorkspaceRootPath": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\",
"Documents": [
{
"AbsoluteMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|c:\\users\\vivobook 15x\\desktop\\\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\presence_desktop\\presence.desktop\\views\\groupview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}",
"RelativeMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|solutionrelative:presence.desktop\\views\\groupview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}"
},
{
"AbsoluteMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|c:\\users\\vivobook 15x\\desktop\\\u043F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\presence_desktop\\presence.desktop\\views\\presenceview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}",
"RelativeMoniker": "D:0:0:{4A745F7C-B312-4411-AA95-5862597C7B0B}|Presence.Desktop\\Presence.Desktop.csproj|solutionrelative:presence.desktop\\views\\presenceview.axaml||{6D5344A2-2FCD-49DE-A09D-6A14FD1B1224}"
}
],
"DocumentGroupContainers": [
{
"Orientation": 0,
"VerticalTabListWidth": 256,
"DocumentGroups": [
{
"DockedWidth": 200,
"SelectedChildIndex": 1,
"Children": [
{
"$type": "Bookmark",
"Name": "ST:0:0:{1c4feeaa-4718-4aa9-859d-94ce25d182ba}"
},
{
"$type": "Document",
"DocumentIndex": 0,
"Title": "GroupView.axaml",
"DocumentMoniker": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\GroupView.axaml",
"RelativeDocumentMoniker": "Presence.Desktop\\Views\\GroupView.axaml",
"ToolTip": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\GroupView.axaml",
"RelativeToolTip": "Presence.Desktop\\Views\\GroupView.axaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2024-12-23T11:11:10.505Z",
"EditorCaption": ""
},
{
"$type": "Document",
"DocumentIndex": 1,
"Title": "PresenceView.axaml",
"DocumentMoniker": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\PresenceView.axaml",
"RelativeDocumentMoniker": "Presence.Desktop\\Views\\PresenceView.axaml",
"ToolTip": "C:\\Users\\VivoBook 15X\\Desktop\\\u041F\u0440\u043E\u0433\u0440\u0430\u043C\u043C\u043D\u044B\u0435 \u043C\u043E\u0434\u0443\u043B\u0438\\Presence_Desktop\\Presence.Desktop\\Views\\PresenceView.axaml",
"RelativeToolTip": "Presence.Desktop\\Views\\PresenceView.axaml",
"Icon": "ae27a6b0-e345-4288-96df-5eaf394ee369.003549|",
"WhenOpened": "2024-12-23T11:11:08.188Z",
"EditorCaption": ""
}
]
}
]
}
]
}

View File

@ -0,0 +1,13 @@
<Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Presence.Desktop.App"
RequestedThemeVariant="Default">
<!-- "Default" ThemeVariant follows system theme variant. "Dark" or "Light" are other available options. -->
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
</Application.Styles>
</Application>

View File

@ -0,0 +1,46 @@
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
using Microsoft.Extensions.DependencyInjection;
using Presence.Desktop.DI;
using Presence.Desktop.ViewModels;
using Presence.Desktop.Views;
using domain.UseCase;
namespace Presence.Desktop
{
public partial class App : Application
{
public override void Initialize()
{
AvaloniaXamlLoader.Load(this);
}
public override void OnFrameworkInitializationCompleted()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddCommonService();
serviceCollection.AddSingleton<GroupUseCase>();
var services = serviceCollection.BuildServiceProvider();
var groupUseCase = services.GetRequiredService<GroupUseCase>();
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
desktop.MainWindow = new MainWindow()
{
DataContext = new MainWindowViewModel(services),
};
}
base.OnFrameworkInitializationCompleted();
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

View File

@ -0,0 +1,22 @@
using data.RemoteData;
using data.Repository;
using domain.UseCase;
using Microsoft.Extensions.DependencyInjection;
using Presence.Desktop.ViewModels;
namespace Presence.Desktop.DI
{
public static class ServiceCollectionExtensions
{
public static void AddCommonService(this IServiceCollection collection)
{
collection
.AddDbContext<RemoteDatabaseContext>()
.AddSingleton<IGroupRepository, SQLGroupRepositoryImpl>()
.AddSingleton<IUserRepository, SQLUserRepositoryImpl>()
.AddSingleton<IPresenceRepository, SQLPresenceRepositoryImpl>()
.AddSingleton<UseCaseGeneratePresence>()
.AddSingleton<UserUseCase>()
.AddTransient<GroupUseCase>(); }
}
}

View File

@ -0,0 +1,50 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<AvaloniaXaml Remove="Models\**" />
<Compile Remove="Models\**" />
<EmbeddedResource Remove="Models\**" />
<None Remove="Models\**" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="11.2.1" />
<PackageReference Include="Avalonia.Desktop" Version="11.2.1" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.2.1" />
<PackageReference Include="Avalonia.Fonts.Inter" Version="11.2.1" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Include="Avalonia.Diagnostics" Version="11.2.1">
<IncludeAssets Condition="'$(Configuration)' != 'Debug'">None</IncludeAssets>
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="Avalonia.ReactiveUI" Version="11.2.1" />
<PackageReference Include="CsvHelper" Version="33.0.1" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="9.0.0" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\data\data.csproj" />
<ProjectReference Include="..\domain\domain.csproj" />
</ItemGroup>
<ItemGroup>
<None Update="Views\AttendanceView.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
<None Update="Views\GroupView.xaml">
<Generator>MSBuild:Compile</Generator>
</None>
</ItemGroup>
</Project>

View File

@ -0,0 +1,24 @@
using Avalonia;
using Avalonia.ReactiveUI;
using System;
namespace Presence.Desktop
{
internal 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<App>()
.UsePlatformDetect()
.WithInterFont()
.LogToTrace()
.UseReactiveUI();
}
}

View File

@ -0,0 +1,19 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Presence.Desktop.ViewModels;
using System;
using Presence.Desktop.Views;
using ReactiveUI;
namespace Presence.Desktop
{
public class ViewLocator : IViewLocator
{
public IViewFor? ResolveView<T>(T? viewModel, string? contract = null) => viewModel switch
{
GroupViewModel groupViewModel => new GroupView { DataContext = groupViewModel },
PresenceViewModel presenceViewModel => new PresenceView { DataContext = presenceViewModel },
_ => throw new ArgumentOutOfRangeException(nameof(viewModel))
};
}
}

View File

@ -0,0 +1,11 @@
using System.Collections.Generic;
namespace Presence.Desktop.ViewModels
{
public class GroupPresenter
{
public int Id { get; set; }
public string Name { get; set; }
public IEnumerable<UserPresenter>? users { get; set; } = null;
}
}

View File

@ -0,0 +1,342 @@
using Avalonia.Controls.ApplicationLifetimes;
using domain.Models;
using domain.UseCase;
using Presence.Desktop.Views;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Windows.Input;
using Avalonia;
using System.IO;
using CsvHelper;
using CsvHelper.Configuration;
using data.RemoteData;
namespace Presence.Desktop.ViewModels
{
public class GroupViewModel : ViewModelBase, IRoutableViewModel
{
// Объявляем поле для доступа к удаленной базе данных. Используется для доступа к данным о группах и пользователях.
private readonly RemoteDatabaseContext _remoteDatabaseContext;
// URL-сегмент для маршрутизации. Используется ReactiveUI для навигации.
public string? UrlPathSegment { get; }
// Экземпляр экрана, используемый для навигации. Предоставляет возможность перехода к другим представлениям.
public IScreen HostScreen { get; }
// Объекты для работы с бизнес-логикой. Обеспечивают взаимодействие с бизнес-слоем приложения.
private readonly UseCaseGeneratePresence _presenceUseCase; // Для генерации присутствия (возможно, не используется в этом ViewModel)
private readonly GroupUseCase _groupUseCase; // Для работы с группами (добавление, удаление, обновление)
// Временный источник данных для групп. Используется до загрузки данных из базы данных.
private List<GroupPresenter> groupPresentersDataSource = new List<GroupPresenter>();
// Коллекция групп, отображаемых в интерфейсе. Используется ReactiveUI для обновления интерфейса при изменении данных.
private ObservableCollection<GroupPresenter> _groups;
public ObservableCollection<GroupPresenter> Groups => _groups;
// Выбранная группа в интерфейсе. Изменения этого свойства вызывают обновление списка пользователей.
private GroupPresenter? _selectedGroupItem;
public GroupPresenter? SelectedGroupItem
{
get => _selectedGroupItem;
set => this.RaiseAndSetIfChanged(ref _selectedGroupItem, value);
}
// Коллекция пользователей, связанных с выбранной группой. Обновляется при изменении выбранной группы.
public ObservableCollection<UserPresenter> Users { get => _users; }
private ObservableCollection<UserPresenter> _users;
// Список доступных опций сортировки пользователей.
public List<string> SortOptions { get; } = new List<string> { "По фамилии", "По убыванию" };
// Выбранная опция сортировки пользователей. Изменение этого свойства вызывает сортировку списка пользователей.
private string _selectedSortOption;
public string SelectedSortOption
{
get => _selectedSortOption;
set => this.RaiseAndSetIfChanged(ref _selectedSortOption, value);
}
// Свойства, указывающие, доступны ли команды удаления и редактирования. Зависят от количества выбранных пользователей.
public bool CanDelete => SelectedUsers?.Count > 0;
public bool CanEdit => SelectedUsers?.Count == 1;
// Коллекция выбранных пользователей. Изменения в этой коллекции обновляют состояние команд.
public ObservableCollection<UserPresenter> SelectedUsers { get; set; } = new ObservableCollection<UserPresenter>();
// Реактивные команды для обработки действий пользователя. Обеспечивают реактивное поведение интерфейса.
public ReactiveCommand<Unit, Unit> OnDeleteUserClicks { get; }
public ReactiveCommand<Unit, Unit> EditUserCommand { get; }
public ReactiveCommand<Unit, Unit> NextPageCommand { get; }
// Команды для удаления всех студентов и добавления студента.
public ICommand RemoveAllStudentsCommand { get; }
public ICommand AddStudentCommand { get; }
// Конструктор ViewModel. Инициализирует зависимости и устанавливает начальные значения.
public GroupViewModel(IScreen screen, GroupUseCase groupUseCase, UseCaseGeneratePresence presenceUseCase, RemoteDatabaseContext remoteDatabaseContext)
{
_groupUseCase = groupUseCase;
_presenceUseCase = presenceUseCase;
HostScreen = screen;
_remoteDatabaseContext = remoteDatabaseContext; // Инициализация контекста базы данных
// Инициализация реактивных команд с условиями выполнения.
OnDeleteUserClicks = ReactiveCommand.Create(OnDeleteUserClick, this.WhenAnyValue(vm => vm.CanDelete));
EditUserCommand = ReactiveCommand.Create(OnEditUserClick, this.WhenAnyValue(vm => vm.CanEdit));
RefreshGroups(); // Загрузка и обновление списка групп
_groups = new ObservableCollection<GroupPresenter>(groupPresentersDataSource);
_users = new ObservableCollection<UserPresenter>();
// Подписка на изменения выбранной группы для обновления списка пользователей.
this.WhenAnyValue(vm => vm.SelectedGroupItem)
.Subscribe(vm => SetUsers());
// Подписка на изменения выбранной опции сортировки для сортировки списка пользователей.
this.WhenAnyValue(vm => vm.SelectedSortOption)
.Subscribe(_ => SortUsers());
// Инициализация команд добавления и удаления студентов.
RemoveAllStudentsCommand = ReactiveCommand.Create(RemoveAllStudents);
AddStudentCommand = ReactiveCommand.Create(AddStudent);
// Обработчик изменений в коллекции выбранных пользователей для обновления состояния команд.
SelectedUsers.CollectionChanged += (s, e) =>
{
this.RaisePropertyChanged(nameof(CanDelete));
this.RaisePropertyChanged(nameof(CanEdit));
};
NextPageCommand = ReactiveCommand.Create(NextPageButton);
}
// Установка списка пользователей для выбранной группы. Очищает существующий список и добавляет пользователей из выбранной группы.
private void SetUsers()
{
if (SelectedGroupItem?.users == null) return;
Users.Clear(); // Очищаем список пользователей
foreach (var item in SelectedGroupItem.users)
{
Users.Add(item); // Добавляем пользователей из выбранной группы
}
SortUsers(); // Сортируем пользователей
}
// Сортировка списка пользователей по выбранному критерию.
private void SortUsers()
{
if (SelectedGroupItem?.users == null) return;
var sortedUsers = SelectedGroupItem.users.ToList(); // Создаем копию списка пользователей
switch (SelectedSortOption)
{
case "По фамилии":
sortedUsers = sortedUsers.OrderBy(u => u.Name).ToList(); // Сортировка по имени
break;
case "По убыванию":
sortedUsers = sortedUsers.OrderByDescending(u => u.Name).ToList(); // Сортировка в обратном порядке
break;
}
Users.Clear(); // Очищаем список пользователей
foreach (var item in sortedUsers)
{
Users.Add(item); // Добавляем отсортированных пользователей
}
}
// Удаление всех студентов из выбранной группы.
private void RemoveAllStudents()
{
if (SelectedGroupItem == null) return;
_groupUseCase.RemoveAllStudentsFromGroup(SelectedGroupItem.Id); // Удаляем студентов из группы через UseCase
SelectedGroupItem.users = new List<UserPresenter>(); // Обновляем список пользователей в группе
SetUsers(); // Обновляем список пользователей в ViewModel
}
// Переход на следующую страницу (представление).
private void NextPageButton()
{
// Создаем новые экземпляры репозитория и UseCase для следующего представления.
var groupRepository = new SQLGroupRepositoryImpl(_remoteDatabaseContext);
var groupUseCase = new GroupUseCase(groupRepository);
// Переход к представлению PresenceViewModel
HostScreen.Router.Navigate.Execute(new PresenceViewModel(HostScreen, groupUseCase, _presenceUseCase));
}
// Добавление студентов из CSV файла.
private void AddStudent()
{
// Замените на корректный путь к вашему CSV файлу.
string csvFilePath = @"C:\Users\VivoBook 15X\Desktop\Программные модули\Group.csv";
List<UserPresenter> students;
try
{
students = ReadStudentsFromCsv(csvFilePath); // Читаем студентов из CSV файла
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка при чтении CSV: {ex.Message}"); // Обработка исключений
return;
}
if (SelectedGroupItem == null) return;
// Добавляем каждого студента в выбранную группу.
foreach (var student in students)
{
_groupUseCase.AddStudentToGroup(SelectedGroupItem.Id, new User { FIO = student.Name }); // Добавляем студента через UseCase
var newStudent = new UserPresenter
{
Name = student.Name,
Group = SelectedGroupItem // Устанавливаем ссылку на группу
};
var updatedUsers = SelectedGroupItem.users?.ToList() ?? new List<UserPresenter>();
updatedUsers.Add(newStudent);
SelectedGroupItem.users = updatedUsers;
}
SetUsers(); // Обновляем список пользователей
}
// Чтение студентов из CSV файла.
private List<UserPresenter> ReadStudentsFromCsv(string filePath)
{
var students = new List<UserPresenter>();
try
{
using (var reader = new StreamReader(filePath))
using (var csv = new CsvReader(reader, new CsvConfiguration(System.Globalization.CultureInfo.InvariantCulture)
{
HasHeaderRecord = true, // Указываем, что в CSV есть заголовок
Delimiter = "," // Разделитель в CSV файле
}))
{
var records = csv.GetRecords<StudentCsvModel>().ToList(); // Читаем записи из CSV
foreach (var record in records)
{
var student = new UserPresenter
{
Guid = Guid.NewGuid(), // Генерируем уникальный идентификатор
Name = record.Name // Устанавливаем имя студента
};
students.Add(student); // Добавляем студента в список
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Ошибка при чтении CSV файла: {ex.Message}"); // Обработка исключений
}
return students; // Возвращаем список студентов
}
// Обработка события удаления пользователя.
public void OnDeleteUserClick()
{
if (SelectedUsers.Count == 0 || SelectedGroupItem?.users == null)
return;
foreach (var user in SelectedUsers.ToList())
{
_groupUseCase.RemoveUserByGuid(user.Guid); // Удаляем пользователя через UseCase
// Обновляем список пользователей в выбранной группе.
var updatedUsers = SelectedGroupItem.users.Where(u => u.Guid != user.Guid).ToList();
SelectedGroupItem.users = new List<UserPresenter>(updatedUsers);
}
SetUsers(); // Обновляем список пользователей
SelectedUsers.Clear(); // Очищаем список выбранных пользователей
this.RaisePropertyChanged(nameof(CanDelete)); // Обновляем состояние команды удаления
this.RaisePropertyChanged(nameof(CanEdit)); // Обновляем состояние команды редактирования
}
// Обработка события редактирования пользователя.
public async void OnEditUserClick()
{
var user = SelectedUsers.FirstOrDefault(); // Получаем первого выбранного пользователя
if (user == null) return;
var groups = _groupUseCase.GetAllGroups(); // Получаем все группы
// Преобразуем группы из domain.Models.Group в GroupPresenter
var groupPresenters = groups.Select(g => new GroupPresenter
{
Id = g.Id,
Name = g.Name,
users = g.Users?.Select(u => new UserPresenter
{
Name = u.FIO,
Guid = u.Guid,
Group = new GroupPresenter { Id = g.Id, Name = g.Name }
}).ToList()
}).ToList();
// Создаем диалоговое окно для редактирования пользователя.
var editDialog = new EditUserDialog(user.Guid, user.Name, user.Group.Id, groupPresenters);
var mainWindow = (Application.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
if (mainWindow == null) return;
// Открываем диалоговое окно и ожидаем результата.
var result = await editDialog.ShowDialog(mainWindow);
if (result != (null, null))
{
var newName = result.Item1; // Новое имя пользователя
var newGroup = result.Item2; // Новая группа пользователя
// Обновляем пользователя.
user.Name = newName;
user.Group = newGroup;
_groupUseCase.UpdateUser(user.Guid, user.Name, user.Group.Id); // Сохраняем изменения через UseCase
SetUsers(); // Обновляем список пользователей
SelectedUsers.Clear(); // Очищаем список выбранных пользователей
this.RaisePropertyChanged(nameof(CanEdit)); // Обновляем состояние команды редактирования
this.RaisePropertyChanged(nameof(CanDelete)); // Обновляем состояние команды удаления
}
RefreshGroups(); // Обновляем список групп
}
// Обновление списка групп из UseCase.
private void RefreshGroups()
{
groupPresentersDataSource.Clear(); // Очищаем текущий список групп
foreach (var item in _groupUseCase.GetAllGroups())
{
GroupPresenter groupPresenter = new GroupPresenter
{
Id = item.Id,
Name = item.Name,
users = item.Users?.Select(user => new UserPresenter
{
Name = user.FIO,
Guid = user.Guid,
Group = new GroupPresenter { Id = item.Id, Name = item.Name }
}).ToList()
};
groupPresentersDataSource.Add(groupPresenter); // Добавляем группу в список
}
_groups = new ObservableCollection<GroupPresenter>(groupPresentersDataSource); // Обновляем коллекцию групп
}
}
}

View File

@ -0,0 +1,35 @@
using System;
using data.RemoteData;
using domain.UseCase;
using Microsoft.Extensions.DependencyInjection;
using ReactiveUI;
namespace Presence.Desktop.ViewModels
{
public class MainWindowViewModel : ViewModelBase, IScreen
{
// Состояние маршрутизации для навигации внутри приложения
public RoutingState Router { get; } = new RoutingState();
// Конструктор для MainWindowViewModel, получает провайдер сервисов для внедрения зависимостей
public MainWindowViewModel(IServiceProvider serviceProvider)
{
// Получение необходимых зависимостей из провайдера сервисов
var groupUseCase = serviceProvider.GetRequiredService<GroupUseCase>();
var presenceUseCase = serviceProvider.GetRequiredService<UseCaseGeneratePresence>();
var remoteDatabaseContext = serviceProvider.GetRequiredService<RemoteDatabaseContext>();
// Навигация к GroupViewModel, передача необходимых зависимостей
NavigateToGroupViewModel(groupUseCase, presenceUseCase, remoteDatabaseContext);
}
// Приватный метод для обработки навигации к GroupViewModel
private void NavigateToGroupViewModel(GroupUseCase groupUseCase, UseCaseGeneratePresence presenceUseCase, RemoteDatabaseContext remoteDatabaseContext)
{
// Создание нового экземпляра GroupViewModel, передача зависимостей
var groupViewModel = new GroupViewModel(this, groupUseCase, presenceUseCase, remoteDatabaseContext);
// Навигация к GroupViewModel с использованием ReactiveUI Router
Router.Navigate.Execute(groupViewModel);
}
}
}

View File

@ -0,0 +1,10 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Presence.Desktop.ViewModels
{
}

View File

@ -0,0 +1,96 @@
using Avalonia.Data.Converters;
using domain.Models;
using domain.UseCase;
using Presence.Desktop.ViewModels;
using ReactiveUI;
using System;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Reactive;
namespace Presence.Desktop.ViewModels
{
public class PresenceViewModel : ViewModelBase, IRoutableViewModel
{
public string? UrlPathSegment { get; } = "Presence";
public IScreen HostScreen { get; }
private readonly GroupUseCase _groupUseCase;
private readonly UseCaseGeneratePresence _presenceUseCase;
public ObservableCollection<PresenceLocalEntity> AttendanceRecords { get; set; } = new();
public ObservableCollection<Group> Groups { get; set; } = new();
private Group? _selectedGroup;
public Group? SelectedGroup
{
get => _selectedGroup;
set
{
this.RaiseAndSetIfChanged(ref _selectedGroup, value);
FilterAttendanceRecords();
}
}
private DateTime? _selectedDate;
public DateTime? SelectedDate
{
get => _selectedDate;
set
{
this.RaiseAndSetIfChanged(ref _selectedDate, value);
FilterAttendanceRecords();
}
}
public ReactiveCommand<Unit, Unit> NavigateBackCommand { get; }
public PresenceViewModel(IScreen hostScreen, GroupUseCase groupUseCase, UseCaseGeneratePresence presenceUseCase)
{
_groupUseCase = groupUseCase;
_presenceUseCase = presenceUseCase;
HostScreen = hostScreen;
NavigateBackCommand = ReactiveCommand.Create(() => { });
LoadGroups();
}
private void LoadGroups()
{
Groups.Clear();
var groups = _groupUseCase.GetAllGroups();
foreach (var group in groups)
{
Groups.Add(group);
}
}
private void FilterAttendanceRecords()
{
if (SelectedGroup == null || SelectedDate == null)
{
AttendanceRecords.Clear();
return;
}
var records = _presenceUseCase.GetPresenceByGroupAndDate(
SelectedGroup.Id,
SelectedDate.Value);
AttendanceRecords.Clear();
foreach (var record in records)
{
AttendanceRecords.Add(record);
}
}
public void UpdateAttendanceType(PresenceLocalEntity presence)
{
_presenceUseCase.UpdateAttendance(presence);
}
}
}

View File

@ -0,0 +1,7 @@
namespace Presence.Desktop.ViewModels
{
public class StudentCsvModel
{
public string Name { get; set; }
}
}

View File

@ -0,0 +1,11 @@
using System;
namespace Presence.Desktop.ViewModels
{
public class UserPresenter
{
public Guid Guid { get; set; }
public string Name { get; set; }
public GroupPresenter Group { get; set; }
}
}

View File

@ -0,0 +1,6 @@
using ReactiveUI;
namespace Presence.Desktop.ViewModels
{
public class ViewModelBase : ReactiveObject { }
}

View File

@ -0,0 +1,6 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="Presence.Desktop.EditUserDialog"
Title="Edit User Dialog">
<!-- Ваш XAML код здесь -->
</Window>

View File

@ -0,0 +1,57 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Presence.Desktop.ViewModels;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
namespace Presence.Desktop
{
public partial class EditUserDialog : Window
{
private TextBox _nameTextBox;
private ComboBox _groupComboBox;
public EditUserDialog(Guid currentUserId, string currentName, int currentGroupId, List<GroupPresenter> groups)
{
_nameTextBox = new TextBox { Text = currentName };
_groupComboBox = new ComboBox
{
ItemsSource = groups,
SelectedItem = groups.FirstOrDefault(g => g.Id == currentGroupId),
ItemTemplate = new FuncDataTemplate<GroupPresenter>((group, _) =>
new TextBlock { Text = group.Name })
};
var confirmButton = new Button { Content = "OK" };
confirmButton.Click += (sender, args) =>
{
var newFio = _nameTextBox.Text;
var selectedGroup = (GroupPresenter)_groupComboBox.SelectedItem;
if (selectedGroup != null)
{
var newGroupId = selectedGroup.Id;
this.Close();
}
};
Content = new StackPanel
{
Children = { _nameTextBox, _groupComboBox, confirmButton }
};
}
public async Task<(string, GroupPresenter)> ShowDialog(Window parent)
{
await base.ShowDialog(parent);
var name = _nameTextBox.Text;
var groupId = (GroupPresenter)_groupComboBox.SelectedItem;
return (name, groupId);
}
}
}

View File

@ -0,0 +1,154 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Presence.Desktop.ViewModels"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Presence.Desktop.Views.GroupView"
x:DataType="vm:GroupViewModel">
<!-- Основной контейнер с красным фоном -->
<DockPanel Background="red">
<!-- Панель для списка студентов -->
<Border Background="yellow"
CornerRadius="10"
Padding="20"
Margin="20"
Width="400">
<ListBox ItemsSource="{Binding Users}"
Width="500"
SelectionMode="Multiple"
SelectedItems="{Binding SelectedUsers}"
Background="Transparent">
<ListBox.ItemTemplate>
<DataTemplate>
<Border Background="#F0F2F5"
CornerRadius="5"
Padding="10"
Margin="5">
<TextBlock Text="{Binding Name}" FontSize="16" VerticalAlignment="Center" />
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
<!-- Контекстное меню для списка -->
<ListBox.ContextMenu>
<ContextMenu>
<MenuItem Header="Удалить" Click="OnDeleteUserClick"/>
<!-- Кнопка для удаления пользователя -->
<MenuItem Header="Редактировать" Click="OnEditUserClick"/>
<!-- Кнопка для редактирования пользователя -->
</ContextMenu>
</ListBox.ContextMenu>
</ListBox>
</Border>
<!-- Верхняя панель с комбобоксами, расположенная в правом верхнем углу -->
<Border DockPanel.Dock="Top"
Background="yellow"
CornerRadius="10"
Padding="20"
Margin="0,20,20,0">
<StackPanel HorizontalAlignment="Center" Spacing="20">
<ComboBox ItemsSource="{Binding Groups}"
SelectedValue="{Binding SelectedGroupItem}"
Width="300"
Background="Green"
CornerRadius="5"
FontSize="14">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" FontSize="14" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<ComboBox ItemsSource="{Binding SortOptions}"
SelectedItem="{Binding SelectedSortOption}"
Width="300"
Background="Green"
CornerRadius="5"
FontSize="14">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding}" FontSize="14" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Border>
<!-- Нижняя панель с кнопками, расположенная снизу справа -->
<Border DockPanel.Dock="Bottom"
Background="yellow"
CornerRadius="10"
Padding="20"
Margin="0,20,20,0">
<StackPanel HorizontalAlignment="Right" Spacing="30">
<Button Content="Добавить"
Command="{Binding AddStudentCommand}"
Width="250"
Background="Blue"
Foreground="#FFFFFF"
CornerRadius="5"
FontSize="14">
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#43A047"/>
<!-- Изменение фона при наведении -->
</Style>
</Button.Styles>
</Button>
<Button Content="Посещаемость по дате"
Command="{Binding NextPageCommand}"
Width="250"
Background="Green"
Foreground="#FFFFFF"
CornerRadius="5"
FontSize="14">
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#1E88E5"/>
<!-- Изменение фона при наведении -->
</Style>
</Button.Styles>
</Button>
<Button Content="Удалить всех студентов"
Command="{Binding RemoveAllStudentsCommand}"
Width="250"
Background="Red"
Foreground="#FFFFFF"
CornerRadius="5"
FontSize="14">
<Button.Styles>
<Style Selector="Button:pointerover">
<Setter Property="Background" Value="#E53935"/>
<!-- Изменение фона при наведении -->
</Style>
</Button.Styles>
</Button>
</StackPanel>
</Border>
</DockPanel>
</UserControl>

View File

@ -0,0 +1,33 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Presence.Desktop.ViewModels;
using ReactiveUI;
namespace Presence.Desktop.Views
{
public partial class GroupView : ReactiveUserControl<GroupViewModel>
{
public GroupView()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
private void OnDeleteUserClick(object sender, RoutedEventArgs e)
{
var viewModel = (GroupViewModel)DataContext;
viewModel.OnDeleteUserClick();
}
private void OnEditUserClick(object sender, RoutedEventArgs e)
{
var viewModel = (GroupViewModel)DataContext;
viewModel.OnEditUserClick();
}
}
}

View File

@ -0,0 +1,25 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:app="clr-namespace:Presence.Desktop"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Presence.Desktop.ViewModels"
xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Presence.Desktop.Views.MainWindow"
x:DataType="vm:MainWindowViewModel"
Title="Посещаемость">
<!-- DockPanel используется как контейнер для компоновки -->
<DockPanel>
<!-- RoutedViewHost — это элемент управления, предоставляемый ReactiveUI для управления маршрутизацией представлений -->
<reactiveUi:RoutedViewHost Router="{Binding Router}" DockPanel.Dock="Right" Background="AliceBlue">
<!-- ViewLocator используется для поиска и разрешения представлений для моделей представлений -->
<reactiveUi:RoutedViewHost.ViewLocator>
<app:ViewLocator/>
</reactiveUi:RoutedViewHost.ViewLocator>
</reactiveUi:RoutedViewHost>
</DockPanel>
</Window>

View File

@ -0,0 +1,18 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Presence.Desktop.ViewModels;
using ReactiveUI;
namespace Presence.Desktop.Views;
public partial class MainWindow : ReactiveWindow<MainWindowViewModel>
{
public MainWindow()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,62 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="using:Presence.Desktop.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Presence.Desktop.Views.PresenceView"
x:DataType="vm:PresenceViewModel">
<Grid Margin="10">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<!-- Для DataGrid, ширина автоматически подстраивается -->
<ColumnDefinition Width="250" />
<!-- Для Calendar, уменьшенная ширина -->
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<!-- Для StackPanel с ComboBox -->
<RowDefinition Height="*" />
<!-- Для DataGrid и Calendar -->
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Row="0" Grid.ColumnSpan="2" Spacing="10">
<!-- Выбор группы -->
<ComboBox ItemsSource="{Binding Groups}" SelectedItem="{Binding SelectedGroup}" Width="200" PlaceholderText="Выберите группу">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<!-- Создаем новую строку для DataGrid и Calendar -->
<Grid Grid.Row="1">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<!-- Для DataGrid, ширина автоматически подстраивается -->
<ColumnDefinition Width="100" />
<!-- Для Calendar, уменьшенная ширина -->
</Grid.ColumnDefinitions>
<!-- Таблица с посещаемостью слева -->
<DataGrid Grid.Column="0" AutoGenerateColumns="False" ItemsSource="{Binding AttendanceRecords}" CanUserSortColumns="True">
<DataGrid.Columns>
<DataGridTextColumn Header="Дата" Binding="{Binding Date}" />
<DataGridTextColumn Header="Номер урока" Binding="{Binding LessonNumber}" />
<DataGridTextColumn Header="ФИО" Binding="{Binding UserGuid}" />
<!-- Тип посещаемости -->
<DataGridCheckBoxColumn Header="Тип посещаемости" Binding="{Binding IsAttedance, Mode=TwoWay}" />
</DataGrid.Columns>
</DataGrid>
<!-- Календарь справа с уменьшенной шириной -->
<Calendar SelectedDate="{Binding SelectedDate}" Width="350" Height="300" Grid.Column="1" Margin="10" />
</Grid>
</Grid>
</UserControl>

View File

@ -0,0 +1,17 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Presence.Desktop.ViewModels;
using ReactiveUI;
namespace Presence.Desktop.Views;
public partial class PresenceView : ReactiveUserControl<PresenceViewModel>
{
public PresenceView()
{
this.WhenActivated(disposables => { });
AvaloniaXamlLoader.Load(this);
}
}

View File

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
<!-- This manifest is used on Windows only.
Don't remove it as it might cause problems with window transparency and embedded controls.
For more details visit https://learn.microsoft.com/en-us/windows/win32/sbscs/application-manifests -->
<assemblyIdentity version="1.0.0.0" name="Presence.Desktop.Desktop"/>
<compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
<application>
<!-- A list of the Windows versions that this application has been tested on
and is designed to work with. Uncomment the appropriate elements
and Windows will automatically select the most compatible environment. -->
<!-- Windows 10 -->
<supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
</application>
</compatibility>
</assembly>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because it is too large Load Diff

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,14 @@
{
"runtimeOptions": {
"tfm": "net8.0",
"framework": {
"name": "Microsoft.NETCore.App",
"version": "8.0.0"
},
"configProperties": {
"System.Reflection.NullabilityInfoContext.IsSupported": true,
"System.Runtime.InteropServices.BuiltInComInterop.IsSupported": true,
"System.Runtime.Serialization.EnableUnsafeBinaryFormatterSerialization": false
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show More