feat(pos-ui): вёрстка кассы 1:1 по эталон-макету

Перенёс макет Mac-Claude (docs/pos-design/pos-mockup.html) в Avalonia по
размерам, а не на глаз:

- Оплата: модалка 880px поверх затемнённого экрана продажи (как в макете);
  сегменты Нал/Карта/Смешанная с векторными иконками (активный зелёный —
  иконка белая через стиль Button.seg.on Path); быстрые суммы 3×2
  (без сдачи/4000/5000/10000/20000/своя сумма); клавиатура 3×4 по 72px.
- Продажа: исправлена подсветка выбранной строки — локальные
  Background/IsVisible перекрывали сеттеры стиля :selected; теперь зелёный
  акцент слева + светло-зелёный фон работают. Бейдж «Отложенные · N».
  Чип связи «На связи · очередь пуста».
- Демо: --demo-pay открывает модалку оплаты для скриншота на Windows;
  PayScrim в скриншотере рендерит оплату поверх реальной оболочки продажи.
- Инструмент сверки: CompareRunner (--compare, SkiaSharp) складывает
  «макет | касса» рядом → docs/pos-design/compare-*.png.

Скорость не ухудшена (старт 74мс, скан 13.7мс). --uitest OK, 82 unit зелёные.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-08 14:35:18 +05:00
parent 85e486abc7
commit 6ae79e37fb
11 changed files with 448 additions and 210 deletions

View file

@ -20,6 +20,8 @@ public sealed class PosProduct
public bool IsArchived { get; init; } public bool IsArchived { get; init; }
public bool IsFavorite { get; init; } public bool IsFavorite { get; init; }
public bool IsWeighed => Packaging == PosPackaging.Weight; public bool IsWeighed => Packaging == PosPackaging.Weight;
/// <summary>Цена для плитки избранного: «690 ₸/кг» для весового, «280 ₸» для штучного.</summary>
public string PriceText => IsWeighed ? $"{Price:N0} ₸/{UnitCode}" : $"{Price:N0} ₸";
} }
/// <summary>Строка чека. Количество дробное (весовой/разлив). Скидка — абсолютная /// <summary>Строка чека. Количество дробное (весовой/разлив). Скидка — абсолютная

View file

@ -13,6 +13,9 @@ public partial class App : Application
/// сервера — для скриншота реального окна на Windows.</summary> /// сервера — для скриншота реального окна на Windows.</summary>
public static bool DemoMode { get; set; } public static bool DemoMode { get; set; }
/// <summary>Демо с открытой модалкой оплаты (--demo-pay) — для скриншота оплаты на Windows.</summary>
public static bool DemoPay { get; set; }
public override void Initialize() => AvaloniaXamlLoader.Load(this); public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted() public override void OnFrameworkInitializationCompleted()
@ -23,9 +26,15 @@ public override void OnFrameworkInitializationCompleted()
{ {
// Демо: окно с примерным чеком, без AppServices/логина; развёрнуто // Демо: окно с примерным чеком, без AppServices/логина; развёрнуто
// на весь экран — для скриншота реального окна на Windows. // на весь экран — для скриншота реального окна на Windows.
var demoVm = new MainWindowViewModel();
if (DemoPay)
{
demoVm.Payment = PaymentViewModel.SampleCash();
demoVm.IsPaymentOpen = true;
}
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
{ {
DataContext = new MainWindowViewModel(), DataContext = demoVm,
WindowState = Avalonia.Controls.WindowState.Maximized, WindowState = Avalonia.Controls.WindowState.Maximized,
}; };
} }

View file

@ -15,10 +15,12 @@ public static int Main(string[] args)
// --uitest : headless UI-автотест экрана продажи (Avalonia.Headless, для CI/xvfb). // --uitest : headless UI-автотест экрана продажи (Avalonia.Headless, для CI/xvfb).
if (args.Contains("--smoke")) return SmokeRunner.Run(args); if (args.Contains("--smoke")) return SmokeRunner.Run(args);
if (args.Contains("--screenshots")) return ScreenshotRunner.Run(args); if (args.Contains("--screenshots")) return ScreenshotRunner.Run(args);
if (args.Contains("--compare")) return CompareRunner.Run(args);
if (args.Contains("--uitest")) return UiTestRunner.Run(args); if (args.Contains("--uitest")) return UiTestRunner.Run(args);
// --demo : показать реальное окно с примерами данных (для скриншота на Windows). // --demo : показать реальное окно с примерами данных (для скриншота на Windows).
if (args.Contains("--demo")) App.DemoMode = true; if (args.Contains("--demo") || args.Contains("--demo-pay")) App.DemoMode = true;
if (args.Contains("--demo-pay")) App.DemoPay = true;
BuildAvaloniaApp().StartWithClassicDesktopLifetime(args); BuildAvaloniaApp().StartWithClassicDesktopLifetime(args);
return 0; return 0;

View file

@ -16,20 +16,21 @@
<Setter Property="Padding" Value="12,6"/> <Setter Property="Padding" Value="12,6"/>
<Setter Property="Background" Value="{DynamicResource PosSubtle}"/> <Setter Property="Background" Value="{DynamicResource PosSubtle}"/>
</Style> </Style>
<Style Selector="Border.chip > TextBlock"> <Style Selector="Border.chip TextBlock">
<Setter Property="Foreground" Value="{DynamicResource PosTextMuted}"/> <Setter Property="Foreground" Value="{DynamicResource PosTextMuted}"/>
<Setter Property="FontSize" Value="13.5"/> <Setter Property="FontSize" Value="14"/>
<Setter Property="FontWeight" Value="Medium"/> <Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="VerticalAlignment" Value="Center"/> <Setter Property="VerticalAlignment" Value="Center"/>
</Style> </Style>
<Style Selector="Border.chip.ok"> <Style Selector="Border.chip.ok">
<Setter Property="Background" Value="{DynamicResource PosBrandSoft}"/> <Setter Property="Background" Value="{DynamicResource PosBrandSoft}"/>
</Style> </Style>
<Style Selector="Border.chip.ok > TextBlock"><Setter Property="Foreground" Value="{DynamicResource PosBrand}"/></Style> <Style Selector="Border.chip.ok TextBlock"><Setter Property="Foreground" Value="{DynamicResource PosBrandDark}"/></Style>
<Style Selector="Border.chip.net"><Setter Property="Background" Value="{DynamicResource PosChipNet}"/></Style>
<Style Selector="Border.chip.warn"> <Style Selector="Border.chip.warn">
<Setter Property="Background" Value="{DynamicResource PosWarnSoft}"/> <Setter Property="Background" Value="{DynamicResource PosWarnSoft}"/>
</Style> </Style>
<Style Selector="Border.chip.warn > TextBlock"><Setter Property="Foreground" Value="{DynamicResource PosWarn}"/></Style> <Style Selector="Border.chip.warn TextBlock"><Setter Property="Foreground" Value="{DynamicResource PosWarn}"/></Style>
<!-- ── Кнопки ───────────────────────────────────────────────────── --> <!-- ── Кнопки ───────────────────────────────────────────────────── -->
<Style Selector="Button.pos"> <Style Selector="Button.pos">
@ -86,8 +87,9 @@
<Style Selector="Button.danger"> <Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource PosCard}"/> <Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosDanger}"/> <Setter Property="Foreground" Value="{DynamicResource PosDanger}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosDanger}"/> <Setter Property="BorderBrush" Value="{DynamicResource PosDangerBorder}"/>
</Style> </Style>
<Style Selector="Button.danger TextBlock"><Setter Property="Foreground" Value="{DynamicResource PosDanger}"/></Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter"> <Style Selector="Button.danger:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosDangerSoft}"/> <Setter Property="Background" Value="{DynamicResource PosDangerSoft}"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource PosDanger}"/> <Setter Property="TextElement.Foreground" Value="{DynamicResource PosDanger}"/>
@ -136,6 +138,8 @@
<Style Selector="Button.seg.on /template/ ContentPresenter"> <Style Selector="Button.seg.on /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosBrand}"/> <Setter Property="Background" Value="{DynamicResource PosBrand}"/>
</Style> </Style>
<Style Selector="Button.seg Path"><Setter Property="Stroke" Value="{DynamicResource PosTextMuted}"/></Style>
<Style Selector="Button.seg.on Path"><Setter Property="Stroke" Value="{DynamicResource PosBrandText}"/></Style>
<!-- ── Поля ввода ──────────────────────────────────────────────── --> <!-- ── Поля ввода ──────────────────────────────────────────────── -->
<Style Selector="TextBox.pos"> <Style Selector="TextBox.pos">
@ -165,4 +169,45 @@
<Setter Property="CornerRadius" Value="8"/> <Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="9,4"/> <Setter Property="Padding" Value="9,4"/>
</Style> </Style>
<!-- kbd-капсула (как <kbd> в макете) -->
<Style Selector="Border.kbd">
<Setter Property="MinWidth" Value="30"/>
<Setter Property="Height" Value="24"/>
<Setter Property="Padding" Value="7,0"/>
<Setter Property="CornerRadius" Value="6"/>
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosBorder}"/>
<Setter Property="BorderThickness" Value="1,1,1,2"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style Selector="Border.kbd > TextBlock">
<Setter Property="FontSize" Value="12"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Foreground" Value="{DynamicResource PosTextMuted}"/>
<Setter Property="HorizontalAlignment" Value="Center"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style Selector="Border.kbd.onbrand"><Setter Property="Background" Value="#33FFFFFF"/><Setter Property="BorderThickness" Value="0"/></Style>
<Style Selector="Border.kbd.onbrand > TextBlock"><Setter Property="Foreground" Value="White"/></Style>
<!-- Грань степпера (− / +) — слитный блок -->
<Style Selector="Button.stepbtn">
<Setter Property="Background" Value="{DynamicResource PosStepBg}"/>
<Setter Property="Foreground" Value="{DynamicResource PosText}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="CornerRadius" Value="0"/>
<Setter Property="FontSize" Value="22"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="Padding" Value="0"/>
<Setter Property="HorizontalAlignment" Value="Stretch"/>
<Setter Property="VerticalAlignment" Value="Stretch"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style Selector="Button.stepbtn /template/ ContentPresenter">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
</Style>
<Style Selector="Button.stepbtn:pointerover /template/ ContentPresenter"><Setter Property="Background" Value="{DynamicResource PosSubtle}"/></Style>
</Styles> </Styles>

