Compare commits

..

2 Commits

Author SHA1 Message Date
Exest
8242184c18 версия 2.0.2 (раздел «Отправить» — отправка накладных)
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 13:47:15 +03:00
Exest
124a891025 Отправить: отправка неотправленных накладных (из Заказы), без поиска/наименований
Раздел переделан: вместо поиска по прайсу и корзины с наименованиями — список
неотправленных накладных (черновиков) и кнопка «Отправить все» (плюс отправка
по одной). Отправка создаёт реальный заказ у поставщика и убирает черновик.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-08-11 13:41:34 +03:00
4 changed files with 159 additions and 153 deletions

View File

@ -3,7 +3,7 @@
#define MyAppName "Электронная Фармация" #define MyAppName "Электронная Фармация"
#define MyAppBrand "ЭльФиСА" #define MyAppBrand "ЭльФиСА"
#define MyAppVersion "2.0.1" #define MyAppVersion "2.0.2"
#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.1</Version> <Version>2.0.2</Version>
<AssemblyVersion>2.0.1.0</AssemblyVersion> <AssemblyVersion>2.0.2.0</AssemblyVersion>
<FileVersion>2.0.1.0</FileVersion> <FileVersion>2.0.2.0</FileVersion>
<Product>ЭльФиСА — Электронная Фармация</Product> <Product>ЭльФиСА — Электронная Фармация</Product>
<Company>PharmData</Company> <Company>PharmData</Company>
<!-- Кросс-платформенная публикация задаётся флагами CLI (RID + self-contained). --> <!-- Кросс-платформенная публикация задаётся флагами CLI (RID + self-contained). -->

View File

