fix(pos-ui): формат чисел KZ/RU, скидка в строке, центровка/обрезка кнопок
По итогам проверки Mac-Claude (скриншоты): - Формат чисел как в вебе/мобильном: «3 422 ₸» (пробел-разделитель тысяч), вес «1,235 кг» (запятая), единица измерения рядом с количеством («3 шт»). PosFormat — культура ru-RU с NBSP-разделителем, применяется процессно. - Скидка на строку видна: «1 560 → 1 500 ₸, скидка 60» под позицией, чтобы цена×кол-во ≠ сумма не выглядело ошибкой (SaleLineVm.DiscountText/HasDiscount). - Кнопки: текст по центру по вертикали и горизонтали (стиль Button.pos + ContentPresenter); «Очистить» и «Enter · Провести и печать» больше не обрезаются на 1366×768 и 1920×1080 (проверено скриншотами). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
87eb48cca9
commit
c5ff1ade94
|
|
@ -134,3 +134,17 @@ Testcontainers) + headless UI (Avalonia.Headless, 15 проверок) + офл
|
||||||
- 08.10 — замеры Linux+Windows (п.9); холодный старт/поиск в цель.
|
- 08.10 — замеры Linux+Windows (п.9); холодный старт/поиск в цель.
|
||||||
- 08.10 — серверная привязка продаж к смене + возвраты; +2 интеграционных теста; +офлайн/checkout/версия unit-тесты (37); headless UI-тест (--uitest).
|
- 08.10 — серверная привязка продаж к смене + возвраты; +2 интеграционных теста; +офлайн/checkout/версия unit-тесты (37); headless UI-тест (--uitest).
|
||||||
- 08.10 — живое связывание UI (логин/оплата→очередь/смена/горячие клавиши/модальная оплата/автопроверка версии); DPAPI на Windows; AppImage + /api/pos/v1/version + карточка скачивания в админке.
|
- 08.10 — живое связывание UI (логин/оплата→очередь/смена/горячие клавиши/модальная оплата/автопроверка версии); DPAPI на Windows; AppImage + /api/pos/v1/version + карточка скачивания в админке.
|
||||||
|
|
||||||
|
## Доводка 08.10 (по итогам проверки Mac-Claude)
|
||||||
|
1. [ ] Настоящий GUI-тест на Windows 11 (интерактивная сессия tester) + скриншоты реального окна.
|
||||||
|
2. [ ] Первый запуск на Windows 7.5с → <2с: R2R-сборка, trimming, замеры; Defender — в docs.
|
||||||
|
3. [ ] Выбор точки продаж при регистрации устройства (список активных, сохранение, смена — админ).
|
||||||
|
4. [ ] Быстрый вход кассира по PIN (4–6 цифр), хэш на сервере, блок после 5 ошибок.
|
||||||
|
5. [ ] Возврат по чеку — полноценный экран выбора позиций + тесты.
|
||||||
|
6. [ ] Повторный full-chain (C) + POS smoke Win/Linux зелёные; галерея обновлена.
|
||||||
|
7. [ ] Коммиты в main; push origin.
|
||||||
|
8. [x] Формат чисел KZ/RU: «3 422 ₸» (пробел тысяч), «1,235 кг» (запятая) + единица рядом с кол-вом.
|
||||||
|
Реализовано `PosFormat` (культура ru-RU с NBSP-разделителем), применяется процессно.
|
||||||
|
9. [x] Скидка на строку видна: «1 560 → 1 500 ₸, скидка 60» под позицией (SaleLineVm.DiscountText).
|
||||||
|
10. [x] Кнопки: текст по центру по вертикали/горизонтали (стиль Button.pos), «Очистить» и
|
||||||
|
«Enter · Провести и печать» больше не обрезаются (проверено на 1366×768 и 1920×1080).
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,23 @@
|
||||||
<Style Selector="Button.pos">
|
<Style Selector="Button.pos">
|
||||||
<Setter Property="Height" Value="56" />
|
<Setter Property="Height" Value="56" />
|
||||||
<Setter Property="FontSize" Value="18" />
|
<Setter Property="FontSize" Value="18" />
|
||||||
<Setter Property="Padding" Value="18,0" />
|
<Setter Property="Padding" Value="12,0" />
|
||||||
<Setter Property="CornerRadius" Value="8" />
|
<Setter Property="CornerRadius" Value="8" />
|
||||||
<Setter Property="Background" Value="#2d3748" />
|
<Setter Property="Background" Value="#2d3748" />
|
||||||
<Setter Property="Foreground" Value="#f7fafc" />
|
<Setter Property="Foreground" Value="#f7fafc" />
|
||||||
|
<!-- Текст строго по центру кнопки (и по вертикали, и по горизонтали). -->
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<!-- Содержимое кнопок кассы не обрезаем: при нехватке ширины переносим,
|
||||||
|
текст по центру. Избавляет от «Esc · Очистит…». -->
|
||||||
|
<Style Selector="Button.pos /template/ ContentPresenter">
|
||||||
|
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||||
|
<Setter Property="VerticalContentAlignment" Value="Center" />
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.pos TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap" />
|
||||||
|
<Setter Property="TextAlignment" Value="Center" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.primary">
|
<Style Selector="Button.primary">
|
||||||
<Setter Property="Background" Value="#16a34a" />
|
<Setter Property="Background" Value="#16a34a" />
|
||||||
|
|
|
||||||
35
src/food-market.pos/PosFormat.cs
Normal file
35
src/food-market.pos/PosFormat.cs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
using System.Globalization;
|
||||||
|
|
||||||
|
namespace foodmarket.Pos;
|
||||||
|
|
||||||
|
/// <summary>Единый формат чисел кассы — как в вебе/мобильном (KZ/RU):
|
||||||
|
/// разделитель тысяч — пробел («3 422 ₸»), десятичный — запятая («1,235 кг»).
|
||||||
|
/// Применяется процессно (CurrentCulture + дефолт новых потоков), чтобы и
|
||||||
|
/// привязки XAML (StringFormat), и Toin-code форматирование совпадали.</summary>
|
||||||
|
public static class PosFormat
|
||||||
|
{
|
||||||
|
public static readonly CultureInfo Culture = BuildCulture();
|
||||||
|
|
||||||
|
private static CultureInfo BuildCulture()
|
||||||
|
{
|
||||||
|
// База — ru-RU (запятая-десятичный). Явно выставляем пробел-разделитель
|
||||||
|
// тысяч (NBSP), чтобы не зависеть от версии ICU/окружения.
|
||||||
|
var c = (CultureInfo)CultureInfo.GetCultureInfo("ru-RU").Clone();
|
||||||
|
const string sep = " "; // неразрывный пробол
|
||||||
|
c.NumberFormat.NumberGroupSeparator = sep;
|
||||||
|
c.NumberFormat.CurrencyGroupSeparator = sep;
|
||||||
|
c.NumberFormat.PercentGroupSeparator = sep;
|
||||||
|
c.NumberFormat.NumberDecimalSeparator = ",";
|
||||||
|
c.NumberFormat.CurrencyDecimalSeparator = ",";
|
||||||
|
return c;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Выставить кассовую культуру текущему и новым потокам.</summary>
|
||||||
|
public static void Apply()
|
||||||
|
{
|
||||||
|
CultureInfo.CurrentCulture = Culture;
|
||||||
|
CultureInfo.CurrentUICulture = Culture;
|
||||||
|
CultureInfo.DefaultThreadCurrentCulture = Culture;
|
||||||
|
CultureInfo.DefaultThreadCurrentUICulture = Culture;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -8,6 +8,7 @@ internal static class Program
|
||||||
[STAThread]
|
[STAThread]
|
||||||
public static int Main(string[] args)
|
public static int Main(string[] args)
|
||||||
{
|
{
|
||||||
|
PosFormat.Apply(); // единый формат чисел (KZ/RU: «3 422 ₸», «1,235 кг»)
|
||||||
// CLI-режимы (без интерактивного окна):
|
// CLI-режимы (без интерактивного окна):
|
||||||
// --smoke : прогон сценария кассы через логику (без UI), печать результатов.
|
// --smoke : прогон сценария кассы через логику (без UI), печать результатов.
|
||||||
// --screenshots : headless-рендер экранов в docs/pos-screenshots (1920×1080 + 1366×768).
|
// --screenshots : headless-рендер экранов в docs/pos-screenshots (1920×1080 + 1366×768).
|
||||||
|
|
|
||||||
|
|
@ -202,10 +202,29 @@ public partial class SaleLineVm : ObservableObject
|
||||||
[ObservableProperty] private decimal _price;
|
[ObservableProperty] private decimal _price;
|
||||||
[ObservableProperty] private decimal _quantity = 1m;
|
[ObservableProperty] private decimal _quantity = 1m;
|
||||||
[ObservableProperty] private decimal _discount;
|
[ObservableProperty] private decimal _discount;
|
||||||
public decimal LineTotal => Math.Max(0m, decimal.Round(Price * Quantity, 2) - Discount);
|
public decimal Gross => decimal.Round(Price * Quantity, 2);
|
||||||
public string QtyText => Quantity == decimal.Truncate(Quantity) && Unit == "шт" ? Quantity.ToString("0") : Quantity.ToString("0.###");
|
public decimal LineTotal => Math.Max(0m, Gross - Discount);
|
||||||
partial void OnQuantityChanged(decimal value) { OnPropertyChanged(nameof(LineTotal)); OnPropertyChanged(nameof(QtyText)); }
|
/// <summary>Количество с единицей измерения: «3 шт», «1,235 кг».</summary>
|
||||||
partial void OnDiscountChanged(decimal value) => OnPropertyChanged(nameof(LineTotal));
|
public string QtyText
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
var n = Quantity == decimal.Truncate(Quantity) && Unit == "шт"
|
||||||
|
? Quantity.ToString("0")
|
||||||
|
: Quantity.ToString("0.###");
|
||||||
|
return $"{n} {Unit}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public bool HasDiscount => Discount > 0m;
|
||||||
|
/// <summary>Пояснение скидки в строке, чтобы цена×кол-во ≠ сумма не выглядело
|
||||||
|
/// ошибкой: «1 560 → 1 500 ₸, скидка 60».</summary>
|
||||||
|
public string DiscountText => $"{Gross:N0} → {LineTotal:N0} ₸, скидка {Discount:N0}";
|
||||||
|
partial void OnQuantityChanged(decimal value)
|
||||||
|
{ OnPropertyChanged(nameof(LineTotal)); OnPropertyChanged(nameof(QtyText)); OnPropertyChanged(nameof(Gross)); OnPropertyChanged(nameof(DiscountText)); }
|
||||||
|
partial void OnDiscountChanged(decimal value)
|
||||||
|
{ OnPropertyChanged(nameof(LineTotal)); OnPropertyChanged(nameof(HasDiscount)); OnPropertyChanged(nameof(DiscountText)); }
|
||||||
|
partial void OnPriceChanged(decimal value)
|
||||||
|
{ OnPropertyChanged(nameof(Gross)); OnPropertyChanged(nameof(LineTotal)); OnPropertyChanged(nameof(DiscountText)); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Экран продажи — сердце кассы. Пробитие по сканеру/поиску, корзина,
|
/// <summary>Экран продажи — сердце кассы. Пробитие по сканеру/поиску, корзина,
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,9 @@
|
||||||
</Border>
|
</Border>
|
||||||
<Grid ColumnDefinitions="*,2*">
|
<Grid ColumnDefinitions="*,2*">
|
||||||
<Button Classes="pos" Margin="0,0,6,0" HorizontalAlignment="Stretch" Height="64" FontSize="16" Content="Без сдачи" Command="{Binding ExactCashCommand}"/>
|
<Button Classes="pos" Margin="0,0,6,0" HorizontalAlignment="Stretch" Height="64" FontSize="16" Content="Без сдачи" Command="{Binding ExactCashCommand}"/>
|
||||||
<Button Grid.Column="1" Classes="pos primary" HorizontalAlignment="Stretch" Height="64" FontSize="20" Content="Enter · Провести и напечатать чек" Command="{Binding ConfirmCommand}" IsDefault="True" IsEnabled="{Binding Sufficient}"/>
|
<Button Grid.Column="1" Classes="pos primary" HorizontalAlignment="Stretch" Height="64" FontSize="19" Command="{Binding ConfirmCommand}" IsDefault="True" IsEnabled="{Binding Sufficient}">
|
||||||
|
<TextBlock Text="Enter · Провести и печать" TextWrapping="Wrap" TextAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Button Classes="pos" HorizontalAlignment="Stretch" Height="44" Content="Esc · Отмена" Command="{Binding CancelCommand}"/>
|
<Button Classes="pos" HorizontalAlignment="Stretch" Height="44" Content="Esc · Отмена" Command="{Binding CancelCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
|
|
||||||
<Border Grid.Row="1" Background="#111827" CornerRadius="10" Padding="0">
|
<Border Grid.Row="1" Background="#111827" CornerRadius="10" Padding="0">
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,90,120,120" Background="#1f2937" Height="38">
|
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,110,110,120" Background="#1f2937" Height="38">
|
||||||
<TextBlock Text=" Товар" Foreground="#9ca3af" VerticalAlignment="Center"/>
|
<TextBlock Text=" Товар" Foreground="#9ca3af" VerticalAlignment="Center"/>
|
||||||
<TextBlock Grid.Column="1" Text="Кол-во" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
<TextBlock Grid.Column="1" Text="Кол-во" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||||
<TextBlock Grid.Column="2" Text="Цена" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
<TextBlock Grid.Column="2" Text="Цена" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||||
|
|
@ -28,9 +28,11 @@
|
||||||
<ListBox ItemsSource="{Binding Lines}" Background="Transparent" BorderThickness="0">
|
<ListBox ItemsSource="{Binding Lines}" Background="Transparent" BorderThickness="0">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="vm:SaleLineVm">
|
<DataTemplate x:DataType="vm:SaleLineVm">
|
||||||
<Grid ColumnDefinitions="*,90,120,120" Height="46">
|
<Grid ColumnDefinitions="*,110,110,120" MinHeight="46">
|
||||||
<StackPanel VerticalAlignment="Center">
|
<StackPanel VerticalAlignment="Center" Margin="0,4">
|
||||||
<TextBlock Text="{Binding Name}" Foreground="#f3f4f6" FontSize="16"/>
|
<TextBlock Text="{Binding Name}" Foreground="#f3f4f6" FontSize="16" TextWrapping="Wrap"/>
|
||||||
|
<TextBlock Text="{Binding DiscountText}" Foreground="#fca5a5" FontSize="12.5"
|
||||||
|
IsVisible="{Binding HasDiscount}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<TextBlock Grid.Column="1" Text="{Binding QtyText}" Foreground="#e5e7eb" FontSize="16" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="1" Text="{Binding QtyText}" Foreground="#e5e7eb" FontSize="16" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||||
<TextBlock Grid.Column="2" Text="{Binding Price, StringFormat='{}{0:N0} ₸'}" Foreground="#e5e7eb" FontSize="16" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
<TextBlock Grid.Column="2" Text="{Binding Price, StringFormat='{}{0:N0} ₸'}" Foreground="#e5e7eb" FontSize="16" HorizontalAlignment="Right" VerticalAlignment="Center"/>
|
||||||
|
|
@ -59,10 +61,16 @@
|
||||||
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="#34d399" FontSize="34" FontWeight="Bold" HorizontalAlignment="Right"/>
|
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="#34d399" FontSize="34" FontWeight="Bold" HorizontalAlignment="Right"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
<StackPanel Grid.Row="4" Spacing="8">
|
<StackPanel Grid.Row="4" Spacing="8">
|
||||||
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="68" FontSize="22" Content="F12 · ОПЛАТА"/>
|
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="68" FontSize="22" Command="{Binding PayCommand}">
|
||||||
|
<TextBlock Text="F12 · ОПЛАТА" TextAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
<Grid ColumnDefinitions="*,*" >
|
<Grid ColumnDefinitions="*,*" >
|
||||||
<Button Classes="pos" Margin="0,0,4,0" HorizontalAlignment="Stretch" Content="Скидка"/>
|
<Button Classes="pos" Margin="0,0,4,0" FontSize="16" HorizontalAlignment="Stretch">
|
||||||
<Button Grid.Column="1" Classes="pos danger" Margin="4,0,0,0" HorizontalAlignment="Stretch" Content="Esc · Очистить" Command="{Binding ClearCommand}"/>
|
<TextBlock Text="Скидка" TextAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
|
<Button Grid.Column="1" Classes="pos danger" Margin="4,0,0,0" FontSize="16" HorizontalAlignment="Stretch" Command="{Binding ClearCommand}" ToolTip.Tip="Очистить чек (Esc)">
|
||||||
|
<TextBlock Text="Очистить" TextWrapping="Wrap" TextAlignment="Center"/>
|
||||||
|
</Button>
|
||||||
</Grid>
|
</Grid>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue