������� �� Avalonia 2.0 (�����-���������, ����������, �������� ��������) #1
@ -15,6 +15,8 @@ public sealed class OrderItemRow
|
|||||||
public decimal Qty { get; init; }
|
public decimal Qty { get; init; }
|
||||||
public decimal Price { get; init; }
|
public decimal Price { get; init; }
|
||||||
public decimal Sum { get; init; }
|
public decimal Sum { get; init; }
|
||||||
|
public string? SupplierPriceId { get; init; }
|
||||||
|
public string? Barcode { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class OrderSummary
|
public sealed class OrderSummary
|
||||||
@ -27,12 +29,16 @@ public sealed class OrderSummary
|
|||||||
public int ItemsCount { get; init; }
|
public int ItemsCount { get; init; }
|
||||||
public decimal Sum { get; init; }
|
public decimal Sum { get; init; }
|
||||||
public ObservableCollection<OrderItemRow> Items { get; init; } = new();
|
public ObservableCollection<OrderItemRow> Items { get; init; } = new();
|
||||||
|
|
||||||
|
public bool IsDraft { get; init; }
|
||||||
|
public string? DraftId { get; init; }
|
||||||
|
public string? LocationId { get; init; }
|
||||||
|
public string? Comment { get; init; }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Заказы: список + состав. Пока строится из /api/buyer/report (он работает);
|
/// Заказы: локальные черновики (статус «Не отправлен») + серверные заказы (из /api/buyer/report).
|
||||||
/// после деплоя фикса /api/buyer/orders можно перейти на «родной» эндпоинт
|
/// Черновик можно отправить (создать реальный заказ) или удалить.
|
||||||
/// (даст ещё PlacedAt, Comment, отмену/размещение).
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class OrdersViewModel : ViewModelBase
|
public partial class OrdersViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
@ -57,11 +63,38 @@ public partial class OrdersViewModel : ViewModelBase
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
Busy = true;
|
Busy = true;
|
||||||
|
Orders.Clear();
|
||||||
|
|
||||||
|
// 1) Локальные черновики — вверху, статус «Не отправлен»
|
||||||
|
foreach (var d in DraftStore.All)
|
||||||
|
{
|
||||||
|
var items = new ObservableCollection<OrderItemRow>(
|
||||||
|
d.Items.Select(i => new OrderItemRow
|
||||||
|
{
|
||||||
|
Name = i.Name, Qty = i.Qty, Price = i.Price, Sum = i.LineSum,
|
||||||
|
SupplierPriceId = i.SupplierPriceId, Barcode = i.Barcode
|
||||||
|
}));
|
||||||
|
Orders.Add(new OrderSummary
|
||||||
|
{
|
||||||
|
Date = d.CreatedAt.ToString("dd.MM.yyyy"),
|
||||||
|
Number = d.Number,
|
||||||
|
Supplier = string.Join(", ", d.Items.Select(i => i.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
|
||||||
|
Location = d.LocationAddress ?? "",
|
||||||
|
Status = "Не отправлен",
|
||||||
|
ItemsCount = d.ItemsCount,
|
||||||
|
Sum = d.Total,
|
||||||
|
Items = items,
|
||||||
|
IsDraft = true,
|
||||||
|
DraftId = d.Id,
|
||||||
|
LocationId = d.LocationId,
|
||||||
|
Comment = d.Comment
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Серверные заказы
|
||||||
var to = DateTime.Today;
|
var to = DateTime.Today;
|
||||||
var from = to.AddMonths(-24);
|
var from = to.AddMonths(-24);
|
||||||
var lines = await _api.GetBuyerReportAsync(from, to);
|
var lines = await _api.GetBuyerReportAsync(from, to);
|
||||||
|
|
||||||
Orders.Clear();
|
|
||||||
foreach (var g in lines.GroupBy(l => l.OrderId)
|
foreach (var g in lines.GroupBy(l => l.OrderId)
|
||||||
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
|
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
|
||||||
{
|
{
|
||||||
@ -69,19 +102,14 @@ public partial class OrdersViewModel : ViewModelBase
|
|||||||
var items = new ObservableCollection<OrderItemRow>(
|
var items = new ObservableCollection<OrderItemRow>(
|
||||||
g.Select(l => new OrderItemRow
|
g.Select(l => new OrderItemRow
|
||||||
{
|
{
|
||||||
Name = l.ItemName ?? "",
|
Name = l.ItemName ?? "", Code = l.ItemCode ?? "",
|
||||||
Code = l.ItemCode ?? "",
|
Qty = l.Qty, Price = l.UnitPrice, Sum = l.Sum
|
||||||
Qty = l.Qty,
|
|
||||||
Price = l.UnitPrice,
|
|
||||||
Sum = l.Sum
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
Orders.Add(new OrderSummary
|
Orders.Add(new OrderSummary
|
||||||
{
|
{
|
||||||
Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
|
Date = f.OrderDate?.ToLocalTime().ToString("dd.MM.yyyy") ?? "",
|
||||||
Number = f.GlobalSign ?? "",
|
Number = f.GlobalSign ?? "",
|
||||||
Supplier = string.Join(", ", g.Select(x => x.Supplier)
|
Supplier = string.Join(", ", g.Select(x => x.Supplier).Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
|
||||||
.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct()),
|
|
||||||
Location = f.Location ?? "",
|
Location = f.Location ?? "",
|
||||||
Status = f.Status ?? "",
|
Status = f.Status ?? "",
|
||||||
ItemsCount = g.Count(),
|
ItemsCount = g.Count(),
|
||||||
@ -91,9 +119,46 @@ public partial class OrdersViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
SelectedOrder = Orders.FirstOrDefault();
|
SelectedOrder = Orders.FirstOrDefault();
|
||||||
if (Orders.Count == 0) Status = "Заказов за последние 2 года нет.";
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
catch (Exception ex) { Status = ex.Message; }
|
||||||
finally { Busy = false; }
|
finally { Busy = false; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task SendDraftAsync()
|
||||||
|
{
|
||||||
|
if (SelectedOrder is not { IsDraft: true } d) return;
|
||||||
|
Status = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Busy = true;
|
||||||
|
var req = new BuyerOrderCreateRequest
|
||||||
|
{
|
||||||
|
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()
|
||||||
|
};
|
||||||
|
var resp = await _api.CreateOrderAsync(req);
|
||||||
|
if (d.DraftId is { } id) DraftStore.Remove(id);
|
||||||
|
Status = $"✓ Заказ {resp.GlobalSign ?? resp.OrderId} отправлен ({resp.ItemsCount} поз., {resp.TotalAmount:N2}).";
|
||||||
|
await LoadAsync();
|
||||||
|
}
|
||||||
|
catch (Exception ex) { Status = "Ошибка отправки: " + ex.Message; }
|
||||||
|
finally { Busy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task DeleteDraftAsync()
|
||||||
|
{
|
||||||
|
if (SelectedOrder is not { IsDraft: true } d || d.DraftId is not { } id) return;
|
||||||
|
DraftStore.Remove(id);
|
||||||
|
await LoadAsync();
|
||||||
|
Status = "Черновик удалён.";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
@ -7,32 +8,57 @@ using Elfisa.Core;
|
|||||||
|
|
||||||
namespace Elfisa.Avalonia.ViewModels;
|
namespace Elfisa.Avalonia.ViewModels;
|
||||||
|
|
||||||
/// <summary>Прайс-лист (сводный прайс) + добавление позиций в заказ (общая корзина).</summary>
|
/// <summary>
|
||||||
|
/// Прайс-лист + сборка заказа: слева каталог, справа «Мой заказ» (общая корзина).
|
||||||
|
/// «Сохранить заказ» кладёт черновик в «Заказы» (статус «Не отправлен»).
|
||||||
|
/// </summary>
|
||||||
public partial class PriceListViewModel : ViewModelBase
|
public partial class PriceListViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
private readonly ApiClient _api = new();
|
private readonly ApiClient _api = new();
|
||||||
private readonly CartService _cart = CartService.Instance;
|
private readonly CartService _cart = CartService.Instance;
|
||||||
|
|
||||||
public ObservableCollection<PriceItem> Items { get; } = new();
|
public ObservableCollection<PriceItem> Items { get; } = new();
|
||||||
|
public ObservableCollection<CartItem> Cart => _cart.Items;
|
||||||
|
public ObservableCollection<BuyerLocationDto> Locations { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private string _search = "";
|
[ObservableProperty] private string _search = "";
|
||||||
[ObservableProperty] private PriceItem? _selectedItem;
|
[ObservableProperty] private PriceItem? _selectedItem;
|
||||||
[ObservableProperty] private decimal _qty = 1;
|
[ObservableProperty] private decimal _qty = 1;
|
||||||
|
[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 int _total;
|
[ObservableProperty] private int _total;
|
||||||
[ObservableProperty] private string _cartInfo = "";
|
[ObservableProperty] private decimal _cartTotal;
|
||||||
|
|
||||||
public PriceListViewModel()
|
public PriceListViewModel()
|
||||||
{
|
{
|
||||||
_api.SetToken(Session.Current.Token);
|
_api.SetToken(Session.Current.Token);
|
||||||
_cart.Changed += UpdateCartInfo;
|
_cart.Changed += Recalc;
|
||||||
UpdateCartInfo();
|
Recalc();
|
||||||
if (Session.Current.IsAuthenticated) _ = SearchAsync();
|
if (Session.Current.IsAuthenticated)
|
||||||
|
{
|
||||||
|
_ = SearchAsync();
|
||||||
|
_ = LoadLocationsAsync();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateCartInfo() =>
|
private void Recalc() => CartTotal = _cart.Total;
|
||||||
CartInfo = _cart.Count == 0 ? "Корзина пуста" : $"В заказе: {_cart.Count} поз. · {_cart.Total:N2}";
|
|
||||||
|
private async Task LoadLocationsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var locs = await _api.GetBuyerLocationsAsync();
|
||||||
|
Locations.Clear();
|
||||||
|
foreach (var l in locs) Locations.Add(l);
|
||||||
|
var savedId = SettingsStore.Current.DefaultLocationId;
|
||||||
|
SelectedLocation = Locations.FirstOrDefault(l => l.BuyerLocationId == savedId)
|
||||||
|
?? Locations.FirstOrDefault(l => l.IsDefault)
|
||||||
|
?? Locations.FirstOrDefault();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SearchAsync()
|
private async Task SearchAsync()
|
||||||
@ -45,8 +71,6 @@ public partial class PriceListViewModel : ViewModelBase
|
|||||||
Items.Clear();
|
Items.Clear();
|
||||||
foreach (var it in resp.Summary ?? new()) Items.Add(it);
|
foreach (var it in resp.Summary ?? new()) Items.Add(it);
|
||||||
Total = resp.TotalDrugs;
|
Total = resp.TotalDrugs;
|
||||||
Status = $"Показано {Items.Count} из {Total} позиций"
|
|
||||||
+ (string.IsNullOrWhiteSpace(Search) ? "" : $" по запросу «{Search.Trim()}»");
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) { Status = ex.Message; }
|
catch (Exception ex) { Status = ex.Message; }
|
||||||
finally { Busy = false; }
|
finally { Busy = false; }
|
||||||
@ -56,8 +80,36 @@ public partial class PriceListViewModel : ViewModelBase
|
|||||||
private void AddToOrder()
|
private void AddToOrder()
|
||||||
{
|
{
|
||||||
if (SelectedItem is null) { Status = "Выберите позицию в списке."; return; }
|
if (SelectedItem is null) { Status = "Выберите позицию в списке."; return; }
|
||||||
var q = Qty <= 0 ? 1 : Qty;
|
_cart.Add(SelectedItem, Qty <= 0 ? 1 : Qty);
|
||||||
_cart.Add(SelectedItem, q);
|
Status = null;
|
||||||
Status = $"Добавлено в заказ: {SelectedItem.DrugName} × {q:N0}";
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RemoveFromCart(CartItem? item) => _cart.Remove(item!);
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void SaveDraft()
|
||||||
|
{
|
||||||
|
if (_cart.Count == 0) { Status = "Заказ пуст — добавьте позиции."; return; }
|
||||||
|
|
||||||
|
var draft = new DraftOrder
|
||||||
|
{
|
||||||
|
LocationId = SelectedLocation?.BuyerLocationId,
|
||||||
|
LocationAddress = SelectedLocation?.Address,
|
||||||
|
Comment = string.IsNullOrWhiteSpace(Comment) ? null : Comment.Trim(),
|
||||||
|
Items = _cart.Items.Select(c => new DraftItem
|
||||||
|
{
|
||||||
|
SupplierPriceId = c.Source.SupplierPriceId ?? "",
|
||||||
|
Name = c.Name,
|
||||||
|
Supplier = c.Supplier,
|
||||||
|
Barcode = c.Source.Barcode,
|
||||||
|
Price = c.Price,
|
||||||
|
Qty = c.Qty
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
var saved = DraftStore.Add(draft);
|
||||||
|
_cart.Clear();
|
||||||
|
Comment = "";
|
||||||
|
Status = $"Заказ сохранён как черновик {saved.Number} → раздел «Заказы» (не отправлен).";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,14 +43,22 @@
|
|||||||
<TextBlock Text="{Binding SelectedOrder.Location}" Classes="muted" TextWrapping="Wrap"/>
|
<TextBlock Text="{Binding SelectedOrder.Location}" Classes="muted" TextWrapping="Wrap"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Border DockPanel.Dock="Bottom" Margin="0,12,0,0" Padding="0,10,0,0"
|
<StackPanel DockPanel.Dock="Bottom" Spacing="10" Margin="0,12,0,0">
|
||||||
BorderThickness="0,1,0,0" BorderBrush="{DynamicResource Border}">
|
<Border Padding="0,10,0,0" BorderThickness="0,1,0,0" BorderBrush="{DynamicResource Border}">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" Foreground="{DynamicResource Text}"/>
|
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" Foreground="{DynamicResource Text}"/>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding SelectedOrder.Sum, StringFormat='{}{0:N2}'}"
|
<TextBlock Grid.Column="1" Text="{Binding SelectedOrder.Sum, StringFormat='{}{0:N2}'}"
|
||||||
FontWeight="SemiBold" Foreground="{DynamicResource Accent}"/>
|
FontWeight="SemiBold" Foreground="{DynamicResource Accent}"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
<!-- Действия для черновика (не отправленного заказа) -->
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="10" IsVisible="{Binding SelectedOrder.IsDraft, FallbackValue=False}">
|
||||||
|
<Button Classes="primary" Content="Отправить заказ" Command="{Binding SendDraftCommand}" IsEnabled="{Binding !Busy}"/>
|
||||||
|
<Button Content="Удалить" Command="{Binding DeleteDraftCommand}"
|
||||||
|
Background="Transparent" Foreground="{DynamicResource Muted}"
|
||||||
|
BorderBrush="{DynamicResource Border}" BorderThickness="1" CornerRadius="8" Padding="16,9" Cursor="Hand"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<DataGrid ItemsSource="{Binding SelectedOrder.Items}" x:CompileBindings="False"
|
<DataGrid ItemsSource="{Binding SelectedOrder.Items}" x:CompileBindings="False"
|
||||||
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
||||||
|
|||||||
@ -9,45 +9,91 @@
|
|||||||
<Grid DockPanel.Dock="Top" Margin="0,0,0,14">
|
<Grid DockPanel.Dock="Top" Margin="0,0,0,14">
|
||||||
<StackPanel Spacing="2" VerticalAlignment="Center">
|
<StackPanel Spacing="2" VerticalAlignment="Center">
|
||||||
<TextBlock Text="Прайс-лист" Classes="h1"/>
|
<TextBlock Text="Прайс-лист" Classes="h1"/>
|
||||||
<TextBlock Text="Каталог доступных позиций и цены" Classes="muted"/>
|
<TextBlock Text="Каталог позиций — собери заказ и сохрани" Classes="muted"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Border DockPanel.Dock="Top" Classes="card" Margin="0,0,0,12" Padding="16">
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10">
|
|
||||||
<TextBox Text="{Binding Search}" Width="440"
|
|
||||||
Watermark="поиск по наименованию (от 2 символов)…"/>
|
|
||||||
<Button Classes="primary" Content="Найти" Command="{Binding SearchCommand}" IsEnabled="{Binding !Busy}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<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"
|
||||||
Background="{DynamicResource AccentSoft}"
|
Background="{DynamicResource AccentSoft}"
|
||||||
IsVisible="{Binding Status, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
|
IsVisible="{Binding Status, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
|
||||||
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" FontSize="12.5"/>
|
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" TextWrapping="Wrap" FontSize="12.5"/>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<Border DockPanel.Dock="Bottom" Classes="card" Margin="0,10,0,0" Padding="14">
|
<Grid ColumnDefinitions="*,16,430">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
|
|
||||||
<TextBlock Text="Кол-во" Classes="label"/>
|
|
||||||
<NumericUpDown Value="{Binding Qty}" Minimum="1" Maximum="100000" Increment="1" FormatString="0" Width="130"/>
|
|
||||||
<Button Classes="primary" Content="Добавить в заказ ▸" Command="{Binding AddToOrderCommand}"/>
|
|
||||||
<TextBlock Text="{Binding CartInfo}" Classes="muted" VerticalAlignment="Center" Margin="10,0,0,0"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<Border Classes="card" Padding="0" ClipToBounds="True">
|
<!-- Каталог -->
|
||||||
<DataGrid ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" x:CompileBindings="False"
|
<Border Grid.Column="0" Classes="card" Padding="0" ClipToBounds="True">
|
||||||
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
<DockPanel>
|
||||||
CanUserResizeColumns="True" CanUserSortColumns="False">
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="10" Margin="14">
|
||||||
<DataGrid.Columns>
|
<TextBox Text="{Binding Search}" Width="330" Watermark="поиск по наименованию (от 2 символов)…"/>
|
||||||
<DataGridTextColumn Header="Наименование" Width="*" Binding="{Binding DrugName}"/>
|
<Button Classes="primary" Content="Найти" Command="{Binding SearchCommand}" IsEnabled="{Binding !Busy}"/>
|
||||||
<DataGridTextColumn Header="Форма" Width="150" Binding="{Binding CureForm}"/>
|
</StackPanel>
|
||||||
<DataGridTextColumn Header="Производитель" Width="220" Binding="{Binding Manufacturer}"/>
|
<Border DockPanel.Dock="Bottom" Margin="14" Padding="0">
|
||||||
<DataGridTextColumn Header="Поставщик" Width="130" Binding="{Binding SupplierName}"/>
|
<StackPanel Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
|
||||||
<DataGridTextColumn Header="Цена" Width="110" Binding="{Binding Price, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
|
<TextBlock Text="Кол-во" Classes="label"/>
|
||||||
</DataGrid.Columns>
|
<NumericUpDown Value="{Binding Qty}" Minimum="1" Maximum="100000" Increment="1" FormatString="0" Width="120"/>
|
||||||
</DataGrid>
|
<Button Classes="primary" Content="Добавить в заказ ▸" Command="{Binding AddToOrderCommand}"/>
|
||||||
</Border>
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
<DataGrid ItemsSource="{Binding Items}" SelectedItem="{Binding SelectedItem}" x:CompileBindings="False"
|
||||||
|
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
|
||||||
|
CanUserResizeColumns="True" CanUserSortColumns="False">
|
||||||
|
<DataGrid.Columns>
|
||||||
|
<DataGridTextColumn Header="Наименование" Width="*" Binding="{Binding DrugName}"/>
|
||||||
|
<DataGridTextColumn Header="Производитель" Width="180" Binding="{Binding Manufacturer}"/>
|
||||||
|
<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">
|
||||||
|
<DockPanel>
|
||||||
|
<TextBlock DockPanel.Dock="Top" Text="Мой заказ" Classes="h2" Margin="0,0,0,10"/>
|
||||||
|
|
||||||
|
<StackPanel DockPanel.Dock="Bottom" Spacing="10" Margin="0,12,0,0">
|
||||||
|
<StackPanel Spacing="5">
|
||||||
|
<TextBlock Text="Грузополучатель" Classes="muted"/>
|
||||||
|
<ComboBox ItemsSource="{Binding Locations}" SelectedItem="{Binding SelectedLocation}" HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBox Text="{Binding Comment}" Watermark="комментарий (необязательно)"/>
|
||||||
|
<Grid ColumnDefinitions="*,Auto">
|
||||||
|
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" VerticalAlignment="Center" Foreground="{DynamicResource Text}"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding CartTotal, StringFormat='{}{0:N2}'}" FontWeight="Bold" FontSize="16" Foreground="{DynamicResource Accent}"/>
|
||||||
|
</Grid>
|
||||||
|
<Button Classes="primary" Content="Сохранить заказ" HorizontalAlignment="Stretch" Command="{Binding SaveDraftCommand}"/>
|
||||||
|
<TextBlock Text="Черновик появится в «Заказы» со статусом «Не отправлен»." Classes="muted" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<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}"/>
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource Text}" TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"/>
|
||||||
|
<TextBlock Text="{Binding Supplier}" Classes="muted"/>
|
||||||
|
<Grid ColumnDefinitions="Auto,*,Auto" VerticalAlignment="Center">
|
||||||
|
<NumericUpDown Grid.Column="0" Value="{Binding Qty}" Minimum="1" Maximum="100000" Increment="1" FormatString="0" Width="120"/>
|
||||||
|
<TextBlock Grid.Column="1" Text="{Binding Price, StringFormat='× {0:N2}'}" Classes="muted" VerticalAlignment="Center" Margin="10,0"/>
|
||||||
|
<TextBlock Grid.Column="2" Text="{Binding LineSum, StringFormat='{}{0:N2}'}" FontWeight="SemiBold" Foreground="{DynamicResource Text}" VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</DockPanel>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
77
src/Elfisa.Core/DraftStore.cs
Normal file
77
src/Elfisa.Core/DraftStore.cs
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
|
namespace Elfisa.Core;
|
||||||
|
|
||||||
|
public sealed class DraftItem
|
||||||
|
{
|
||||||
|
public string SupplierPriceId { get; set; } = "";
|
||||||
|
public string Name { get; set; } = "";
|
||||||
|
public string Supplier { get; set; } = "";
|
||||||
|
public string? Barcode { get; set; }
|
||||||
|
public decimal Price { get; set; }
|
||||||
|
public decimal Qty { get; set; } = 1;
|
||||||
|
public decimal LineSum => Price * Qty;
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DraftOrder
|
||||||
|
{
|
||||||
|
public string Id { get; set; } = Guid.NewGuid().ToString();
|
||||||
|
public string Number { get; set; } = "";
|
||||||
|
public DateTime CreatedAt { get; set; } = DateTime.Now;
|
||||||
|
public string? LocationId { get; set; }
|
||||||
|
public string? LocationAddress { get; set; }
|
||||||
|
public string? Comment { get; set; }
|
||||||
|
public List<DraftItem> Items { get; set; } = new();
|
||||||
|
public decimal Total => Items.Sum(i => i.LineSum);
|
||||||
|
public int ItemsCount => Items.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Локальные черновики заказов (%APPDATA%\ElfisaAvalonia\drafts.json).</summary>
|
||||||
|
public static class DraftStore
|
||||||
|
{
|
||||||
|
private sealed class Box { public int Seq { get; set; } public List<DraftOrder> Drafts { get; set; } = new(); }
|
||||||
|
|
||||||
|
private static readonly string Dir =
|
||||||
|
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "ElfisaAvalonia");
|
||||||
|
private static readonly string File = Path.Combine(Dir, "drafts.json");
|
||||||
|
|
||||||
|
private static Box _box = Load();
|
||||||
|
|
||||||
|
public static IReadOnlyList<DraftOrder> All => _box.Drafts;
|
||||||
|
|
||||||
|
private static Box Load()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (System.IO.File.Exists(File))
|
||||||
|
return JsonSerializer.Deserialize<Box>(System.IO.File.ReadAllText(File)) ?? new Box();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
return new Box();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Save()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(Dir);
|
||||||
|
System.IO.File.WriteAllText(File, JsonSerializer.Serialize(_box, new JsonSerializerOptions { WriteIndented = true }));
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DraftOrder Add(DraftOrder draft)
|
||||||
|
{
|
||||||
|
_box.Seq++;
|
||||||
|
draft.Number = $"ЧРН-{_box.Seq:0000}";
|
||||||
|
_box.Drafts.Insert(0, draft);
|
||||||
|
Save();
|
||||||
|
return draft;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Remove(string id)
|
||||||
|
{
|
||||||
|
_box.Drafts.RemoveAll(d => d.Id == id);
|
||||||
|
Save();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user