@ -9,98 +9,119 @@ using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels; namespace Elfisa.Avalonia.ViewModels;
/// <summary> /// <summary>
/// Отправить: поиск по прайсу → корзина (общая с Прайс-листом) → создание заказа. /// Отправить — отправка НЕОТПРАВЛЕННЫХ накладных (черновиков заказов из раздела
/// ВНИМАНИЕ: отправка создаёт реальный заказ у поставщика. /// «Заказы»). Список готовых накладных, кнопка «Отправить» (создаёт реальный заказ
/// у поставщика). Без поиска по прайсу и наименований — только сами накладные.
/// </summary> /// </summary>
public partial class SendOrderViewModel : ViewModelBase public partial class SendOrderViewModel : ViewModelBase
{ {
private readonly ApiClient _api = new(); private readonly ApiClient _api = new();
private readonly CartService _cart = CartService.Instance;
public ObservableCollection<PriceItem> Results { get; } = new(); /// <summary>Неотправленные накладные (черновики).</summary>
public ObservableCollection<CartItem> Cart => _cart.Items; public ObservableCollection<DraftOrder> Drafts { get; } = new();
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
[ObservableProperty] private string _search = ""; [ObservableProperty] private DraftOrder? _selected;
[ObservableProperty] private PriceItem? _selectedResult;
[ObservableProperty] private BuyerLocationDto? _selectedLocation;
[ObservableProperty] private string _comment = "";
[ObservableProperty] private bool _busy; [ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status; [ObservableProperty] private string? _status;
[ObservableProperty] private decimal _cartTotal; [ObservableProperty] private decimal _totalSum;
[ObservableProperty] private bool _isEmpty = true;
[ObservableProperty] private int _count;
public SendOrderViewModel() public SendOrderViewModel()
{ {
_api.SetToken(Session.Current.Token); _api.SetToken(Session.Current.Token);
_cart.Changed += Recalc; Load();
Recalc();
if (Session.Current.IsAuthenticated) _ = InitAsync();
}
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] [RelayCommand]
private async Task SearchAsync() private void Refresh() => Load();
private void Load()
{ {
try Drafts.Clear();
{ foreach (var d in DraftStore.All.OrderByDescending(x => x.CreatedAt))
Busy = true; Status = null; Drafts.Add(d);
var resp = await _api.GetPriceSummaryAsync(Search, limit: 200); TotalSum = Drafts.Sum(d => d.Total);
Results.Clear(); Count = Drafts.Count;
foreach (var it in resp.Summary ?? new()) Results.Add(it); IsEmpty = Drafts.Count == 0;
} if (Selected is null || !Drafts.Contains(Selected))
catch (Exception ex) { Status = ex.Message; } Selected = Drafts.FirstOrDefault();
finally { Busy = false; }
} }
/// <summary>Отправить одну накладную.</summary>
[RelayCommand] [RelayCommand]
private void AddToCart() private async Task SendAsync(DraftOrder? draft)
{
if (SelectedResult is null) { Status = "Выберите позицию в списке слева."; return; }
_cart.Add(SelectedResult);
}
[RelayCommand]
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
private void Recalc() => CartTotal = _cart.Total;
[RelayCommand]
private async Task SendAsync()
{ {
var d = draft ?? Selected;
if (d is null) { Status = "Выберите накладную для отправки."; return; }
Status = null; Status = null;
if (_cart.Count == 0) { Status = "Корзина пуста."; return; }
try try
{ {
Busy = true; Busy = true;
var req = new BuyerOrderCreateRequest var resp = await CreateFromDraft(d);
{ DraftStore.Remove(d.Id);
LocationId = SelectedLocation?.BuyerLocationId, Status = $"✓ Накладная {resp.GlobalSign ?? resp.OrderId} отправлена ({resp.ItemsCount} поз., {resp.TotalAmount:N2}).";
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
Items = _cart.Items.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();
} }
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; } catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
finally { Busy = false; } finally { Busy = false; Load(); }
}
/// <summary>Отправить все неотправленные накладные.</summary>
[RelayCommand]
private async Task SendAllAsync()
{
if (Drafts.Count == 0) { Status = "Нет неотправленных накладных."; return; }
Status = null;
int ok = 0;
string? lastErr = null;
try
{
Busy = true;
foreach (var d in DraftStore.All.ToList())
{
try
{
await CreateFromDraft(d);
DraftStore.Remove(d.Id);
ok++;
}
catch (Exception ex) { lastErr = ex.Message; }
}
}
finally { Busy = false; Load(); }
Status = lastErr is null
? $"✓ Отправлено накладных: {ok}."
: $"Отправлено: {ok}. Не удалось часть — {lastErr}";
}
/// <summary>Удалить неотправленную накладную (не отправляя).</summary>
[RelayCommand]
private void Delete(DraftOrder? draft)
{
var d = draft ?? Selected;
if (d is null) return;
DraftStore.Remove(d.Id);
Load();
Status = "Накладная удалена.";
}
private async Task<BuyerOrderResponse> CreateFromDraft(DraftOrder d)
{
var req = new BuyerOrderCreateRequest
{
// Номер EX резервировался при сохранении черновика — используем его.
GlobalSign = d.Number.StartsWith("EX-", StringComparison.OrdinalIgnoreCase) ? d.Number : null,
LocationId = d.LocationId,
Comment = d.Comment,
Items = d.Items.Select(i => new BuyerOrderItemReq
{
SupplierPriceId = i.SupplierPriceId,
Qty = (double)i.Qty,
ItemName = i.Name,
Barcode = i.Barcode
}).ToList()
};
return await _api.CreateOrderAsync(req);
} }
} }

View File

@ -1,16 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui" <UserControl xmlns="https://github.com/avaloniaui"
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:core="using:Elfisa.Core"
xmlns:conv="using:Avalonia.Data.Converters" xmlns:conv="using:Avalonia.Data.Converters"
x:Class="Elfisa.Avalonia.Views.SendOrderView" x:Class="Elfisa.Avalonia.Views.SendOrderView"
x:DataType="vm:SendOrderViewModel"> x:DataType="vm:SendOrderViewModel">
<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>
<Button Grid.Column="1" Content="Обновить" VerticalAlignment="Center"
Command="{Binding RefreshCommand}"/>
</Grid> </Grid>
<Border DockPanel.Dock="Bottom" Margin="0,10,0,0" Padding="10,7" CornerRadius="6" <Border DockPanel.Dock="Bottom" Margin="0,10,0,0" Padding="10,7" CornerRadius="6"
@ -19,86 +22,68 @@
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" TextWrapping="Wrap" FontSize="12.5"/> <TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" TextWrapping="Wrap" FontSize="12.5"/>
</Border> </Border>
<Grid ColumnDefinitions="*,16,440"> <!-- Панель отправки: главное действие — отправить все -->
<Border DockPanel.Dock="Top" Classes="card" Padding="16" Margin="0,0,0,12">
<Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<StackPanel Grid.Column="0" Spacing="2" VerticalAlignment="Center">
<TextBlock Foreground="{DynamicResource Text}" FontWeight="SemiBold" FontSize="15"
Text="{Binding Count, StringFormat='Неотправленных накладных: {0}'}"/>
<TextBlock Classes="muted" FontSize="12.5"
Text="{Binding TotalSum, StringFormat='на сумму {0:N2}'}"/>
</StackPanel>
<Button Grid.Column="1" Classes="primary" Padding="26,12" FontSize="15"
Content="Отправить все ▸" Command="{Binding SendAllCommand}"
IsEnabled="{Binding !Busy}" IsVisible="{Binding !IsEmpty}"/>
</Grid>
</Border>
<!-- Прайс: поиск + результаты --> <!-- Список накладных -->
<Border Grid.Column="0" Classes="card" Padding="0" ClipToBounds="True"> <Border Classes="card" Padding="0" ClipToBounds="True">
<DockPanel> <Grid>
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="10" Margin="14"> <!-- Пусто -->
<TextBox Text="{Binding Search}" Width="330" Watermark="поиск по наименованию…"/> <StackPanel IsVisible="{Binding IsEmpty}" VerticalAlignment="Center" HorizontalAlignment="Center"
<Button Classes="primary" Content="Найти" Command="{Binding SearchCommand}" IsEnabled="{Binding !Busy}"/> Spacing="8" Margin="24">
</StackPanel> <TextBlock Text="📭" FontSize="34" HorizontalAlignment="Center"/>
<Button DockPanel.Dock="Bottom" Classes="primary" Content="Добавить в заказ ▸" <TextBlock Text="Нет неотправленных накладных" Classes="h2" HorizontalAlignment="Center"/>
Margin="14" HorizontalAlignment="Left" <TextBlock Classes="muted" HorizontalAlignment="Center" TextAlignment="Center" MaxWidth="360" TextWrapping="Wrap"
Command="{Binding AddToCartCommand}"/> Text="Соберите заказ в разделе «Прайс-лист» и нажмите «Сохранить заказ» — он появится здесь для отправки."/>
<DataGrid ItemsSource="{Binding Results}" SelectedItem="{Binding SelectedResult}" </StackPanel>
x:CompileBindings="False" AutoGenerateColumns="False" IsReadOnly="True"
HeadersVisibility="Column" CanUserResizeColumns="True" CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Наименование" Width="*" Binding="{Binding DrugName}"/>
<DataGridTextColumn Header="Поставщик" Width="120" Binding="{Binding SupplierName}"/>
<DataGridTextColumn Header="Цена" Width="100" Binding="{Binding Price, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Border>
<!-- Корзина --> <!-- Есть накладные -->
<Border Grid.Column="2" Classes="card" Padding="16"> <ScrollViewer IsVisible="{Binding !IsEmpty}">
<DockPanel> <ItemsControl ItemsSource="{Binding Drafts}" x:CompileBindings="False" Margin="0,0,0,14">
<TextBlock DockPanel.Dock="Top" Text="Корзина" Classes="h2" Margin="0,0,0,10"/> <ItemsControl.ItemTemplate>
<DataTemplate x:DataType="core:DraftOrder">
<StackPanel DockPanel.Dock="Bottom" Spacing="10" Margin="0,12,0,0"> <Border Padding="14" Margin="14,14,14,0" CornerRadius="10"
<StackPanel Spacing="5"> Background="{DynamicResource RowAlt}" BorderThickness="1" BorderBrush="{DynamicResource Border}">
<TextBlock Text="Грузополучатель" Classes="muted"/> <Grid ColumnDefinitions="*,Auto" VerticalAlignment="Center">
<ComboBox ItemsSource="{Binding Locations}" SelectedItem="{Binding SelectedLocation}" <StackPanel Grid.Column="0" Spacing="3">
HorizontalAlignment="Stretch"/> <TextBlock Text="{Binding Number}" FontWeight="Bold" FontSize="15" Foreground="{DynamicResource Text}"/>
</StackPanel> <TextBlock Classes="muted" FontSize="12.5"
<TextBox Text="{Binding Comment}" Watermark="комментарий к заказу (необязательно)"/> Text="{Binding CreatedAt, StringFormat='dd.MM.yyyy HH:mm'}"/>
<Grid ColumnDefinitions="*,Auto"> <TextBlock Classes="muted" FontSize="12.5" TextTrimming="CharacterEllipsis"
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" VerticalAlignment="Center" Text="{Binding LocationAddress}"/>
Foreground="{DynamicResource Text}"/> <TextBlock Classes="muted" FontSize="12.5"
<TextBlock Grid.Column="1" Text="{Binding CartTotal, StringFormat='{}{0:N2}'}" Text="{Binding ItemsCount, StringFormat='Позиций: {0}'}"/>
FontWeight="Bold" FontSize="16" Foreground="{DynamicResource Accent}"/> </StackPanel>
</Grid> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
<Button Classes="primary" Content="Отправить заказ" HorizontalAlignment="Stretch" <TextBlock Text="{Binding Total, StringFormat='{}{0:N2}'}" FontWeight="Bold"
Command="{Binding SendCommand}" IsEnabled="{Binding !Busy}"/> FontSize="16" Foreground="{DynamicResource Accent}" VerticalAlignment="Center"/>
</StackPanel> <Button Classes="primary" Content="Отправить"
Command="{Binding $parent[ItemsControl].DataContext.SendCommand}"
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Cart}" x:CompileBindings="False">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:CartItem">
<Border Padding="10" Margin="0,0,0,8" CornerRadius="9"
Background="{DynamicResource RowAlt}" BorderThickness="1" BorderBrush="{DynamicResource Border}">
<DockPanel>
<Button DockPanel.Dock="Right" Content="✕" Width="30" Height="30" Padding="0"
VerticalAlignment="Top" Background="Transparent"
Foreground="{DynamicResource Muted}" Cursor="Hand"
Command="{Binding $parent[ItemsControl].DataContext.RemoveFromCartCommand}"
CommandParameter="{Binding}"/> CommandParameter="{Binding}"/>
<StackPanel Spacing="6"> <Button Content="✕" Width="32" Height="32" Padding="0" VerticalAlignment="Center"
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Text}" Background="Transparent" Foreground="{DynamicResource Muted}" Cursor="Hand"
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"/> Command="{Binding $parent[ItemsControl].DataContext.DeleteCommand}"
<TextBlock Text="{Binding Supplier}" Classes="muted"/> CommandParameter="{Binding}"/>
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center"> </StackPanel>
<NumericUpDown Grid.Column="0" Value="{Binding Qty}" Minimum="1" Maximum="100000" </Grid>
Increment="1" FormatString="0" Width="120"/> </Border>
<TextBlock Grid.Column="1" Text="{Binding Price, StringFormat='× {0:N2}'}" </DataTemplate>
Classes="muted" VerticalAlignment="Center" Margin="10,0"/> </ItemsControl.ItemTemplate>
<TextBlock Grid.Column="2" Text="{Binding LineSum, StringFormat='{}{0:N2}'}" </ItemsControl>
FontWeight="SemiBold" Foreground="{DynamicResource Text}" </ScrollViewer>
VerticalAlignment="Center"/> </Grid>
</Grid> </Border>
</StackPanel>
</DockPanel>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Border>
</Grid>
</DockPanel> </DockPanel>
</UserControl> </UserControl>