/
kochkatech
/
Queue
Обзор
Документация
Войти
/
kochkatech
/
Queue
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
0
CI/CD
Аналитика
dev
src/Queue.OperatorApp/ViewModels/OperatorViewModel.cs
301 строка
9 KB
kochkareal
feat: пауза оператора с блокировкой экрана и повторным входом
27 июл 2026, 19:44
27 июл 2026, 19:44
3b2de85
Код
Авторство
О чём код?
using System.Collections.ObjectModel; using System.Linq; using System.Net.Http; using System.Threading.Tasks; using System.Windows.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; using Queue.OperatorApp.Models; using Queue.OperatorApp.Services; namespace Queue.OperatorApp.ViewModels; public partial class OperatorViewModel : ObservableObject { private readonly QueueApiClient _apiClient; private readonly LoginResult _session; private readonly string _login; // См. Queue.Display.BoardViewModel — тот же приём: обработчик QueueChanged вызывается SignalR // на своём потоке, а RefreshServedTodayAsync/RefreshQueueCountAsync меняют ObservableCollection // (ServedToday) и свойства, привязанные к UI, поэтому нужно вернуться на UI-поток. private readonly Dispatcher _dispatcher; public ObservableCollection<WindowDto> Windows { get; } = new(); public ObservableCollection<ServiceDto> Services { get; } = new(); public ObservableCollection<TicketDto> ServedToday { get; } = new(); // Раньше оператор видел только число ожидающих (QueueCount), но не сам список — не было // понятно, кто следующий и по какой услуге. Заполняется вместе с QueueCount в том же запросе. public ObservableCollection<TicketDto> WaitingQueue { get; } = new(); [ObservableProperty] private string employeeName = string.Empty; [ObservableProperty] private WindowDto? selectedWindow; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StartServiceCommand))] [NotifyCanExecuteChangedFor(nameof(FinishCommand))] [NotifyCanExecuteChangedFor(nameof(NoShowCommand))] [NotifyCanExecuteChangedFor(nameof(CancelCommand))] [NotifyCanExecuteChangedFor(nameof(TransferCommand))] private TicketDto? currentTicket; [ObservableProperty] private int queueCount; [ObservableProperty] private ServiceDto? transferTargetService; [ObservableProperty] private string statusMessage = string.Empty; // Пауза: окно перекрывается блюром/затемнением, продолжить работу можно только повторным // вводом своего пароля — проверяется по-настоящему через /api/auth/login на сервере, а не // локальной сверкой, чтобы разблокировка требовала реального подтверждения личности. [ObservableProperty] private bool isPaused; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(UnlockCommand))] private bool isUnlocking; [ObservableProperty] private string unlockError = string.Empty; public OperatorViewModel(QueueApiClient apiClient, LoginResult session, string login) { _apiClient = apiClient; _session = session; _login = login; _dispatcher = Dispatcher.CurrentDispatcher; EmployeeName = session.FullName; _apiClient.QueueChanged += () => _dispatcher.InvokeAsync(async () => await RefreshQueueCountAsync()); _ = InitializeAsync(); } [RelayCommand] private void Pause() { UnlockError = string.Empty; IsPaused = true; } private bool CanUnlock() => !IsUnlocking; // password передаётся из code-behind — PasswordBox.Password не поддерживает биндинг. [RelayCommand(CanExecute = nameof(CanUnlock))] private async Task UnlockAsync(string? password) { if (string.IsNullOrWhiteSpace(password)) { UnlockError = "Введите пароль."; return; } IsUnlocking = true; try { var result = await _apiClient.LoginAsync(_login, password); if (result is null) { UnlockError = "Неверный пароль."; return; } UnlockError = string.Empty; IsPaused = false; } catch (HttpRequestException ex) { UnlockError = $"Не удалось проверить пароль: {ex.Message}"; } finally { IsUnlocking = false; } } private async Task InitializeAsync() { var windows = await _apiClient.GetWindowsAsync(); Windows.Clear(); foreach (var window in windows) { Windows.Add(window); } var services = await _apiClient.GetServicesAsync(); Services.Clear(); foreach (var service in services) { Services.Add(service); } SelectedWindow = Windows.FirstOrDefault(w => w.Id == _session.CurrentWindowId) ?? Windows.FirstOrDefault(); await RefreshQueueCountAsync(); await RefreshServedTodayAsync(); } // Список окна на панели действий зависит от выбранного окна оператора. partial void OnSelectedWindowChanged(WindowDto? value) { _ = RefreshQueueCountAsync(); } private async Task RefreshQueueCountAsync() { if (SelectedWindow is null) { QueueCount = 0; WaitingQueue.Clear(); return; } var queue = await _apiClient.GetQueueAsync(SelectedWindow.Id); QueueCount = queue.Count; WaitingQueue.Clear(); foreach (var ticket in queue) { WaitingQueue.Add(ticket); } } private async Task RefreshServedTodayAsync() { var served = await _apiClient.GetServedTodayAsync(_session.EmployeeId); ServedToday.Clear(); foreach (var ticket in served) { ServedToday.Add(ticket); } } [RelayCommand] private async Task CallNextAsync() { if (SelectedWindow is null) { StatusMessage = "Выберите окно."; return; } var ticket = await _apiClient.CallNextAsync(SelectedWindow.Id, _session.EmployeeId); if (ticket is null) { StatusMessage = "Очередь пуста."; await RefreshQueueCountAsync(); return; } CurrentTicket = ticket; StatusMessage = string.Empty; await RefreshQueueCountAsync(); } private bool CanActOnCurrent() => CurrentTicket is not null; [RelayCommand(CanExecute = nameof(CanActOnCurrent))] private async Task StartServiceAsync() { if (CurrentTicket is null) { return; } try { CurrentTicket = await _apiClient.StartAsync(CurrentTicket.Id); } catch (HttpRequestException ex) { StatusMessage = $"Не удалось выполнить операцию: {ex.Message}"; } } [RelayCommand(CanExecute = nameof(CanActOnCurrent))] private async Task FinishAsync() { if (CurrentTicket is null) { return; } try { await _apiClient.FinishAsync(CurrentTicket.Id); CurrentTicket = null; await RefreshServedTodayAsync(); } catch (HttpRequestException ex) { StatusMessage = $"Не удалось выполнить операцию: {ex.Message}"; } } [RelayCommand(CanExecute = nameof(CanActOnCurrent))] private async Task NoShowAsync() { if (CurrentTicket is null) { return; } try { await _apiClient.NoShowAsync(CurrentTicket.Id); CurrentTicket = null; await RefreshServedTodayAsync(); } catch (HttpRequestException ex) { StatusMessage = $"Не удалось выполнить операцию: {ex.Message}"; } } [RelayCommand(CanExecute = nameof(CanActOnCurrent))] private async Task CancelAsync() { if (CurrentTicket is null) { return; } try { await _apiClient.CancelAsync(CurrentTicket.Id); CurrentTicket = null; await RefreshQueueCountAsync(); } catch (HttpRequestException ex) { StatusMessage = $"Не удалось выполнить операцию: {ex.Message}"; } } [RelayCommand(CanExecute = nameof(CanActOnCurrent))] private async Task TransferAsync() { if (CurrentTicket is null || TransferTargetService is null) { return; } try { await _apiClient.TransferAsync(CurrentTicket.Id, TransferTargetService.Id); CurrentTicket = null; await RefreshQueueCountAsync(); } catch (HttpRequestException ex) { StatusMessage = $"Не удалось выполнить операцию: {ex.Message}"; } } }