Avalonia: верхняя навигация (как в WinForms) + раздел «Заказы»

- Навигация перенесена из левого сайдбара в ВЕРХНЮЮ горизонтальную
  панель, как в исходной программе: логотип слева, вкладки
  (иконка+подпись) по центру, пользователь+Выйти справа
- Раздел «Заказы»: мастер-детали (список слева, состав заказа справа
  с позициями и итогом), на данных /api/buyer/report
- Числовые колонки выровнены вправо (стиль DataGridCell.num)
- Отчёты: тулбар на WrapPanel, стабильные ширины колонок

Проверено запуском (скриншоты): верхняя навигация, отчёты, заказы.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Exest 2026-08-08 10:00:57 +03:00
parent f6a1012e9b
commit 132ce1531f
6 changed files with 250 additions and 37 deletions

View File

@ -116,6 +116,25 @@
<Setter Property="FontWeight" Value="SemiBold"/>
</Style>
<!-- ============ Верхняя навигация (как в WinForms-версии) ============ -->
<Style Selector="ListBox.topnav">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
<Style Selector="ListBox.topnav ListBoxItem">
<Setter Property="Padding" Value="10,7"/>
<Setter Property="Margin" Value="3,0"/>
<Setter Property="CornerRadius" Value="10"/>
<Setter Property="Cursor" Value="Hand"/>
</Style>
<Style Selector="ListBox.topnav ListBoxItem:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
</Style>
<Style Selector="ListBox.topnav ListBoxItem:selected /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource SidebarActive}"/>
</Style>
<!-- ============ Таблица ============ -->
<Style Selector="DataGrid">
<Setter Property="Background" Value="{DynamicResource Surface}"/>
@ -139,6 +158,10 @@
<Style Selector="DataGridCell">
<Setter Property="Padding" Value="10,0"/>
</Style>
<!-- числовые ячейки — вправо -->
<Style Selector="DataGridCell.num TextBlock">
<Setter Property="TextAlignment" Value="Right"/>
</Style>
<Style Selector="DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource AccentSoft}"/>
<Setter Property="Opacity" Value="1"/>

View File

@ -0,0 +1,99 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Elfisa.Core;
namespace Elfisa.Avalonia.ViewModels;
public sealed class OrderItemRow
{
public string Name { get; init; } = "";
public string Code { get; init; } = "";
public decimal Qty { get; init; }
public decimal Price { get; init; }
public decimal Sum { get; init; }
}
public sealed class OrderSummary
{
public string Date { get; init; } = "";
public string Number { get; init; } = "";
public string Supplier { get; init; } = "";
public string Location { get; init; } = "";
public string Status { get; init; } = "";
public int ItemsCount { get; init; }
public decimal Sum { get; init; }
public ObservableCollection<OrderItemRow> Items { get; init; } = new();
}
/// <summary>
/// Заказы: список + состав. Пока строится из /api/buyer/report (он работает);
/// после деплоя фикса /api/buyer/orders можно перейти на «родной» эндпоинт
/// (даст ещё PlacedAt, Comment, отмену/размещение).
/// </summary>
public partial class OrdersViewModel : ViewModelBase
{
private readonly ApiClient _api = new();
public ObservableCollection<OrderSummary> Orders { get; } = new();
[ObservableProperty] private OrderSummary? _selectedOrder;
[ObservableProperty] private bool _busy;
[ObservableProperty] private string? _status;
public OrdersViewModel()
{
_api.SetToken(Session.Current.Token);
if (Session.Current.IsAuthenticated) _ = LoadAsync();
}
[RelayCommand]
private async Task LoadAsync()
{
Status = null;
try
{
Busy = true;
var to = DateTime.Today;
var from = to.AddMonths(-24);
var lines = await _api.GetBuyerReportAsync(from, to);
Orders.Clear();
foreach (var g in lines.GroupBy(l => l.OrderId)
.OrderByDescending(x => x.First().OrderDate ?? DateTime.MinValue))
{
var f = g.First();
var items = new ObservableCollection<OrderItemRow>(
g.Select(l => new OrderItemRow
{
Name = l.ItemName ?? "",
Code = l.ItemCode ?? "",
Qty = l.Qty,
Price = l.UnitPrice,
Sum = l.Sum
}));
Orders.Add(new OrderSummary
{
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 ?? "",
Status = f.Status ?? "",
ItemsCount = g.Count(),
Sum = g.Sum(x => x.Sum),
Items = items
});
}
SelectedOrder = Orders.FirstOrDefault();
if (Orders.Count == 0) Status = "Заказов за последние 2 года нет.";
}
catch (Exception ex) { Status = ex.Message; }
finally { Busy = false; }
}
}

View File

