feat(pos-ui): светлая стильная тема по умолчанию (Square/Loyverse-подход)

По просьбе владельца («фон мрачный — сделай светлее, стильнее, удобнее»).
- Дизайн-система на токенах: Styles/Tokens.axaml (ThemeDictionaries Light/Dark) +
  Styles/PosStyles.axaml (карточки/кнопки/чипы/клавиши/сегменты). RequestedThemeVariant=Light.
  Палитра: фон #F4F6F5, карточки белые, бренд-зелёный #16A34A, опасное — красный контур,
  предупреждение — янтарь. Inter, крупная иерархия, скругления 12–14, мягкие тени, сетка 8px.
- Экран продажи: белая верхняя полоса (лого/точка/кассир + чипы смены и связи), поиск со
  скан-иконкой, корзина-карточки со степпером [−]N[+] и удалением, скидка в строке зелёным,
  пустая корзина-заглушка, правая карточка чека с крупным зелёным ИТОГО и «Оплатить · F12»,
  вторичные кнопки-контуры, капсулы горячих клавиш.
- Оплата (светлое модальное): сегменты Наличные/Карта/Смешанная, быстрые суммы, цифровая
  клавиатура, крупная «Сдача».
- Вход (логин + PIN-пад), смена/Z-отчёт (разница цветом), возврат (степперы) — в том же стиле.
- Кнопки ≥48px, клавиши крупные. Скорость не ухудшилась (старт 149мс, скан 11мс, поиск 33мс).
  --uitest OK, 43 unit зелёные. Галерея: разделы «Стало»/«Было».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-08 13:26:07 +05:00
parent 7437305265
commit 7a93c1941d
58 changed files with 713 additions and 267 deletions

50
docs/pos-design.md Normal file
View file

@ -0,0 +1,50 @@
# Касса food-market — дизайн-система (светлая тема)
Подход: чисто, светло, крупно, ничего лишнего (как Square POS / Loyverse / Poster).
Всё строится на токенах из одного места — `Styles/Tokens.axaml` (ResourceDictionary).
Тёмная тема — те же токены, другой набор значений (опция в настройках, не по умолчанию).
## Палитра (токены)
| Токен | Светлая | Назначение |
|---|---|---|
| `PosBg` | #F4F6F5 | фон приложения |
| `PosCard` | #FFFFFF | карточки, полосы, модальные |
| `PosBorder` | #E3E8E6 | рамки, разделители |
| `PosText` | #0F1F17 | основной текст |
| `PosтатText2` / `PosTextMuted` | #5F6F68 | вторичный текст |
| `PosBrand` | #16A34A | бренд/главное действие (зелёный Food Market) |
| `PosBrandHover` | #15803D | hover |
| `PosBrandPressed` | #166534 | pressed |
| `PosDanger` | #DC2626 | опасное (Очистить/Удалить) — только контур |
| `PosWarn` | #D97706 | предупреждение (смена закрыта, нет связи) — янтарный |
| `PosBrandSoft` | #DCFCE7 | мягкий зелёный фон (чипы, успех) |
| `PosWarnSoft` | #FEF3C7 | мягкий янтарный фон |
Контраст текста к фону ≥ WCAG AA (PosText на PosCard/PosBg — ~14:1; PosTextMuted — ~5:1).
## Типографика (Inter)
- ИТОГО: 44–56 pt, Bold.
- Суммы строк: 18–20 pt, SemiBold, **tabular** (моноширинные цифры — колонки не прыгают).
- Названия товаров: 17–18 pt.
- Служебное/подписи: 13–14 pt, PosTextMuted.
- Моноширинные цифры: `<TextBlock Classes="num">` → FontFeatures "tnum=1" / FontFamily с tabular.
## Форма и ритм
- Скругления: карточки/кнопки 12–14, чипы 999 (капсула).
- Тени: мягкие у карточек (BoxShadow 0 1 3 #00000014, 0 4 12 #00000010).
- Сетка отступов: шаг 8 px (8/12/16/24).
- Кнопки: высота ≥ 48 px (сенсор ≥ 44). Главная — заливка PosBrand; вторичные —
контур PosBorder на белом; опасная — контур PosDanger (без заливки).
## Компоненты (классы стилей)
- `Button.primary` — зелёная заливка, белый текст, крупная.
- `Button.ghost` — белая, контур PosBorder, текст PosText (вторичные действия).
- `Button.danger` — белая, контур PosDanger, текст PosDanger (Очистить/Удалить).
- `Button.key` — клавиша цифровой клавиатуры/степпера.
- `Border.card` — белая карточка со скруглением и тенью.
- `Border.chip` / `Border.chip.ok` / `.warn` — капсулы статуса.
- `TextBox.search` — поле поиска/скана с иконкой.
## Тёмная тема (опция)
Те же имена токенов, тёмные значения (PosBg #0F1713, PosCard #15201B, PosText #E8EFEA…),
переключается через `RequestedThemeVariant` + подмену словаря. По умолчанию — светлая.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 40 KiB

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 35 KiB

After

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 38 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 100 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 81 KiB

After

Width:  |  Height:  |  Size: 106 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 32 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

File diff suppressed because one or more lines are too long

39
docs/pos-ui-progress.md Normal file
View file

@ -0,0 +1,39 @@
# Новый внешний вид кассы — прогресс
Задача владельца (08.10.2026): «визуально выглядит ужасно, фон мрачный — сделай
интерфейс более стильным, светлым, удобным». Светлая тема по умолчанию,
ориентиры Square/Loyverse/Poster. Скорость не ухудшать.
## Замеры скорости (до/после)
| Метрика | До (dark) | После (light) |
|---|---|---|
| Холодный старт (процесс→логика), Linux | 146 мс | 149 мс (в пределах шума) |
| Поиск по штрихкоду | 13,9 мс | 11,2 мс |
| Поиск по имени на 30 000 | 41,9 мс | 33,2 мс |
Скорость не ухудшилась (светлая тема — только ресурсы/стили, логика та же).
## План
1. [x] Токены светлой темы в `Styles/Tokens.axaml` (ThemeDictionaries Light/Dark) + docs/pos-design.md.
2. [x] Светлая тема по умолчанию (`RequestedThemeVariant=Light`); тёмная — те же токены (вариант Dark).
3. [x] Экран продажи: белая верхняя полоса (лого/точка/кассир/чипы смены+связи),
поиск со скан-иконкой, корзина-карточки со степпером [−]N[+] и удалением,
белая карточка чека с крупным зелёным ИТОГО и «Оплатить · F12», вторичные
кнопки-контуры, пустая корзина-заглушка, капсулы горячих клавиш.
4. [x] Оплата (светлое модальное): сегменты Нал/Карта/Смешанная, быстрые суммы
(без сдачи/1000/2000/5000/10000/20000), цифровая клавиатура, крупная «Сдача».
5. [x] Остальные экраны в стиле: вход (логин + PIN-пад), регистрация/выбор точки,
смена/Z-отчёт (разница цветом), возврат (степперы). Нет связи — чип в шапке.
6. [ ] Избранные товары: сервер (флаг/порядок у товара) + миграция + плитки UI.
7. [x] Удобство: кнопки ≥48px, клавиши крупные; фокус в поле скана; адаптив 1366/1920.
8. [~] Скриншоты «Было/Стало» 1920×1080 + 1366×768 (галерея с разделами); реальное окно Windows 11 — в конце.
9. [x] --uitest OK; unit 43 зелёные; скорость не хуже. Smoke Linux/Windows — в конце.
10. [~] Коммиты в main; push (по ходу).
Осталось: избранные товары (п.6), тёмная тема как переключатель в настройках,
реальное окно Windows 11, smoke Win/Linux, итоговый push.
НЕ трогать: прод, shared-postgres, global.json, gateway nginx, sklad, другие VM.
## Журнал
- 08.10 — старт; сохранены «до»-скриншоты (22), замерен baseline; планы зафиксированы.

View file

@ -2,47 +2,19 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:foodmarket.Pos" xmlns:local="clr-namespace:foodmarket.Pos"
x:Class="foodmarket.Pos.App" x:Class="foodmarket.Pos.App"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Light">
<Application.DataTemplates> <Application.DataTemplates>
<local:ViewLocator /> <local:ViewLocator />
</Application.DataTemplates> </Application.DataTemplates>
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://foodmarket.Pos/Styles/Tokens.axaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
<!-- Крупный кассовый стиль: большие кнопки, читаемые цифры, без анимаций. --> <StyleInclude Source="avares://foodmarket.Pos/Styles/PosStyles.axaml"/>
<Style Selector="Button.pos">
<Setter Property="Height" Value="56" />
<Setter Property="FontSize" Value="18" />
<Setter Property="Padding" Value="12,0" />
<Setter Property="CornerRadius" Value="8" />
<Setter Property="Background" Value="#2d3748" />
<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 Selector="Button.primary">
<Setter Property="Background" Value="#16a34a" />
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
</Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="#dc2626" />
<Setter Property="Foreground" Value="White" />
</Style>
<Style Selector="TextBox.pos">
<Setter Property="FontSize" Value="20" />
<Setter Property="Height" Value="52" />
<Setter Property="VerticalContentAlignment" Value="Center" />
</Style>
</Application.Styles> </Application.Styles>
</Application> </Application>

View file

@ -0,0 +1,152 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<!-- ── Карточки ─────────────────────────────────────────────────── -->
<Style Selector="Border.card">
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosBorder}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="14"/>
<Setter Property="BoxShadow" Value="0 1 3 0 #14000000, 0 6 16 0 #0D000000"/>
</Style>
<!-- ── Чипы статуса (капсулы) ───────────────────────────────────── -->
<Style Selector="Border.chip">
<Setter Property="CornerRadius" Value="999"/>
<Setter Property="Padding" Value="12,6"/>
<Setter Property="Background" Value="{DynamicResource PosSubtle}"/>
</Style>
<Style Selector="Border.chip > TextBlock">
<Setter Property="Foreground" Value="{DynamicResource PosTextMuted}"/>
<Setter Property="FontSize" Value="13.5"/>
<Setter Property="FontWeight" Value="Medium"/>
<Setter Property="VerticalAlignment" Value="Center"/>
</Style>
<Style Selector="Border.chip.ok">
<Setter Property="Background" Value="{DynamicResource PosBrandSoft}"/>
</Style>
<Style Selector="Border.chip.ok > TextBlock"><Setter Property="Foreground" Value="{DynamicResource PosBrand}"/></Style>
<Style Selector="Border.chip.warn">
<Setter Property="Background" Value="{DynamicResource PosWarnSoft}"/>
</Style>
<Style Selector="Border.chip.warn > TextBlock"><Setter Property="Foreground" Value="{DynamicResource PosWarn}"/></Style>
<!-- ── Кнопки ───────────────────────────────────────────────────── -->
<Style Selector="Button.pos">
<Setter Property="Height" Value="50"/>
<Setter Property="MinHeight" Value="48"/>
<Setter Property="FontSize" Value="16"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="Padding" Value="16,0"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="HorizontalContentAlignment" Value="Center"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosText}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosBorder}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button.pos /template/ ContentPresenter">
<Setter Property="Transitions">
<Transitions><BrushTransition Property="Background" Duration="0:0:0.08"/></Transitions>
</Setter>
</Style>
<Style Selector="Button.pos TextBlock">
<Setter Property="TextWrapping" Value="Wrap"/>
<Setter Property="TextAlignment" Value="Center"/>
</Style>
<!-- Главная зелёная -->
<Style Selector="Button.primary">
<Setter Property="Background" Value="{DynamicResource PosBrand}"/>
<Setter Property="Foreground" Value="{DynamicResource PosBrandText}"/>
<Setter Property="BorderThickness" Value="0"/>
<Setter Property="FontWeight" Value="Bold"/>
</Style>
<Style Selector="Button.primary:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosBrandHover}"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource PosBrandText}"/>
</Style>
<Style Selector="Button.primary:pressed /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosBrandPressed}"/>
</Style>
<!-- Вторичная (контур) -->
<Style Selector="Button.ghost">
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosText}"/>
</Style>
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosSubtle}"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource PosText}"/>
</Style>
<!-- Опасная (красный контур, без заливки) -->
<Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosDanger}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosDanger}"/>
</Style>
<Style Selector="Button.danger:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosDangerSoft}"/>
<Setter Property="TextElement.Foreground" Value="{DynamicResource PosDanger}"/>
</Style>
<!-- Клавиша клавиатуры/степпер -->
<Style Selector="Button.key">
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosText}"/>
<Setter Property="FontSize" Value="22"/>
<Setter Property="FontWeight" Value="SemiBold"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosBorder}"/>
<Setter Property="BorderThickness" Value="1"/>
</Style>
<Style Selector="Button.key:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosSubtle}"/>
</Style>
<!-- Сегмент переключателя (выбранный — зелёный) -->
<Style Selector="Button.seg">
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosText}"/>
<Setter Property="Height" Value="52"/>
</Style>
<Style Selector="Button.seg.on">
<Setter Property="Background" Value="{DynamicResource PosBrand}"/>
<Setter Property="Foreground" Value="{DynamicResource PosBrandText}"/>
<Setter Property="BorderThickness" Value="0"/>
</Style>
<Style Selector="Button.seg.on /template/ ContentPresenter">
<Setter Property="Background" Value="{DynamicResource PosBrand}"/>
</Style>
<!-- ── Поля ввода ──────────────────────────────────────────────── -->
<Style Selector="TextBox.pos">
<Setter Property="FontSize" Value="17"/>
<Setter Property="Height" Value="52"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="CornerRadius" Value="12"/>
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosText}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosBorder}"/>
</Style>
<Style Selector="TextBox.search">
<Setter Property="FontSize" Value="19"/>
<Setter Property="Height" Value="58"/>
<Setter Property="VerticalContentAlignment" Value="Center"/>
<Setter Property="CornerRadius" Value="14"/>
<Setter Property="Background" Value="{DynamicResource PosCard}"/>
<Setter Property="Foreground" Value="{DynamicResource PosText}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosBorder}"/>
</Style>
<!-- Капсулы горячих клавиш -->
<Style Selector="Border.hotkey">
<Setter Property="Background" Value="{DynamicResource PosSubtle}"/>
<Setter Property="BorderBrush" Value="{DynamicResource PosBorder}"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="CornerRadius" Value="8"/>
<Setter Property="Padding" Value="9,4"/>
</Style>
</Styles>

