/
kochkatech
/
Queue
Обзор
Документация
Войти
/
kochkatech
/
Queue
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev
src/Queue.Admin/ViewModels/EmployeesViewModel.cs
304 строки
9 KB
kochkareal
refactor: применить общие стили и добавить поиск/фильтры в панели администратора
27 июл 2026, 18:37
27 июл 2026, 18:37
cd6a08b
Код
Авторство
О чём код?
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; using System.Threading.Tasks; using System.Windows.Data; using System.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Microsoft.EntityFrameworkCore; using Queue.Core.Models; using Queue.Core.Security; using Queue.Data; namespace Queue.Admin.ViewModels; public record RoleOption(EmployeeRole Value, string Display); // Строка грида: сотрудник + прозрачный доступ к его полям (для инлайн-редактирования ФИО/логина/активности) // и подготовленные для отображения строки роли/окна. public class EmployeeRow { public Employee Employee { get; } public EmployeeRow(Employee employee) { Employee = employee; } public string FullName { get => Employee.FullName; set => Employee.FullName = value; } public string Login { get => Employee.Login; set => Employee.Login = value; } public bool IsActive { get => Employee.IsActive; set => Employee.IsActive = value; } public string RoleDisplay => Employee.Role == EmployeeRole.Administrator ? "Администратор" : "Оператор"; public string WindowDisplay { get; init; } = "—"; } public partial class EmployeesViewModel : ObservableObject { private static readonly ServiceWindow NoWindowOption = new() { Id = 0, Name = "Без окна" }; private readonly QueueDbContextFactory _dbContextFactory; public ObservableCollection<EmployeeRow> Employees { get; } = new(); // См. TicketsViewModel.TicketsView — тот же приём для поиска/фильтра по роли. public ICollectionView EmployeesView { get; } // Первый пункт — синтетический "Без окна" (Id = 0), не хранится в БД. public ObservableCollection<ServiceWindow> WindowOptions { get; } = new(); public IReadOnlyList<RoleOption> Roles { get; } = new List<RoleOption> { new(EmployeeRole.Administrator, "Администратор"), new(EmployeeRole.Operator, "Оператор") }; // Первый пункт — "Все роли" (null), сбрасывает фильтр по роли. public IReadOnlyList<RoleOption?> RoleFilterOptions { get; } = new List<RoleOption?> { null, new(EmployeeRole.Administrator, "Администратор"), new(EmployeeRole.Operator, "Оператор") }; [ObservableProperty] private string searchText = string.Empty; [ObservableProperty] private RoleOption? roleFilter; [ObservableProperty] private string newFullName = string.Empty; [ObservableProperty] private string newLogin = string.Empty; [ObservableProperty] private EmployeeRole newRole = EmployeeRole.Operator; [ObservableProperty] private EmployeeRow? selectedEmployee; [ObservableProperty] private EmployeeRole editingRole; [ObservableProperty] private ServiceWindow? editingWindow; [ObservableProperty] private string statusMessage = string.Empty; public EmployeesViewModel(QueueDbContextFactory dbContextFactory) { _dbContextFactory = dbContextFactory; EmployeesView = CollectionViewSource.GetDefaultView(Employees); EmployeesView.Filter = FilterEmployee; _ = LoadAsync(); } partial void OnSearchTextChanged(string value) => EmployeesView.Refresh(); partial void OnRoleFilterChanged(RoleOption? value) => EmployeesView.Refresh(); private bool FilterEmployee(object obj) { var row = (EmployeeRow)obj; if (RoleFilter is not null && row.Employee.Role != RoleFilter.Value) { return false; } if (string.IsNullOrWhiteSpace(SearchText)) { return true; } var text = SearchText.Trim(); return row.FullName.Contains(text, StringComparison.OrdinalIgnoreCase) || row.Login.Contains(text, StringComparison.OrdinalIgnoreCase); } [RelayCommand] private async Task LoadAsync() { using var db = _dbContextFactory.CreateDbContext(); var windows = await db.Windows.OrderBy(w => w.Name).ToListAsync(); WindowOptions.Clear(); WindowOptions.Add(NoWindowOption); foreach (var window in windows) { WindowOptions.Add(window); } var windowNames = windows.ToDictionary(w => w.Id, w => w.Name); var employees = await db.Employees.OrderBy(e => e.FullName).ToListAsync(); Employees.Clear(); foreach (var employee in employees) { Employees.Add(new EmployeeRow(employee) { WindowDisplay = employee.CurrentWindowId.HasValue && windowNames.TryGetValue(employee.CurrentWindowId.Value, out var name) ? name : "—" }); } } // password передаётся из code-behind, так как PasswordBox.Password не поддерживает биндинг. [RelayCommand] private async Task AddAsync(string? password) { StatusMessage = string.Empty; if (string.IsNullOrWhiteSpace(NewFullName) || string.IsNullOrWhiteSpace(NewLogin) || string.IsNullOrWhiteSpace(password)) { return; } var employee = new Employee { FullName = NewFullName.Trim(), Login = NewLogin.Trim(), PasswordHash = PasswordHasher.Hash(password), Role = NewRole, IsActive = true }; try { using var db = _dbContextFactory.CreateDbContext(); db.Employees.Add(employee); await db.SaveChangesAsync(); } catch (DbUpdateException) { // Логин уникален (см. QueueDbContext.OnModelCreating) — самая вероятная причина сбоя // здесь: сотрудник с таким логином уже есть. StatusMessage = "Сотрудник с таким логином уже существует."; return; } NewFullName = string.Empty; NewLogin = string.Empty; NewRole = EmployeeRole.Operator; await LoadAsync(); } // Сохраняет ФИО/логин/активность после редактирования строки в гриде. [RelayCommand] private async Task UpdateAsync(Employee? employee) { StatusMessage = string.Empty; if (employee is null) { return; } try { using var db = _dbContextFactory.CreateDbContext(); db.Attach(employee); db.Entry(employee).Property(e => e.FullName).IsModified = true; db.Entry(employee).Property(e => e.Login).IsModified = true; db.Entry(employee).Property(e => e.IsActive).IsModified = true; await db.SaveChangesAsync(); } catch (DbUpdateException) { StatusMessage = "Сотрудник с таким логином уже существует."; await LoadAsync(); } } [RelayCommand] private async Task DeleteAsync(EmployeeRow? row) { if (row is null) { return; } using (var db = _dbContextFactory.CreateDbContext()) { db.Employees.Attach(row.Employee); db.Employees.Remove(row.Employee); await db.SaveChangesAsync(); } Employees.Remove(row); } partial void OnSelectedEmployeeChanged(EmployeeRow? value) { if (value is null) { return; } EditingRole = value.Employee.Role; EditingWindow = WindowOptions.FirstOrDefault(w => w.Id == (value.Employee.CurrentWindowId ?? 0)) ?? NoWindowOption; } [RelayCommand] private async Task SaveRoleAndWindowAsync() { if (SelectedEmployee is null) { return; } using var db = _dbContextFactory.CreateDbContext(); var employee = await db.Employees.FindAsync(SelectedEmployee.Employee.Id); if (employee is null) { return; } employee.Role = EditingRole; employee.CurrentWindowId = EditingWindow is null || EditingWindow.Id == 0 ? null : EditingWindow.Id; await db.SaveChangesAsync(); await LoadAsync(); } // newPassword передаётся из code-behind (см. AddAsync). [RelayCommand] private async Task UpdatePasswordAsync(string? newPassword) { if (SelectedEmployee is null || string.IsNullOrWhiteSpace(newPassword)) { return; } using var db = _dbContextFactory.CreateDbContext(); var employee = await db.Employees.FindAsync(SelectedEmployee.Employee.Id); if (employee is null) { return; } employee.PasswordHash = PasswordHasher.Hash(newPassword); await db.SaveChangesAsync(); } }