@ -42,13 +42,13 @@ public partial class ShellViewModel : ViewModelBase
_logout = logout;
NavItems = new ObservableCollection<NavItem>
{
new("Прайс-лист", "P", () => new PlaceholderPageViewModel("Прайс-лист", "Каталог, поиск и добавление позиций в заказ.")),
new("Заказы", "З", () => new PlaceholderPageViewModel("Заказы", "История заказов и их статусы.")),
new("Накладные", "Н", () => new PlaceholderPageViewModel("Накладные", "Приход товара по заказам.")),
new("Отчёты", "О", () => new ReportsViewModel()),
new("Отправить", "", () => new PlaceholderPageViewModel("Отправить", "Формирование и выгрузка заявок поставщику (DBF).")),
new("Отказы", "!", () => new PlaceholderPageViewModel("Отказы", "Отказные позиции по заказам.")),
new("Справочник", "С", () => new PlaceholderPageViewModel("Справочник", "Контрагенты, грузополучатели, настройки.")),
new("Прайс-лист", "🧾", () => new PlaceholderPageViewModel("Прайс-лист", "Каталог, поиск и добавление позиций в заказ.")),
new("Заказы", "🛒", () => new OrdersViewModel()),
new("Накладные", "📄", () => new PlaceholderPageViewModel("Накладные", "Приход товара по заказам.")),
new("Отчёты", "📊", () => new ReportsViewModel()),
new("Отправить", "📤", () => new PlaceholderPageViewModel("Отправить", "Формирование и выгрузка заявок поставщику (DBF).")),
new("Отказы", "", () => new PlaceholderPageViewModel("Отказы", "Отказные позиции по заказам.")),
new("Справочник", "📖", () => new PlaceholderPageViewModel("Справочник", "Контрагенты, грузополучатели, настройки.")),
};
SelectedNav = NavItems.First(n => n.Title == "Отчёты");
}

View File

@ -0,0 +1,75 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:Elfisa.Avalonia.ViewModels"
xmlns:conv="using:Avalonia.Data.Converters"
x:Class="Elfisa.Avalonia.Views.OrdersView"
x:DataType="vm:OrdersViewModel">
<Grid Margin="24" RowDefinitions="Auto,*">
<StackPanel Grid.Row="0" Spacing="2" Margin="0,0,0,14">
<TextBlock Text="Заказы" Classes="h1"/>
<TextBlock Text="История заказов и их состав" Classes="muted"/>
</StackPanel>
<Grid Grid.Row="1" ColumnDefinitions="*,16,430">
<!-- Список заказов -->
<Border Grid.Column="0" Classes="card" Padding="0" ClipToBounds="True">
<DataGrid ItemsSource="{Binding Orders}" SelectedItem="{Binding SelectedOrder}"
x:CompileBindings="False"
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
CanUserResizeColumns="True" CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Дата" Width="112" Binding="{Binding Date}"/>
<DataGridTextColumn Header="Номер" Width="140" Binding="{Binding Number}"/>
<DataGridTextColumn Header="Поставщик" Width="*" Binding="{Binding Supplier}"/>
<DataGridTextColumn Header="Позиций" Width="90" Binding="{Binding ItemsCount}" CellStyleClasses="num"/>
<DataGridTextColumn Header="Сумма" Width="132" Binding="{Binding Sum, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
<DataGridTextColumn Header="Статус" Width="95" Binding="{Binding Status}"/>
</DataGrid.Columns>
</DataGrid>
</Border>
<!-- Карточка заказа -->
<Border Grid.Column="2" Classes="card" Padding="18">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Spacing="4" Margin="0,0,0,14">
<TextBlock Text="{Binding SelectedOrder.Number}" Classes="h2"/>
<StackPanel Orientation="Horizontal" Spacing="10">
<TextBlock Text="{Binding SelectedOrder.Date}" Classes="muted"/>
<TextBlock Text="{Binding SelectedOrder.Status}" Classes="muted"/>
</StackPanel>
<TextBlock Text="{Binding SelectedOrder.Supplier}" Classes="muted" TextWrapping="Wrap"/>
<TextBlock Text="{Binding SelectedOrder.Location}" Classes="muted" TextWrapping="Wrap"/>
</StackPanel>
<Border DockPanel.Dock="Bottom" Margin="0,12,0,0" Padding="0,10,0,0"
BorderThickness="0,1,0,0" BorderBrush="{DynamicResource Border}">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Text="Итого" FontWeight="SemiBold" Foreground="{DynamicResource Text}"/>
<TextBlock Grid.Column="1" Text="{Binding SelectedOrder.Sum, StringFormat='{}{0:N2}'}"
FontWeight="SemiBold" Foreground="{DynamicResource Accent}"/>
</Grid>
</Border>
<DataGrid ItemsSource="{Binding SelectedOrder.Items}" x:CompileBindings="False"
AutoGenerateColumns="False" IsReadOnly="True" HeadersVisibility="Column"
CanUserResizeColumns="True" CanUserSortColumns="False">
<DataGrid.Columns>
<DataGridTextColumn Header="Товар" Width="*" Binding="{Binding Name}"/>
<DataGridTextColumn Header="Кол-во" Width="78" Binding="{Binding Qty, StringFormat='{}{0:N0}'}" CellStyleClasses="num"/>
<DataGridTextColumn Header="Сумма" Width="110" Binding="{Binding Sum, StringFormat='{}{0:N2}'}" CellStyleClasses="num"/>
</DataGrid.Columns>
</DataGrid>
</DockPanel>
</Border>
</Grid>
<!-- Статус -->
<Border Grid.Row="1" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="4"
Padding="10,7" CornerRadius="6" Background="{DynamicResource AccentSoft}"
IsVisible="{Binding Status, Converter={x:Static conv:ObjectConverters.IsNotNull}}">
<TextBlock Text="{Binding Status}" Foreground="{DynamicResource AccentPressed}" FontSize="12.5"/>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,9 @@
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Elfisa.Avalonia.Views;
public partial class OrdersView : UserControl
{
public OrdersView() => AvaloniaXamlLoader.Load(this);
}