View file

@ -0,0 +1,46 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.ThemeDictionaries>
<!-- ── Светлая тема (по умолчанию) ─────────────────────────────── -->
<ResourceDictionary x:Key="Light">
<SolidColorBrush x:Key="PosBg" Color="#F4F6F5"/>
<SolidColorBrush x:Key="PosCard" Color="#FFFFFF"/>
<SolidColorBrush x:Key="PosSubtle" Color="#F0F3F1"/>
<SolidColorBrush x:Key="PosBorder" Color="#E3E8E6"/>
<SolidColorBrush x:Key="PosText" Color="#0F1F17"/>
<SolidColorBrush x:Key="PosTextMuted" Color="#5F6F68"/>
<SolidColorBrush x:Key="PosBrand" Color="#16A34A"/>
<SolidColorBrush x:Key="PosBrandHover" Color="#15803D"/>
<SolidColorBrush x:Key="PosBrandPressed" Color="#166534"/>
<SolidColorBrush x:Key="PosBrandText" Color="#FFFFFF"/>
<SolidColorBrush x:Key="PosBrandSoft" Color="#DCFCE7"/>
<SolidColorBrush x:Key="PosDanger" Color="#DC2626"/>
<SolidColorBrush x:Key="PosDangerSoft" Color="#FEE2E2"/>
<SolidColorBrush x:Key="PosWarn" Color="#B45309"/>
<SolidColorBrush x:Key="PosWarnSoft" Color="#FEF3C7"/>
<SolidColorBrush x:Key="PosScrim" Color="#33203028"/>
</ResourceDictionary>
<!-- ── Тёмная тема (опция, те же токены) ───────────────────────── -->
<ResourceDictionary x:Key="Dark">
<SolidColorBrush x:Key="PosBg" Color="#0F1713"/>
<SolidColorBrush x:Key="PosCard" Color="#15201B"/>
<SolidColorBrush x:Key="PosSubtle" Color="#1B2821"/>
<SolidColorBrush x:Key="PosBorder" Color="#2A3A31"/>
<SolidColorBrush x:Key="PosText" Color="#E8EFEA"/>
<SolidColorBrush x:Key="PosTextMuted" Color="#9BAAA2"/>
<SolidColorBrush x:Key="PosBrand" Color="#22C55E"/>
<SolidColorBrush x:Key="PosBrandHover" Color="#16A34A"/>
<SolidColorBrush x:Key="PosBrandPressed" Color="#15803D"/>
<SolidColorBrush x:Key="PosBrandText" Color="#06140C"/>
<SolidColorBrush x:Key="PosBrandSoft" Color="#13331F"/>
<SolidColorBrush x:Key="PosDanger" Color="#F87171"/>
<SolidColorBrush x:Key="PosDangerSoft" Color="#3A1A1A"/>
<SolidColorBrush x:Key="PosWarn" Color="#FBBF24"/>
<SolidColorBrush x:Key="PosWarnSoft" Color="#33280D"/>
<SolidColorBrush x:Key="PosScrim" Color="#66000000"/>
</ResourceDictionary>
</ResourceDictionary.ThemeDictionaries>
</ResourceDictionary>

View file

@ -90,11 +90,32 @@ private static void WriteGallery(string dir, List<(string file, string caption)>
".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:16px;padding:20px}" + ".grid{display:grid;grid-template-columns:repeat(auto-fill,minmax(420px,1fr));gap:16px;padding:20px}" +
"figure{margin:0;background:#1e293b;border:1px solid #334155;border-radius:10px;overflow:hidden}" + "figure{margin:0;background:#1e293b;border:1px solid #334155;border-radius:10px;overflow:hidden}" +
"img{width:100%;display:block;background:#000;cursor:zoom-in}figcaption{padding:8px 10px;color:#cbd5e1;font-size:13px}</style></head><body>"); "img{width:100%;display:block;background:#000;cursor:zoom-in}figcaption{padding:8px 10px;color:#cbd5e1;font-size:13px}</style></head><body>");
sb.Append($"<header><h1>Касса food-market — скриншоты процессов</h1><div style=\"color:#94a3b8\">рендер headless (Avalonia/Skia, Linux) — 1920×1080 и 1366×768. Кадров: {shots.Count}</div></header><div class=\"grid\">"); sb.Append($"<header><h1>Касса food-market — скриншоты процессов</h1><div style=\"color:#94a3b8\">Светлая тема (Avalonia/Skia, headless, Linux) — 1920×1080 и 1366×768. Кадров: {shots.Count}</div></header>");
sb.Append("<header style=\"background:#eef2f0;color:#0f1f17\"><h1 style=\"color:#16a34a\">Стало (светлая тема)</h1></header>");
sb.Append("<div class=\"grid\">");
foreach (var (file, caption) in shots) foreach (var (file, caption) in shots)
sb.Append($"<figure><a href=\"{file}\" target=\"_blank\"><img src=\"{file}\" loading=\"lazy\"></a><figcaption>{System.Net.WebUtility.HtmlEncode(caption)}</figcaption></figure>"); sb.Append($"<figure><a href=\"{file}\" target=\"_blank\"><img src=\"{file}\" loading=\"lazy\"></a><figcaption>{System.Net.WebUtility.HtmlEncode(caption)}</figcaption></figure>");
sb.Append("</div>"); sb.Append("</div>");
// Раздел «Было» — прежняя тёмная тема (если сохранена в before/).
var beforeDir = Path.Combine(dir, "before");
if (Directory.Exists(beforeDir))
{
var befs = Directory.GetFiles(beforeDir, "*.png").OrderBy(f => f).ToList();
if (befs.Count > 0)
{
sb.Append("<header style=\"background:#1e293b;margin-top:8px\"><h1 style=\"color:#e2e8f0\">Было (тёмная тема)</h1>" +
"<div style=\"color:#94a3b8\">для сравнения — прежний мрачный интерфейс</div></header>");
sb.Append("<div class=\"grid\" style=\"background:#0f172a\">");
foreach (var b in befs)
{
var rel = "before/" + Path.GetFileName(b);
sb.Append($"<figure style=\"background:#1e293b;border-color:#334155\"><a href=\"{rel}\" target=\"_blank\"><img src=\"{rel}\" loading=\"lazy\"></a><figcaption style=\"color:#cbd5e1\">{System.Net.WebUtility.HtmlEncode(Path.GetFileNameWithoutExtension(b))}</figcaption></figure>");
}
sb.Append("</div>");
}
}
// Раздел «Windows 11» — реальные скриншоты окна на Windows (если есть). // Раздел «Windows 11» — реальные скриншоты окна на Windows (если есть).
var winDir = Path.Combine(dir, "windows"); var winDir = Path.Combine(dir, "windows");
if (Directory.Exists(winDir)) if (Directory.Exists(winDir))

