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 @@ + + + + + + + + + + + + + +