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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Elfisa.Avalonia/Views/SendOrderView.axaml.cs b/src/Elfisa.Avalonia/Views/SendOrderView.axaml.cs
new file mode 100644
index 0000000..c8528af
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/SendOrderView.axaml.cs
@@ -0,0 +1,9 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Elfisa.Avalonia.Views;
+
+public partial class SendOrderView : UserControl
+{
+ public SendOrderView() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/Elfisa.Core/ApiClient.cs b/src/Elfisa.Core/ApiClient.cs
index 443fe5c..5237428 100644
--- a/src/Elfisa.Core/ApiClient.cs
+++ b/src/Elfisa.Core/ApiClient.cs
@@ -108,6 +108,27 @@ public sealed class ApiClient
return JsonSerializer.Deserialize(text, Json) ?? new PriceSummaryResponse();
}
+ /// Создаёт заказ (Placed). ВНИМАНИЕ: создаёт реальный заказ у поставщика.
+ public async Task CreateOrderAsync(BuyerOrderCreateRequest request)
+ {
+ EnsureAuth();
+ if (request.Items.Count == 0) throw new ApiException("Корзина пуста.");
+ var body = JsonSerializer.Serialize(request, Json);
+ using var req = new HttpRequestMessage(HttpMethod.Post, Url("/api/buyer/orders"))
+ {
+ Content = new StringContent(body, Encoding.UTF8, "application/json")
+ };
+ req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", _token);
+ HttpResponseMessage resp;
+ try { resp = await _http.SendAsync(req).ConfigureAwait(false); }
+ catch (Exception ex) { throw new ApiException($"Не удалось отправить заказ: {ex.Message}", ex); }
+
+ var text = await resp.Content.ReadAsStringAsync().ConfigureAwait(false);
+ if (resp.StatusCode == HttpStatusCode.Unauthorized) { _token = null; throw new ApiException("Сессия истекла, войдите снова."); }
+ if (!resp.IsSuccessStatusCode) throw new ApiException(TryError(text) ?? $"Ошибка создания заказа ({(int)resp.StatusCode}).");
+ return JsonSerializer.Deserialize(text, Json) ?? new BuyerOrderResponse();
+ }
+
/// Адреса-грузополучатели покупателя.
public async Task> GetBuyerLocationsAsync()
{
diff --git a/src/Elfisa.Core/Dtos.cs b/src/Elfisa.Core/Dtos.cs
index cb45e78..d09a197 100644
--- a/src/Elfisa.Core/Dtos.cs
+++ b/src/Elfisa.Core/Dtos.cs
@@ -79,4 +79,29 @@ public sealed class BuyerLocationDto
[JsonPropertyName("address")] public string? Address { get; set; }
[JsonPropertyName("region_name")] public string? RegionName { get; set; }
[JsonPropertyName("is_default")] public bool IsDefault { get; set; }
+ public override string ToString() => Address ?? "";
+}
+
+public sealed class BuyerOrderItemReq
+{
+ [JsonPropertyName("supplier_price_id")] public string SupplierPriceId { get; set; } = "";
+ [JsonPropertyName("qty")] public double Qty { get; set; }
+ [JsonPropertyName("item_name")] public string? ItemName { get; set; }
+ [JsonPropertyName("barcode")] public string? Barcode { get; set; }
+}
+
+public sealed class BuyerOrderCreateRequest
+{
+ [JsonPropertyName("location_id")] public string? LocationId { get; set; }
+ [JsonPropertyName("comment")] public string? Comment { get; set; }
+ [JsonPropertyName("items")] public List Items { get; set; } = new();
+}
+
+public sealed class BuyerOrderResponse
+{
+ [JsonPropertyName("order_id")] public string? OrderId { get; set; }
+ [JsonPropertyName("status")] public string? Status { get; set; }
+ [JsonPropertyName("global_sign")] public string? GlobalSign { get; set; }
+ [JsonPropertyName("total_amount")] public decimal TotalAmount { get; set; }
+ [JsonPropertyName("items_count")] public int ItemsCount { get; set; }
}