View file

@ -24,19 +24,24 @@ public partial class MainWindowViewModel : ViewModelBase
[ObservableProperty] private object? _currentPage; [ObservableProperty] private object? _currentPage;
[ObservableProperty] private string _cashierName = "—"; [ObservableProperty] private string _cashierName = "—";
[ObservableProperty] private string _shiftNumber = "смена не открыта"; [ObservableProperty] private string _shiftNumber = "смена не открыта";
[ObservableProperty] private bool _isShiftOpen;
[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;
[ObservableProperty] private bool _isPaymentOpen; [ObservableProperty] private bool _isPaymentOpen;
[ObservableProperty] private string? _updateMessage; [ObservableProperty] private string? _updateMessage;
public string NetBadge => IsOnline ? "● на связи" : "○ нет связи"; public string NetBadge => IsOnline ? "На связи" : "Нет связи";
public string QueueBadge => QueueCount > 0 ? $"{QueueCount} чек(ов) в очереди" : "очередь пуста"; public string ShiftBadge => IsShiftOpen ? $"Смена {ShiftNumber}" : "Смена закрыта";
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 OnShiftNumberChanged(string value) => OnPropertyChanged(nameof(ShiftBadge));
/// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary> /// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary>
public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000001"; } public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000001"; IsShiftOpen = true; PointName = "Магазин №1 · ул. Абая 10"; }
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(); }
@ -86,7 +91,7 @@ private static string LocalVersion()
private void RefreshStatus() { if (_svc is not null) QueueCount = _svc.Store.PendingCount(); } private void RefreshStatus() { if (_svc is not null) QueueCount = _svc.Store.PendingCount(); }
[RelayCommand] private void ShowSale() { CurrentPage = _sale ??= (_svc is null ? SaleViewModel.Sample() : new SaleViewModel(_svc) { PaymentRequested = OpenPayment, SaleCommitted = _ => RefreshStatus() }); } [RelayCommand] private void ShowSale() { CurrentPage = _sale ??= (_svc is null ? SaleViewModel.Sample() : new SaleViewModel(_svc) { PaymentRequested = OpenPayment, SaleCommitted = _ => RefreshStatus() }); }
[RelayCommand] private void ShowShift() => CurrentPage = _svc is null ? ShiftViewModel.Sample() : new ShiftViewModel(_svc) { OnShiftChanged = s => { ShiftNumber = s ?? "смена не открыта"; } }; [RelayCommand] private void ShowShift() => CurrentPage = _svc is null ? ShiftViewModel.Sample() : new ShiftViewModel(_svc) { OnShiftChanged = s => { ShiftNumber = s ?? "смена не открыта"; IsShiftOpen = s is not null; } };
[RelayCommand] private void ShowReturn() => CurrentPage = _svc is null ? ReturnViewModel.Sample() : new ReturnViewModel(_svc); [RelayCommand] private void ShowReturn() => CurrentPage = _svc is null ? ReturnViewModel.Sample() : new ReturnViewModel(_svc);
private void OpenPayment(PaymentViewModel pvm) private void OpenPayment(PaymentViewModel pvm)
@ -369,6 +374,10 @@ public partial class SaleViewModel : ViewModelBase
[ObservableProperty] private decimal _total; [ObservableProperty] private decimal _total;
[ObservableProperty] private int _itemCount; [ObservableProperty] private int _itemCount;
[ObservableProperty] private SaleLineVm? _selectedLine; [ObservableProperty] private SaleLineVm? _selectedLine;
public bool IsCartEmpty => ItemCount == 0;
public bool HasAnyDiscount => Discount > 0m;
partial void OnItemCountChanged(int value) => OnPropertyChanged(nameof(IsCartEmpty));
partial void OnDiscountChanged(decimal value) => OnPropertyChanged(nameof(HasAnyDiscount));
/// <summary>Поднять модальную оплату (обрабатывает оболочка).</summary> /// <summary>Поднять модальную оплату (обрабатывает оболочка).</summary>
public Action<PaymentViewModel>? PaymentRequested; public Action<PaymentViewModel>? PaymentRequested;
@ -433,6 +442,17 @@ public void SetLineQuantity(SaleLineVm line, decimal qty)
Recalc(); Recalc();
} }
[RelayCommand] private void IncLine(SaleLineVm line) { if (line.Model is { } m) SetLineQuantity(line, m.Quantity + 1m); }
[RelayCommand] private void DecLine(SaleLineVm line) { if (line.Model is { } m) SetLineQuantity(line, m.Quantity - 1m); }
[RelayCommand]
private void RemoveLine(SaleLineVm line)
{
if (line.Model is { } m) _cart.Remove(m);
Lines.Remove(line);
if (ReferenceEquals(SelectedLine, line)) SelectedLine = Lines.FirstOrDefault();
Recalc();
}
public void SetCartDiscount(decimal amount) { _cart.SetCartDiscount(amount); Recalc(); Message = $"Скидка на чек: {amount:N0} ₸"; } public void SetCartDiscount(decimal amount) { _cart.SetCartDiscount(amount); Recalc(); Message = $"Скидка на чек: {amount:N0} ₸"; }
[RelayCommand] [RelayCommand]
@ -524,16 +544,35 @@ public partial class PaymentViewModel : ViewModelBase
public decimal Change => PaymentMath.Compute(Total, PaidCash, PaidCard).Change; public decimal Change => PaymentMath.Compute(Total, PaidCash, PaidCard).Change;
public decimal Shortfall => PaymentMath.Compute(Total, PaidCash, PaidCard).Shortfall; public decimal Shortfall => PaymentMath.Compute(Total, PaidCash, PaidCard).Shortfall;
public bool Sufficient => PaymentMath.Compute(Total, PaidCash, PaidCard).Sufficient; public bool Sufficient => PaymentMath.Compute(Total, PaidCash, PaidCard).Sufficient;
public bool IsCash => Method == "Наличные";
public bool IsCard => Method == "Карта";
public bool IsMixed => Method == "Смешанная";
partial void OnPaidCashChanged(decimal value) => RaisePay(); partial void OnPaidCashChanged(decimal value) => RaisePay();
partial void OnPaidCardChanged(decimal value) => RaisePay(); partial void OnPaidCardChanged(decimal value) => RaisePay();
partial void OnTotalChanged(decimal value) => RaisePay(); partial void OnTotalChanged(decimal value) => RaisePay();
partial void OnMethodChanged(string value) { OnPropertyChanged(nameof(IsCash)); OnPropertyChanged(nameof(IsCard)); OnPropertyChanged(nameof(IsMixed)); }
private void RaisePay() { OnPropertyChanged(nameof(Change)); OnPropertyChanged(nameof(Shortfall)); OnPropertyChanged(nameof(Sufficient)); } private void RaisePay() { OnPropertyChanged(nameof(Change)); OnPropertyChanged(nameof(Shortfall)); OnPropertyChanged(nameof(Sufficient)); }
/// <summary>Вызывается при подтверждении оплаты (cash, card, change).</summary> /// <summary>Вызывается при подтверждении оплаты (cash, card, change).</summary>
public Func<decimal, decimal, decimal, Task>? OnConfirm; public Func<decimal, decimal, decimal, Task>? OnConfirm;
public Action? OnCancel; public Action? OnCancel;
[RelayCommand] private void ExactCash() { PaidCash = Total; PaidCard = 0m; Method = "Наличные"; } [RelayCommand] private void ExactCash() { PaidCash = Total; PaidCard = 0m; }
[RelayCommand] private void SelectCash() { Method = "Наличные"; PaidCard = 0m; }
[RelayCommand] private void SelectCard() { Method = "Карта"; PaidCard = Total; PaidCash = 0m; }
[RelayCommand] private void SelectMixed() { Method = "Смешанная"; }
/// <summary>Быстрая сумма наличных: «Без сдачи» (ровно) или номинал.</summary>
[RelayCommand] private void QuickCash(string amount)
{
if (amount == "exact") { PaidCash = Total; return; }
if (decimal.TryParse(amount, out var a)) PaidCash = a;
}
// Экранная цифровая клавиатура для наличных (целые тенге).
[RelayCommand] private void KeyDigit(string d) { if (int.TryParse(d, out var n)) PaidCash = PaidCash * 10m + n; }
[RelayCommand] private void KeyBack() { PaidCash = Math.Floor(PaidCash / 10m); }
[RelayCommand] private void KeyClear() { PaidCash = 0m; }
[RelayCommand] [RelayCommand]
private async Task Confirm() private async Task Confirm()
@ -544,8 +583,8 @@ private async Task Confirm()
[RelayCommand] private void Cancel() => OnCancel?.Invoke(); [RelayCommand] private void Cancel() => OnCancel?.Invoke();
public static PaymentViewModel SampleCash() => new() { Total = 3365m, PaidCash = 5000m, Method = "Наличные" }; public static PaymentViewModel SampleCash() => new() { Total = 3362m, PaidCash = 5000m, Method = "Наличные" };
public static PaymentViewModel SampleMixed() => new() { Total = 3365m, PaidCash = 1365m, PaidCard = 2000m, Method = "Смешанная" }; public static PaymentViewModel SampleMixed() => new() { Total = 3362m, PaidCash = 1362m, PaidCard = 2000m, Method = "Смешанная" };
} }
/// <summary>Смена: открыть (размен) / закрыть (факт → должно/факт/разница), /// <summary>Смена: открыть (размен) / закрыть (факт → должно/факт/разница),
@ -568,9 +607,12 @@ public partial class ShiftViewModel : ViewModelBase
public decimal ExpectedCash => ShiftMath.ExpectedCash(OpeningFloat, SalesCash, ReturnsCash, DrawerIn, DrawerOut); public decimal ExpectedCash => ShiftMath.ExpectedCash(OpeningFloat, SalesCash, ReturnsCash, DrawerIn, DrawerOut);
public decimal Difference => ShiftMath.CashDifference(ActualCash, ExpectedCash); public decimal Difference => ShiftMath.CashDifference(ActualCash, ExpectedCash);
public bool DiffSurplus => Difference > 0m; // излишек
public bool DiffShortage => Difference < 0m; // недостача
public string DiffLabel => Difference == 0m ? "Сходится" : Difference > 0m ? "Излишек" : "Недостача";
partial void OnOpeningFloatChanged(decimal value) => RaiseCalc(); partial void OnOpeningFloatChanged(decimal value) => RaiseCalc();
partial void OnActualCashChanged(decimal value) => RaiseCalc(); partial void OnActualCashChanged(decimal value) => RaiseCalc();
private void RaiseCalc() { OnPropertyChanged(nameof(ExpectedCash)); OnPropertyChanged(nameof(Difference)); } private void RaiseCalc() { OnPropertyChanged(nameof(ExpectedCash)); OnPropertyChanged(nameof(Difference)); OnPropertyChanged(nameof(DiffSurplus)); OnPropertyChanged(nameof(DiffShortage)); OnPropertyChanged(nameof(DiffLabel)); }
/// <summary>Сообщить оболочке новый номер смены (или null — закрыта).</summary> /// <summary>Сообщить оболочке новый номер смены (или null — закрыта).</summary>
public Action<string?>? OnShiftChanged; public Action<string?>? OnShiftChanged;

