diff --git a/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs
index 7c14ea7..9f7fe96 100644
--- a/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs
+++ b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs
@@ -1,6 +1,8 @@
using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
@@ -8,8 +10,23 @@ using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
+/// Строка предложения с редактируемым количеством «Заказ» (как колонка «Заказ» в WinForms).
+public partial class OfferRow : ObservableObject
+{
+ public PriceItem Source { get; }
+ public string? DrugName => Source.DrugName;
+ public string? SupplierName => Source.SupplierName;
+ public decimal Price => Source.Price;
+ public decimal Quantity => Source.Quantity;
+
+ [ObservableProperty] private decimal _orderQty = 1;
+
+ public OfferRow(PriceItem src) => Source = src;
+}
+
///
-/// Прайс-лист + сборка заказа: слева каталог, справа «Мой заказ» (общая корзина).
+/// Прайс-лист + сборка заказа. Каталог — как в старой программе:
+/// база (название) → дозировка (МГ) → предложения поставщиков. Справа «Мой заказ».
/// «Сохранить заказ» кладёт черновик в «Заказы» (статус «Не отправлен»).
///
public partial class PriceListViewModel : ViewModelBase
@@ -17,13 +34,26 @@ public partial class PriceListViewModel : ViewModelBase
private readonly ApiClient _api = new();
private readonly CartService _cart = CartService.Instance;
- public ObservableCollection Items { get; } = new();
+ // Плоский список предложений + предвычисленные база/МГ (как «Базовое наименование»/«МГ» в WinForms).
+ private readonly List<(PriceItem Item, string BaseName, string Mg)> _indexed = new();
+
+ private const string EmptyMgDisplay = "(без МГ)";
+ private static readonly Regex DosageRegex = new(
+ @"(\d+(?:[.,]\d+)?\s*(?:мг|мкг|г|мл|МЕ|ме)\b)",
+ RegexOptions.IgnoreCase | RegexOptions.Compiled);
+
+ // Три панели дрилдауна
+ public ObservableCollection BaseNames { get; } = new();
+ public ObservableCollection MgOptions { get; } = new();
+ public ObservableCollection Offers { get; } = new();
+
public ObservableCollection Cart => _cart.Items;
public ObservableCollection Locations { get; } = new();
[ObservableProperty] private string _search = "";
- [ObservableProperty] private PriceItem? _selectedItem;
- [ObservableProperty] private decimal _qty = 1;
+ [ObservableProperty] private string? _selectedBaseName;
+ [ObservableProperty] private string? _selectedMg;
+ [ObservableProperty] private OfferRow? _selectedOffer;
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
[ObservableProperty] private string _comment = "";
[ObservableProperty] private bool _busy;
@@ -68,19 +98,115 @@ public partial class PriceListViewModel : ViewModelBase
{
Busy = true;
var resp = await _api.GetPriceSummaryAsync(Search, limit: 300);
- Items.Clear();
- foreach (var it in resp.Summary ?? new()) Items.Add(it);
+ RebuildIndex(resp.Summary ?? new());
Total = resp.TotalDrugs;
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
- [RelayCommand]
- private void AddToOrder()
+ /// Пересобирает индекс база/МГ и список базовых наименований.
+ private void RebuildIndex(List items)
{
- if (SelectedItem is null) { Status = "Выберите позицию в списке."; return; }
- _cart.Add(SelectedItem, Qty <= 0 ? 1 : Qty);
+ _indexed.Clear();
+ foreach (var it in items)
+ {
+ var mg = ExtractDosage(it.DrugName);
+ var baseName = BuildBaseName(it.DrugName, it.TradeName, mg);
+ if (string.IsNullOrWhiteSpace(baseName)) continue;
+ _indexed.Add((it, baseName, mg));
+ }
+
+ // Сброс выбора и панелей
+ SelectedBaseName = null;
+ MgOptions.Clear();
+ Offers.Clear();
+ SelectedOffer = null;
+
+ var names = _indexed
+ .Select(x => x.BaseName)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(n => n, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ BaseNames.Clear();
+ foreach (var n in names) BaseNames.Add(n);
+ }
+
+ // База выбрана → заполняем список дозировок.
+ partial void OnSelectedBaseNameChanged(string? value)
+ {
+ MgOptions.Clear();
+ Offers.Clear();
+ SelectedOffer = null;
+ SelectedMg = null;
+ if (string.IsNullOrEmpty(value)) return;
+
+ var mgs = _indexed
+ .Where(x => string.Equals(x.BaseName, value, StringComparison.OrdinalIgnoreCase))
+ .Select(x => string.IsNullOrEmpty(x.Mg) ? EmptyMgDisplay : x.Mg)
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .OrderBy(m => m, StringComparer.OrdinalIgnoreCase)
+ .ToList();
+
+ foreach (var m in mgs) MgOptions.Add(m);
+
+ // Если дозировка одна — сразу её выбираем (меньше кликов).
+ if (MgOptions.Count == 1) SelectedMg = MgOptions[0];
+ }
+
+ // Дозировка выбрана → показываем предложения (фильтр база + МГ).
+ partial void OnSelectedMgChanged(string? value)
+ {
+ Offers.Clear();
+ SelectedOffer = null;
+ if (string.IsNullOrEmpty(SelectedBaseName) || string.IsNullOrEmpty(value)) return;
+
+ var wantMg = string.Equals(value, EmptyMgDisplay, StringComparison.Ordinal) ? "" : value;
+
+ var offers = _indexed
+ .Where(x => string.Equals(x.BaseName, SelectedBaseName, StringComparison.OrdinalIgnoreCase)
+ && string.Equals(x.Mg, wantMg, StringComparison.OrdinalIgnoreCase))
+ .OrderBy(x => x.Item.Price)
+ .Select(x => new OfferRow(x.Item))
+ .ToList();
+
+ foreach (var o in offers) Offers.Add(o);
+ SelectedOffer = Offers.FirstOrDefault();
+ }
+
+ private static string ExtractDosage(string? drugName)
+ {
+ if (string.IsNullOrWhiteSpace(drugName)) return string.Empty;
+ var m = DosageRegex.Match(drugName);
+ return m.Success ? m.Value.Trim() : string.Empty;
+ }
+
+ private static string BuildBaseName(string? drugName, string? tradeName, string dosage)
+ {
+ if (!string.IsNullOrWhiteSpace(tradeName)) return tradeName.Trim();
+
+ var name = drugName ?? string.Empty;
+ if (!string.IsNullOrWhiteSpace(dosage))
+ {
+ var idx = name.IndexOf(dosage, StringComparison.OrdinalIgnoreCase);
+ if (idx >= 0) name = name.Remove(idx, dosage.Length);
+ }
+
+ name = Regex.Replace(name, @"\s{2,}", " ").Trim(' ', ',', '-', '.', ';');
+ return string.IsNullOrWhiteSpace(name) ? (drugName ?? string.Empty).Trim() : name;
+ }
+
+ [RelayCommand]
+ private void AddToOrder() => AddSelectedOffer();
+
+ /// Добавляет выбранное предложение в заказ с его количеством (Enter в таблице / кнопка).
+ public void AddSelectedOffer()
+ {
+ if (SelectedOffer is null) { Status = "Выберите предложение в списке."; return; }
+ var q = SelectedOffer.OrderQty <= 0 ? 1 : SelectedOffer.OrderQty;
+ _cart.Add(SelectedOffer.Source, q);
+ SelectedOffer.OrderQty = 1;
Status = null;
}
diff --git a/src/Elfisa.Avalonia/Views/PriceListView.axaml b/src/Elfisa.Avalonia/Views/PriceListView.axaml
index 35a0ce2..133f9dd 100644
--- a/src/Elfisa.Avalonia/Views/PriceListView.axaml
+++ b/src/Elfisa.Avalonia/Views/PriceListView.axaml
@@ -30,20 +30,48 @@
-
-
+
+
+
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Elfisa.Avalonia/Views/PriceListView.axaml.cs b/src/Elfisa.Avalonia/Views/PriceListView.axaml.cs
index 4c25fdc..4a06044 100644
--- a/src/Elfisa.Avalonia/Views/PriceListView.axaml.cs
+++ b/src/Elfisa.Avalonia/Views/PriceListView.axaml.cs
@@ -1,9 +1,58 @@
+using System;
using Avalonia.Controls;
+using Avalonia.Input;
using Avalonia.Markup.Xaml;
+using Elfisa.Avalonia.ViewModels;
namespace Elfisa.Avalonia.Views;
public partial class PriceListView : UserControl
{
- public PriceListView() => AvaloniaXamlLoader.Load(this);
+ // Строка, в которую сейчас набирают количество с клавиатуры (для многозначного ввода).
+ private OfferRow? _typingRow;
+
+ public PriceListView()
+ {
+ AvaloniaXamlLoader.Load(this);
+ var grid = this.FindControl("OffersGrid");
+ if (grid != null) grid.KeyDown += OffersGrid_KeyDown;
+ }
+
+ // Выделил предложение → набираешь число цифрами (как колонка «Заказ» в старой программе).
+ // Backspace — стереть цифру, Enter — добавить в заказ.
+ private void OffersGrid_KeyDown(object? sender, KeyEventArgs e)
+ {
+ if (sender is not DataGrid grid || grid.SelectedItem is not OfferRow row) return;
+
+ var digit = DigitFromKey(e.Key);
+ if (digit >= 0)
+ {
+ if (!ReferenceEquals(_typingRow, row)) { row.OrderQty = 0; _typingRow = row; }
+ var next = row.OrderQty * 10 + digit;
+ if (next <= 100000) row.OrderQty = next;
+ e.Handled = true;
+ return;
+ }
+
+ switch (e.Key)
+ {
+ case Key.Back:
+ row.OrderQty = Math.Floor(row.OrderQty / 10);
+ _typingRow = row;
+ e.Handled = true;
+ break;
+ case Key.Enter:
+ (DataContext as PriceListViewModel)?.AddSelectedOffer();
+ _typingRow = null;
+ e.Handled = true;
+ break;
+ }
+ }
+
+ private static int DigitFromKey(Key k)
+ {
+ if (k >= Key.D0 && k <= Key.D9) return k - Key.D0;
+ if (k >= Key.NumPad0 && k <= Key.NumPad9) return k - Key.NumPad0;
+ return -1;
+ }
}