diff --git a/src/Elfisa.Avalonia/App.axaml.cs b/src/Elfisa.Avalonia/App.axaml.cs index cba5ce7..cecd0c5 100644 --- a/src/Elfisa.Avalonia/App.axaml.cs +++ b/src/Elfisa.Avalonia/App.axaml.cs @@ -18,6 +18,10 @@ public partial class App : Application public override void OnFrameworkInitializationCompleted() { + RequestedThemeVariant = Elfisa.Core.SettingsStore.Current.Theme == "Dark" + ? global::Avalonia.Styling.ThemeVariant.Dark + : global::Avalonia.Styling.ThemeVariant.Light; + if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) { desktop.MainWindow = new MainWindow diff --git a/src/Elfisa.Avalonia/ViewModels/CartService.cs b/src/Elfisa.Avalonia/ViewModels/CartService.cs new file mode 100644 index 0000000..be09fc1 --- /dev/null +++ b/src/Elfisa.Avalonia/ViewModels/CartService.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using CommunityToolkit.Mvvm.ComponentModel; +using Elfisa.Core; + +namespace Elfisa.Avalonia.ViewModels; + +public partial class CartItem : ObservableObject +{ + public PriceItem Source { get; } + public string Name => Source.DrugName ?? ""; + public string Supplier => Source.SupplierName ?? ""; + public decimal Price => Source.Price; + + [ObservableProperty] private decimal _qty = 1; + public decimal LineSum => Price * Qty; + + public event Action? Changed; + + public CartItem(PriceItem src) => Source = src; + + partial void OnQtyChanged(decimal value) + { + OnPropertyChanged(nameof(LineSum)); + Changed?.Invoke(); + } +} + +/// Единая корзина заказа — общая для «Прайс-листа» и «Отправить». +public sealed class CartService +{ + public static CartService Instance { get; } = new(); + + public ObservableCollection Items { get; } = new(); + public event Action? Changed; + + public decimal Total => Items.Sum(c => c.LineSum); + public int Count => Items.Count; + + public void Add(PriceItem src, decimal qty = 1) + { + if (src is null || string.IsNullOrEmpty(src.SupplierPriceId)) return; + var existing = Items.FirstOrDefault(c => c.Source.SupplierPriceId == src.SupplierPriceId); + if (existing != null) existing.Qty += qty; + else + { + var ci = new CartItem(src) { Qty = qty }; + ci.Changed += Raise; + Items.Add(ci); + } + Raise(); + } + + public void Remove(CartItem item) + { + if (item is null) return; + item.Changed -= Raise; + Items.Remove(item); + Raise(); + } + + public void Clear() + { + foreach (var i in Items) i.Changed -= Raise; + Items.Clear(); + Raise(); + } + + private void Raise() => Changed?.Invoke(); +} diff --git a/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs index 24f6e6f..74c7538 100644 --- a/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs +++ b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs @@ -7,24 +7,33 @@ using Elfisa.Core; namespace Elfisa.Avalonia.ViewModels; -/// Прайс-лист (сводный прайс с /api/supplier-prices/summary — как в WinForms). +/// Прайс-лист (сводный прайс) + добавление позиций в заказ (общая корзина). public partial class PriceListViewModel : ViewModelBase { private readonly ApiClient _api = new(); + private readonly CartService _cart = CartService.Instance; public ObservableCollection Items { get; } = new(); [ObservableProperty] private string _search = ""; + [ObservableProperty] private PriceItem? _selectedItem; + [ObservableProperty] private decimal _qty = 1; [ObservableProperty] private bool _busy; [ObservableProperty] private string? _status; [ObservableProperty] private int _total; + [ObservableProperty] private string _cartInfo = ""; public PriceListViewModel() { _api.SetToken(Session.Current.Token); + _cart.Changed += UpdateCartInfo; + UpdateCartInfo(); if (Session.Current.IsAuthenticated) _ = SearchAsync(); } + private void UpdateCartInfo() => + CartInfo = _cart.Count == 0 ? "Корзина пуста" : $"В заказе: {_cart.Count} поз. · {_cart.Total:N2}"; + [RelayCommand] private async Task SearchAsync() { @@ -42,4 +51,13 @@ public partial class PriceListViewModel : ViewModelBase catch (Exception ex) { Status = ex.Message; } finally { Busy = false; } } + + [RelayCommand] + private void AddToOrder() + { + if (SelectedItem is null) { Status = "Выберите позицию в списке."; return; } + var q = Qty <= 0 ? 1 : Qty; + _cart.Add(SelectedItem, q); + Status = $"Добавлено в заказ: {SelectedItem.DrugName} × {q:N0}"; + } } diff --git a/src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs b/src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs index 86852df..bae2be9 100644 --- a/src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs +++ b/src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs @@ -1,6 +1,5 @@ using System; using System.Collections.ObjectModel; -using System.Collections.Specialized; using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; @@ -9,37 +8,17 @@ using Elfisa.Core; namespace Elfisa.Avalonia.ViewModels; -public partial class CartItem : ObservableObject -{ - public PriceItem Source { get; } - public string Name => Source.DrugName ?? ""; - public string Supplier => Source.SupplierName ?? ""; - public decimal Price => Source.Price; - - [ObservableProperty] private decimal _qty = 1; - public decimal LineSum => Price * Qty; - - public event Action? Changed; - - public CartItem(PriceItem src) => Source = src; - - partial void OnQtyChanged(decimal value) - { - OnPropertyChanged(nameof(LineSum)); - Changed?.Invoke(); - } -} - /// -/// Отправить: поиск по прайсу → корзина → создание заказа (POST /api/buyer/orders). +/// Отправить: поиск по прайсу → корзина (общая с Прайс-листом) → создание заказа. /// ВНИМАНИЕ: отправка создаёт реальный заказ у поставщика. /// public partial class SendOrderViewModel : ViewModelBase { private readonly ApiClient _api = new(); + private readonly CartService _cart = CartService.Instance; public ObservableCollection Results { get; } = new(); - public ObservableCollection Cart { get; } = new(); + public ObservableCollection Cart => _cart.Items; public ObservableCollection Locations { get; } = new(); [ObservableProperty] private string _search = ""; @@ -53,12 +32,11 @@ public partial class SendOrderViewModel : ViewModelBase public SendOrderViewModel() { _api.SetToken(Session.Current.Token); - Cart.CollectionChanged += OnCartChanged; + _cart.Changed += Recalc; + Recalc(); if (Session.Current.IsAuthenticated) _ = InitAsync(); } - private void OnCartChanged(object? s, NotifyCollectionChangedEventArgs e) => Recalc(); - private async Task InitAsync() { try @@ -90,33 +68,19 @@ public partial class SendOrderViewModel : ViewModelBase private void AddToCart() { if (SelectedResult is null) { Status = "Выберите позицию в списке слева."; return; } - var existing = Cart.FirstOrDefault(c => c.Source.SupplierPriceId == SelectedResult.SupplierPriceId); - if (existing != null) { existing.Qty += 1; } - else - { - var ci = new CartItem(SelectedResult); - ci.Changed += Recalc; - Cart.Add(ci); - } - Recalc(); + _cart.Add(SelectedResult); } [RelayCommand] - private void RemoveFromCart(CartItem? item) - { - if (item is null) return; - item.Changed -= Recalc; - Cart.Remove(item); - Recalc(); - } + private void RemoveFromCart(CartItem? item) => _cart.Remove(item!); - private void Recalc() => CartTotal = Cart.Sum(c => c.LineSum); + private void Recalc() => CartTotal = _cart.Total; [RelayCommand] private async Task SendAsync() { Status = null; - if (Cart.Count == 0) { Status = "Корзина пуста."; return; } + if (_cart.Count == 0) { Status = "Корзина пуста."; return; } try { Busy = true; @@ -124,7 +88,7 @@ public partial class SendOrderViewModel : ViewModelBase { LocationId = SelectedLocation?.BuyerLocationId, Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(), - Items = Cart.Select(c => new BuyerOrderItemReq + Items = _cart.Items.Select(c => new BuyerOrderItemReq { SupplierPriceId = c.Source.SupplierPriceId ?? "", Qty = (double)c.Qty, @@ -134,8 +98,7 @@ public partial class SendOrderViewModel : ViewModelBase }; var resp = await _api.CreateOrderAsync(req); Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} создан: {resp.ItemsCount} поз., {resp.TotalAmount:N2}."; - Cart.Clear(); - Recalc(); + _cart.Clear(); } catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; } finally { Busy = false; } diff --git a/src/Elfisa.Avalonia/ViewModels/SettingsViewModel.cs b/src/Elfisa.Avalonia/ViewModels/SettingsViewModel.cs new file mode 100644 index 0000000..61db261 --- /dev/null +++ b/src/Elfisa.Avalonia/ViewModels/SettingsViewModel.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.ObjectModel; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using Elfisa.Core; + +namespace Elfisa.Avalonia.ViewModels; + +public partial class SettingsViewModel : ViewModelBase +{ + private readonly ApiClient _api = new(); + + [ObservableProperty] private string _apiBaseUrl = ""; + public string[] Themes { get; } = { "Светлая", "Тёмная" }; + [ObservableProperty] private int _themeIndex; + + public ObservableCollection Locations { get; } = new(); + [ObservableProperty] private BuyerLocationDto? _selectedLocation; + + public SettingsViewModel() + { + _api.SetToken(Session.Current.Token); + ApiBaseUrl = SettingsStore.Current.ApiBaseUrl ?? AppConfig.DefaultApiBaseUrl; + ThemeIndex = SettingsStore.Current.Theme == "Dark" ? 1 : 0; + if (Session.Current.IsAuthenticated) _ = LoadLocationsAsync(); + } + + private async Task LoadLocationsAsync() + { + try + { + var locs = await _api.GetBuyerLocationsAsync(); + Locations.Clear(); + foreach (var l in locs) Locations.Add(l); + var savedId = SettingsStore.Current.DefaultLocationId; + SelectedLocation = Locations.FirstOrDefault(l => l.BuyerLocationId == savedId) + ?? Locations.FirstOrDefault(l => l.IsDefault) + ?? Locations.FirstOrDefault(); + } + catch { /* локации не критичны для настроек */ } + } +} diff --git a/src/Elfisa.Avalonia/Views/PriceListView.axaml b/src/Elfisa.Avalonia/Views/PriceListView.axaml index 6f15e10..b2eaf4c 100644 --- a/src/Elfisa.Avalonia/Views/PriceListView.axaml +++ b/src/Elfisa.Avalonia/Views/PriceListView.axaml @@ -27,8 +27,17 @@ + + + + +