View file

@ -7,19 +7,25 @@
<SolidColorBrush x:Key="PosBg" Color="#F4F6F5"/> <SolidColorBrush x:Key="PosBg" Color="#F4F6F5"/>
<SolidColorBrush x:Key="PosCard" Color="#FFFFFF"/> <SolidColorBrush x:Key="PosCard" Color="#FFFFFF"/>
<SolidColorBrush x:Key="PosSubtle" Color="#F0F3F1"/> <SolidColorBrush x:Key="PosSubtle" Color="#F0F3F1"/>
<SolidColorBrush x:Key="PosChipNet" Color="#EEF2F1"/>
<SolidColorBrush x:Key="PosStepBg" Color="#F7F9F8"/>
<SolidColorBrush x:Key="PosBorder" Color="#E3E8E6"/> <SolidColorBrush x:Key="PosBorder" Color="#E3E8E6"/>
<SolidColorBrush x:Key="PosText" Color="#0F1F17"/> <SolidColorBrush x:Key="PosText" Color="#0F1F17"/>
<SolidColorBrush x:Key="PosTextMuted" Color="#5F6F68"/> <SolidColorBrush x:Key="PosTextMuted" Color="#5F6F68"/>
<SolidColorBrush x:Key="PosPlaceholder" Color="#8A9993"/>
<SolidColorBrush x:Key="PosBrand" Color="#16A34A"/> <SolidColorBrush x:Key="PosBrand" Color="#16A34A"/>
<SolidColorBrush x:Key="PosBrandDark" Color="#12863D"/>
<SolidColorBrush x:Key="PosBrandHover" Color="#15803D"/> <SolidColorBrush x:Key="PosBrandHover" Color="#15803D"/>
<SolidColorBrush x:Key="PosBrandPressed" Color="#166534"/> <SolidColorBrush x:Key="PosBrandPressed" Color="#166534"/>
<SolidColorBrush x:Key="PosBrandText" Color="#FFFFFF"/> <SolidColorBrush x:Key="PosBrandText" Color="#FFFFFF"/>
<SolidColorBrush x:Key="PosBrandSoft" Color="#DCFCE7"/> <SolidColorBrush x:Key="PosBrandSoft" Color="#E8F7EE"/>
<SolidColorBrush x:Key="PosSelRow" Color="#F7FBF8"/>
<SolidColorBrush x:Key="PosDanger" Color="#DC2626"/> <SolidColorBrush x:Key="PosDanger" Color="#DC2626"/>
<SolidColorBrush x:Key="PosDangerSoft" Color="#FEE2E2"/> <SolidColorBrush x:Key="PosDangerSoft" Color="#FEE2E2"/>
<SolidColorBrush x:Key="PosDangerBorder" Color="#F3C4C4"/>
<SolidColorBrush x:Key="PosWarn" Color="#B45309"/> <SolidColorBrush x:Key="PosWarn" Color="#B45309"/>
<SolidColorBrush x:Key="PosWarnSoft" Color="#FEF3C7"/> <SolidColorBrush x:Key="PosWarnSoft" Color="#FEF3C7"/>
<SolidColorBrush x:Key="PosScrim" Color="#33203028"/> <SolidColorBrush x:Key="PosScrim" Color="#6B0F1F17"/>
</ResourceDictionary> </ResourceDictionary>
<!-- ── Тёмная тема (опция, те же токены) ───────────────────────── --> <!-- ── Тёмная тема (опция, те же токены) ───────────────────────── -->
@ -27,19 +33,25 @@
<SolidColorBrush x:Key="PosBg" Color="#0F1713"/> <SolidColorBrush x:Key="PosBg" Color="#0F1713"/>
<SolidColorBrush x:Key="PosCard" Color="#15201B"/> <SolidColorBrush x:Key="PosCard" Color="#15201B"/>
<SolidColorBrush x:Key="PosSubtle" Color="#1B2821"/> <SolidColorBrush x:Key="PosSubtle" Color="#1B2821"/>
<SolidColorBrush x:Key="PosChipNet" Color="#1B2821"/>
<SolidColorBrush x:Key="PosStepBg" Color="#1B2821"/>
<SolidColorBrush x:Key="PosBorder" Color="#2A3A31"/> <SolidColorBrush x:Key="PosBorder" Color="#2A3A31"/>
<SolidColorBrush x:Key="PosText" Color="#E8EFEA"/> <SolidColorBrush x:Key="PosText" Color="#E8EFEA"/>
<SolidColorBrush x:Key="PosTextMuted" Color="#9BAAA2"/> <SolidColorBrush x:Key="PosTextMuted" Color="#9BAAA2"/>
<SolidColorBrush x:Key="PosPlaceholder" Color="#7E8E86"/>
<SolidColorBrush x:Key="PosBrand" Color="#22C55E"/> <SolidColorBrush x:Key="PosBrand" Color="#22C55E"/>
<SolidColorBrush x:Key="PosBrandDark" Color="#34D37F"/>
<SolidColorBrush x:Key="PosBrandHover" Color="#16A34A"/> <SolidColorBrush x:Key="PosBrandHover" Color="#16A34A"/>
<SolidColorBrush x:Key="PosBrandPressed" Color="#15803D"/> <SolidColorBrush x:Key="PosBrandPressed" Color="#15803D"/>
<SolidColorBrush x:Key="PosBrandText" Color="#06140C"/> <SolidColorBrush x:Key="PosBrandText" Color="#06140C"/>
<SolidColorBrush x:Key="PosBrandSoft" Color="#13331F"/> <SolidColorBrush x:Key="PosBrandSoft" Color="#13331F"/>
<SolidColorBrush x:Key="PosSelRow" Color="#132A1D"/>
<SolidColorBrush x:Key="PosDanger" Color="#F87171"/> <SolidColorBrush x:Key="PosDanger" Color="#F87171"/>
<SolidColorBrush x:Key="PosDangerSoft" Color="#3A1A1A"/> <SolidColorBrush x:Key="PosDangerSoft" Color="#3A1A1A"/>
<SolidColorBrush x:Key="PosDangerBorder" Color="#5A2A2A"/>
<SolidColorBrush x:Key="PosWarn" Color="#FBBF24"/> <SolidColorBrush x:Key="PosWarn" Color="#FBBF24"/>
<SolidColorBrush x:Key="PosWarnSoft" Color="#33280D"/> <SolidColorBrush x:Key="PosWarnSoft" Color="#33280D"/>
<SolidColorBrush x:Key="PosScrim" Color="#66000000"/> <SolidColorBrush x:Key="PosScrim" Color="#6B000000"/>
</ResourceDictionary> </ResourceDictionary>
</ResourceDictionary.ThemeDictionaries> </ResourceDictionary.ThemeDictionaries>

View file