View file

@ -3,63 +3,65 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels" xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.LoginView" x:Class="foodmarket.Pos.Views.LoginView"
x:DataType="vm:LoginViewModel"> x:DataType="vm:LoginViewModel">
<Grid Background="#0f172a"> <Grid Background="{DynamicResource PosBg}">
<Border Background="#1e293b" CornerRadius="12" Width="440" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center"> <Border Classes="card" Width="440" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center" CornerRadius="18">
<StackPanel Spacing="14"> <StackPanel Spacing="16">
<TextBlock Text="FOOD MARKET" Foreground="#22c55e" FontWeight="Bold" FontSize="22" HorizontalAlignment="Center"/> <StackPanel Spacing="4" HorizontalAlignment="Center">
<TextBlock Text="Вход кассира" Foreground="#f8fafc" FontSize="18" HorizontalAlignment="Center"/> <Border Background="{DynamicResource PosBrand}" CornerRadius="12" Width="48" Height="48" HorizontalAlignment="Center">
<TextBlock Text="FM" Foreground="White" FontWeight="Bold" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Быстрый вход по PIN (зарегистрированное устройство) -->
<StackPanel Spacing="10" IsVisible="{Binding PinMode}">
<Border Background="#0b1220" CornerRadius="8" Height="56">
<TextBlock Text="{Binding PinMasked}" Foreground="#f8fafc" FontSize="30" LetterSpacing="6" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border> </Border>
<TextBlock Text="{Binding Error}" Foreground="#fca5a5" TextWrapping="Wrap" HorizontalAlignment="Center" IsVisible="{Binding Error, Converter={x:Static ObjectConverters.IsNotNull}}"/> <TextBlock Text="Food Market · Касса" Foreground="{DynamicResource PosText}" FontWeight="Bold" FontSize="18" HorizontalAlignment="Center" Margin="0,8,0,0"/>
<Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,Auto,Auto,Auto" > <TextBlock Text="Вход кассира" Foreground="{DynamicResource PosTextMuted}" FontSize="14" HorizontalAlignment="Center"/>
<Button Grid.Row="0" Grid.Column="0" Classes="pos" Margin="3" Height="56" FontSize="22" Content="1" Command="{Binding PinDigitCommand}" CommandParameter="1"/>
<Button Grid.Row="0" Grid.Column="1" Classes="pos" Margin="3" Height="56" FontSize="22" Content="2" Command="{Binding PinDigitCommand}" CommandParameter="2"/>
<Button Grid.Row="0" Grid.Column="2" Classes="pos" Margin="3" Height="56" FontSize="22" Content="3" Command="{Binding PinDigitCommand}" CommandParameter="3"/>
<Button Grid.Row="1" Grid.Column="0" Classes="pos" Margin="3" Height="56" FontSize="22" Content="4" Command="{Binding PinDigitCommand}" CommandParameter="4"/>
<Button Grid.Row="1" Grid.Column="1" Classes="pos" Margin="3" Height="56" FontSize="22" Content="5" Command="{Binding PinDigitCommand}" CommandParameter="5"/>
<Button Grid.Row="1" Grid.Column="2" Classes="pos" Margin="3" Height="56" FontSize="22" Content="6" Command="{Binding PinDigitCommand}" CommandParameter="6"/>
<Button Grid.Row="2" Grid.Column="0" Classes="pos" Margin="3" Height="56" FontSize="22" Content="7" Command="{Binding PinDigitCommand}" CommandParameter="7"/>
<Button Grid.Row="2" Grid.Column="1" Classes="pos" Margin="3" Height="56" FontSize="22" Content="8" Command="{Binding PinDigitCommand}" CommandParameter="8"/>
<Button Grid.Row="2" Grid.Column="2" Classes="pos" Margin="3" Height="56" FontSize="22" Content="9" Command="{Binding PinDigitCommand}" CommandParameter="9"/>
<Button Grid.Row="3" Grid.Column="0" Classes="pos" Margin="3" Height="56" FontSize="18" Content="⌫" Command="{Binding PinBackCommand}"/>
<Button Grid.Row="3" Grid.Column="1" Classes="pos" Margin="3" Height="56" FontSize="22" Content="0" Command="{Binding PinDigitCommand}" CommandParameter="0"/>
<Button Grid.Row="3" Grid.Column="2" Classes="pos primary" Margin="3" Height="56" FontSize="18" Content="Войти" Command="{Binding PinLoginCommand}" IsEnabled="{Binding !Busy}"/>
</Grid>
<Button Classes="pos" HorizontalAlignment="Stretch" Height="40" FontSize="13" Content="Вход по логину и паролю" Command="{Binding UsePasswordCommand}"/>
</StackPanel> </StackPanel>
<!-- PIN -->
<StackPanel Spacing="12" IsVisible="{Binding PinMode}">
<Border Background="{DynamicResource PosSubtle}" CornerRadius="12" Height="56">
<TextBlock Text="{Binding PinMasked}" Foreground="{DynamicResource PosText}" FontSize="30" LetterSpacing="8" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<TextBlock Text="{Binding Error}" Foreground="{DynamicResource PosDanger}" TextWrapping="Wrap" HorizontalAlignment="Center" IsVisible="{Binding Error, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,Auto,Auto,Auto">
<Button Grid.Row="0" Grid.Column="0" Classes="key" Margin="4" Height="58" Content="1" Command="{Binding PinDigitCommand}" CommandParameter="1"/>
<Button Grid.Row="0" Grid.Column="1" Classes="key" Margin="4" Height="58" Content="2" Command="{Binding PinDigitCommand}" CommandParameter="2"/>
<Button Grid.Row="0" Grid.Column="2" Classes="key" Margin="4" Height="58" Content="3" Command="{Binding PinDigitCommand}" CommandParameter="3"/>
<Button Grid.Row="1" Grid.Column="0" Classes="key" Margin="4" Height="58" Content="4" Command="{Binding PinDigitCommand}" CommandParameter="4"/>
<Button Grid.Row="1" Grid.Column="1" Classes="key" Margin="4" Height="58" Content="5" Command="{Binding PinDigitCommand}" CommandParameter="5"/>
<Button Grid.Row="1" Grid.Column="2" Classes="key" Margin="4" Height="58" Content="6" Command="{Binding PinDigitCommand}" CommandParameter="6"/>
<Button Grid.Row="2" Grid.Column="0" Classes="key" Margin="4" Height="58" Content="7" Command="{Binding PinDigitCommand}" CommandParameter="7"/>
<Button Grid.Row="2" Grid.Column="1" Classes="key" Margin="4" Height="58" Content="8" Command="{Binding PinDigitCommand}" CommandParameter="8"/>
<Button Grid.Row="2" Grid.Column="2" Classes="key" Margin="4" Height="58" Content="9" Command="{Binding PinDigitCommand}" CommandParameter="9"/>
<Button Grid.Row="3" Grid.Column="0" Classes="key" Margin="4" Height="58" FontSize="18" Content="⌫" Command="{Binding PinBackCommand}"/>
<Button Grid.Row="3" Grid.Column="1" Classes="key" Margin="4" Height="58" Content="0" Command="{Binding PinDigitCommand}" CommandParameter="0"/>
<Button Grid.Row="3" Grid.Column="2" Classes="key primary" Margin="4" Height="58" FontSize="17" Content="Войти" Command="{Binding PinLoginCommand}" IsEnabled="{Binding !Busy}"/>
</Grid>
<Button Classes="pos ghost" HorizontalAlignment="Stretch" Height="42" FontSize="13" Content="Вход по логину и паролю" Command="{Binding UsePasswordCommand}"/>
</StackPanel>
<!-- Логин/пароль -->
<StackPanel Spacing="14" IsVisible="{Binding !PinMode}"> <StackPanel Spacing="14" IsVisible="{Binding !PinMode}">
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Адрес сервера" Foreground="#94a3b8"/> <TextBlock Text="Адрес сервера" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
<TextBox Classes="pos" Text="{Binding ServerUrl}" FontSize="16"/> <TextBox Classes="pos" Text="{Binding ServerUrl}" FontSize="15"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="Логин кассира" Foreground="#94a3b8"/> <TextBlock Text="Логин кассира" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
<TextBox Classes="pos" Text="{Binding Username}" FontSize="18"/> <TextBox Classes="pos" Text="{Binding Username}" FontSize="17"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Text="PIN / пароль" Foreground="#94a3b8"/> <TextBlock Text="PIN / пароль" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
<TextBox Classes="pos" Text="{Binding Pin}" PasswordChar="●" FontSize="18"/> <TextBox Classes="pos" Text="{Binding Pin}" PasswordChar="●" FontSize="17"/>
</StackPanel> </StackPanel>
<TextBlock Text="{Binding Error}" Foreground="#fca5a5" TextWrapping="Wrap" IsVisible="{Binding Error, Converter={x:Static ObjectConverters.IsNotNull}}"/> <TextBlock Text="{Binding Error}" Foreground="{DynamicResource PosDanger}" TextWrapping="Wrap" IsVisible="{Binding Error, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="56" FontSize="18" Content="Войти" Command="{Binding LoginCommand}" IsDefault="True" IsEnabled="{Binding !Busy}" IsVisible="{Binding !RequirePointSelection}"/> <Button Classes="pos primary" HorizontalAlignment="Stretch" Height="54" FontSize="18" Content="Войти" Command="{Binding LoginCommand}" IsDefault="True" IsEnabled="{Binding !Busy}" IsVisible="{Binding !RequirePointSelection}"/>
<!-- Регистрация устройства: выбор точки продаж (только администратор) -->
<StackPanel Spacing="8" IsVisible="{Binding RequirePointSelection}"> <StackPanel Spacing="8" IsVisible="{Binding RequirePointSelection}">
<TextBlock Text="Регистрация кассы — выберите точку продаж:" Foreground="#f8fafc"/> <TextBlock Text="Регистрация кассы — выберите точку продаж:" Foreground="{DynamicResource PosText}" FontSize="14"/>
<ComboBox ItemsSource="{Binding Points}" SelectedItem="{Binding SelectedPoint}" HorizontalAlignment="Stretch" FontSize="16"/> <ComboBox ItemsSource="{Binding Points}" SelectedItem="{Binding SelectedPoint}" HorizontalAlignment="Stretch" FontSize="15"/>
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="52" Content="Зарегистрировать на этой точке" Command="{Binding ConfirmPointCommand}"/> <Button Classes="pos primary" HorizontalAlignment="Stretch" Height="50" Content="Зарегистрировать на этой точке" Command="{Binding ConfirmPointCommand}"/>
</StackPanel> </StackPanel>
<Button Classes="pos" HorizontalAlignment="Stretch" Height="40" FontSize="13" Content="Быстрый вход по PIN" Command="{Binding UsePinCommand}" IsVisible="{Binding DeviceRegistered}"/> <Button Classes="pos ghost" HorizontalAlignment="Stretch" Height="40" FontSize="13" Content="Быстрый вход по PIN" Command="{Binding UsePinCommand}" IsVisible="{Binding DeviceRegistered}"/>
<Button Classes="pos ghost" HorizontalAlignment="Stretch" Height="38" FontSize="12.5" Content="Сменить точку продаж (админ)" Command="{Binding ResetDeviceCommand}" ToolTip.Tip="Сбросить регистрацию устройства. Новую точку выберет только администратор."/>
<Button Classes="pos" HorizontalAlignment="Stretch" Height="40" FontSize="13" Command="{Binding ResetDeviceCommand}" ToolTip.Tip="Сбросить регистрацию устройства. Новую точку выберет только администратор.">
<TextBlock Text="Сменить точку продаж (админ)" TextWrapping="Wrap" TextAlignment="Center"/>
</Button>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Border> </Border>

