diff --git a/src/food-market.pos.core/Models/PosModels.cs b/src/food-market.pos.core/Models/PosModels.cs
index 726df57..59b8fb7 100644
--- a/src/food-market.pos.core/Models/PosModels.cs
+++ b/src/food-market.pos.core/Models/PosModels.cs
@@ -20,6 +20,8 @@ public sealed class PosProduct
public bool IsArchived { get; init; }
public bool IsFavorite { get; init; }
public bool IsWeighed => Packaging == PosPackaging.Weight;
+ /// Цена для плитки избранного: «690 ₸/кг» для весового, «280 ₸» для штучного.
+ public string PriceText => IsWeighed ? $"{Price:N0} ₸/{UnitCode}" : $"{Price:N0} ₸";
}
/// Строка чека. Количество дробное (весовой/разлив). Скидка — абсолютная
diff --git a/src/food-market.pos/App.axaml.cs b/src/food-market.pos/App.axaml.cs
index ea0243e..9fa1f7c 100644
--- a/src/food-market.pos/App.axaml.cs
+++ b/src/food-market.pos/App.axaml.cs
@@ -13,6 +13,9 @@ public partial class App : Application
/// сервера — для скриншота реального окна на Windows.
public static bool DemoMode { get; set; }
+ /// Демо с открытой модалкой оплаты (--demo-pay) — для скриншота оплаты на Windows.
+ public static bool DemoPay { get; set; }
+
public override void Initialize() => AvaloniaXamlLoader.Load(this);
public override void OnFrameworkInitializationCompleted()
@@ -23,9 +26,15 @@ public override void OnFrameworkInitializationCompleted()
{
// Демо: окно с примерным чеком, без AppServices/логина; развёрнуто
// на весь экран — для скриншота реального окна на Windows.
+ var demoVm = new MainWindowViewModel();
+ if (DemoPay)
+ {
+ demoVm.Payment = PaymentViewModel.SampleCash();
+ demoVm.IsPaymentOpen = true;
+ }
desktop.MainWindow = new MainWindow
{
- DataContext = new MainWindowViewModel(),
+ DataContext = demoVm,
WindowState = Avalonia.Controls.WindowState.Maximized,
};
}
diff --git a/src/food-market.pos/Program.cs b/src/food-market.pos/Program.cs
index 75750df..ecfddc7 100644
--- a/src/food-market.pos/Program.cs
+++ b/src/food-market.pos/Program.cs
@@ -15,10 +15,12 @@ public static int Main(string[] args)
// --uitest : headless UI-автотест экрана продажи (Avalonia.Headless, для CI/xvfb).
if (args.Contains("--smoke")) return SmokeRunner.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);
// --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);
return 0;
diff --git a/src/food-market.pos/Styles/PosStyles.axaml b/src/food-market.pos/Styles/PosStyles.axaml
index d242085..26cf749 100644
--- a/src/food-market.pos/Styles/PosStyles.axaml
+++ b/src/food-market.pos/Styles/PosStyles.axaml
@@ -16,20 +16,21 @@
-
-
+
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/food-market.pos/Styles/Tokens.axaml b/src/food-market.pos/Styles/Tokens.axaml
index f85d281..f0a8d15 100644
--- a/src/food-market.pos/Styles/Tokens.axaml
+++ b/src/food-market.pos/Styles/Tokens.axaml
@@ -7,19 +7,25 @@
+
+
+
+
-
+
+
+
-
+
@@ -27,19 +33,25 @@
+
+
+
+
+
+
-
+
diff --git a/src/food-market.pos/Tooling/CompareRunner.cs b/src/food-market.pos/Tooling/CompareRunner.cs
new file mode 100644
index 0000000..760e032
--- /dev/null
+++ b/src/food-market.pos/Tooling/CompareRunner.cs
@@ -0,0 +1,72 @@
+using SkiaSharp;
+
+namespace foodmarket.Pos.Tooling;
+
+/// Складывает «макет | касса» рядом в одно изображение для поэлементной
+/// сверки вёрстки. Использует уже отрисованные PNG: docs/pos-design/mockup-*.png
+/// (эталон) и docs/pos-screenshots/*.png (наш рендер).
+/// Запуск: dotnet run -- --compare
+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();
+ }
+}
diff --git a/src/food-market.pos/Tooling/ScreenshotRunner.cs b/src/food-market.pos/Tooling/ScreenshotRunner.cs
index fcb1d7d..9039410 100644
--- a/src/food-market.pos/Tooling/ScreenshotRunner.cs
+++ b/src/food-market.pos/Tooling/ScreenshotRunner.cs
@@ -78,18 +78,15 @@ private static Control ShellWith(SaleViewModel sale)
vm.CurrentPage = sale;
return new MainWindow { DataContext = vm };
}
- /// Модальная оплата на затемнённой подложке (как в оболочке):
- /// светлый фон → полупрозрачная тёмная подложка → белое модальное окно.
+ /// Модальная оплата поверх затемнённого экрана продажи — как в оболочке
+ /// и как в макете: чек виден под полупрозрачной тёмной подложкой.
private static Control PayScrim(PaymentViewModel pay)
- => new Border
- {
- Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#F4F6F5")),
- Child = new Border
- {
- Background = new Avalonia.Media.SolidColorBrush(Avalonia.Media.Color.Parse("#660F1F17")),
- Child = new PaymentView { DataContext = pay },
- },
- };
+ {
+ var vm = new MainWindowViewModel(); // в CurrentPage уже лежит пример продажи
+ vm.Payment = pay;
+ vm.IsPaymentOpen = true;
+ return new MainWindow { DataContext = vm };
+ }
private static Control ShellOffline()
{
@@ -160,7 +157,9 @@ private static void WriteGallery(string dir, List<(string file, string caption)>
foreach (var w in wins)
{
var rel = "windows/" + Path.GetFileName(w);
- sb.Append($"
Windows 11 — экран продажи (реальное окно)");
+ var name = Path.GetFileNameWithoutExtension(w);
+ var label = name.Contains("pay", StringComparison.OrdinalIgnoreCase) ? "экран оплаты" : "экран продажи";
+ sb.Append($"
Windows 11 — {label} (реальное окно)");
}
sb.Append("");
}
diff --git a/src/food-market.pos/ViewModels/ViewModels.cs b/src/food-market.pos/ViewModels/ViewModels.cs
index 11c9641..8d0920a 100644
--- a/src/food-market.pos/ViewModels/ViewModels.cs
+++ b/src/food-market.pos/ViewModels/ViewModels.cs
@@ -25,7 +25,8 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty] private string _cashierName = "—";
[ObservableProperty] private string _shiftNumber = "смена не открыта";
[ObservableProperty] private bool _isShiftOpen;
- [ObservableProperty] private string _pointName = "Магазин";
+ [ObservableProperty] private string _storeName = "Магазин";
+ [ObservableProperty] private string _pointName = "Касса";
[ObservableProperty] private bool _isOnline = true;
[ObservableProperty] private int _queueCount;
[ObservableProperty] private PaymentViewModel? _payment;
@@ -34,14 +35,14 @@ public partial class MainWindowViewModel : ViewModelBase
public string NetBadge => IsOnline ? "На связи" : "Нет связи";
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 OnQueueCountChanged(int value) { OnPropertyChanged(nameof(QueueBadge)); }
partial void OnIsShiftOpenChanged(bool value) => OnPropertyChanged(nameof(ShiftBadge));
partial void OnShiftNumberChanged(string value) => OnPropertyChanged(nameof(ShiftBadge));
/// Конструктор для дизайнера/скриншотов (демо-данные).
- 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(); }
@@ -363,9 +364,10 @@ public string QtyText
}
}
public bool HasDiscount => Discount > 0m;
- /// Пояснение скидки в строке, чтобы цена×кол-во ≠ сумма не выглядело
- /// ошибкой: «1 560 → 1 500 ₸, скидка 60».
- public string DiscountText => $"{Gross:N0} → {LineTotal:N0} ₸, скидка {Discount:N0}";
+ /// Подпись цены за единицу: «280 ₸ за шт», «690 ₸ за кг».
+ public string UnitPriceText => $"{Price:N0} ₸ за {Unit}";
+ /// Строка со скидкой: «520 ₸ за шт · скидка 60 ₸ (1 560 → 1 500 ₸)».
+ public string DiscountText => $"{Price:N0} ₸ за {Unit} · скидка {Discount:N0} ₸ ({Gross:N0} → {LineTotal:N0} ₸)";
partial void OnQuantityChanged(decimal value)
{ OnPropertyChanged(nameof(LineTotal)); OnPropertyChanged(nameof(QtyText)); OnPropertyChanged(nameof(Gross)); OnPropertyChanged(nameof(DiscountText)); }
partial void OnDiscountChanged(decimal value)
@@ -396,6 +398,10 @@ public partial class SaleViewModel : ViewModelBase
[ObservableProperty] private SaleLineVm? _selectedLine;
public bool IsCartEmpty => ItemCount == 0;
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 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 = "Чек очищен"; }
- public int ParkedCount => _parked.Count;
+ private int _demoParked; // только для скриншотов/демо (Sample)
+ public int ParkedCount => _parked.Count + _demoParked;
+ public bool HasParked => ParkedCount > 0;
/// Отложить текущий чек и начать новый (обслужить другого покупателя).
[RelayCommand]
@@ -567,12 +575,15 @@ public static SaleViewModel Sample()
vm.Total = decimal.Round(vm.Subtotal - vm.Discount, 2);
vm.ItemCount = vm.Lines.Count;
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()
{ Id = Guid.NewGuid(), Name = n, Barcodes = Array.Empty(), UnitCode = unit, Packaging = weighed ? PosPackaging.Weight : PosPackaging.Piece, VatPercent = 12m, VatEnabled = true, Price = price, IsFavorite = true };
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("Пакет-майка", "шт", 20m, false), Fav("Вода 0,5л", "шт", 150m, false) })
+ Fav("Пакет-майка", "шт", 20m, false), Fav("Вода 0,5 л", "шт", 150m, false) })
vm.Favorites.Add(f);
return vm;
}
diff --git a/src/food-market.pos/Views/MainWindow.axaml b/src/food-market.pos/Views/MainWindow.axaml
index f0747be..802f96a 100644
--- a/src/food-market.pos/Views/MainWindow.axaml
+++ b/src/food-market.pos/Views/MainWindow.axaml
@@ -7,63 +7,61 @@
Background="{DynamicResource PosBg}" Title="Касса food-market"
WindowStartupLocation="CenterScreen">
-
+
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
+
+
+
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/food-market.pos/Views/PaymentView.axaml b/src/food-market.pos/Views/PaymentView.axaml
index f1b657e..bd099b6 100644
--- a/src/food-market.pos/Views/PaymentView.axaml
+++ b/src/food-market.pos/Views/PaymentView.axaml
@@ -3,76 +3,105 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.PaymentView"
x:DataType="vm:PaymentViewModel">
-
-
+
+
+
-
-
-
-
+
+
+
+
-
-
+
+
-
-
-
+
+
+
+
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
-
+
-
-
+
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
diff --git a/src/food-market.pos/Views/SaleView.axaml b/src/food-market.pos/Views/SaleView.axaml
index 721a2fa..59d5253 100644
--- a/src/food-market.pos/Views/SaleView.axaml
+++ b/src/food-market.pos/Views/SaleView.axaml
@@ -3,93 +3,107 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.SaleView"
x:DataType="vm:SaleViewModel">
-
+
-
-
+
+
-
-
-
+
+
+
+
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
+
-
-
-
-
-
+
+
+
+
+
+
+
-
-
+
+ Background="Transparent" BorderThickness="0" Padding="0" IsVisible="{Binding !IsCartEmpty}">
-
+
+
+
+
+
+
-
-
-
-
-
-
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
-
+
@@ -98,40 +112,85 @@
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+