@ -0,0 +1,72 @@
using SkiaSharp;
namespace foodmarket.Pos.Tooling;
/// <summary>Складывает «макет | касса» рядом в одно изображение для поэлементной
/// сверки вёрстки. Использует уже отрисованные PNG: docs/pos-design/mockup-*.png
/// (эталон) и docs/pos-screenshots/*.png (наш рендер).
/// Запуск: dotnet run -- --compare</summary>
public static class CompareRunner
{
private sealed record Pair(string Name, string Mockup, string Shot);
public static int Run(string[] args)
{
var root = FindRepoRoot();
var design = Path.Combine(root, "docs", "pos-design");
var shots = Path.Combine(root, "docs", "pos-screenshots");
Directory.CreateDirectory(design);
var pairs = new[]
{
new Pair("compare-sale", Path.Combine(design, "mockup-sale-1920x1080.png"), Path.Combine(shots, "04-sale-items-1920x1080.png")),
new Pair("compare-payment", Path.Combine(design, "mockup-payment-1920x1080.png"), Path.Combine(shots, "05-payment-cash-1920x1080.png")),
};
foreach (var p in pairs)
{
if (!File.Exists(p.Mockup)) { Console.WriteLine($"[skip] нет макета {p.Mockup}"); continue; }
if (!File.Exists(p.Shot)) { Console.WriteLine($"[skip] нет рендера {p.Shot}"); continue; }
using var left = SKBitmap.Decode(p.Mockup);
using var right = SKBitmap.Decode(p.Shot);
const int gap = 24;
const int header = 56;
int h = Math.Max(left.Height, right.Height);
int w = left.Width + right.Width + gap;
var outPath = Path.Combine(design, p.Name + ".png");
using var surface = SKSurface.Create(new SKImageInfo(w, h + header));
var c = surface.Canvas;
c.Clear(new SKColor(0xB4, 0xB4, 0xB4));
using var font = new SKPaint { Color = SKColors.White, TextSize = 26, IsAntialias = true, FakeBoldText = true };
c.DrawText("МАКЕТ (эталон)", 16, 38, font);
c.DrawText("КАССА (Avalonia)", left.Width + gap + 16, 38, font);
c.DrawBitmap(left, 0, header);
c.DrawBitmap(right, left.Width + gap, header);
// вертикальный разделитель
using var line = new SKPaint { Color = new SKColor(0x30, 0x30, 0x30), StrokeWidth = 2 };
c.DrawLine(left.Width + gap / 2f, header, left.Width + gap / 2f, h + header, line);
using var img = surface.Snapshot();
using var data = img.Encode(SKEncodedImageFormat.Png, 90);
using var fs = File.OpenWrite(outPath);
data.SaveTo(fs);
Console.WriteLine($"[compare] {outPath} ({w}×{h + header})");
}
return 0;
}
private static string FindRepoRoot()
{
var dir = AppContext.BaseDirectory;
while (dir is not null && !Directory.Exists(Path.Combine(dir, ".git")))
dir = Directory.GetParent(dir)?.FullName;
return dir ?? Directory.GetCurrentDirectory();
}
}

View file

@ -78,18 +78,15 @@ private static Control ShellWith(SaleViewModel sale)
vm.CurrentPage = sale; vm.CurrentPage = sale;
return new MainWindow { DataContext = vm }; return new MainWindow { DataContext = vm };
} }
/// <summary>Модальная оплата на затемнённой подложке (как в оболочке): /// <summary>Модальная оплата поверх затемнённого экрана продажи — как в оболочке
/// светлый фон → полупрозрачная тёмная подложка → белое модальное окно.</summary> /// и как в макете: чек виден под полупрозрачной тёмной подложкой.</summary>
private static Control PayScrim(PaymentViewModel pay) private static Control PayScrim(PaymentViewModel pay)
=> new Border
{ {
Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#F4F6F5")), var vm = new MainWindowViewModel(); // в CurrentPage уже лежит пример продажи
Child = new Border vm.Payment = pay;
{ vm.IsPaymentOpen = true;
Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#660F1F17")), return new MainWindow { DataContext = vm };
Child = new PaymentView { DataContext = pay }, }
},
};
private static Control ShellOffline() private static Control ShellOffline()
{ {
@ -160,7 +157,9 @@ private static void WriteGallery(string dir, List<(string file, string caption)>
foreach (var w in wins) foreach (var w in wins)
{ {
var rel = "windows/" + Path.GetFileName(w); var rel = "windows/" + Path.GetFileName(w);
sb.Append($"<figure><a href=\"{rel}\" target=\"_blank\"><img src=\"{rel}\" loading=\"lazy\"></a><figcaption>Windows 11 — экран продажи (реальное окно)</figcaption></figure>"); var name = Path.GetFileNameWithoutExtension(w);
var label = name.Contains("pay", StringComparison.OrdinalIgnoreCase) ? "экран оплаты" : "экран продажи";
sb.Append($"<figure><a href=\"{rel}\" target=\"_blank\"><img src=\"{rel}\" loading=\"lazy\"></a><figcaption>Windows 11 — {label} (реальное окно)</figcaption></figure>");
} }
sb.Append("</div>"); sb.Append("</div>");
} }

View file

@ -25,7 +25,8 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty] private string _cashierName = "—"; [ObservableProperty] private string _cashierName = "—";
[ObservableProperty] private string _shiftNumber = "смена не открыта"; [ObservableProperty] private string _shiftNumber = "смена не открыта";
[ObservableProperty] private bool _isShiftOpen; [ObservableProperty] private bool _isShiftOpen;
[ObservableProperty] private string _pointName = "Магазин"; [ObservableProperty] private string _storeName = "Магазин";
[ObservableProperty] private string _pointName = "Касса";
[ObservableProperty] private bool _isOnline = true; [ObservableProperty] private bool _isOnline = true;
[ObservableProperty] private int _queueCount; [ObservableProperty] private int _queueCount;
[ObservableProperty] private PaymentViewModel? _payment; [ObservableProperty] private PaymentViewModel? _payment;
@ -34,14 +35,14 @@ public partial class MainWindowViewModel : ViewModelBase
public string NetBadge => IsOnline ? "На связи" : "Нет связи"; public string NetBadge => IsOnline ? "На связи" : "Нет связи";
public string ShiftBadge => IsShiftOpen ? $"Смена {ShiftNumber}" : "Смена закрыта"; public string ShiftBadge => IsShiftOpen ? $"Смена {ShiftNumber}" : "Смена закрыта";
public string QueueBadge => QueueCount > 0 ? $"· {QueueCount} в очереди" : ""; public string QueueBadge => QueueCount > 0 ? $"· {QueueCount} в очереди" : "· очередь пуста";
partial void OnIsOnlineChanged(bool value) { OnPropertyChanged(nameof(NetBadge)); } partial void OnIsOnlineChanged(bool value) { OnPropertyChanged(nameof(NetBadge)); }
partial void OnQueueCountChanged(int value) { OnPropertyChanged(nameof(QueueBadge)); } partial void OnQueueCountChanged(int value) { OnPropertyChanged(nameof(QueueBadge)); }
partial void OnIsShiftOpenChanged(bool value) => OnPropertyChanged(nameof(ShiftBadge)); partial void OnIsShiftOpenChanged(bool value) => OnPropertyChanged(nameof(ShiftBadge));
partial void OnShiftNumberChanged(string value) => OnPropertyChanged(nameof(ShiftBadge)); partial void OnShiftNumberChanged(string value) => OnPropertyChanged(nameof(ShiftBadge));
/// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary> /// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary>
public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000001"; IsShiftOpen = true; PointName = "Магазин №1 · ул. Абая 10"; } public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000012 открыта с 09:00"; IsShiftOpen = true; StoreName = "Демо-магазин"; PointName = "Касса 1"; }
public MainWindowViewModel(AppServices svc) { _svc = svc; if (svc.Session is not null) StartSession(); else ShowLogin(); } public MainWindowViewModel(AppServices svc) { _svc = svc; if (svc.Session is not null) StartSession(); else ShowLogin(); }
@ -363,9 +364,10 @@ public string QtyText
} }
} }
public bool HasDiscount => Discount > 0m; public bool HasDiscount => Discount > 0m;
/// <summary>Пояснение скидки в строке, чтобы цена×кол-во ≠ сумма не выглядело /// <summary>Подпись цены за единицу: «280 ₸ за шт», «690 ₸ за кг».</summary>
/// ошибкой: «1 560 → 1 500 ₸, скидка 60».</summary> public string UnitPriceText => $"{Price:N0} ₸ за {Unit}";
public string DiscountText => $"{Gross:N0} → {LineTotal:N0} ₸, скидка {Discount:N0}"; /// <summary>Строка со скидкой: «520 ₸ за шт · скидка 60 ₸ (1 560 → 1 500 ₸)».</summary>
public string DiscountText => $"{Price:N0} ₸ за {Unit} · скидка {Discount:N0} ₸ ({Gross:N0} → {LineTotal:N0} ₸)";
partial void OnQuantityChanged(decimal value) partial void OnQuantityChanged(decimal value)
{ OnPropertyChanged(nameof(LineTotal)); OnPropertyChanged(nameof(QtyText)); OnPropertyChanged(nameof(Gross)); OnPropertyChanged(nameof(DiscountText)); } { OnPropertyChanged(nameof(LineTotal)); OnPropertyChanged(nameof(QtyText)); OnPropertyChanged(nameof(Gross)); OnPropertyChanged(nameof(DiscountText)); }
partial void OnDiscountChanged(decimal value) partial void OnDiscountChanged(decimal value)
@ -396,6 +398,10 @@ public partial class SaleViewModel : ViewModelBase
[ObservableProperty] private SaleLineVm? _selectedLine; [ObservableProperty] private SaleLineVm? _selectedLine;
public bool IsCartEmpty => ItemCount == 0; public bool IsCartEmpty => ItemCount == 0;
public bool HasAnyDiscount => Discount > 0m; public bool HasAnyDiscount => Discount > 0m;
// Блок «Последний чек / Чеков за смену» (информационный, заполняет панель).
[ObservableProperty] private string _lastReceiptNumber = "—";
[ObservableProperty] private string _lastReceiptInfo = "—";
[ObservableProperty] private int _receiptsInShift;
partial void OnItemCountChanged(int value) => OnPropertyChanged(nameof(IsCartEmpty)); partial void OnItemCountChanged(int value) => OnPropertyChanged(nameof(IsCartEmpty));
partial void OnDiscountChanged(decimal value) => OnPropertyChanged(nameof(HasAnyDiscount)); partial void OnDiscountChanged(decimal value) => OnPropertyChanged(nameof(HasAnyDiscount));
@ -520,7 +526,9 @@ public async Task CommitPaymentAsync(decimal cash, decimal card, decimal change)
[RelayCommand] private void Clear() { _cart.Clear(); Lines.Clear(); SelectedLine = null; Recalc(); Message = "Чек очищен"; } [RelayCommand] private void Clear() { _cart.Clear(); Lines.Clear(); SelectedLine = null; Recalc(); Message = "Чек очищен"; }
public int ParkedCount => _parked.Count; private int _demoParked; // только для скриншотов/демо (Sample)
public int ParkedCount => _parked.Count + _demoParked;
public bool HasParked => ParkedCount > 0;
/// <summary>Отложить текущий чек и начать новый (обслужить другого покупателя).</summary> /// <summary>Отложить текущий чек и начать новый (обслужить другого покупателя).</summary>
[RelayCommand] [RelayCommand]
@ -567,12 +575,15 @@ public static SaleViewModel Sample()
vm.Total = decimal.Round(vm.Subtotal - vm.Discount, 2); vm.Total = decimal.Round(vm.Subtotal - vm.Discount, 2);
vm.ItemCount = vm.Lines.Count; vm.ItemCount = vm.Lines.Count;
vm.Message = "Чек готов к оплате — F12 Оплата"; vm.Message = "Чек готов к оплате — F12 Оплата";
vm.SelectedLine = vm.Lines[3]; // выбранная строка (Сахар) — зелёный акцент
vm.LastReceiptNumber = "№ 000148"; vm.LastReceiptInfo = "1 250 ₸ · нал"; vm.ReceiptsInShift = 47;
vm._demoParked = 2; // в демо — 2 отложенных чека (бейдж на кнопке «Отложенные»)
PosProduct Fav(string n, string unit, decimal price, bool weighed) => new() PosProduct Fav(string n, string unit, decimal price, bool weighed) => new()
{ Id = Guid.NewGuid(), Name = n, Barcodes = Array.Empty<string>(), UnitCode = unit, Packaging = weighed ? PosPackaging.Weight : PosPackaging.Piece, VatPercent = 12m, VatEnabled = true, Price = price, IsFavorite = true }; { Id = Guid.NewGuid(), Name = n, Barcodes = Array.Empty<string>(), UnitCode = unit, Packaging = weighed ? PosPackaging.Weight : PosPackaging.Piece, VatPercent = 12m, VatEnabled = true, Price = price, IsFavorite = true };
foreach (var f in new[] { foreach (var f in new[] {
Fav("Хлеб «Семейный»", "шт", 280m, false), Fav("Молоко 2.5%", "шт", 450m, false), Fav("Хлеб «Семейный»", "шт", 280m, false), Fav("Молоко 2,5%", "шт", 450m, false),
Fav("Яблоки (вес)", "кг", 690m, true), Fav("Картофель (вес)", "кг", 280m, true), Fav("Яблоки (вес)", "кг", 690m, true), Fav("Картофель (вес)", "кг", 280m, true),
Fav("Пакет-майка", "шт", 20m, false), Fav("Вода 0,5л", "шт", 150m, false) }) Fav("Пакет-майка", "шт", 20m, false), Fav("Вода 0,5 л", "шт", 150m, false) })
vm.Favorites.Add(f); vm.Favorites.Add(f);
return vm; return vm;
} }

