2.0.1: «Мои прайсы» в Прайс-листе + подключённые прайсы в «Загрузить»

- Прайс-лист: дропдаун «Мои прайсы» (подключённые прайсы + «Все прайсы»),
  выбор фильтрует каталог по price_list_id; один прайс — выбирается сам
- Загрузить: показывает список подключённых прайсов и качает их
- Core: GetBuyerPriceListsAsync + priceListId в GetPriceSummaryAsync

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Exest 2026-08-10 17:42:53 +03:00
parent 028c588d2a
commit f98978a5d2
8 changed files with 118 additions and 10 deletions

View File

@ -3,7 +3,7 @@
#define MyAppName "Электронная Фармация" #define MyAppName "Электронная Фармация"
#define MyAppBrand "ЭльФиСА" #define MyAppBrand "ЭльФиСА"
#define MyAppVersion "2.0.0" #define MyAppVersion "2.0.1"
#define MyAppPublisher "PharmData" #define MyAppPublisher "PharmData"
#define MyAppURL "https://cdn.24pharmdata.ru" #define MyAppURL "https://cdn.24pharmdata.ru"
#define MyAppSupportURL "https://24pharmdata.ru" #define MyAppSupportURL "https://24pharmdata.ru"

View File

@ -5,9 +5,9 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ApplicationManifest>app.manifest</ApplicationManifest> <ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<Version>2.0.0</Version> <Version>2.0.1</Version>
<AssemblyVersion>2.0.0.0</AssemblyVersion> <AssemblyVersion>2.0.1.0</AssemblyVersion>
<FileVersion>2.0.0.0</FileVersion> <FileVersion>2.0.1.0</FileVersion>
<Product>ЭльФиСА — Электронная Фармация</Product> <Product>ЭльФиСА — Электронная Фармация</Product>
<Company>PharmData</Company> <Company>PharmData</Company>
<!-- Кросс-платформенная публикация задаётся флагами CLI (RID + self-contained). --> <!-- Кросс-платформенная публикация задаётся флагами CLI (RID + self-contained). -->

View File

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
@ -8,15 +9,19 @@ using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels; namespace Elfisa.Avalonia.ViewModels;
/// <summary>«Загрузить» — скачивание актуального прайса с сервера в память.</summary> /// <summary>«Загрузить» — скачивание подключённых прайсов с сервера в память.</summary>
public partial class LoadPriceViewModel : ViewModelBase public partial class LoadPriceViewModel : ViewModelBase
{ {
private readonly ApiClient _api = new(); private readonly ApiClient _api = new();
/// <summary>Прайсы, подключённые покупателю (какие именно скачиваем).</summary>
public ObservableCollection<BuyerPriceListDto> PriceLists { get; } = new();
[ObservableProperty] private bool _busy; [ObservableProperty] private bool _busy;
[ObservableProperty] private double _progress; // 0..100 [ObservableProperty] private double _progress; // 0..100
[ObservableProperty] private string _progressText = ""; [ObservableProperty] private string _progressText = "";
[ObservableProperty] private string? _result; [ObservableProperty] private string? _result;
[ObservableProperty] private string _priceListsInfo = "Загрузка списка прайсов…";
public string LastLoaded => public string LastLoaded =>
PriceCache.LoadedAt is { } dt PriceCache.LoadedAt is { } dt
@ -26,6 +31,23 @@ public partial class LoadPriceViewModel : ViewModelBase
public LoadPriceViewModel() public LoadPriceViewModel()
{ {
_api.SetToken(Session.Current.Token); _api.SetToken(Session.Current.Token);
if (Session.Current.IsAuthenticated)
_ = LoadPriceListsAsync();
}
/// <summary>Тянет список подключённых прайсов — чтобы было видно, что именно скачиваем.</summary>
private async Task LoadPriceListsAsync()
{
try
{
var lists = await _api.GetBuyerPriceListsAsync();
PriceLists.Clear();
foreach (var pl in lists) PriceLists.Add(pl);
PriceListsInfo = lists.Count == 0
? "Прайсы пока не подключены — обратитесь к менеджеру."
: $"Подключено прайсов: {lists.Count}";
}
catch { PriceListsInfo = "Не удалось получить список подключённых прайсов."; }
} }
[RelayCommand] [RelayCommand]

View File

@ -49,8 +49,11 @@ public partial class PriceListViewModel : ViewModelBase
public ObservableCollection<CartItem> Cart => _cart.Items; public ObservableCollection<CartItem> Cart => _cart.Items;
public ObservableCollection<BuyerLocationDto> Locations { get; } = new(); public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
public ObservableCollection<BuyerPriceListDto> MyPriceLists { get; } = new();
[ObservableProperty] private string _search = ""; [ObservableProperty] private string _search = "";
[ObservableProperty] private BuyerPriceListDto? _selectedPriceList;
[ObservableProperty] private bool _hasMultiplePriceLists;
[ObservableProperty] private string? _selectedBaseName; [ObservableProperty] private string? _selectedBaseName;
[ObservableProperty] private string? _selectedMg; [ObservableProperty] private string? _selectedMg;
[ObservableProperty] private OfferRow? _selectedOffer; [ObservableProperty] private OfferRow? _selectedOffer;
@ -68,11 +71,39 @@ public partial class PriceListViewModel : ViewModelBase
Recalc(); Recalc();
if (Session.Current.IsAuthenticated) if (Session.Current.IsAuthenticated)
{ {
_ = SearchAsync(); _ = LoadPriceListsAsync(); // грузит «Мои прайсы» → ставит выбор → запускает поиск
_ = LoadLocationsAsync(); _ = LoadLocationsAsync();
} }
} }
private bool _suppressPriceListSearch;
/// <summary>Загружает подключённые покупателю прайсы в дропдаун «Мои прайсы».</summary>
private async Task LoadPriceListsAsync()
{
List<BuyerPriceListDto> lists;
try { lists = await _api.GetBuyerPriceListsAsync(); }
catch { lists = new(); }
MyPriceLists.Clear();
MyPriceLists.Add(new BuyerPriceListDto { PriceListId = null, Name = "Все прайсы" });
foreach (var pl in lists) MyPriceLists.Add(pl);
HasMultiplePriceLists = lists.Count > 1;
_suppressPriceListSearch = true;
// Один подключён — выбираем его; несколько или ни одного — «Все прайсы».
SelectedPriceList = lists.Count == 1 ? MyPriceLists[1] : MyPriceLists[0];
_suppressPriceListSearch = false;
await SearchAsync();
}
partial void OnSelectedPriceListChanged(BuyerPriceListDto? value)
{
if (_suppressPriceListSearch) return;
_ = SearchAsync();
}
private void Recalc() => CartTotal = _cart.Total; private void Recalc() => CartTotal = _cart.Total;
private async Task LoadLocationsAsync() private async Task LoadLocationsAsync()
@ -97,7 +128,7 @@ public partial class PriceListViewModel : ViewModelBase
try try
{ {
Busy = true; Busy = true;
var resp = await _api.GetPriceSummaryAsync(Search, limit: 300); var resp = await _api.GetPriceSummaryAsync(Search, limit: 300, priceListId: SelectedPriceList?.PriceListId);
RebuildIndex(resp.Summary ?? new()); RebuildIndex(resp.Summary ?? new());
Total = resp.TotalDrugs; Total = resp.TotalDrugs;
} }