View file

@ -3,43 +3,69 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels" xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.MainWindow" x:Class="foodmarket.Pos.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Width="1280" Height="800" MinWidth="1024" MinHeight="680" Width="1366" Height="768" MinWidth="1100" MinHeight="680"
Background="#0f172a" Title="Касса food-market" Background="{DynamicResource PosBg}" Title="Касса food-market"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<Panel> <Panel>
<Grid RowDefinitions="54,*,34"> <Grid RowDefinitions="64,*,46">
<!-- Статус-бар -->
<Border Grid.Row="0" Background="#1e293b" Padding="16,0"> <!-- Верхняя полоса -->
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto"> <Border Grid.Row="0" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1" Padding="20,0">
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center"> <Grid ColumnDefinitions="Auto,*,Auto">
<TextBlock Text="FOOD MARKET · Касса" FontWeight="Bold" Foreground="#22c55e" FontSize="16" VerticalAlignment="Center"/> <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
<Border Background="{DynamicResource PosBrand}" CornerRadius="9" Width="34" Height="34">
<TextBlock Text="FM" Foreground="White" FontWeight="Bold" FontSize="15" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border>
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Food Market · Касса" Foreground="{DynamicResource PosText}" FontWeight="Bold" FontSize="16"/>
<TextBlock Text="{Binding PointName}" Foreground="{DynamicResource PosTextMuted}" FontSize="12.5"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="18" VerticalAlignment="Center">
<TextBlock Text="{Binding CashierName}" Foreground="#cbd5e1" VerticalAlignment="Center"/>
<TextBlock Text="{Binding ShiftNumber}" Foreground="#93c5fd" VerticalAlignment="Center"/>
</StackPanel> </StackPanel>
<TextBlock Grid.Column="1" Text="{Binding UpdateMessage}" Foreground="#fde68a" VerticalAlignment="Center" HorizontalAlignment="Center"
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}" FontSize="12"/> <StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<TextBlock Grid.Column="3" Margin="18,0" Text="{Binding NetBadge}" VerticalAlignment="Center" Foreground="#34d399"/> <Border Classes="chip" Classes.ok="{Binding IsShiftOpen}" Classes.warn="{Binding !IsShiftOpen}">
<TextBlock Grid.Column="4" Text="{Binding QueueBadge}" Foreground="#fbbf24" VerticalAlignment="Center"/> <TextBlock Text="{Binding ShiftBadge}"/>
</Border>
<Border Classes="chip" Classes.ok="{Binding IsOnline}" Classes.warn="{Binding !IsOnline}">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{Binding NetBadge}"/>
<TextBlock Text="{Binding QueueBadge}" IsVisible="{Binding QueueBadge, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</Border>
<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>
<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> </Grid>
</Border> </Border>
<!-- Текущая страница --> <!-- Текущая страница -->
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}" Margin="0"/> <ContentControl Grid.Row="1" Content="{Binding CurrentPage}"/>
<!-- Нижняя панель навигации --> <!-- Горячие клавиши -->
<Border Grid.Row="2" Background="#1e293b"> <Border Grid.Row="2" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,1,0,0">
<StackPanel Orientation="Horizontal" Spacing="2" HorizontalAlignment="Center"> <ItemsControl VerticalAlignment="Center" HorizontalAlignment="Center">
<TextBlock Text="F1 Помощь · F2 Поиск · F7 Внесение/Выплата · F9 Возврат · F10 Смена · F12 Оплата · Esc Отмена" <ItemsControl.ItemsPanel><ItemsPanelTemplate><StackPanel Orientation="Horizontal" Spacing="8"/></ItemsPanelTemplate></ItemsControl.ItemsPanel>
Foreground="#94a3b8" VerticalAlignment="Center" FontSize="12"/> <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> <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>
<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>
<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>
<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>
<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>
<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>
<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>
</ItemsControl>
</Border> </Border>
</Grid> </Grid>
<!-- Модальная оплата поверх экрана продажи --> <!-- Модальная оплата -->
<Border Background="#cc0b1220" IsVisible="{Binding IsPaymentOpen}"> <Border Background="{DynamicResource PosScrim}" IsVisible="{Binding IsPaymentOpen}">
<ContentControl Content="{Binding Payment}"/> <ContentControl Content="{Binding Payment}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</Border> </Border>
</Panel> </Panel>
</Window> </Window>

View file