View file

@ -7,63 +7,61 @@
Background="{DynamicResource PosBg}" Title="Касса food-market" Background="{DynamicResource PosBg}" Title="Касса food-market"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<Panel> <Panel>
<Grid RowDefinitions="64,*,46"> <Grid RowDefinitions="64,*,40">
<!-- Верхняя полоса --> <!-- Верхняя полоса -->
<Border Grid.Row="0" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1" Padding="20,0"> <Border Grid.Row="0" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1" Padding="24,0">
<Grid ColumnDefinitions="Auto,*,Auto"> <StackPanel Orientation="Horizontal" Spacing="24" VerticalAlignment="Center">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center"> <TextBlock VerticalAlignment="Center" FontSize="20">
<Border Background="{DynamicResource PosBrand}" CornerRadius="9" Width="34" Height="34"> <Run Text="FOOD MARKET" Foreground="{DynamicResource PosBrand}" FontWeight="ExtraBold"/><Run Text=" · Касса" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
<TextBlock Text="FM" Foreground="White" FontWeight="Bold" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Center"/> </TextBlock>
</Border> <Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/>
<StackPanel VerticalAlignment="Center"> <TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}">
<TextBlock Text="Food Market · Касса" Foreground="{DynamicResource PosText}" FontWeight="Bold" FontSize="16"/> <Run Text="Магазин "/><Run Text="{Binding StoreName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/><Run Text=" · Точка "/><Run Text="{Binding PointName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
<TextBlock Text="{Binding PointName}" Foreground="{DynamicResource PosTextMuted}" FontSize="12.5"/> </TextBlock>
</StackPanel> <Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/>
</StackPanel> <TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}">
<Run Text="Кассир "/><Run Text="{Binding CashierName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
</TextBlock>
<Border Width="1" Height="28" Background="{DynamicResource PosBorder}"
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock VerticalAlignment="Center" FontSize="13" Foreground="{DynamicResource PosWarn}" Text="{Binding UpdateMessage}"
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Right">
<Border Classes="chip" Classes.ok="{Binding IsShiftOpen}" Classes.warn="{Binding !IsShiftOpen}"> <Border Classes="chip" Classes.ok="{Binding IsShiftOpen}" Classes.warn="{Binding !IsShiftOpen}">
<StackPanel Orientation="Horizontal" Spacing="8">
<Ellipse Width="8" Height="8" VerticalAlignment="Center" Fill="{DynamicResource PosBrand}"/>
<TextBlock Text="{Binding ShiftBadge}"/> <TextBlock Text="{Binding ShiftBadge}"/>
</StackPanel>
</Border> </Border>
<Border Classes="chip" Classes.ok="{Binding IsOnline}" Classes.warn="{Binding !IsOnline}"> <Border Classes="chip net">
<StackPanel Orientation="Horizontal" Spacing="6"> <StackPanel Orientation="Horizontal" Spacing="8">
<Ellipse Width="8" Height="8" VerticalAlignment="Center" Fill="{DynamicResource PosBrand}"/>
<TextBlock Text="{Binding NetBadge}"/> <TextBlock Text="{Binding NetBadge}"/>
<TextBlock Text="{Binding QueueBadge}" IsVisible="{Binding QueueBadge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/> <TextBlock Text="{Binding QueueBadge}" IsVisible="{Binding QueueBadge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
</Border> </Border>
<Button Classes="pos ghost" Width="42" Height="38" Padding="0" Command="{Binding ToggleThemeCommand}" ToolTip.Tip="Тёмная / светлая тема">
<PathIcon Width="18" Height="18" Foreground="{DynamicResource PosTextMuted}"
Data="M12 3c-4.97 0-9 4.03-9 9s4.03 9 9 9 9-4.03 9-9c0-.46-.04-.92-.1-1.36-.98 1.37-2.58 2.26-4.4 2.26-2.98 0-5.4-2.42-5.4-5.4 0-1.81.89-3.42 2.26-4.4-.44-.06-.9-.1-1.36-.1z"/>
</Button>
<Border Width="1" Height="26" Background="{DynamicResource PosBorder}" Margin="4,0"/>
<StackPanel VerticalAlignment="Center" HorizontalAlignment="Right">
<TextBlock Text="{Binding CashierName}" Foreground="{DynamicResource PosText}" FontSize="14" FontWeight="SemiBold" HorizontalAlignment="Right"/>
<TextBlock Text="кассир" Foreground="{DynamicResource PosTextMuted}" FontSize="12" HorizontalAlignment="Right"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<TextBlock Grid.Column="1" Text="{Binding UpdateMessage}" Foreground="{DynamicResource PosWarn}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="12.5"
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</Grid>
</Border> </Border>
<!-- Текущая страница --> <!-- Текущая страница -->
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}"/> <ContentControl Grid.Row="1" Content="{Binding CurrentPage}"/>
<!-- Горячие клавиши --> <!-- Полоса горячих клавиш -->
<Border Grid.Row="2" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0"> <Border Grid.Row="2" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0" Padding="24,0">
<ItemsControl VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Orientation="Horizontal" Spacing="28" VerticalAlignment="Center" HorizontalAlignment="Center">
<ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="8"/></ItemsPanelTemplate></ItemsControl.ItemsPanel> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F1"/></Border><TextBlock Text="Помощь" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="F1" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosTextMuted}"/><TextBlock Text="Помощь" FontSize="12" Foreground="{DynamicResource PosTextMuted}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F2"/></Border><TextBlock Text="Поиск" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="F2" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosTextMuted}"/><TextBlock Text="Поиск" FontSize="12" Foreground="{DynamicResource PosTextMuted}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F4"/></Border><TextBlock Text="Скидка" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="F7" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosTextMuted}"/><TextBlock Text="Отложить" FontSize="12" Foreground="{DynamicResource PosTextMuted}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F7"/></Border><TextBlock Text="Отложить" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="F9" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosTextMuted}"/><TextBlock Text="Возврат" FontSize="12" Foreground="{DynamicResource PosTextMuted}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F9"/></Border><TextBlock Text="Возврат" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="F10" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosTextMuted}"/><TextBlock Text="Смена" FontSize="12" Foreground="{DynamicResource PosTextMuted}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F10"/></Border><TextBlock Text="Смена" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="F12" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosBrand}"/><TextBlock Text="Оплата" FontSize="12" Foreground="{DynamicResource PosBrand}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="F12"/></Border><TextBlock Text="Оплата" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="Del" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosTextMuted}"/><TextBlock Text="Удалить" FontSize="12" Foreground="{DynamicResource PosTextMuted}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="Del"/></Border><TextBlock Text="Удалить строку" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
<Border Classes="hotkey"><StackPanel Orientation="Horizontal" Spacing="6"><TextBlock Text="Esc" FontWeight="Bold" FontSize="11.5" Foreground="{DynamicResource PosTextMuted}"/><TextBlock Text="Отмена" FontSize="12" Foreground="{DynamicResource PosTextMuted}"/></StackPanel></Border> <StackPanel Orientation="Horizontal" Spacing="8"><Border Classes="kbd"><TextBlock Text="Esc"/></Border><TextBlock Text="Отмена" FontSize="14" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/></StackPanel>
</ItemsControl> </StackPanel>
</Border> </Border>
</Grid> </Grid>

