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;
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
{
private readonly ApiClient _api = new();
private readonly CartService _cart = CartService.Instance;
// Плоский список предложений + предвычисленные база/МГ (как «Базовое наименование»/«МГ» в 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 string? _selectedBaseName;
[ObservableProperty] private string? _selectedMg;
[ObservableProperty] private OfferRow? _selectedOffer;
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
[ObservableProperty] private string _comment = "";
[ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status;
[ObservableProperty] private int _total;
[ObservableProperty] private decimal _cartTotal;
public PriceListViewModel()
{
_api.SetToken(Session.Current.Token);
_cart.Changed += Recalc;
Recalc();
if (Session.Current.IsAuthenticated)
{
_ = SearchAsync();
_ = LoadLocationsAsync();
}
}
private void Recalc() => CartTotal = _cart.Total;
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 { }
}
[RelayCommand]
private async Task SearchAsync()
{
Status = null;
try
{
Busy = true;
var resp = await _api.GetPriceSummaryAsync(Search, limit: 300);
RebuildIndex(resp.Summary ?? new());
Total = resp.TotalDrugs;
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
/// Пересобирает индекс база/МГ и список базовых наименований.
private void RebuildIndex(List items)
{
_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;
}
[RelayCommand]
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
[RelayCommand]
private async Task SaveDraftAsync()
{
if (_cart.Count == 0) { Status = "Заказ пуст — добавьте позиции."; return; }
try
{
Busy = true;
// Резервируем настоящий номер заказа EX-####### (как в WinForms)
var sign = await _api.NextGlobalSignAsync();
var draft = new DraftOrder
{
Number = string.IsNullOrWhiteSpace(sign) ? $"ЧРН-{DateTime.Now:HHmmss}" : sign!,
LocationId = SelectedLocation?.BuyerLocationId,
LocationAddress = SelectedLocation?.Address,
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
Items = _cart.Items.Select(c => new DraftItem
{
SupplierPriceId = c.Source.SupplierPriceId ?? "",
Name = c.Name,
Supplier = c.Supplier,
Barcode = c.Source.Barcode,
Price = c.Price,
Qty = c.Qty
}).ToList()
};
DraftStore.Add(draft);
_cart.Clear();
Comment = "";
Status = $"Заказ сохранён: {draft.Number} → раздел «Заказы» (не отправлен).";
}
catch (Exception ex) { Status = "Ошибка сохранения: " + ex.Message; }
finally { Busy = false; }
}
}