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 @@
+
+
+
+
+
+
+
+
+
-
diff --git a/src/Elfisa.Avalonia/Views/SettingsWindow.axaml b/src/Elfisa.Avalonia/Views/SettingsWindow.axaml
new file mode 100644
index 0000000..7219608
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/SettingsWindow.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Elfisa.Avalonia/Views/SettingsWindow.axaml.cs b/src/Elfisa.Avalonia/Views/SettingsWindow.axaml.cs
new file mode 100644
index 0000000..4bbf4ba
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/SettingsWindow.axaml.cs
@@ -0,0 +1,31 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Interactivity;
+using Avalonia.Markup.Xaml;
+using Avalonia.Styling;
+using Elfisa.Avalonia.ViewModels;
+using Elfisa.Core;
+
+namespace Elfisa.Avalonia.Views;
+
+public partial class SettingsWindow : Window
+{
+ public SettingsWindow() => AvaloniaXamlLoader.Load(this);
+
+ private void OnSave(object? sender, RoutedEventArgs e)
+ {
+ if (DataContext is SettingsViewModel vm)
+ {
+ SettingsStore.Current.ApiBaseUrl = string.IsNullOrWhiteSpace(vm.ApiBaseUrl) ? null : vm.ApiBaseUrl.Trim().TrimEnd('/');
+ SettingsStore.Current.Theme = vm.ThemeIndex == 1 ? "Dark" : "Light";
+ SettingsStore.Current.DefaultLocationId = vm.SelectedLocation?.BuyerLocationId;
+ SettingsStore.Save();
+
+ if (Application.Current is { } app)
+ app.RequestedThemeVariant = vm.ThemeIndex == 1 ? ThemeVariant.Dark : ThemeVariant.Light;
+ }
+ Close();
+ }
+
+ private void OnCancel(object? sender, RoutedEventArgs e) => Close();
+}
diff --git a/src/Elfisa.Avalonia/Views/ShellView.axaml b/src/Elfisa.Avalonia/Views/ShellView.axaml
index fd7cf34..5fffb24 100644
--- a/src/Elfisa.Avalonia/Views/ShellView.axaml
+++ b/src/Elfisa.Avalonia/Views/ShellView.axaml
@@ -44,6 +44,10 @@
+
diff --git a/src/Elfisa.Avalonia/Views/ShellView.axaml.cs b/src/Elfisa.Avalonia/Views/ShellView.axaml.cs
index 9797272..5aa2e3f 100644
--- a/src/Elfisa.Avalonia/Views/ShellView.axaml.cs
+++ b/src/Elfisa.Avalonia/Views/ShellView.axaml.cs
@@ -1,9 +1,21 @@
+using Avalonia;
using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
+using Elfisa.Avalonia.ViewModels;
namespace Elfisa.Avalonia.Views;
public partial class ShellView : UserControl
{
public ShellView() => AvaloniaXamlLoader.Load(this);
+
+ private async void OnSettingsClick(object? sender, RoutedEventArgs e)
+ {
+ var win = new SettingsWindow { DataContext = new SettingsViewModel() };
+ var owner = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow;
+ if (owner is not null) await win.ShowDialog(owner);
+ else win.Show();
+ }
}
diff --git a/src/Elfisa.Core/AppConfig.cs b/src/Elfisa.Core/AppConfig.cs
index 4c2c73a..ca8302e 100644
--- a/src/Elfisa.Core/AppConfig.cs
+++ b/src/Elfisa.Core/AppConfig.cs
@@ -17,6 +17,8 @@ public static class AppConfig
{
get
{
+ var fromSettings = SettingsStore.Current.ApiBaseUrl;
+ if (!string.IsNullOrWhiteSpace(fromSettings)) return fromSettings.Trim().TrimEnd('/');
var env = Environment.GetEnvironmentVariable(ApiEnv);
return string.IsNullOrWhiteSpace(env) ? DefaultApiBaseUrl : env.Trim().TrimEnd('/');
}
diff --git a/src/Elfisa.Core/SettingsStore.cs b/src/Elfisa.Core/SettingsStore.cs
new file mode 100644
index 0000000..c0a1364
--- /dev/null
+++ b/src/Elfisa.Core/SettingsStore.cs
@@ -0,0 +1,44 @@
+using System.Text.Json;
+
+namespace Elfisa.Core;
+
+public sealed class AppSettings
+{
+ public string? ApiBaseUrl { get; set; }
+ public string Theme { get; set; } = "Light"; // Light | Dark
+ public string? DefaultLocationId { get; set; }
+ public string? Token { get; set; } // сохранённая сессия
+ public string? Role { get; set; }
+ public string? Username { get; set; }
+}
+
+/// Настройки приложения в %APPDATA%\ElfisaAvalonia\settings.json.
+public static class SettingsStore
+{
+ private static readonly string Dir =
+ Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ElfisaAvalonia");
+ private static readonly string File = Path.Combine(Dir, "settings.json");
+
+ public static AppSettings Current { get; private set; } = Load();
+
+ public static AppSettings Load()
+ {
+ try
+ {
+ if (System.IO.File.Exists(File))
+ return JsonSerializer.Deserialize(System.IO.File.ReadAllText(File)) ?? new AppSettings();
+ }
+ catch { /* повреждённый файл — начинаем с чистых настроек */ }
+ return new AppSettings();
+ }
+
+ public static void Save()
+ {
+ try
+ {
+ Directory.CreateDirectory(Dir);
+ System.IO.File.WriteAllText(File, JsonSerializer.Serialize(Current, new JsonSerializerOptions { WriteIndented = true }));
+ }
+ catch { /* нет прав/диска — молча пропускаем */ }
+ }
+}