diff --git a/src/Elfisa.Avalonia/README.md b/src/Elfisa.Avalonia/README.md
index d716404..c09c527 100644
--- a/src/Elfisa.Avalonia/README.md
+++ b/src/Elfisa.Avalonia/README.md
@@ -52,14 +52,22 @@ dotnet publish src/Elfisa.Avalonia -c Release -r win-x64 --self-contained #
| Раздел | Что нужно | Сложность |
|---|---|---|
-| Прайс-лист | поиск/каталог, наценки, добавление в заказ, большой грид | высокая |
-| Заказы | список + карточка заказа + позиции | средняя |
-| Накладные | список приходов | средняя |
-| Отправить | сборка заявки + выгрузка DBF (портировать `orderexport`) | средняя |
-| Отказы | список отказных позиций | низкая |
-| Справочник | контрагенты, грузополучатели, настройки | средняя |
-| Автообновление | аналог AutoUpdater под каждую ОС | средняя |
+| ✅ Прайс-лист | поиск + грид (/api/supplier-prices/summary) | готово (без добавления в заказ) |
+| ✅ Заказы | список + карточка заказа с позициями | готово |
+| ✅ Накладные | реестр приходных документов | готово |
+| ✅ Отчёты | 4 типа + ИИ → Word/Excel | готово |
+| ✅ Справочник | профиль + грузополучатели | готово |
+| ⏳ Отправить | сборка заявки (корзина) + выгрузка DBF (`orderexport`) | нужен flow создания заказа |
+| ⏳ Отказы | список отказных позиций | нужен эндпоинт |
+| ⏳ Автообновление | аналог AutoUpdater под каждую ОС | не начато |
-**Порядок рекомендую:** Заказы → Прайс-лист → Отправить → остальное.
-Бизнес-логика каждого — обычный C#, переносится как есть; переписывается
-только UI (WinForms → Avalonia XAML), что и есть основная работа.
+**Прайс-лист/Заказы/Накладные** строятся на рабочих эндпоинтах
+(`supplier-prices/summary`, `buyer/report`). «Родные» `buyer/orders` и
+`buyer/catalog` возвращают 500 (баг квотинга PascalCase в GORM-билдере).
+Фикс для **buyer/orders** готов и застейджен (`es_api_service.exe.new`,
+apply-new-exe.bat) — после деплоя Заказы/Накладные перейдут на него.
+Каталог не используется (берём рабочий summary), его фикс — по желанию.
+
+Осталось: **Отправить** (корзина + создание заказа + DBF) и **Отказы**,
+плюс автообновление и сохранение сессии. Логика переносится из WinForms,
+переписывается только UI (XAML).
diff --git a/src/Elfisa.Avalonia/ViewModels/DirectoryViewModel.cs b/src/Elfisa.Avalonia/ViewModels/DirectoryViewModel.cs
new file mode 100644
index 0000000..7b8550d
--- /dev/null
+++ b/src/Elfisa.Avalonia/ViewModels/DirectoryViewModel.cs
@@ -0,0 +1,48 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Elfisa.Core;
+
+namespace Elfisa.Avalonia.ViewModels;
+
+/// Справочник покупателя: профиль + грузополучатели (адреса).
+public partial class DirectoryViewModel : ViewModelBase
+{
+ private readonly ApiClient _api = new();
+
+ public string UserName => Session.Current.Username ?? "";
+ public string RoleLabel => Session.Current.Role switch
+ {
+ "buyer" => "Покупатель",
+ "supplier" => "Поставщик",
+ "manager" => "Менеджер",
+ "admin" => "Администратор",
+ _ => Session.Current.Role ?? ""
+ };
+
+ public ObservableCollection Locations { get; } = new();
+
+ [ObservableProperty] private bool _busy;
+ [ObservableProperty] private string? _status;
+
+ public DirectoryViewModel()
+ {
+ _api.SetToken(Session.Current.Token);
+ if (Session.Current.IsAuthenticated) _ = LoadAsync();
+ }
+
+ private async Task LoadAsync()
+ {
+ try
+ {
+ Busy = true;
+ var locs = await _api.GetBuyerLocationsAsync();
+ Locations.Clear();
+ foreach (var l in locs) Locations.Add(l);
+ if (Locations.Count == 0) Status = "Грузополучатели не заданы.";
+ }
+ catch (Exception ex) { Status = ex.Message; }
+ finally { Busy = false; }
+ }
+}
diff --git a/src/Elfisa.Avalonia/ViewModels/InvoicesViewModel.cs b/src/Elfisa.Avalonia/ViewModels/InvoicesViewModel.cs
new file mode 100644
index 0000000..0e75c6d
--- /dev/null
+++ b/src/Elfisa.Avalonia/ViewModels/InvoicesViewModel.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Elfisa.Core;
+
+namespace Elfisa.Avalonia.ViewModels;
+
+public sealed class InvoiceRow
+{
+ public string Date { get; init; } = "";
+ public string Number { get; init; } = "";
+ public string Supplier { get; init; } = "";
+ public string Location { get; init; } = "";
+ public int ItemsCount { get; init; }
+ public decimal Sum { get; init; }
+}
+
+///
+/// Накладные — реестр приходных документов. Строится из /api/buyer/report
+/// (после деплоя фикса /api/buyer/orders можно перейти на «родной» источник).
+///
+public partial class InvoicesViewModel : ViewModelBase
+{
+ private readonly ApiClient _api = new();
+
+ public ObservableCollection Rows { get; } = new();
+
+ [ObservableProperty] private bool _busy;
+ [ObservableProperty] private string? _status;
+ [ObservableProperty] private string _totals = "";
+
+ public InvoicesViewModel()
+ {
+ _api.SetToken(Session.Current.Token);
+ if (Session.Current.IsAuthenticated) _ = LoadAsync();
+ }
+
+ private async Task LoadAsync()
+ {
+ try
+ {
+ Busy = true;
+ var to = DateTime.Today;
+ var from = to.AddMonths(-24);
+ var lines = await _api.GetBuyerReportAsync(from, to);
+
+ Rows.Clear();
+ foreach (var g in lines.GroupBy(l => l.OrderId)
+ .OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
+ {
+ var f = g.First();
+ Rows.Add(new InvoiceRow
+ {
+ 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()),
+ Location = f.Location ?? "",
+ ItemsCount = g.Count(),
+ Sum = g.Sum(x => x.Sum)
+ });
+ }
+
+ Totals = $"Накладных: {Rows.Count} Сумма: {Rows.Sum(r => r.Sum):N2}";
+ if (Rows.Count == 0) Status = "Накладных за последние 2 года нет.";
+ }
+ catch (Exception ex) { Status = ex.Message; }
+ finally { Busy = false; }
+ }
+}
diff --git a/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs
new file mode 100644
index 0000000..24f6e6f
--- /dev/null
+++ b/src/Elfisa.Avalonia/ViewModels/PriceListViewModel.cs
@@ -0,0 +1,45 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Threading.Tasks;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+using Elfisa.Core;
+
+namespace Elfisa.Avalonia.ViewModels;
+
+/// Прайс-лист (сводный прайс с /api/supplier-prices/summary — как в WinForms).
+public partial class PriceListViewModel : ViewModelBase
+{
+ private readonly ApiClient _api = new();
+
+ public ObservableCollection Items { get; } = new();
+
+ [ObservableProperty] private string _search = "";
+ [ObservableProperty] private bool _busy;
+ [ObservableProperty] private string? _status;
+ [ObservableProperty] private int _total;
+
+ public PriceListViewModel()
+ {
+ _api.SetToken(Session.Current.Token);
+ if (Session.Current.IsAuthenticated) _ = SearchAsync();
+ }
+
+ [RelayCommand]
+ private async Task SearchAsync()
+ {
+ Status = null;
+ try
+ {
+ Busy = true;
+ var resp = await _api.GetPriceSummaryAsync(Search, limit: 300);
+ 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; }
+ }
+}
diff --git a/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs b/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs
index 8e9212a..8c00e27 100644
--- a/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs
+++ b/src/Elfisa.Avalonia/ViewModels/ShellViewModel.cs
@@ -42,13 +42,13 @@ public partial class ShellViewModel : ViewModelBase
_logout = logout;
NavItems = new ObservableCollection
{
- new("Прайс-лист", "🧾", () => new PlaceholderPageViewModel("Прайс-лист", "Каталог, поиск и добавление позиций в заказ.")),
+ new("Прайс-лист", "🧾", () => new PriceListViewModel()),
new("Заказы", "🛒", () => new OrdersViewModel()),
- new("Накладные", "📄", () => new PlaceholderPageViewModel("Накладные", "Приход товара по заказам.")),
+ new("Накладные", "📄", () => new InvoicesViewModel()),
new("Отчёты", "📊", () => new ReportsViewModel()),
new("Отправить", "📤", () => new PlaceholderPageViewModel("Отправить", "Формирование и выгрузка заявок поставщику (DBF).")),
new("Отказы", "⛔", () => new PlaceholderPageViewModel("Отказы", "Отказные позиции по заказам.")),
- new("Справочник", "📖", () => new PlaceholderPageViewModel("Справочник", "Контрагенты, грузополучатели, настройки.")),
+ new("Справочник", "📖", () => new DirectoryViewModel()),
};
SelectedNav = NavItems.First(n => n.Title == "Отчёты");
}
diff --git a/src/Elfisa.Avalonia/Views/DirectoryView.axaml b/src/Elfisa.Avalonia/Views/DirectoryView.axaml
new file mode 100644
index 0000000..0e2bcac
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/DirectoryView.axaml
@@ -0,0 +1,56 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Elfisa.Avalonia/Views/DirectoryView.axaml.cs b/src/Elfisa.Avalonia/Views/DirectoryView.axaml.cs
new file mode 100644
index 0000000..dae19af
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/DirectoryView.axaml.cs
@@ -0,0 +1,9 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Elfisa.Avalonia.Views;
+
+public partial class DirectoryView : UserControl
+{
+ public DirectoryView() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/Elfisa.Avalonia/Views/InvoicesView.axaml b/src/Elfisa.Avalonia/Views/InvoicesView.axaml
new file mode 100644
index 0000000..496c5a6
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/InvoicesView.axaml
@@ -0,0 +1,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Elfisa.Avalonia/Views/InvoicesView.axaml.cs b/src/Elfisa.Avalonia/Views/InvoicesView.axaml.cs
new file mode 100644
index 0000000..cf8f32e
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/InvoicesView.axaml.cs
@@ -0,0 +1,9 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Elfisa.Avalonia.Views;
+
+public partial class InvoicesView : UserControl
+{
+ public InvoicesView() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/Elfisa.Avalonia/Views/PriceListView.axaml b/src/Elfisa.Avalonia/Views/PriceListView.axaml
new file mode 100644
index 0000000..6f15e10
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/PriceListView.axaml
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Elfisa.Avalonia/Views/PriceListView.axaml.cs b/src/Elfisa.Avalonia/Views/PriceListView.axaml.cs
new file mode 100644
index 0000000..4c25fdc
--- /dev/null
+++ b/src/Elfisa.Avalonia/Views/PriceListView.axaml.cs
@@ -0,0 +1,9 @@
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Elfisa.Avalonia.Views;
+
+public partial class PriceListView : UserControl
+{
+ public PriceListView() => AvaloniaXamlLoader.Load(this);
+}
diff --git a/src/Elfisa.Core/ApiClient.cs b/src/Elfisa.Core/ApiClient.cs
index e0c6299..443fe5c 100644
--- a/src/Elfisa.Core/ApiClient.cs
+++ b/src/Elfisa.Core/ApiClient.cs
@@ -97,6 +97,26 @@ public sealed class ApiClient
return new AiReportResult { Content = bytes, FileName = name.Trim('"') };
}
+ /// Сводный прайс (то же, что грузит WinForms-десктоп). Поиск по q (от 2 символов).
+ public async Task GetPriceSummaryAsync(string? q = null, int limit = 200, int offset = 0)
+ {
+ EnsureAuth();
+ var path = $"/api/supplier-prices/summary?limit={limit}&offset={offset}";
+ if (!string.IsNullOrWhiteSpace(q) && q.Trim().Length >= 2)
+ path += "&q=" + Uri.EscapeDataString(q.Trim());
+ var text = await AuthorizedGetAsync(path, "загрузку прайса").ConfigureAwait(false);
+ return JsonSerializer.Deserialize(text, Json) ?? new PriceSummaryResponse();
+ }
+
+ /// Адреса-грузополучатели покупателя.
+ public async Task> GetBuyerLocationsAsync()
+ {
+ EnsureAuth();
+ var text = await AuthorizedGetAsync("/api/buyer/locations", "загрузку грузополучателей").ConfigureAwait(false);
+ try { return JsonSerializer.Deserialize>(text, Json) ?? new(); }
+ catch { return new(); }
+ }
+
private async Task AuthorizedGetAsync(string path, string op)
{
using var req = new HttpRequestMessage(HttpMethod.Get, Url(path));
diff --git a/src/Elfisa.Core/Dtos.cs b/src/Elfisa.Core/Dtos.cs
index b4383c9..cb45e78 100644
--- a/src/Elfisa.Core/Dtos.cs
+++ b/src/Elfisa.Core/Dtos.cs
@@ -52,3 +52,31 @@ public sealed class ErrorResponse
{
[JsonPropertyName("error")] public string? Error { get; set; }
}
+
+public sealed class PriceSummaryResponse
+{
+ [JsonPropertyName("summary")] public List? Summary { get; set; }
+ [JsonPropertyName("total_drugs")] public int TotalDrugs { get; set; }
+}
+
+public sealed class PriceItem
+{
+ [JsonPropertyName("supplier_price_id")] public string? SupplierPriceId { get; set; }
+ [JsonPropertyName("supplier_name")] public string? SupplierName { get; set; }
+ [JsonPropertyName("drug_name")] public string? DrugName { get; set; }
+ [JsonPropertyName("cure_form")] public string? CureForm { get; set; }
+ [JsonPropertyName("barcode")] public string? Barcode { get; set; }
+ [JsonPropertyName("price")] public decimal Price { get; set; }
+ [JsonPropertyName("quantity")] public decimal Quantity { get; set; }
+ [JsonPropertyName("manufacturer")] public string? Manufacturer { get; set; }
+ [JsonPropertyName("trade_name")] public string? TradeName { get; set; }
+ [JsonPropertyName("es_code")] public long EsCode { get; set; }
+}
+
+public sealed class BuyerLocationDto
+{
+ [JsonPropertyName("buyer_location_id")] public string? BuyerLocationId { get; set; }
+ [JsonPropertyName("address")] public string? Address { get; set; }
+ [JsonPropertyName("region_name")] public string? RegionName { get; set; }
+ [JsonPropertyName("is_default")] public bool IsDefault { get; set; }
+}