diff --git a/src/Elfisa.Avalonia/ViewModels/OrdersViewModel.cs b/src/Elfisa.Avalonia/ViewModels/OrdersViewModel.cs index 8c467c6..ca2320c 100644 --- a/src/Elfisa.Avalonia/ViewModels/OrdersViewModel.cs +++ b/src/Elfisa.Avalonia/ViewModels/OrdersViewModel.cs @@ -15,6 +15,8 @@ public sealed class OrderItemRow public decimal Qty { get; init; } public decimal Price { get; init; } public decimal Sum { get; init; } + public string? SupplierPriceId { get; init; } + public string? Barcode { get; init; } } public sealed class OrderSummary @@ -27,12 +29,16 @@ public sealed class OrderSummary public int ItemsCount { get; init; } public decimal Sum { get; init; } public ObservableCollection Items { get; init; } = new(); + + public bool IsDraft { get; init; } + public string? DraftId { get; init; } + public string? LocationId { get; init; } + public string? Comment { get; init; } } /// -/// Заказы: список + состав. Пока строится из /api/buyer/report (он работает); -/// после деплоя фикса /api/buyer/orders можно перейти на «родной» эндпоинт -/// (даст ещё PlacedAt, Comment, отмену/размещение). +/// Заказы: локальные черновики (статус «Не отправлен») + серверные заказы (из /api/buyer/report). +/// Черновик можно отправить (создать реальный заказ) или удалить. /// public partial class OrdersViewModel : ViewModelBase { @@ -57,11 +63,38 @@ public partial class OrdersViewModel : ViewModelBase try { Busy = true; + Orders.Clear(); + + // 1) Локальные черновики — вверху, статус «Не отправлен» + foreach (var d in DraftStore.All) + { + var items = new ObservableCollection( + d.Items.Select(i => new OrderItemRow + { + Name = i.Name, Qty = i.Qty, Price = i.Price, Sum = i.LineSum, + SupplierPriceId = i.SupplierPriceId, Barcode = i.Barcode + })); + Orders.Add(new OrderSummary + { + Date = d.CreatedAt.ToString("dd.MM.yyyy"), + Number = d.Number, + Supplier = string.Join(", ", d.Items.Select(i => i.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()), + Location = d.LocationAddress ?? "", + Status = "Не отправлен", + ItemsCount = d.ItemsCount, + Sum = d.Total, + Items = items, + IsDraft = true, + DraftId = d.Id, + LocationId = d.LocationId, + Comment = d.Comment + }); + } + + // 2) Серверные заказы var to = DateTime.Today; var from = to.AddMonths(-24); var lines = await _api.GetBuyerReportAsync(from, to); - - Orders.Clear(); foreach (var g in lines.GroupBy(l => l.OrderId) .OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue)) { @@ -69,19 +102,14 @@ public partial class OrdersViewModel : ViewModelBase var items = new ObservableCollection( g.Select(l => new OrderItemRow { - Name = l.ItemName ?? "", - Code = l.ItemCode ?? "", - Qty = l.Qty, - Price = l.UnitPrice, - Sum = l.Sum + Name = l.ItemName ?? "", Code = l.ItemCode ?? "", + Qty = l.Qty, Price = l.UnitPrice, Sum = l.Sum })); - Orders.Add(new OrderSummary { Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "", Number = f.GlobalSign ?? "", - Supplier = string.Join(", ", g.Select(x => x.Supplier) - .Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()), + Supplier = string.Join(", ", g.Select(x => x.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()), Location = f.Location ?? "", Status = f.Status ?? "", ItemsCount = g.Count(), @@ -91,9 +119,46 @@ public partial class OrdersViewModel : ViewModelBase } SelectedOrder = Orders.FirstOrDefault(); - if (Orders.Count == 0) Status = "Заказов за последние 2 года нет."; } catch (Exception ex) { Status = ex.Message; } finally { Busy = false; } } + + [RelayCommand] + private async Task SendDraftAsync() + { + if (SelectedOrder is not { IsDraft: true } d) return; + Status = null; + try + { + Busy = true; + var req = new BuyerOrderCreateRequest + { + LocationId = d.LocationId, + Comment = d.Comment, + Items = d.Items.Select(i => new BuyerOrderItemReq + { + SupplierPriceId = i.SupplierPriceId ?? "", + Qty = (double)i.Qty, + ItemName = i.Name, + Barcode = i.Barcode + }).ToList() + }; + var resp = await _api.CreateOrderAsync(req); + if (d.DraftId is { } id) DraftStore.Remove(id); + Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} отправлен ({resp.ItemsCount} поз., {resp.TotalAmount:N2})."; + await LoadAsync(); + } + catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; } + finally { Busy = false; } + } + + [RelayCommand] + private async Task DeleteDraftAsync() + { + if (SelectedOrder is not { IsDraft: true } d || d.DraftId is not { } id) return; + DraftStore.Remove(id); + await LoadAsync(); + Status = "Черновик удалён."; + } } diff --git a/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs index 74c7538..3dec96f 100644 --- a/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs +++ b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs @@ -1,5 +1,6 @@ using System; using System.Collections.ObjectModel; +using System.Linq; using System.Threading.Tasks; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -7,32 +8,57 @@ using Elfisa.Core; namespace Elfisa.Avalonia.ViewModels; -/// Прайс-лист (сводный прайс) + добавление позиций в заказ (общая корзина). +/// +/// Прайс-лист + сборка заказа: слева каталог, справа «Мой заказ» (общая корзина). +/// «Сохранить заказ» кладёт черновик в «Заказы» (статус «Не отправлен»). +/// public partial class PriceListViewModel : ViewModelBase { private readonly ApiClient _api = new(); private readonly CartService _cart = CartService.Instance; public ObservableCollection Items { 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 BuyerLocationDto? _selectedLocation; + [ObservableProperty] private string _comment = ""; [ObservableProperty] private bool _busy; [ObservableProperty] private string? _status; [ObservableProperty] private int _total; - [ObservableProperty] private string _cartInfo = ""; + [ObservableProperty] private decimal _cartTotal; public PriceListViewModel() { _api.SetToken(Session.Current.Token); - _cart.Changed += UpdateCartInfo; - UpdateCartInfo(); - if (Session.Current.IsAuthenticated) _ = SearchAsync(); + _cart.Changed += Recalc; + Recalc(); + if (Session.Current.IsAuthenticated) + { + _ = SearchAsync(); + _ = LoadLocationsAsync(); + } } - private void UpdateCartInfo() => - CartInfo = _cart.Count == 0 ? "Корзина пуста" : $"В заказе: {_cart.Count} поз. · {_cart.Total:N2}"; + 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() @@ -45,8 +71,6 @@ public partial class PriceListViewModel : ViewModelBase Items.Clear(); foreach (var it in resp.Summary ?? new()) Items.Add(it); Total = resp.TotalDrugs; - Status = $"Показано {Items.Count} из {Total} позиций" - + (string.IsNullOrWhiteSpace(Search) ? "" : $" по запросу «{Search.Trim()}»"); } catch (Exception ex) { Status = ex.Message; } finally { Busy = false; } @@ -56,8 +80,36 @@ public partial class PriceListViewModel : ViewModelBase private void AddToOrder() { if (SelectedItem is null) { Status = "Выберите позицию в списке."; return; } - var q = Qty <= 0 ? 1 : Qty; - _cart.Add(SelectedItem, q); - Status = $"Добавлено в заказ: {SelectedItem.DrugName} × {q:N0}"; + _cart.Add(SelectedItem, Qty <= 0 ? 1 : Qty); + Status = null; + } + + [RelayCommand] + private void RemoveFromCart(CartItem? item) => _cart.Remove(item!); + + [RelayCommand] + private void SaveDraft() + { + if (_cart.Count == 0) { Status = "Заказ пуст — добавьте позиции."; return; } + + var draft = new DraftOrder + { + 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() + }; + var saved = DraftStore.Add(draft); + _cart.Clear(); + Comment = ""; + Status = $"Заказ сохранён как черновик {saved.Number} → раздел «Заказы» (не отправлен)."; } } diff --git a/src/Elfisa.Avalonia/Views/OrdersView.axaml b/src/Elfisa.Avalonia/Views/OrdersView.axaml index 1573992..c2844d9 100644 --- a/src/Elfisa.Avalonia/Views/OrdersView.axaml +++ b/src/Elfisa.Avalonia/Views/OrdersView.axaml @@ -43,14 +43,22 @@ - - - - - - + + + + + + + + + +