View file

@ -3,76 +3,105 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels" xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.PaymentView" x:Class="foodmarket.Pos.Views.PaymentView"
x:DataType="vm:PaymentViewModel"> x:DataType="vm:PaymentViewModel">
<Border Classes="card" Width="720" Padding="28" CornerRadius="18" VerticalAlignment="Center" HorizontalAlignment="Center"> <Border Width="880" Background="{DynamicResource PosCard}" CornerRadius="20" Padding="32" VerticalAlignment="Center" HorizontalAlignment="Center"
<StackPanel Spacing="16"> BoxShadow="0 24 64 0 #40000000">
<StackPanel Spacing="24">
<!-- Заголовок -->
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Оплата" FontSize="22" FontWeight="Bold" Foreground="{DynamicResource PosText}" VerticalAlignment="Center"/> <TextBlock Text="Оплата" FontSize="24" FontWeight="Bold" Foreground="{DynamicResource PosText}" VerticalAlignment="Bottom"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Bottom">
<TextBlock Text="К оплате" Foreground="{DynamicResource PosTextMuted}" FontSize="16" VerticalAlignment="Center"/> <TextBlock Text="К оплате" Foreground="{DynamicResource PosTextMuted}" FontSize="16" VerticalAlignment="Bottom" Margin="0,0,0,6"/>
<TextBlock Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="30" FontWeight="Bold"/> <TextBlock Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="40" FontWeight="ExtraBold"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<!-- Сегменты способа оплаты (слитный переключатель) --> <!-- Сегменты способа оплаты (в серой подложке) -->
<Border Background="{DynamicResource PosSubtle}" CornerRadius="12" Padding="4"> <Border Background="{DynamicResource PosChipNet}" CornerRadius="12" Padding="4">
<UniformGrid Columns="3"> <UniformGrid Columns="3">
<Button Classes="pos seg" Classes.on="{Binding IsCash}" Margin="2" Content="Наличные" Command="{Binding SelectCashCommand}"/> <Button Classes="pos seg" Classes.on="{Binding IsCash}" Margin="2" Command="{Binding SelectCashCommand}">
<Button Classes="pos seg" Classes.on="{Binding IsCard}" Margin="2" Content="Карта" Command="{Binding SelectCardCommand}"/> <StackPanel Orientation="Horizontal" Spacing="10">
<Button Classes="pos seg" Classes.on="{Binding IsMixed}" Margin="2" Content="Смешанная" Command="{Binding SelectMixedCommand}"/> <Path Width="20" Height="20" Stretch="Uniform" StrokeThickness="2" StrokeJoin="Round" Data="M3 6h18v12H3zM9 12a3 3 0 106 0 3 3 0 10-6 0"/>
<TextBlock Text="Наличные" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Classes="pos seg" Classes.on="{Binding IsCard}" Margin="2" Command="{Binding SelectCardCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="20" Height="20" Stretch="Uniform" StrokeThickness="2" StrokeJoin="Round" Data="M3 6h18v12H3zM3 10h18"/>
<TextBlock Text="Карта" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Classes="pos seg" Classes.on="{Binding IsMixed}" Margin="2" Command="{Binding SelectMixedCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="20" Height="20" Stretch="Uniform" StrokeThickness="2" StrokeLineCap="Round" StrokeJoin="Round" Data="M7 7h12l-3-3M17 17H5l3 3"/>
<TextBlock Text="Смешанная" VerticalAlignment="Center"/>
</StackPanel>
</Button>
</UniformGrid> </UniformGrid>
</Border> </Border>
<!-- Тело: поле+быстрые суммы | клавиатура -->
<Grid ColumnDefinitions="*,300"> <Grid ColumnDefinitions="*,300">
<!-- Левая часть: суммы --> <StackPanel Grid.Column="0" Margin="0,0,24,0">
<StackPanel Grid.Column="0" Spacing="12" Margin="0,0,20,0"> <TextBlock Text="Получено наличными" Foreground="{DynamicResource PosTextMuted}" FontSize="14" FontWeight="SemiBold" Margin="0,0,0,8"/>
<StackPanel Spacing="4"> <Border Height="64" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBrand}" BorderThickness="2" CornerRadius="12" Padding="20,0">
<TextBlock Text="Получено наличными" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/> <TextBlock Text="{Binding PaidCash, StringFormat='{}{0:N0} ₸'}" FontSize="30" FontWeight="Bold" Foreground="{DynamicResource PosText}" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<TextBox Classes="pos" FontSize="24" Text="{Binding PaidCash}"/> </Border>
<StackPanel Spacing="4" Margin="0,10,0,0" IsVisible="{Binding IsMixed}">
<TextBlock Text="Картой" Foreground="{DynamicResource PosTextMuted}" FontSize="14" FontWeight="SemiBold"/>
<TextBox Classes="pos" FontSize="22" Text="{Binding PaidCard}"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding IsMixed}">
<TextBlock Text="Картой" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/> <!-- Быстрые суммы 3×2 -->
<TextBox Classes="pos" FontSize="24" Text="{Binding PaidCard}"/> <UniformGrid Columns="3" Margin="0,14,0,0" IsVisible="{Binding !IsCard}">
</StackPanel> <Button Classes="pos" FontSize="16" Height="52" Margin="0,0,5,10" Foreground="{DynamicResource PosBrandDark}" Background="{DynamicResource PosBrandSoft}" BorderBrush="#BFE6CD" Content="Без сдачи" Command="{Binding QuickCashCommand}" CommandParameter="exact"/>
<!-- Быстрые суммы --> <Button Classes="pos ghost" FontSize="16" Height="52" Margin="5,0,5,10" Content="4 000" Command="{Binding QuickCashCommand}" CommandParameter="4000"/>
<UniformGrid Columns="3" IsVisible="{Binding !IsCard}"> <Button Classes="pos ghost" FontSize="16" Height="52" Margin="5,0,0,10" Content="5 000" Command="{Binding QuickCashCommand}" CommandParameter="5000"/>
<Button Classes="pos ghost" Margin="3" FontSize="14.5" Padding="4,0" Content="Без сдачи" Command="{Binding QuickCashCommand}" CommandParameter="exact"/> <Button Classes="pos ghost" FontSize="16" Height="52" Margin="0,0,5,0" Content="10 000" Command="{Binding QuickCashCommand}" CommandParameter="10000"/>
<Button Classes="pos ghost" Margin="3" Content="1 000" Command="{Binding QuickCashCommand}" CommandParameter="1000"/> <Button Classes="pos ghost" FontSize="16" Height="52" Margin="5,0,5,0" Content="20 000" Command="{Binding QuickCashCommand}" CommandParameter="20000"/>
<Button Classes="pos ghost" Margin="3" Content="2 000" Command="{Binding QuickCashCommand}" CommandParameter="2000"/> <Button Classes="pos ghost" FontSize="16" Height="52" Margin="5,0,0,0" Content="Своя сумма" Command="{Binding KeyClearCommand}"/>
<Button Classes="pos ghost" Margin="3" Content="5 000" Command="{Binding QuickCashCommand}" CommandParameter="5000"/>
<Button Classes="pos ghost" Margin="3" Content="10 000" Command="{Binding QuickCashCommand}" CommandParameter="10000"/>
<Button Classes="pos ghost" Margin="3" Content="20 000" Command="{Binding QuickCashCommand}" CommandParameter="20000"/>
</UniformGrid> </UniformGrid>
<!-- Сдача --> <!-- Сдача -->
<Border Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Padding="16,14"> <Border Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Height="84" Margin="0,14,0,0" Padding="22,0">
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="СДАЧА" Foreground="{DynamicResource PosText}" FontSize="18" FontWeight="SemiBold" VerticalAlignment="Center"/> <TextBlock Text="СДАЧА" Foreground="{DynamicResource PosBrandDark}" FontSize="16" FontWeight="Bold" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Change, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrand}" FontSize="36" FontWeight="Bold"/> <TextBlock Grid.Column="1" Text="{Binding Change, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrandDark}" FontSize="44" FontWeight="ExtraBold" VerticalAlignment="Center"/>
</Grid> </Grid>
</Border> </Border>
</StackPanel> </StackPanel>
<!-- Правая часть: цифровая клавиатура 3×4 (ровные квадраты) --> <!-- Клавиатура 3×4 одинаковых 72px -->
<UniformGrid Grid.Column="1" Columns="3" Rows="4" VerticalAlignment="Top" IsVisible="{Binding !IsCard}"> <UniformGrid Grid.Column="1" Columns="3" Rows="4" VerticalAlignment="Top" IsVisible="{Binding !IsCard}">
<Button Classes="key" Margin="4" Height="72" Content="1" Command="{Binding KeyDigitCommand}" CommandParameter="1"/> <Button Classes="key" Margin="5" Height="72" Content="1" Command="{Binding KeyDigitCommand}" CommandParameter="1"/>
<Button Classes="key" Margin="4" Height="72" Content="2" Command="{Binding KeyDigitCommand}" CommandParameter="2"/> <Button Classes="key" Margin="5" Height="72" Content="2" Command="{Binding KeyDigitCommand}" CommandParameter="2"/>
<Button Classes="key" Margin="4" Height="72" Content="3" Command="{Binding KeyDigitCommand}" CommandParameter="3"/> <Button Classes="key" Margin="5" Height="72" Content="3" Command="{Binding KeyDigitCommand}" CommandParameter="3"/>
<Button Classes="key" Margin="4" Height="72" Content="4" Command="{Binding KeyDigitCommand}" CommandParameter="4"/> <Button Classes="key" Margin="5" Height="72" Content="4" Command="{Binding KeyDigitCommand}" CommandParameter="4"/>
<Button Classes="key" Margin="4" Height="72" Content="5" Command="{Binding KeyDigitCommand}" CommandParameter="5"/> <Button Classes="key" Margin="5" Height="72" Content="5" Command="{Binding KeyDigitCommand}" CommandParameter="5"/>
<Button Classes="key" Margin="4" Height="72" Content="6" Command="{Binding KeyDigitCommand}" CommandParameter="6"/> <Button Classes="key" Margin="5" Height="72" Content="6" Command="{Binding KeyDigitCommand}" CommandParameter="6"/>
<Button Classes="key" Margin="4" Height="72" Content="7" Command="{Binding KeyDigitCommand}" CommandParameter="7"/> <Button Classes="key" Margin="5" Height="72" Content="7" Command="{Binding KeyDigitCommand}" CommandParameter="7"/>
<Button Classes="key" Margin="4" Height="72" Content="8" Command="{Binding KeyDigitCommand}" CommandParameter="8"/> <Button Classes="key" Margin="5" Height="72" Content="8" Command="{Binding KeyDigitCommand}" CommandParameter="8"/>
<Button Classes="key" Margin="4" Height="72" Content="9" Command="{Binding KeyDigitCommand}" CommandParameter="9"/> <Button Classes="key" Margin="5" Height="72" Content="9" Command="{Binding KeyDigitCommand}" CommandParameter="9"/>
<Button Classes="key" Margin="4" Height="72" FontSize="15" Content="Сброс" Command="{Binding KeyClearCommand}"/> <Button Classes="key" Margin="5" Height="72" FontSize="16" Foreground="{DynamicResource PosTextMuted}" Content="Сброс" Command="{Binding KeyClearCommand}"/>
<Button Classes="key" Margin="4" Height="72" Content="0" Command="{Binding KeyDigitCommand}" CommandParameter="0"/> <Button Classes="key" Margin="5" Height="72" Content="0" Command="{Binding KeyDigitCommand}" CommandParameter="0"/>
<Button Classes="key" Margin="4" Height="72" FontSize="20" Content="⌫" Command="{Binding KeyBackCommand}"/> <Button Classes="key" Margin="5" Height="72" FontSize="20" Foreground="{DynamicResource PosTextMuted}" Content="⌫" Command="{Binding KeyBackCommand}"/>
</UniformGrid> </UniformGrid>
</Grid> </Grid>
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="66" FontSize="20" Command="{Binding ConfirmCommand}" IsDefault="True" IsEnabled="{Binding Sufficient}"> <!-- Действия: Назад 200px | Провести 1fr -->
<TextBlock Text="Провести и печатать · Enter" TextAlignment="Center"/> <Grid ColumnDefinitions="200,*">
<Button Classes="pos ghost" Height="68" Margin="0,0,12,0" Command="{Binding CancelCommand}">
<StackPanel Orientation="Horizontal" Spacing="10" HorizontalAlignment="Center">
<Border Classes="kbd"><TextBlock Text="Esc"/></Border>
<TextBlock Text="Назад" FontSize="17" VerticalAlignment="Center"/>
</StackPanel>
</Button> </Button>
<Button Classes="pos ghost" HorizontalAlignment="Stretch" Height="44" Content="Esc · Назад" Command="{Binding CancelCommand}"/> <Button Grid.Column="1" Classes="pos primary" Height="68" Command="{Binding ConfirmCommand}" IsDefault="True" IsEnabled="{Binding Sufficient}">
<StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Center">
<TextBlock Text="Провести и печатать" FontSize="21" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/>
<Border Classes="kbd onbrand"><TextBlock Text="Enter"/></Border>
</StackPanel>
</Button>
</Grid>
</StackPanel> </StackPanel>
</Border> </Border>
</UserControl> </UserControl>

