From b13a867f8d707ede24b8e4b88a6d862ec2f6ccb1 Mon Sep 17 00:00:00 2001 From: Exest Date: Sat, 8 Aug 2026 10:30:57 +0300 Subject: [PATCH] =?UTF-8?q?Avalonia:=20=D1=80=D0=B0=D0=B7=D0=B4=D0=B5?= =?UTF-8?q?=D0=BB=20=C2=AB=D0=9E=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=C2=BB=20(=D0=BA=D0=BE=D1=80=D0=B7=D0=B8=D0=BD=D0=B0=20+?= =?UTF-8?q?=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5=20=D0=B7?= =?UTF-8?q?=D0=B0=D0=BA=D0=B0=D0=B7=D0=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Отправить: поиск по прайсу → корзина (кол-во через NumericUpDown, сумма/итого) → выбор грузополучателя + комментарий → создание заказа POST /api/buyer/orders - Core: CreateOrderAsync + DTO (BuyerOrderCreateRequest/ItemReq/Response) - Отказы: заглушка с точным пояснением (нужен серверный эндпоинт; RefuseItemsCount живёт только в локальной SQLite WinForms) Итог: 6/7 разделов функциональны (Прайс-лист, Заказы, Накладные, Отчёты, Справочник, Отправить), Отказы — заглушка. Собирается. ⚠️ Реальная отправка заказа (POST) не тестировалась — создаёт заказ на проде; проверять вживую отдельно. Co-Authored-By: Claude Opus 4.8 --- src/Elfisa.Avalonia/README.md | 4 +- .../ViewModels/SendOrderViewModel.cs | 143 ++++++++++++++++++ .../ViewModels/ShellViewModel.cs | 5 +- src/Elfisa.Avalonia/Views/SendOrderView.axaml | 104 +++++++++++++ .../Views/SendOrderView.axaml.cs | 9 ++ src/Elfisa.Core/ApiClient.cs | 21 +++ src/Elfisa.Core/Dtos.cs | 25 +++ 7 files changed, 307 insertions(+), 4 deletions(-) create mode 100644 src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs create mode 100644 src/Elfisa.Avalonia/Views/SendOrderView.axaml create mode 100644 src/Elfisa.Avalonia/Views/SendOrderView.axaml.cs diff --git a/src/Elfisa.Avalonia/README.md b/src/Elfisa.Avalonia/README.md index c09c527..7a84296 100644 --- a/src/Elfisa.Avalonia/README.md +++ b/src/Elfisa.Avalonia/README.md @@ -57,8 +57,8 @@ dotnet publish src/Elfisa.Avalonia -c Release -r win-x64 --self-contained # | ✅ Накладные | реестр приходных документов | готово | | ✅ Отчёты | 4 типа + ИИ → Word/Excel | готово | | ✅ Справочник | профиль + грузополучатели | готово | -| ⏳ Отправить | сборка заявки (корзина) + выгрузка DBF (`orderexport`) | нужен flow создания заказа | -| ⏳ Отказы | список отказных позиций | нужен эндпоинт | +| ✅ Отправить | поиск → корзина → создание заказа (POST /api/buyer/orders) | готов UI; реальная отправка не тестировалась (создаёт заказ на проде) | +| ⏳ Отказы | список отказных позиций | нужен серверный эндпоинт (RefuseItemsCount есть только в локальной БД WinForms) | | ⏳ Автообновление | аналог AutoUpdater под каждую ОС | не начато | **Прайс-лист/Заказы/Накладные** строятся на рабочих эндпоинтах diff --git a/src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs b/src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs new file mode 100644 index 0000000..86852df --- /dev/null +++ b/src/Elfisa.Avalonia/ViewModels/SendOrderViewModel.cs @@ -0,0 +1,143 @@ +using System; +using System.Collections.ObjectModel; +using System.Collections.Specialized; +using System.Linq; +using System.Threading.Tasks; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +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(); + + public ObservableCollection Results { get; } = new(); + public ObservableCollection Cart { get; } = new(); + public ObservableCollection Locations { get; } = new(); + + [ObservableProperty] private string _search = ""; + [ObservableProperty] private PriceItem? _selectedResult; + [ObservableProperty] private BuyerLocationDto? _selectedLocation; + [ObservableProperty] private string _comment = ""; + [ObservableProperty] private bool _busy; + [ObservableProperty] private string? _status; + [ObservableProperty] private decimal _cartTotal; + + public SendOrderViewModel() + { + _api.SetToken(Session.Current.Token); + Cart.CollectionChanged += OnCartChanged; + if (Session.Current.IsAuthenticated) _ = InitAsync(); + } + + private void OnCartChanged(object? s, NotifyCollectionChangedEventArgs e) => Recalc(); + + private async Task InitAsync() + { + try + { + var locs = await _api.GetBuyerLocationsAsync(); + Locations.Clear(); + foreach (var l in locs) Locations.Add(l); + SelectedLocation = Locations.FirstOrDefault(l => l.IsDefault) ?? Locations.FirstOrDefault(); + await SearchAsync(); + } + catch (Exception ex) { Status = ex.Message; } + } + + [RelayCommand] + private async Task SearchAsync() + { + try + { + Busy = true; Status = null; + var resp = await _api.GetPriceSummaryAsync(Search, limit: 200); + Results.Clear(); + foreach (var it in resp.Summary ?? new()) Results.Add(it); + } + catch (Exception ex) { Status = ex.Message; } + finally { Busy = false; } + } + + [RelayCommand] + 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(); + } + + [RelayCommand] + private void RemoveFromCart(CartItem? item) + { + if (item is null) return; + item.Changed -= Recalc; + Cart.Remove(item); + Recalc(); + } + + private void Recalc() => CartTotal = Cart.Sum(c => c.LineSum); + + [RelayCommand] + private async Task SendAsync() + { + Status = null; + if (Cart.Count == 0) { Status = "Корзина пуста."; return; } + try + { + Busy = true; + var req = new BuyerOrderCreateRequest + { + LocationId = SelectedLocation?.BuyerLocationId, + Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(), + Items = Cart.Select(c => new BuyerOrderItemReq + { + SupplierPriceId = c.Source.SupplierPriceId ?? "", + Qty = (double)c.Qty, + ItemName = c.Source.DrugName, + Barcode = c.Source.Barcode + }).ToList() + }; + var resp = await _api.CreateOrderAsync(req); + Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} создан: {resp.ItemsCount} поз., {resp.TotalAmount:N2}."; + Cart.Clear(); + Recalc(); + } + catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; } + finally { Busy = false; } + } +} diff --git a/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs b/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs index 8c00e27..24b8851 100644 --- a/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs +++ b/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs @@ -46,8 +46,9 @@ public partial class ShellViewModel : ViewModelBase new("Заказы", "🛒", () => new OrdersViewModel()), new("Накладные", "📄", () => new InvoicesViewModel()), new("Отчёты", "📊", () => new ReportsViewModel()), - new("Отправить", "📤", () => new PlaceholderPageViewModel("Отправить", "Формирование и выгрузка заявок поставщику (DBF).")), - new("Отказы", "⛔", () => new PlaceholderPageViewModel("Отказы", "Отказные позиции по заказам.")), + new("Отправить", "📤", () => new SendOrderViewModel()), + new("Отказы", "⛔", () => new PlaceholderPageViewModel("Отказы", + "Отказные позиции по заказам. Нужен серверный эндпоинт: данные (RefuseItemsCount) сейчас есть только в локальной БД WinForms-клиента.")), new("Справочник", "📖", () => new DirectoryViewModel()), }; SelectedNav = NavItems.First(n => n.Title == "Отчёты"); diff --git a/src/Elfisa.Avalonia/Views/SendOrderView.axaml b/src/Elfisa.Avalonia/Views/SendOrderView.axaml new file mode 100644 index 0000000..3af0ce7 --- /dev/null +++ b/src/Elfisa.Avalonia/Views/SendOrderView.axaml @@ -0,0 +1,104 @@ + + + + + + + + + + + + + + + + + + + + + +