@ -3,37 +3,74 @@
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">
<Grid Background="#0f172a"> <Border Classes="card" Width="760" Padding="28" CornerRadius="18">
<Border Background="#1e293b" CornerRadius="12" Width="560" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center"> <StackPanel Spacing="16">
<StackPanel Spacing="14">
<TextBlock Text="Оплата чека" FontSize="24" FontWeight="Bold" Foreground="#f8fafc" HorizontalAlignment="Center"/>
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="К оплате" Foreground="#94a3b8" FontSize="18" VerticalAlignment="Center"/> <TextBlock Text="Оплата" FontSize="22" FontWeight="Bold" Foreground="{DynamicResource PosText}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="#34d399" FontSize="30" FontWeight="Bold"/> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<TextBlock Text="К оплате" Foreground="{DynamicResource PosTextMuted}" FontSize="16" VerticalAlignment="Center"/>
<TextBlock Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="30" FontWeight="Bold"/>
</StackPanel>
</Grid> </Grid>
<Separator Background="#334155"/>
<Grid ColumnDefinitions="140,*"> <!-- Сегменты способа оплаты -->
<TextBlock Text="Наличные" Foreground="#cbd5e1" VerticalAlignment="Center"/> <Grid ColumnDefinitions="*,*,*">
<TextBox Grid.Column="1" Classes="pos" Text="{Binding PaidCash}" FontSize="22"/> <Button Classes="pos seg" Classes.on="{Binding IsCash}" Margin="0,0,5,0" Content="Наличные" Command="{Binding SelectCashCommand}"/>
<Button Grid.Column="1" Classes="pos seg" Classes.on="{Binding IsCard}" Margin="5,0" Content="Карта" Command="{Binding SelectCardCommand}"/>
<Button Grid.Column="2" Classes="pos seg" Classes.on="{Binding IsMixed}" Margin="5,0,0,0" Content="Смешанная" Command="{Binding SelectMixedCommand}"/>
</Grid> </Grid>
<Grid ColumnDefinitions="140,*">
<TextBlock Text="Картой" Foreground="#cbd5e1" VerticalAlignment="Center"/> <Grid ColumnDefinitions="*,320">
<TextBox Grid.Column="1" Classes="pos" Text="{Binding PaidCard}" FontSize="22"/> <!-- Левая часть: суммы -->
<StackPanel Grid.Column="0" Spacing="12" Margin="0,0,20,0">
<StackPanel Spacing="4">
<TextBlock Text="Получено наличными" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/>
<TextBox Classes="pos" FontSize="24" Text="{Binding PaidCash}"/>
</StackPanel>
<StackPanel Spacing="4" IsVisible="{Binding IsMixed}">
<TextBlock Text="Картой" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/>
<TextBox Classes="pos" FontSize="24" Text="{Binding PaidCard}"/>
</StackPanel>
<!-- Быстрые суммы -->
<Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,Auto" IsVisible="{Binding !IsCard}">
<Button Classes="pos ghost" Margin="0,0,4,8" Content="Без сдачи" Command="{Binding QuickCashCommand}" CommandParameter="exact"/>
<Button Grid.Column="1" Classes="pos ghost" Margin="4,0,4,8" Content="1 000" Command="{Binding QuickCashCommand}" CommandParameter="1000"/>
<Button Grid.Column="2" Classes="pos ghost" Margin="4,0,0,8" Content="2 000" Command="{Binding QuickCashCommand}" CommandParameter="2000"/>
<Button Grid.Row="1" Classes="pos ghost" Margin="0,0,4,0" Content="5 000" Command="{Binding QuickCashCommand}" CommandParameter="5000"/>
<Button Grid.Row="1" Grid.Column="1" Classes="pos ghost" Margin="4,0" Content="10 000" Command="{Binding QuickCashCommand}" CommandParameter="10000"/>
<Button Grid.Row="1" Grid.Column="2" Classes="pos ghost" Margin="4,0,0,0" Content="20 000" Command="{Binding QuickCashCommand}" CommandParameter="20000"/>
</Grid> </Grid>
<Border Background="#0b1220" CornerRadius="8" Padding="14">
<!-- Сдача -->
<Border Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Padding="16,14">
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="СДАЧА" Foreground="#f8fafc" FontSize="20" FontWeight="SemiBold" VerticalAlignment="Center"/> <TextBlock Text="СДАЧА" Foreground="{DynamicResource PosText}" FontSize="18" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Change, StringFormat='{}{0:N0} ₸'}" Foreground="#fbbf24" FontSize="30" FontWeight="Bold"/> <TextBlock Grid.Column="1" Text="{Binding Change, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrand}" FontSize="36" FontWeight="Bold"/>
</Grid> </Grid>
</Border> </Border>
<Grid ColumnDefinitions="*,2*"> </StackPanel>
<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="19" Command="{Binding ConfirmCommand}" IsDefault="True" IsEnabled="{Binding Sufficient}"> <!-- Правая часть: цифровая клавиатура -->
<TextBlock Text="Enter · Провести и печать" TextWrapping="Wrap" TextAlignment="Center"/> <Grid Grid.Column="1" ColumnDefinitions="*,*,*" RowDefinitions="*,*,*,*" IsVisible="{Binding !IsCard}">
</Button> <Button Grid.Row="0" Grid.Column="0" Classes="key" Margin="3" Height="54" Content="1" Command="{Binding KeyDigitCommand}" CommandParameter="1"/>
<Button Grid.Row="0" Grid.Column="1" Classes="key" Margin="3" Height="54" Content="2" Command="{Binding KeyDigitCommand}" CommandParameter="2"/>
<Button Grid.Row="0" Grid.Column="2" Classes="key" Margin="3" Height="54" Content="3" Command="{Binding KeyDigitCommand}" CommandParameter="3"/>
<Button Grid.Row="1" Grid.Column="0" Classes="key" Margin="3" Height="54" Content="4" Command="{Binding KeyDigitCommand}" CommandParameter="4"/>
<Button Grid.Row="1" Grid.Column="1" Classes="key" Margin="3" Height="54" Content="5" Command="{Binding KeyDigitCommand}" CommandParameter="5"/>
<Button Grid.Row="1" Grid.Column="2" Classes="key" Margin="3" Height="54" Content="6" Command="{Binding KeyDigitCommand}" CommandParameter="6"/>
<Button Grid.Row="2" Grid.Column="0" Classes="key" Margin="3" Height="54" Content="7" Command="{Binding KeyDigitCommand}" CommandParameter="7"/>
<Button Grid.Row="2" Grid.Column="1" Classes="key" Margin="3" Height="54" Content="8" Command="{Binding KeyDigitCommand}" CommandParameter="8"/>
<Button Grid.Row="2" Grid.Column="2" Classes="key" Margin="3" Height="54" Content="9" Command="{Binding KeyDigitCommand}" CommandParameter="9"/>
<Button Grid.Row="3" Grid.Column="0" Classes="key" Margin="3" Height="54" FontSize="15" Content="Сброс" Command="{Binding KeyClearCommand}"/>
<Button Grid.Row="3" Grid.Column="1" Classes="key" Margin="3" Height="54" Content="0" Command="{Binding KeyDigitCommand}" CommandParameter="0"/>
<Button Grid.Row="3" Grid.Column="2" Classes="key" Margin="3" Height="54" FontSize="18" Content="⌫" Command="{Binding KeyBackCommand}"/>
</Grid> </Grid>
<Button Classes="pos" HorizontalAlignment="Stretch" Height="44" Content="Esc · Отмена" Command="{Binding CancelCommand}"/> </Grid>
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="66" FontSize="20" Command="{Binding ConfirmCommand}" IsDefault="True" IsEnabled="{Binding Sufficient}">
<TextBlock Text="Провести и печатать · Enter" TextAlignment="Center"/>
</Button>
<Button Classes="pos ghost" HorizontalAlignment="Stretch" Height="44" Content="Esc · Назад" Command="{Binding CancelCommand}"/>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid>
</UserControl> </UserControl>

View file

@ -3,54 +3,57 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels" xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.ReturnView" x:Class="foodmarket.Pos.Views.ReturnView"
x:DataType="vm:ReturnViewModel"> x:DataType="vm:ReturnViewModel">
<Grid Background="#0f172a" RowDefinitions="Auto,*,Auto" Margin="16"> <Grid Background="{DynamicResource PosBg}" RowDefinitions="Auto,*,Auto" Margin="20">
<StackPanel Grid.Row="0" Spacing="8"> <StackPanel Grid.Row="0" Spacing="10">
<TextBlock Text="Возврат по чеку" FontSize="22" FontWeight="Bold" Foreground="#f8fafc"/> <TextBlock Text="Возврат по чеку" FontSize="22" FontWeight="Bold" Foreground="{DynamicResource PosText}"/>
<Border Classes="card" Padding="0">
<Grid ColumnDefinitions="*,170"> <Grid ColumnDefinitions="*,170">
<TextBox Classes="pos" Watermark="Номер чека или скан QR (Enter)" Text="{Binding ReceiptNumber}"> <TextBox BorderThickness="0" Background="Transparent" FontSize="18" VerticalContentAlignment="Center" Margin="16,0"
<TextBox.KeyBindings> Watermark="Номер чека или скан QR (Enter)" Text="{Binding ReceiptNumber}">
<KeyBinding Gesture="Enter" Command="{Binding LookupCommand}"/> <TextBox.KeyBindings><KeyBinding Gesture="Enter" Command="{Binding LookupCommand}"/></TextBox.KeyBindings>
</TextBox.KeyBindings>
</TextBox> </TextBox>
<Button Grid.Column="1" Classes="pos" Margin="8,0,0,0" HorizontalAlignment="Stretch" Content="Найти" Command="{Binding LookupCommand}"/> <Button Grid.Column="1" Classes="pos primary" Margin="6" HorizontalAlignment="Stretch" Content="Найти" Command="{Binding LookupCommand}"/>
</Grid> </Grid>
<TextBlock Text="{Binding Message}" Foreground="#60a5fa" TextWrapping="Wrap"/> </Border>
<TextBlock Text="{Binding Message}" Foreground="{DynamicResource PosTextMuted}" TextWrapping="Wrap" FontSize="14"/>
</StackPanel> </StackPanel>
<Border Grid.Row="1" Background="#111827" CornerRadius="10" Margin="0,10"> <Border Grid.Row="1" Classes="card" Margin="0,12" Padding="8">
<ListBox ItemsSource="{Binding Lines}" Background="Transparent" BorderThickness="0"> <ListBox ItemsSource="{Binding Lines}" Background="Transparent" BorderThickness="0">
<ListBox.Styles><Style Selector="ListBoxItem"><Setter Property="Padding" Value="0"/><Setter Property="Margin" Value="0,3"/></Style></ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:ReturnLineVm"> <DataTemplate x:DataType="vm:ReturnLineVm">
<Grid ColumnDefinitions="*,190,110,120" MinHeight="58"> <Border Background="{DynamicResource PosSubtle}" CornerRadius="10" Padding="12,8">
<StackPanel VerticalAlignment="Center" Margin="4,6"> <Grid ColumnDefinitions="*,210,110,120">
<TextBlock Text="{Binding Name}" Foreground="#f3f4f6" FontSize="16" TextWrapping="Wrap"/> <StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding AvailText}" Foreground="#94a3b8" FontSize="12.5"/> <TextBlock Text="{Binding Name}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="Medium" TextWrapping="Wrap"/>
<TextBlock Text="{Binding AvailText}" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
</StackPanel> </StackPanel>
<!-- Степпер количества к возврату --> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" HorizontalAlignment="Center" VerticalAlignment="Center">
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" HorizontalAlignment="Right" VerticalAlignment="Center"> <Button Classes="key" Width="42" Height="42" Padding="0" FontSize="22" Content="−"
<Button Classes="pos" Width="44" Height="40" Padding="0" FontSize="20" Content="−"
Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).DecCommand}" CommandParameter="{Binding}"/> Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).DecCommand}" CommandParameter="{Binding}"/>
<Border Background="#0b1220" CornerRadius="6" Width="58" Height="40"> <TextBlock Text="{Binding QtyText}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold" MinWidth="54" TextAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Text="{Binding QtyText}" Foreground="#f8fafc" FontSize="17" HorizontalAlignment="Center" VerticalAlignment="Center"/> <Button Classes="key" Width="42" Height="42" Padding="0" FontSize="20" Content="+"
</Border>
<Button Classes="pos" Width="44" Height="40" Padding="0" FontSize="20" Content="+"
Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).IncCommand}" CommandParameter="{Binding}"/> Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).IncCommand}" CommandParameter="{Binding}"/>
</StackPanel> </StackPanel>
<Button Grid.Column="2" Classes="pos" Height="40" FontSize="13" Margin="6,0,0,0" Content="Всё" <Button Grid.Column="2" Classes="pos ghost" Height="40" FontSize="13" Margin="6,0,0,0" Content="Всё"
Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).FullCommand}" CommandParameter="{Binding}"/> Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).FullCommand}" CommandParameter="{Binding}"/>
<TextBlock Grid.Column="3" Text="{Binding LineRefund, StringFormat='{}{0:N0} ₸'}" Foreground="#ffffff" FontWeight="SemiBold" FontSize="17" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,10,0"/> <TextBlock Grid.Column="3" Text="{Binding LineRefund, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold" FontSize="18" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,8,0"/>
</Grid> </Grid>
</Border>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</Border> </Border>
<Grid Grid.Row="2" ColumnDefinitions="*,Auto"> <Border Grid.Row="2" Classes="card" Padding="16,12">
<TextBlock Text="К возврату" Foreground="#cbd5e1" FontSize="18" VerticalAlignment="Center"/> <Grid ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="16"> <StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding RefundTotal, StringFormat='{}{0:N0} ₸'}" Foreground="#fca5a5" FontSize="26" FontWeight="Bold" VerticalAlignment="Center"/> <TextBlock Text="К возврату" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/>
<Button Classes="pos danger" Height="56" Width="230" Content="Оформить возврат" Command="{Binding SubmitCommand}" IsEnabled="{Binding Found}"/> <TextBlock Text="{Binding RefundTotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosDanger}" FontSize="28" FontWeight="Bold"/>
</StackPanel> </StackPanel>
<Button Grid.Column="1" Classes="pos danger" Height="56" Width="240" FontSize="17" Content="Оформить возврат" Command="{Binding SubmitCommand}" IsEnabled="{Binding Found}"/>
</Grid> </Grid>
</Border>
</Grid> </Grid>
</UserControl> </UserControl>

