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 @@ - - + +