View File

@ -4,47 +4,54 @@
x:Class="Elfisa.Avalonia.Views.ShellView"
x:DataType="vm:ShellViewModel">
<Grid ColumnDefinitions="250,*">
<DockPanel>
<!-- Сайдбар -->
<Border Grid.Column="0" Background="{DynamicResource Sidebar}">
<DockPanel>
<StackPanel DockPanel.Dock="Top" Margin="22,24,20,18" Spacing="2">
<TextBlock Text="ЭльФиСА" FontSize="21" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="Электронная Фармация" Foreground="{DynamicResource SidebarText}" FontSize="11"/>
<!-- Верхняя панель навигации (как в исходной программе) -->
<Border DockPanel.Dock="Top" Background="{DynamicResource Sidebar}" Height="82">
<Grid ColumnDefinitions="Auto,*,Auto" Margin="18,0">
<!-- Логотип -->
<StackPanel Grid.Column="0" VerticalAlignment="Center" Margin="0,0,22,0">
<TextBlock Text="ЭльФиСА" FontSize="19" FontWeight="Bold" Foreground="White"/>
<TextBlock Text="Электронная Фармация" Foreground="{DynamicResource SidebarText}" FontSize="10.5"/>
</StackPanel>
<Border DockPanel.Dock="Bottom" Margin="14,10,14,16" Padding="14" CornerRadius="12"
Background="{DynamicResource SidebarActive}">
<StackPanel Spacing="9">
<TextBlock Text="{Binding UserName}" Foreground="White" FontWeight="SemiBold"
FontSize="12.5" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding RoleLabel}" Foreground="{DynamicResource SidebarText}" FontSize="11"/>
<Button Content="Выйти" Command="{Binding LogoutCommand}" HorizontalAlignment="Stretch"
Background="Transparent" Foreground="White" BorderBrush="#55FFFFFF"
BorderThickness="1" CornerRadius="8" Padding="0,7" Cursor="Hand"/>
</StackPanel>
</Border>
<ListBox Classes="nav" Margin="0,6,0,0"
<!-- Навигация -->
<ListBox Grid.Column="1" Classes="topnav" VerticalAlignment="Center" HorizontalAlignment="Left"
ItemsSource="{Binding NavItems}" SelectedItem="{Binding SelectedNav}">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:NavItem">
<StackPanel Orientation="Horizontal" Spacing="12">
<Border Width="26" Height="26" CornerRadius="7" Background="#26FFFFFF">
<TextBlock Text="{Binding Icon}" Foreground="White" FontSize="12.5"
FontWeight="SemiBold"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding Title}" VerticalAlignment="Center"/>
<StackPanel Orientation="Vertical" Width="76" Spacing="3">
<TextBlock Text="{Binding Icon}" FontSize="17" HorizontalAlignment="Center"/>
<TextBlock Text="{Binding Title}" Foreground="{DynamicResource SidebarText}"
FontSize="11.5" HorizontalAlignment="Center"
TextTrimming="CharacterEllipsis"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
<!-- Пользователь + выход -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center" Margin="18,0,0,0">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding UserName}" Foreground="White" FontSize="12" FontWeight="SemiBold"
HorizontalAlignment="Right" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding RoleLabel}" Foreground="{DynamicResource SidebarText}"
FontSize="10.5" HorizontalAlignment="Right"/>
</StackPanel>
<Button Content="Выйти" Command="{Binding LogoutCommand}"
Background="Transparent" Foreground="White" BorderBrush="#55FFFFFF"
BorderThickness="1" CornerRadius="8" Padding="16,9" Cursor="Hand"/>
</StackPanel>
</Grid>
</Border>
<!-- Контент -->
<ContentControl Grid.Column="1" Content="{Binding CurrentPage}"/>
</Grid>
<ContentControl Content="{Binding CurrentPage}"/>
</DockPanel>
</UserControl>