View file

@ -3,74 +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="*,380" Background="#0f172a"> <Grid ColumnDefinitions="*,400" Background="{DynamicResource PosBg}">
<!-- Левая часть: чек -->
<Grid Grid.Column="0" RowDefinitions="Auto,*,Auto" Margin="16,12,8,12"> <!-- ЛЕВО: поиск + корзина -->
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,8"> <Grid Grid.Column="0" RowDefinitions="Auto,*,Auto" Margin="20,18,10,18">
<TextBox Grid.Column="0" Classes="pos" Watermark="Штрихкод / поиск товара (Enter)"
Text="{Binding ScanInput}" FontSize="22"> <!-- Поиск / скан -->
<TextBox.KeyBindings> <Border Grid.Row="0" Classes="card" Padding="0" Margin="0,0,0,14">
<KeyBinding Gesture="Enter" Command="{Binding ScanCommand}"/> <Grid ColumnDefinitions="Auto,*">
</TextBox.KeyBindings> <PathIcon Grid.Column="0" Width="22" Height="22" Margin="18,0,8,0" Foreground="{DynamicResource PosTextMuted}"
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"/>
<TextBox Grid.Column="1" BorderThickness="0" FontSize="19" Background="Transparent" VerticalContentAlignment="Center"
Watermark="Отсканируйте товар или начните вводить название" Text="{Binding ScanInput}">
<TextBox.KeyBindings><KeyBinding Gesture="Enter" Command="{Binding ScanCommand}"/></TextBox.KeyBindings>
</TextBox> </TextBox>
<Button Grid.Column="1" Classes="pos" Margin="8,0,0,0" Content="Отложить" Command="{Binding ParkCommand}"/> </Grid>
<Button Grid.Column="2" Classes="pos" Margin="6,0,0,0" Content="Вернуть" Command="{Binding ResumeCommand}"/> </Border>
<!-- Корзина -->
<Border Grid.Row="1" Classes="card" Padding="0">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,170,120,44" Height="40" Margin="18,6,10,0">
<TextBlock Text="Товар" Foreground="{DynamicResource PosTextMuted}" FontSize="13" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="Количество" Foreground="{DynamicResource PosTextMuted}" FontSize="13" HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Grid.Column="2" Text="Сумма" Foreground="{DynamicResource PosTextMuted}" FontSize="13" HorizontalAlignment="Right" VerticalAlignment="Center"/>
</Grid> </Grid>
<Border Grid.Row="1" Background="#111827" CornerRadius="10" Padding="0"> <!-- Пустая корзина -->
<DockPanel> <StackPanel Grid.Row="1" VerticalAlignment="Center" HorizontalAlignment="Center" Spacing="10" IsVisible="{Binding IsCartEmpty}">
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,110,110,120" Background="#1f2937" Height="38"> <Border Background="{DynamicResource PosSubtle}" CornerRadius="999" Width="72" Height="72">
<TextBlock Text=" Товар" Foreground="#9ca3af" VerticalAlignment="Center"/> <PathIcon Width="34" Height="34" Foreground="{DynamicResource PosTextMuted}"
<TextBlock Grid.Column="1" Text="Кол-во" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/> 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"/>
<TextBlock Grid.Column="2" Text="Цена" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/> </Border>
<TextBlock Grid.Column="3" Text="Сумма " Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/> <TextBlock Text="Отсканируйте первый товар" Foreground="{DynamicResource PosTextMuted}" FontSize="16"/>
</Grid> </StackPanel>
<ListBox ItemsSource="{Binding Lines}" Background="Transparent" BorderThickness="0">
<ListBox Grid.Row="1" ItemsSource="{Binding Lines}" SelectedItem="{Binding SelectedLine}"
Background="Transparent" BorderThickness="0" Padding="8,4" IsVisible="{Binding !IsCartEmpty}">
<ListBox.Styles>
<Style Selector="ListBoxItem"><Setter Property="Padding" Value="0"/><Setter Property="Margin" Value="0,3"/><Setter Property="CornerRadius" Value="10"/></Style>
</ListBox.Styles>
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SaleLineVm"> <DataTemplate x:DataType="vm:SaleLineVm">
<Grid ColumnDefinitions="*,110,110,120" MinHeight="46"> <Border Background="{DynamicResource PosSubtle}" CornerRadius="10" Padding="12,8">
<StackPanel VerticalAlignment="Center" Margin="0,4"> <Grid ColumnDefinitions="*,170,120,40">
<TextBlock Text="{Binding Name}" Foreground="#f3f4f6" FontSize="16" TextWrapping="Wrap"/> <StackPanel VerticalAlignment="Center">
<TextBlock Text="{Binding DiscountText}" Foreground="#fca5a5" FontSize="12.5" <TextBlock Text="{Binding Name}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="Medium" TextWrapping="Wrap"/>
IsVisible="{Binding HasDiscount}"/> <TextBlock Text="{Binding DiscountText}" Foreground="{DynamicResource PosBrand}" FontSize="13" IsVisible="{Binding HasDiscount}"/>
<TextBlock Text="{Binding Price, StringFormat='{}{0:N0} ₸ / ед.'}" Foreground="{DynamicResource PosTextMuted}" FontSize="13" IsVisible="{Binding !HasDiscount}"/>
</StackPanel> </StackPanel>
<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"/> <StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Grid.Column="3" Text="{Binding LineTotal, StringFormat='{}{0:N0} ₸'}" Foreground="#ffffff" FontSize="17" FontWeight="SemiBold" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,10,0"/> <Button Classes="key" Width="40" Height="40" Padding="0" FontSize="22" Content="−"
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).DecLineCommand}" CommandParameter="{Binding}"/>
<TextBlock Text="{Binding QtyText}" Foreground="{DynamicResource PosText}" FontSize="16" FontWeight="SemiBold" MinWidth="58" TextAlignment="Center" VerticalAlignment="Center"/>
<Button Classes="key" Width="40" Height="40" Padding="0" FontSize="20" Content="+"
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).IncLineCommand}" CommandParameter="{Binding}"/>
</StackPanel>
<TextBlock Grid.Column="2" Text="{Binding LineTotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="19" FontWeight="SemiBold" HorizontalAlignment="Right" VerticalAlignment="Center"/>
<Button Grid.Column="3" Classes="key" Width="34" Height="34" Padding="0" Background="Transparent" BorderThickness="0" VerticalAlignment="Center"
Command="{Binding $parent[ItemsControl].((vm:SaleViewModel)DataContext).RemoveLineCommand}" CommandParameter="{Binding}" ToolTip.Tip="Удалить">
<PathIcon Width="18" Height="18" Foreground="{DynamicResource PosDanger}"
Data="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z"/>
</Button>
</Grid> </Grid>
</Border>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</DockPanel> </Grid>
</Border> </Border>
<TextBlock Grid.Row="2" Text="{Binding Message}" Foreground="#60a5fa" Margin="4,8,0,0" FontSize="14"/> <TextBlock Grid.Row="2" Text="{Binding Message}" Foreground="{DynamicResource PosTextMuted}" Margin="4,10,0,0" FontSize="13.5"/>
</Grid> </Grid>
<!-- Правая часть: итоги + оплата --> <!-- ПРАВО: чек + оплата -->
<Border Grid.Column="1" Background="#1e293b" Margin="8,12,16,12" CornerRadius="10" Padding="18"> <Border Grid.Column="1" Classes="card" Margin="10,18,20,18" Padding="20">
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto"> <Grid RowDefinitions="Auto,Auto,*,Auto">
<StackPanel Grid.Row="0" Spacing="6"> <StackPanel Grid.Row="0" Spacing="10">
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Позиций" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding ItemCount}" Foreground="#e5e7eb"/></Grid> <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="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding Subtotal, StringFormat='{}{0:N0} ₸'}" Foreground="#e5e7eb"/></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="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding Discount, StringFormat='-{0:N0} ₸'}" Foreground="#fca5a5"/></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>
</StackPanel> </StackPanel>
<Separator Grid.Row="1" Margin="0,12" Background="#334155"/>
<Grid Grid.Row="2" ColumnDefinitions="*,Auto"> <Border Grid.Row="1" Background="{DynamicResource PosBrandSoft}" CornerRadius="12" Padding="16,14" Margin="0,16,0,0">
<TextBlock Text="ИТОГО" Foreground="#f8fafc" FontSize="22" FontWeight="Bold" VerticalAlignment="Bottom"/> <Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="#34d399" FontSize="34" FontWeight="Bold" HorizontalAlignment="Right"/> <TextBlock Text="ИТОГО" Foreground="{DynamicResource PosText}" FontSize="18" FontWeight="SemiBold" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrand}" FontSize="44" FontWeight="Bold"/>
</Grid> </Grid>
<StackPanel Grid.Row="4" Spacing="8"> </Border>
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="68" FontSize="22" Command="{Binding PayCommand}">
<TextBlock Text="F12 · ОПЛАТА" TextAlignment="Center"/> <StackPanel Grid.Row="3" Spacing="10">
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="70" FontSize="22" Command="{Binding PayCommand}">
<TextBlock Text="Оплатить · F12" TextAlignment="Center"/>
</Button> </Button>
<Grid ColumnDefinitions="*,*"> <Grid ColumnDefinitions="*,*">
<Button Classes="pos" Margin="0,0,4,0" FontSize="16" HorizontalAlignment="Stretch"> <Button Classes="pos ghost" Margin="0,0,5,0" Content="Скидка"/>
<TextBlock Text="Скидка" TextAlignment="Center"/> <Button Grid.Column="1" Margin="5,0,0,0" Classes="pos ghost" Content="Отложить" Command="{Binding ParkCommand}"/>
</Button> </Grid>
<Button Grid.Column="1" Classes="pos danger" Margin="4,0,0,0" FontSize="16" HorizontalAlignment="Stretch" Command="{Binding ClearCommand}" ToolTip.Tip="Очистить чек (Esc)"> <Grid ColumnDefinitions="*,*">
<TextBlock Text="Очистить" TextWrapping="Wrap" TextAlignment="Center"/> <Button Classes="pos ghost" Margin="0,0,5,0" Content="Отложенные" Command="{Binding ResumeCommand}"/>
</Button> <Button Grid.Column="1" Margin="5,0,0,0" Classes="pos danger" Content="Очистить" Command="{Binding ClearCommand}" ToolTip.Tip="Очистить чек (Esc)"/>
</Grid> </Grid>
</StackPanel> </StackPanel>
</Grid> </Grid>

