Отправить: отправка неотправленных накладных (из Заказы), без поиска/наименований
Раздел переделан: вместо поиска по прайсу и корзины с наименованиями — список неотправленных накладных (черновиков) и кнопка «Отправить все» (плюс отправка по одной). Отправка создаёт реальный заказ у поставщика и убирает черновик. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
f98978a5d2
commit
124a891025
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user