View file

@ -3,93 +3,107 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels" xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.SaleView" x:Class="foodmarket.Pos.Views.SaleView"
x:DataType="vm:SaleViewModel"> x:DataType="vm:SaleViewModel">
<Grid ColumnDefinitions="*,400" Background="{DynamicResource PosBg}"> <Grid ColumnDefinitions="*,440" Background="{DynamicResource PosBg}">
<!-- ЛЕВО: поиск + избранное + корзина --> <!-- ЛЕВО -->
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*,Auto" Margin="20,18,10,18"> <Grid Grid.Column="0" RowDefinitions="Auto,Auto,Auto,*" Margin="24,16,8,16">
<!-- Поиск / скан --> <!-- Поиск / скан -->
<Border Grid.Row="0" Classes="card" Padding="0" Margin="0,0,0,14"> <Border Grid.Row="0" Height="64" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBrand}" BorderThickness="2" CornerRadius="14"
<Grid ColumnDefinitions="Auto,*"> BoxShadow="0 0 0 4 #1F16A34A" Margin="0,0,0,16">
<PathIcon Grid.Column="0" Width="22" Height="22" Margin="18,0,8,0" Foreground="{DynamicResource PosTextMuted}" <Grid ColumnDefinitions="Auto,*,Auto">
Data="M15.5 14h-.79l-.28-.27C15.41 12.59 16 11.11 16 9.5 16 5.91 13.09 3 9.5 3S3 5.91 3 9.5 5.91 16 9.5 16c1.61 0 3.09-.59 4.23-1.57l.27.28v.79l5 4.99L20.49 19l-4.99-5zm-6 0C7.01 14 5 11.99 5 9.5S7.01 5 9.5 5 14 7.01 14 9.5 11.99 14 9.5 14z"/> <Path Grid.Column="0" Width="24" Height="24" Margin="20,0,14,0" Stretch="Uniform" Stroke="{DynamicResource PosBrand}" StrokeThickness="2" StrokeLineCap="Round"
Data="M3 5v14M7 5v14M11 5v14M15 5v14M19 5v14"/>
<TextBox Grid.Column="1" BorderThickness="0" FontSize="19" Background="Transparent" VerticalContentAlignment="Center" <TextBox Grid.Column="1" BorderThickness="0" FontSize="19" Background="Transparent" VerticalContentAlignment="Center"
Watermark="Отсканируйте товар или начните вводить название" Text="{Binding ScanInput}"> Watermark="Отсканируйте товар или начните вводить название" Text="{Binding ScanInput}">
<TextBox.KeyBindings><KeyBinding Gesture="Enter" Command="{Binding ScanCommand}"/></TextBox.KeyBindings> <TextBox.KeyBindings><KeyBinding Gesture="Enter" Command="{Binding ScanCommand}"/></TextBox.KeyBindings>
</TextBox> </TextBox>
<Border Grid.Column="2" Classes="kbd" Margin="0,0,16,0"><TextBlock Text="F2"/></Border>
</Grid> </Grid>
</Border> </Border>
<!-- Избранное (быстрые товары) --> <!-- Заголовок избранного -->
<StackPanel Grid.Row="1" Spacing="8" Margin="0,0,0,14" IsVisible="{Binding HasFavorites}"> <Grid Grid.Row="1" ColumnDefinitions="*,Auto" Margin="0,0,0,12" IsVisible="{Binding HasFavorites}">
<Button Classes="pos ghost" HorizontalAlignment="Left" Height="32" Padding="12,0" FontSize="13.5" Command="{Binding ToggleFavoritesCommand}" BorderThickness="0" Background="Transparent"> <TextBlock Text="ИЗБРАННОЕ" FontSize="15" FontWeight="Bold" Foreground="{DynamicResource PosTextMuted}" VerticalAlignment="Center"/>
<StackPanel Orientation="Horizontal" Spacing="6"> <Button Grid.Column="1" Background="Transparent" BorderThickness="0" Padding="0" Command="{Binding ToggleFavoritesCommand}" HorizontalAlignment="Right">
<PathIcon Width="12" Height="12" Foreground="{DynamicResource PosTextMuted}" IsVisible="{Binding FavoritesOpen}" Data="M7 10l5 5 5-5z"/> <Panel>
<PathIcon Width="12" Height="12" Foreground="{DynamicResource PosTextMuted}" IsVisible="{Binding !FavoritesOpen}" Data="M10 17l5-5-5-5z"/> <TextBlock FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource PosBrandDark}" Text="Скрыть ▴" IsVisible="{Binding FavoritesOpen}"/>
<TextBlock Text="Избранное" Foreground="{DynamicResource PosTextMuted}" FontWeight="Medium"/> <TextBlock FontSize="14" FontWeight="SemiBold" Foreground="{DynamicResource PosBrandDark}" Text="Показать ▾" IsVisible="{Binding !FavoritesOpen}"/>
</StackPanel> </Panel>
</Button> </Button>
<ItemsControl ItemsSource="{Binding Favorites}" IsVisible="{Binding FavoritesOpen}"> </Grid>
<ItemsControl.ItemsPanel><ItemsPanelTemplate><UniformGrid Columns="3"/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
<!-- Избранное -->
<ItemsControl Grid.Row="2" ItemsSource="{Binding Favorites}" Margin="0,0,0,16" IsVisible="{Binding FavoritesOpen}">
<ItemsControl.ItemsPanel><ItemsPanelTemplate><UniformGrid Columns="6"/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate> <ItemsControl.ItemTemplate>
<DataTemplate> <DataTemplate>
<Button Classes="pos ghost" Height="72" Margin="4" Padding="6,0" HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" <Button Height="84" Margin="6" Padding="4" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="12"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Center"
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).AddFavoriteCommand}" CommandParameter="{Binding}"> Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).AddFavoriteCommand}" CommandParameter="{Binding}">
<StackPanel Spacing="2" HorizontalAlignment="Stretch"> <StackPanel Spacing="6" HorizontalAlignment="Stretch">
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextAlignment="Center" TextWrapping="Wrap" HorizontalAlignment="Stretch" Foreground="{DynamicResource PosText}"/> <TextBlock Text="{Binding Name}" FontSize="16" FontWeight="SemiBold" TextAlignment="Center" TextWrapping="Wrap" Foreground="{DynamicResource PosText}"/>
<TextBlock Text="{Binding Price, StringFormat='{}{0:N0} ₸'}" FontSize="13" TextAlignment="Center" HorizontalAlignment="Stretch" Foreground="{DynamicResource PosBrand}"/> <TextBlock Text="{Binding PriceText}" FontSize="15" FontWeight="Bold" TextAlignment="Center" Foreground="{DynamicResource PosBrandDark}"/>
</StackPanel> </StackPanel>
</Button> </Button>
</DataTemplate> </DataTemplate>
</ItemsControl.ItemTemplate> </ItemsControl.ItemTemplate>
</ItemsControl> </ItemsControl>
</StackPanel>
<!-- Корзина --> <!-- Корзина -->
<Border Grid.Row="2" Classes="card" Padding="0"> <Border Grid.Row="3" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="14">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,180,120,52" Height="40" Margin="18,6,10,0"> <Border Grid.Row="0" Height="48" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1">
<TextBlock Text="Товар" Foreground="{DynamicResource PosTextMuted}" FontSize="13" VerticalAlignment="Center"/> <Grid ColumnDefinitions="*,200,160,56" Margin="20,0">
<TextBlock Grid.Column="1" Text="Количество" Foreground="{DynamicResource PosTextMuted}" FontSize="13" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="Товар" Foreground="{DynamicResource PosTextMuted}" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="Сумма" Foreground="{DynamicResource PosTextMuted}" FontSize="13" HorizontalAlignment="Right" VerticalAlignment="Center"/> <TextBlock Grid.Column="1" Text="Количество" Foreground="{DynamicResource PosTextMuted}" FontSize="14" FontWeight="SemiBold" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="Сумма" Foreground="{DynamicResource PosTextMuted}" FontSize="14" FontWeight="SemiBold" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid> </Grid>
</Border>
<!-- Пустая корзина -->
<StackPanel Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10" IsVisible="{Binding IsCartEmpty}"> <StackPanel Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10" IsVisible="{Binding IsCartEmpty}">
<Border Background="{DynamicResource PosSubtle}" CornerRadius="999" Width="72" Height="72"> <Border Background="{DynamicResource PosSubtle}" CornerRadius="999" Width="72" Height="72">
<PathIcon Width="34" Height="34" Foreground="{DynamicResource PosTextMuted}" <Path Width="34" Height="34" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="1.6" StrokeLineCap="Round" StrokeJoin="Round"
Data="M7 18c-1.1 0-1.99.9-1.99 2S5.9 22 7 22s2-.9 2-2-.9-2-2-2zM1 2v2h2l3.6 7.59-1.35 2.45c-.16.28-.25.61-.25.96 0 1.1.9 2 2 2h12v-2H7.42c-.14 0-.25-.11-.25-.25l.03-.12.9-1.63h7.45c.75 0 1.41-.41 1.75-1.03l3.58-6.49c.08-.14.12-.31.12-.48 0-.55-.45-1-1-1H5.21l-.94-2H1zm16 16c-1.1 0-1.99.9-1.99 2s.89 2 1.99 2 2-.9 2-2-.9-2-2-2z"/> Data="M6 6h15l-1.5 9h-12zM6 6l-2 -3M9 20a1 1 0 100 .01M18 20a1 1 0 100 .01"/>
</Border> </Border>
<TextBlock Text="Отсканируйте первый товар" Foreground="{DynamicResource PosTextMuted}" FontSize="16"/> <TextBlock Text="Отсканируйте первый товар" Foreground="{DynamicResource PosTextMuted}" FontSize="16"/>
</StackPanel> </StackPanel>
<ListBox Grid.Row="1" ItemsSource="{Binding Lines}" SelectedItem="{Binding SelectedLine}" <ListBox Grid.Row="1" ItemsSource="{Binding Lines}" SelectedItem="{Binding SelectedLine}"
Background="Transparent" BorderThickness="0" Padding="8,4" IsVisible="{Binding !IsCartEmpty}"> Background="Transparent" BorderThickness="0" Padding="0" IsVisible="{Binding !IsCartEmpty}">
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem"><Setter Property="Padding" Value="0"/><Setter Property="Margin" Value="0,3"/><Setter Property="CornerRadius" Value="10"/></Style> <Style Selector="ListBoxItem">
<Setter Property="Padding" Value="0"/><Setter Property="Margin" Value="0"/><Setter Property="CornerRadius" Value="0"/>
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="ListBoxItem:selected /template/ ContentPresenter"><Setter Property="Background" Value="Transparent"/></Style>
<Style Selector="Border.row"><Setter Property="Background" Value="Transparent"/></Style>
<Style Selector="Rectangle.accent"><Setter Property="IsVisible" Value="False"/></Style>
<Style Selector="ListBoxItem:selected Border.row"><Setter Property="Background" Value="{DynamicResource PosSelRow}"/></Style>
<Style Selector="ListBoxItem:selected Rectangle.accent"><Setter Property="IsVisible" Value="True"/></Style>
</ListBox.Styles> </ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SaleLineVm"> <DataTemplate x:DataType="vm:SaleLineVm">
<Border Background="{DynamicResource PosSubtle}" CornerRadius="10" Padding="12,8"> <Border Classes="row" Height="76" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1">
<Grid ColumnDefinitions="*,180,120,52"> <Grid ColumnDefinitions="4,*,200,160,56">
<StackPanel VerticalAlignment="Center"> <Rectangle Grid.Column="0" Classes="accent" Fill="{DynamicResource PosBrand}"/>
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="Medium" TextWrapping="Wrap"/> <StackPanel Grid.Column="1" VerticalAlignment="Center" Margin="16,0,0,0">
<TextBlock Text="{Binding DiscountText}" Foreground="{DynamicResource PosBrand}" FontSize="13" IsVisible="{Binding HasDiscount}"/> <TextBlock Text="{Binding Name}" Foreground="{DynamicResource PosText}" FontSize="18" FontWeight="SemiBold" TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding Price, StringFormat='{}{0:N0} ₸ / ед.'}" Foreground="{DynamicResource PosTextMuted}" FontSize="13" IsVisible="{Binding !HasDiscount}"/> <TextBlock Text="{Binding DiscountText}" Foreground="{DynamicResource PosBrandDark}" FontSize="14" FontWeight="Medium" Margin="0,4,0,0" IsVisible="{Binding HasDiscount}"/>
<TextBlock Text="{Binding UnitPriceText}" Foreground="{DynamicResource PosTextMuted}" FontSize="14" Margin="0,4,0,0" IsVisible="{Binding !HasDiscount}"/>
</StackPanel> </StackPanel>
<!-- Степпер 44×44 --> <Border Grid.Column="2" Height="44" Margin="16,0" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="10" ClipToBounds="True">
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="8" HorizontalAlignment="Center" VerticalAlignment="Center"> <Grid ColumnDefinitions="44,*,44">
<Button Classes="key" Width="44" Height="44" FontSize="22" Content="−" <Button Classes="stepbtn" Content="−" Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).DecLineCommand}" CommandParameter="{Binding}"/>
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).DecLineCommand}" CommandParameter="{Binding}"/> <TextBlock Grid.Column="1" Text="{Binding QtyText}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="{Binding QtyText}" Foreground="{DynamicResource PosText}" FontSize="16" FontWeight="SemiBold" MinWidth="56" TextAlignment="Center" VerticalAlignment="Center"/> <Button Grid.Column="2" Classes="stepbtn" FontSize="20" Content="+" Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).IncLineCommand}" CommandParameter="{Binding}"/>
<Button Classes="key" Width="44" Height="44" FontSize="20" Content="+" </Grid>
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).IncLineCommand}" CommandParameter="{Binding}"/> </Border>
</StackPanel> <TextBlock Grid.Column="3" Text="{Binding LineTotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="20" FontWeight="Bold" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="{Binding LineTotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="19" FontWeight="SemiBold" HorizontalAlignment="Right" VerticalAlignment="Center"/> <Button Grid.Column="4" Width="44" Height="44" Background="Transparent" BorderThickness="0" HorizontalAlignment="Right" VerticalAlignment="Center"
<Button Grid.Column="3" Classes="key" Width="36" Height="36" Background="Transparent" BorderThickness="0" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="12,0,0,0"
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).RemoveLineCommand}" CommandParameter="{Binding}" ToolTip.Tip="Удалить"> Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).RemoveLineCommand}" CommandParameter="{Binding}" ToolTip.Tip="Удалить">
<PathIcon Width="18" Height="18" Foreground="{DynamicResource PosDanger}" <Path Width="20" Height="20" Stretch="Uniform" Stroke="#9AA7A1" StrokeThickness="2" StrokeLineCap="Round" StrokeJoin="Round"
Data="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/> Data="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/>
</Button> </Button>
</Grid> </Grid>
</Border> </Border>
@ -98,40 +112,85 @@
</ListBox> </ListBox>
</Grid> </Grid>
</Border> </Border>
<TextBlock Grid.Row="3" Text="{Binding Message}" Foreground="{DynamicResource PosTextMuted}" Margin="4,10,0,0" FontSize="13.5"/>
</Grid> </Grid>
<!-- ПРАВО: чек + оплата (компактно, без пустот) --> <!-- ПРАВО -->
<Border Grid.Column="1" Classes="card" Margin="10,18,20,18" Padding="18"> <Border Grid.Column="1" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1" CornerRadius="14" Margin="8,16,24,16">
<StackPanel Spacing="12"> <Grid RowDefinitions="Auto,*,Auto" Margin="24">
<StackPanel Spacing="8"> <StackPanel Grid.Row="0" Spacing="16">
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Позиций" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding ItemCount}" Foreground="{DynamicResource PosText}" FontSize="15" FontWeight="Medium"/></Grid> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Позиций" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding ItemCount}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Подытог" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding Subtotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="15"/></Grid> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Подытог" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding Subtotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold"/></Grid>
<Grid ColumnDefinitions="*,Auto" IsVisible="{Binding HasAnyDiscount}"><TextBlock Text="Скидка" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding Discount, StringFormat='−{0:N0} ₸'}" Foreground="{DynamicResource PosBrand}" FontSize="15" FontWeight="Medium"/></Grid> <Grid ColumnDefinitions="*,Auto" IsVisible="{Binding HasAnyDiscount}"><TextBlock Text="Скидка" Foreground="{DynamicResource PosTextMuted}" FontSize="17"/><TextBlock Grid.Column="1" Text="{Binding Discount, StringFormat='−{0:N0} ₸'}" Foreground="{DynamicResource PosBrandDark}" FontSize="17" FontWeight="SemiBold"/></Grid>
</StackPanel> <Border Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Padding="22,20">
<Border Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Padding="16,14">
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="ИТОГО" Foreground="{DynamicResource PosText}" FontSize="18" FontWeight="SemiBold" VerticalAlignment="Center"/> <TextBlock Text="ИТОГО" Foreground="{DynamicResource PosBrandDark}" FontSize="18" FontWeight="Bold" VerticalAlignment="Bottom"/>
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrand}" FontSize="42" FontWeight="Bold"/> <TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrandDark}" FontSize="52" FontWeight="ExtraBold"/>
</Grid> </Grid>
</Border> </Border>
<Border BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0" Padding="0,16,0,0">
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="72" FontSize="22" Command="{Binding PayCommand}"> <StackPanel Spacing="10">
<TextBlock Text="Оплатить · F12" TextAlignment="Center"/> <Grid ColumnDefinitions="*,Auto"><TextBlock Foreground="{DynamicResource PosTextMuted}" FontSize="14"><Run Text="Последний чек "/><Run Text="{Binding LastReceiptNumber}"/></TextBlock><TextBlock Grid.Column="1" Text="{Binding LastReceiptInfo}" Foreground="{DynamicResource PosText}" FontSize="14" FontWeight="SemiBold"/></Grid>
</Button> <Grid ColumnDefinitions="*,Auto"><TextBlock Text="Чеков за смену" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/><TextBlock Grid.Column="1" Text="{Binding ReceiptsInShift}" Foreground="{DynamicResource PosText}" FontSize="14" FontWeight="SemiBold"/></Grid>
<!-- Вторичные действия: ровная сетка 2 колонки -->
<UniformGrid Columns="2">
<Button Classes="pos ghost" Margin="0,0,4,8" Content="Скидка"/>
<Button Classes="pos ghost" Margin="4,0,0,8" Content="Отложить · F7" Command="{Binding ParkCommand}"/>
<Button Classes="pos ghost" Margin="0,0,4,8" Content="Отложенные" Command="{Binding ResumeCommand}"/>
<Button Classes="pos ghost" Margin="4,0,0,8" Content="Возврат · F9" Command="{Binding GoReturnCommand}"/>
</UniformGrid>
<Button Classes="pos ghost" HorizontalAlignment="Stretch" Content="Внесение / Выплата · F10" Command="{Binding GoShiftCommand}"/>
<Button Classes="pos danger" HorizontalAlignment="Stretch" Content="Очистить · Esc" Command="{Binding ClearCommand}"/>
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel>
<StackPanel Grid.Row="2" Spacing="12">
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="76" Command="{Binding PayCommand}">
<StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Center">
<TextBlock Text="Оплатить" FontSize="24" FontWeight="Bold" Foreground="White" VerticalAlignment="Center"/>
<Border Classes="kbd onbrand"><TextBlock Text="F12"/></Border>
</StackPanel>
</Button>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,Auto,Auto,Auto">
<Button Grid.Row="0" Grid.Column="0" Classes="pos ghost" Height="52" Margin="0,0,5,10">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M19 5L5 19M7 7h.01M17 17h.01"/>
<TextBlock Text="Скидка" VerticalAlignment="Center"/><Border Classes="kbd"><TextBlock Text="F4"/></Border>
</StackPanel>
</Button>
<Button Grid.Row="0" Grid.Column="1" Classes="pos ghost" Height="52" Margin="5,0,0,10" Command="{Binding ParkCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeJoin="Round" Data="M6 4h12v16l-6-4-6 4z"/>
<TextBlock Text="Отложить" VerticalAlignment="Center"/><Border Classes="kbd"><TextBlock Text="F7"/></Border>
</StackPanel>
</Button>
<Button Grid.Row="1" Grid.Column="0" Classes="pos ghost" Height="52" Margin="0,0,5,10" Command="{Binding ResumeCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M4 6h16M4 12h16M4 18h10"/>
<TextBlock Text="Отложенные" VerticalAlignment="Center"/>
<Border CornerRadius="999" MinWidth="22" Height="22" Padding="7,0" Background="{DynamicResource PosWarnSoft}" VerticalAlignment="Center" IsVisible="{Binding HasParked}">
<TextBlock Text="{Binding ParkedCount}" FontSize="13" FontWeight="Bold" Foreground="{DynamicResource PosWarn}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
</StackPanel>
</Button>
<Button Grid.Row="1" Grid.Column="1" Classes="pos ghost" Height="52" Margin="5,0,0,10" Command="{Binding GoReturnCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" StrokeJoin="Round" Data="M9 14L4 9l5-5M4 9h11a5 5 0 010 10h-3"/>
<TextBlock Text="Возврат" VerticalAlignment="Center"/><Border Classes="kbd"><TextBlock Text="F9"/></Border>
</StackPanel>
</Button>
<Button Grid.Row="2" Grid.Column="0" Classes="pos ghost" Height="52" Margin="0,0,5,10" Command="{Binding GoShiftCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M12 5v14M5 12h14"/>
<TextBlock Text="Внесение" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Grid.Row="2" Grid.Column="1" Classes="pos ghost" Height="52" Margin="5,0,0,10" Command="{Binding GoShiftCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosTextMuted}" StrokeThickness="2" StrokeLineCap="Round" Data="M5 12h14"/>
<TextBlock Text="Выплата" VerticalAlignment="Center"/>
</StackPanel>
</Button>
<Button Grid.Row="3" Grid.Column="0" Grid.ColumnSpan="2" Classes="pos danger" Height="52" Command="{Binding ClearCommand}">
<StackPanel Orientation="Horizontal" Spacing="10">
<Path Width="18" Height="18" Stretch="Uniform" Stroke="{DynamicResource PosDanger}" StrokeThickness="2" StrokeLineCap="Round" Data="M6 6l12 12M18 6L6 18"/>
<TextBlock Text="Очистить чек" VerticalAlignment="Center"/><Border Classes="kbd"><TextBlock Text="Esc"/></Border>
</StackPanel>
</Button>
</Grid>
</StackPanel>
</Grid>
</Border>
</Grid> </Grid>
</UserControl> </UserControl>