View file

@ -3,33 +3,56 @@
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels" xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
x:Class="foodmarket.Pos.Views.ShiftView" x:Class="foodmarket.Pos.Views.ShiftView"
x:DataType="vm:ShiftViewModel"> x:DataType="vm:ShiftViewModel">
<Grid Background="#0f172a"> <Grid Background="{DynamicResource PosBg}">
<Border Background="#1e293b" CornerRadius="12" Width="620" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center"> <Border Classes="card" Width="640" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center">
<StackPanel Spacing="12"> <StackPanel Spacing="12">
<Grid ColumnDefinitions="*,Auto"> <Grid ColumnDefinitions="*,Auto">
<TextBlock Text="Z-отчёт смены" FontSize="24" FontWeight="Bold" Foreground="#f8fafc"/> <TextBlock Text="Смена — Z-отчёт" FontSize="22" FontWeight="Bold" Foreground="{DynamicResource PosText}"/>
<TextBlock Grid.Column="1" Text="{Binding Number}" Foreground="#93c5fd" FontSize="18" VerticalAlignment="Center"/> <Border Grid.Column="1" Classes="chip" Classes.ok="{Binding IsOpen}" Classes.warn="{Binding !IsOpen}" VerticalAlignment="Center">
</Grid> <TextBlock Text="{Binding Number}"/>
<Separator Background="#334155"/>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Размен (на открытие)" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding OpeningFloat, StringFormat='{}{0:N0} ₸'}" Foreground="#e5e7eb"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Продажи наличными" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding SalesCash, StringFormat='{}{0:N0} ₸'}" Foreground="#e5e7eb"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Продажи картой" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding SalesCard, StringFormat='{}{0:N0} ₸'}" Foreground="#e5e7eb"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Возвраты наличными" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding ReturnsCash, StringFormat='-{0:N0} ₸'}" Foreground="#fca5a5"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Внесения" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding DrawerIn, StringFormat='+{0:N0} ₸'}" Foreground="#86efac"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Выплаты" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding DrawerOut, StringFormat='-{0:N0} ₸'}" Foreground="#fca5a5"/></Grid>
<Separator Background="#334155"/>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Должно быть в кассе" Foreground="#cbd5e1" FontSize="18"/><TextBlock Grid.Column="1" Text="{Binding ExpectedCash, StringFormat='{}{0:N0} ₸'}" Foreground="#f8fafc" FontSize="20" FontWeight="SemiBold"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Факт (пересчёт)" Foreground="#cbd5e1" FontSize="18"/><TextBlock Grid.Column="1" Text="{Binding ActualCash, StringFormat='{}{0:N0} ₸'}" Foreground="#f8fafc" FontSize="20" FontWeight="SemiBold"/></Grid>
<Border Background="#0b1220" CornerRadius="8" Padding="12">
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Разница" Foreground="#f8fafc" FontSize="18"/><TextBlock Grid.Column="1" Text="{Binding Difference, StringFormat='{}{0:+#,0;-#,0;0} ₸'}" Foreground="#fbbf24" FontSize="24" FontWeight="Bold"/></Grid>
</Border> </Border>
</Grid>
<Border Height="1" Background="{DynamicResource PosBorder}" Margin="0,4"/>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Размен (на открытие)" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding OpeningFloat, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="15"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Продажи наличными" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding SalesCash, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="15"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Продажи картой" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding SalesCard, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="15"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Возвраты наличными" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding ReturnsCash, StringFormat='−{0:N0} ₸'}" Foreground="{DynamicResource PosDanger}" FontSize="15"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Внесения" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding DrawerIn, StringFormat='+{0:N0} ₸'}" Foreground="{DynamicResource PosBrand}" FontSize="15"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Выплаты" Foreground="{DynamicResource PosTextMuted}" FontSize="15"/><TextBlock Grid.Column="1" Text="{Binding DrawerOut, StringFormat='−{0:N0} ₸'}" Foreground="{DynamicResource PosDanger}" FontSize="15"/></Grid>
<Border Height="1" Background="{DynamicResource PosBorder}" Margin="0,4"/>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Должно быть в кассе" Foreground="{DynamicResource PosText}" FontSize="16" FontWeight="SemiBold"/><TextBlock Grid.Column="1" Text="{Binding ExpectedCash, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="18" FontWeight="SemiBold"/></Grid>
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Факт (пересчёт)" Foreground="{DynamicResource PosText}" FontSize="16"/><TextBlock Grid.Column="1" Text="{Binding ActualCash, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosText}" FontSize="18" FontWeight="SemiBold"/></Grid>
<Border CornerRadius="12" Padding="16,12" Background="{DynamicResource PosSubtle}"
Classes.bad="{Binding DiffShortage}">
<Border.Styles>
<Style Selector="Border.bad"><Setter Property="Background" Value="{DynamicResource PosDangerSoft}"/></Style>
</Border.Styles>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center">
<TextBlock Text="Разница" Foreground="{DynamicResource PosText}" FontSize="16" FontWeight="SemiBold"/>
<TextBlock Text="{Binding DiffLabel}" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
</StackPanel>
<TextBlock Grid.Column="1" Text="{Binding Difference, StringFormat='{}{0:+#,0;-#,0;0} ₸'}" FontSize="26" FontWeight="Bold" VerticalAlignment="Center"
Foreground="{DynamicResource PosTextMuted}"
Classes.surplus="{Binding DiffSurplus}" Classes.shortage="{Binding DiffShortage}">
<TextBlock.Styles>
<Style Selector="TextBlock.surplus"><Setter Property="Foreground" Value="{DynamicResource PosBrand}"/></Style>
<Style Selector="TextBlock.shortage"><Setter Property="Foreground" Value="{DynamicResource PosDanger}"/></Style>
</TextBlock.Styles>
</TextBlock>
</Grid>
</Border>
<Grid ColumnDefinitions="*,*,*"> <Grid ColumnDefinitions="*,*,*">
<Button Classes="pos" Margin="0,0,4,0" HorizontalAlignment="Stretch" Content="Открыть смену" Command="{Binding OpenShiftCommand}" IsEnabled="{Binding !IsOpen}"/> <Button Classes="pos ghost" Margin="0,0,5,0" Content="Открыть смену" Command="{Binding OpenShiftCommand}" IsEnabled="{Binding !IsOpen}"/>
<Button Grid.Column="1" Classes="pos" Margin="4,0" HorizontalAlignment="Stretch" Content="Внести" Command="{Binding DepositCashCommand}" IsEnabled="{Binding IsOpen}"/> <Button Grid.Column="1" Classes="pos ghost" Margin="5,0" Content="Внести" Command="{Binding DepositCashCommand}" IsEnabled="{Binding IsOpen}"/>
<Button Grid.Column="2" Classes="pos" Margin="4,0,0,0" HorizontalAlignment="Stretch" Content="Выплата" Command="{Binding WithdrawCashCommand}" IsEnabled="{Binding IsOpen}"/> <Button Grid.Column="2" Classes="pos ghost" Margin="5,0,0,0" Content="Выплата" Command="{Binding WithdrawCashCommand}" IsEnabled="{Binding IsOpen}"/>
</Grid> </Grid>
<Button Classes="pos danger" HorizontalAlignment="Stretch" Height="52" Content="Закрыть смену (Z-отчёт)" Command="{Binding CloseShiftCommand}" IsEnabled="{Binding IsOpen}"/> <Button Classes="pos danger" HorizontalAlignment="Stretch" Height="52" Content="Закрыть смену (Z-отчёт)" Command="{Binding CloseShiftCommand}" IsEnabled="{Binding IsOpen}"/>
<TextBlock Text="{Binding Message}" Foreground="#60a5fa" IsVisible="{Binding Message, Converter={x:Static ObjectConverters.IsNotNull}}" TextWrapping="Wrap"/> <TextBlock Text="{Binding Message}" Foreground="{DynamicResource PosTextMuted}" TextWrapping="Wrap" IsVisible="{Binding Message, Converter={x:Static ObjectConverters.IsNotNull}}"/>
</StackPanel> </StackPanel>
</Border> </Border>
</Grid> </Grid>