View File

@ -2,6 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Elfisa.Avalonia.ViewModels" xmlns:vm="using:Elfisa.Avalonia.ViewModels"
xmlns:conv="using:Avalonia.Data.Converters" xmlns:conv="using:Avalonia.Data.Converters"
xmlns:core="using:Elfisa.Core"
x:Class="Elfisa.Avalonia.Views.LoadPriceView" x:Class="Elfisa.Avalonia.Views.LoadPriceView"
x:DataType="vm:LoadPriceViewModel"> x:DataType="vm:LoadPriceViewModel">
@ -20,6 +21,27 @@
<TextBlock Text="{Binding LastLoaded}" Classes="muted" HorizontalAlignment="Center" TextAlignment="Center"/> <TextBlock Text="{Binding LastLoaded}" Classes="muted" HorizontalAlignment="Center" TextAlignment="Center"/>
<!-- Подключённые прайсы — что именно скачиваем -->
<Border Background="{DynamicResource RowAlt}" CornerRadius="10" Padding="16,12" Width="380"
BorderThickness="1" BorderBrush="{DynamicResource Border}">
<StackPanel Spacing="8">
<TextBlock Text="{Binding PriceListsInfo}" FontWeight="SemiBold" Foreground="{DynamicResource Text}"/>
<ItemsControl ItemsSource="{Binding PriceLists}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="core:BuyerPriceListDto">
<Grid ColumnDefinitions="Auto,*" Margin="0,3">
<TextBlock Grid.Column="0" Text="• " Foreground="{DynamicResource Accent}" FontWeight="Bold"/>
<StackPanel Grid.Column="1" Spacing="1">
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Text}" TextWrapping="Wrap"/>
<TextBlock Text="{Binding SupplierName}" Classes="muted" FontSize="12"/>
</StackPanel>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Border>
<Button Classes="primary" Content="Загрузить прайс" HorizontalAlignment="Center" <Button Classes="primary" Content="Загрузить прайс" HorizontalAlignment="Center"
Padding="28,12" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/> Padding="28,12" Command="{Binding LoadCommand}" IsEnabled="{Binding !Busy}"/>

View File

@ -6,11 +6,20 @@
x:DataType="vm:PriceListViewModel"> x:DataType="vm:PriceListViewModel">
<DockPanel Margin="24"> <DockPanel Margin="24">
<Grid DockPanel.Dock="Top" Margin="0,0,0,14"> <Grid DockPanel.Dock="Top" Margin="0,0,0,14" ColumnDefinitions="*,Auto">
<StackPanel Spacing="2" VerticalAlignment="Center"> <StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
<TextBlock Text="Прайс-лист" Classes="h1"/> <TextBlock Text="Прайс-лист" Classes="h1"/>
<TextBlock Text="Каталог позиций — собери заказ и сохрани" Classes="muted"/> <TextBlock Text="Каталог позиций — собери заказ и сохрани" Classes="muted"/>
</StackPanel> </StackPanel>
<!-- Мои прайсы: видно, по какому прайсу заказываем; если подключено несколько — можно выбрать -->
<Border Grid.Column="1" VerticalAlignment="Center" Padding="12,8" CornerRadius="10"
Background="{DynamicResource Panel}" BorderThickness="1" BorderBrush="{DynamicResource Border}">
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<TextBlock Text="Мои прайсы" Classes="label" VerticalAlignment="Center"/>
<ComboBox ItemsSource="{Binding MyPriceLists}" SelectedItem="{Binding SelectedPriceList}"
MinWidth="240" VerticalAlignment="Center"/>
</StackPanel>
</Border>
</Grid> </Grid>
<Border DockPanel.Dock="Top" Margin="0,0,0,10" Padding="10,7" CornerRadius="6" <Border DockPanel.Dock="Top" Margin="0,0,0,10" Padding="10,7" CornerRadius="6"

View File

@ -98,16 +98,27 @@ public sealed class ApiClient
} }
/// <summary>Сводный прайс (то же, что грузит WinForms-десктоп). Поиск по q (от 2 символов).</summary> /// <summary>Сводный прайс (то же, что грузит WinForms-десктоп). Поиск по q (от 2 символов).</summary>
public async Task<PriceSummaryResponse> GetPriceSummaryAsync(string? q = null, int limit = 200, int offset = 0) public async Task<PriceSummaryResponse> GetPriceSummaryAsync(string? q = null, int limit = 200, int offset = 0, string? priceListId = null)
{ {
EnsureAuth(); EnsureAuth();
var path = $"/api/supplier-prices/summary?limit={limit}&offset={offset}"; var path = $"/api/supplier-prices/summary?limit={limit}&offset={offset}";
if (!string.IsNullOrWhiteSpace(q) && q.Trim().Length >= 2) if (!string.IsNullOrWhiteSpace(q) && q.Trim().Length >= 2)
path += "&q=" + Uri.EscapeDataString(q.Trim()); path += "&q=" + Uri.EscapeDataString(q.Trim());
if (!string.IsNullOrWhiteSpace(priceListId))
path += "&price_list_id=" + Uri.EscapeDataString(priceListId);
var text = await AuthorizedGetAsync(path, "загрузку прайса").ConfigureAwait(false); var text = await AuthorizedGetAsync(path, "загрузку прайса").ConfigureAwait(false);
return JsonSerializer.Deserialize<PriceSummaryResponse>(text, Json) ?? new PriceSummaryResponse(); return JsonSerializer.Deserialize<PriceSummaryResponse>(text, Json) ?? new PriceSummaryResponse();
} }
/// <summary>Прайс-листы, подключённые текущему покупателю (для выбора «Мои прайсы»).</summary>
public async Task<List<BuyerPriceListDto>> GetBuyerPriceListsAsync()
{
EnsureAuth();
var text = await AuthorizedGetAsync("/api/buyer/price-lists", "загрузку прайсов").ConfigureAwait(false);
try { return JsonSerializer.Deserialize<List<BuyerPriceListDto>>(text, Json) ?? new(); }
catch { return new(); }
}
/// <summary>Создаёт заказ (Placed). ВНИМАНИЕ: создаёт реальный заказ у поставщика.</summary> /// <summary>Создаёт заказ (Placed). ВНИМАНИЕ: создаёт реальный заказ у поставщика.</summary>
public async Task<BuyerOrderResponse> CreateOrderAsync(BuyerOrderCreateRequest request) public async Task<BuyerOrderResponse> CreateOrderAsync(BuyerOrderCreateRequest request)
{ {

View File

@ -82,6 +82,19 @@ public sealed class BuyerLocationDto
public override string ToString() => Address ?? ""; public override string ToString() => Address ?? "";
} }
/// <summary>Подключённый покупателю прайс-лист («Мои прайсы»).</summary>
public sealed class BuyerPriceListDto
{
[JsonPropertyName("price_list_id")] public string? PriceListId { get; set; }
[JsonPropertyName("name")] public string? Name { get; set; }
[JsonPropertyName("supplier_name")] public string? SupplierName { get; set; }
[JsonPropertyName("markup_pct")] public double MarkupPct { get; set; }
[JsonPropertyName("default_markup_pct")] public double DefaultMarkupPct { get; set; }
public override string ToString() =>
string.IsNullOrWhiteSpace(SupplierName) ? (Name ?? "Прайс")
: $"{Name} · {SupplierName}";
}
public sealed class BuyerOrderItemReq public sealed class BuyerOrderItemReq
{ {
[JsonPropertyName("supplier_price_id")] public string SupplierPriceId { get; set; } = ""; [JsonPropertyName("supplier_price_id")] public string SupplierPriceId { get; set; } = "";