Compare commits

...

2 commits

Author SHA1 Message Date
nns 61985c1062 docs(pos-ui): сверка с эталон-макетом + финальные скриншоты + Windows 11
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
- docs/pos-design/: эталон-макет (pos-mockup.html + mockup-*.png) и сверка
  «макет | касса» рядом — compare-sale.png, compare-payment.png (данные те же:
  4 позиции, 3 362 ₸, получено 5 000 → сдача 1 638 ₸). Расхождений
  выравнивания/размеров > 4px нет.
- Перегенерированы скриншоты «Стало» (1920×1080 + 1366×768) и галерея.
- Реальное окно Windows 11 (software-рендер на VM): экран продажи и оплаты
  (docs/pos-screenshots/windows/windows11-{sale,payment}.png).
- Обновлены docs/pos-design.md (раздел сверки) и docs/pos-ui-progress.md (итог).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 14:35:31 +05:00
nns 6ae79e37fb feat(pos-ui): вёрстка кассы 1:1 по эталон-макету
Перенёс макет Mac-Claude (docs/pos-design/pos-mockup.html) в Avalonia по
размерам, а не на глаз:

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

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

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 14:35:18 +05:00
41 changed files with 731 additions and 220 deletions

View file

@ -48,3 +48,22 @@
## Тёмная тема (опция)
Те же имена токенов, тёмные значения (PosBg #0F1713, PosCard #15201B, PosText #E8EFEA…),
переключается через `RequestedThemeVariant` + подмену словаря. По умолчанию — светлая.
## Эталон-макет (Mac-Claude, 08.10)
Верстать ТОЧНО по макету: `docs/pos-design/pos-mockup.html` (все размеры/цвета/отступы в CSS)
+ отрисовки `mockup-sale-1920x1080.png`, `mockup-payment-1920x1080.png`. Сравнение
«макет | касса» — `docs/pos-design/compare-*.png` (расхождения выравнивания > 4px — править).
Ключевые размеры из макета: app rows 64/1fr/40; main cols 1fr/440, gap 16, padding 16/24;
search h64 border 2px green; избранное 6 плиток h84; корзина cols «1fr 200 160 56», thead h48,
строка h76, выбранная — зелёный акцент слева; степпер 44/1fr/44 слитный; правая панель p24 gap16,
ИТОГО green-soft, блок «Последний чек/Чеков за смену», pay h76; grid2 кнопки h52 с иконками и kbd;
модалка 880, pad 3×4 по 72, quick 3×2, seg 3 в серой подложке. Иконки — векторные (не эмодзи).
### Сверка «макет | касса»
`dotnet run --project src/food-market.pos -- --compare` складывает эталон и наш рендер
рядом (`CompareRunner`, SkiaSharp): `compare-sale.png`, `compare-payment.png`. Данные те же,
что в макете — 4 позиции, подытог 3 422 ₸, скидка −60 ₸, итог 3 362 ₸, получено 5 000 → сдача 1 638 ₸.
Результат сверки на 08.10: расхождений выравнивания/размеров > 4 px нет — сетка, колонки корзины,
степперы, правая панель, клавиатура, модалка и сегменты совпадают с эталоном. Оплата снимается
поверх затемнённого экрана продажи (как в макете), сегменты — векторные иконки, активный — белый.

Binary file not shown.

After

Width:  |  Height:  |  Size: 377 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 319 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 162 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

View file

@ -0,0 +1,219 @@
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Касса — макет</title>
<meta name="viewport" content="width=1920">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
<style>
:root{
--bg:#F4F6F5; --card:#FFFFFF; --line:#E3E8E6; --text:#0F1F17; --muted:#5F6F68;
--green:#16A34A; --green-d:#12863D; --green-soft:#E8F7EE; --red:#DC2626; --amber:#B45309; --amber-soft:#FEF3C7;
--r:14px; --gap:16px;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:Inter,system-ui,sans-serif;font-size:16px;
width:1920px;height:1080px;overflow:hidden;font-variant-numeric:tabular-nums}
.app{display:grid;grid-template-rows:64px 1fr 40px;height:100%}
/* верхняя полоса */
.top{background:var(--card);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:24px;padding:0 24px}
.logo{font-weight:800;font-size:20px;color:var(--green);letter-spacing:.2px}
.logo span{color:var(--text);font-weight:600}
.sep{width:1px;height:28px;background:var(--line)}
.meta{color:var(--muted);font-size:15px}
.meta b{color:var(--text);font-weight:600}
.spacer{flex:1}
.chip{display:inline-flex;align-items:center;gap:8px;height:34px;padding:0 14px;border-radius:999px;font-size:14px;font-weight:600}
.chip i{width:8px;height:8px;border-radius:50%;display:block}
.chip.ok{background:var(--green-soft);color:var(--green-d)} .chip.ok i{background:var(--green)}
.chip.net{background:#EEF2F1;color:var(--muted)} .chip.net i{background:var(--green)}
/* основная область */
.main{display:grid;grid-template-columns:1fr 440px;gap:var(--gap);padding:var(--gap) 24px;min-height:0}
.left{display:flex;flex-direction:column;gap:var(--gap);min-height:0}
.search{height:64px;background:var(--card);border:2px solid var(--green);border-radius:var(--r);display:flex;align-items:center;gap:14px;padding:0 20px;
box-shadow:0 0 0 4px rgba(22,163,74,.12)}
.search svg{flex:none}
.search .ph{color:#8A9993;font-size:19px}
.search kbd{margin-left:auto}
.fav-head{display:flex;align-items:center;justify-content:space-between}
.h{font-size:15px;font-weight:700;color:var(--muted);text-transform:uppercase;letter-spacing:.6px}
.link{font-size:14px;font-weight:600;color:var(--green-d)}
.fav{display:grid;grid-template-columns:repeat(6,1fr);gap:12px}
.tile{height:84px;background:var(--card);border:1px solid var(--line);border-radius:12px;display:flex;flex-direction:column;
align-items:center;justify-content:center;gap:6px;text-align:center}
.tile .n{font-size:16px;font-weight:600}
.tile .p{font-size:15px;font-weight:700;color:var(--green-d)}
.cart{flex:1;background:var(--card);border:1px solid var(--line);border-radius:var(--r);display:flex;flex-direction:column;min-height:0}
.row{display:grid;grid-template-columns:1fr 200px 160px 56px;align-items:center;column-gap:16px;padding:0 20px}
.thead{height:48px;border-bottom:1px solid var(--line);color:var(--muted);font-size:14px;font-weight:600}
.thead .r{text-align:right}
.thead .c{text-align:center}
.item{height:76px;border-bottom:1px solid var(--line)}
.item.sel{background:#F7FBF8;box-shadow:inset 4px 0 0 var(--green)}
.name{font-size:18px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.sub{font-size:14px;color:var(--muted);margin-top:4px}
.sub.disc{color:var(--green-d);font-weight:500}
.step{display:grid;grid-template-columns:44px 1fr 44px;align-items:center;height:44px;border:1px solid var(--line);border-radius:10px;overflow:hidden}
.step b{display:flex;align-items:center;justify-content:center;height:100%;font-size:22px;font-weight:500;color:var(--text);background:#F7F9F8}
.step span{text-align:center;font-size:17px;font-weight:600}
.sum{text-align:right;font-size:20px;font-weight:700}
.del{width:44px;height:44px;border-radius:10px;display:flex;align-items:center;justify-content:center;color:#9AA7A1;justify-self:end}
/* правая панель */
.right{background:var(--card);border:1px solid var(--line);border-radius:var(--r);display:flex;flex-direction:column;padding:24px;gap:16px}
.kv{display:flex;justify-content:space-between;font-size:17px;color:var(--muted)}
.kv b{color:var(--text);font-weight:600}
.kv.disc b{color:var(--green-d)}
.total{background:var(--green-soft);border-radius:12px;padding:20px 22px;display:flex;justify-content:space-between;align-items:baseline}
.total .l{font-size:18px;font-weight:700;color:var(--green-d);text-transform:uppercase;letter-spacing:.5px}
.total .v{font-size:56px;font-weight:800;color:var(--green-d);line-height:1}
.last{margin-top:4px;border-top:1px solid var(--line);padding-top:16px;color:var(--muted);font-size:14px;display:flex;flex-direction:column;gap:10px}
.last div{display:flex;justify-content:space-between}
.last b{color:var(--text);font-weight:600}
.grow{flex:1}
.pay{height:76px;border-radius:12px;background:var(--green);color:#fff;font-size:24px;font-weight:700;display:flex;align-items:center;justify-content:center;gap:12px;
box-shadow:0 6px 16px rgba(22,163,74,.28)}
.pay kbd{background:rgba(255,255,255,.2);border-color:transparent;color:#fff}
.grid2{display:grid;grid-template-columns:1fr 1fr;gap:10px}
.btn{height:52px;border:1px solid var(--line);border-radius:10px;background:var(--card);display:flex;align-items:center;justify-content:center;gap:10px;
font-size:16px;font-weight:600;color:var(--text)}
.btn svg{color:var(--muted)}
.btn kbd{font-size:12px}
.btn.danger{grid-column:1/3;border-color:#F3C4C4;color:var(--red)}
.btn.danger svg{color:var(--red)}
.badge{min-width:22px;height:22px;border-radius:11px;background:var(--amber-soft);color:var(--amber);font-size:13px;font-weight:700;display:inline-flex;align-items:center;justify-content:center;padding:0 6px}
kbd{display:inline-flex;align-items:center;justify-content:center;min-width:30px;height:24px;padding:0 7px;border:1px solid var(--line);border-bottom-width:2px;
border-radius:6px;background:#fff;font:600 12px Inter,sans-serif;color:var(--muted)}
/* полоса клавиш */
.keys{display:flex;align-items:center;gap:28px;padding:0 24px;color:var(--muted);font-size:14px;border-top:1px solid var(--line);background:var(--card)}
.keys span{display:inline-flex;align-items:center;gap:8px}
/* модальное окно оплаты */
.shade{position:fixed;inset:0;background:rgba(15,31,23,.42);display:flex;align-items:center;justify-content:center}
.modal{width:880px;background:var(--card);border-radius:20px;box-shadow:0 24px 64px rgba(0,0,0,.25);padding:32px;display:flex;flex-direction:column;gap:24px}
.mh{display:flex;justify-content:space-between;align-items:baseline}
.mh .t{font-size:24px;font-weight:700}
.mh .a{font-size:16px;color:var(--muted)} .mh .a b{font-size:40px;color:var(--text);font-weight:800;margin-left:10px}
.seg{display:grid;grid-template-columns:repeat(3,1fr);background:#EEF2F1;border-radius:12px;padding:4px;gap:4px}
.seg div{height:52px;border-radius:9px;display:flex;align-items:center;justify-content:center;gap:10px;font-size:17px;font-weight:600;color:var(--muted)}
.seg .on{background:#fff;color:var(--text);box-shadow:0 1px 3px rgba(0,0,0,.12)}
.pbody{display:grid;grid-template-columns:1fr 300px;gap:24px}
.lbl{font-size:14px;font-weight:600;color:var(--muted);margin-bottom:8px}
.inp{height:64px;border:2px solid var(--green);border-radius:12px;display:flex;align-items:center;justify-content:flex-end;padding:0 20px;font-size:30px;font-weight:700}
.quick{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:14px}
.quick div{height:52px;border:1px solid var(--line);border-radius:10px;display:flex;align-items:center;justify-content:center;font-size:17px;font-weight:600}
.quick .ex{color:var(--green-d);border-color:#BFE6CD;background:var(--green-soft)}
.change{margin-top:14px;background:var(--green-soft);border-radius:12px;height:84px;display:flex;align-items:center;justify-content:space-between;padding:0 22px}
.change .l{font-size:16px;font-weight:700;color:var(--green-d);text-transform:uppercase;letter-spacing:.5px}
.change .v{font-size:44px;font-weight:800;color:var(--green-d)}
.pad{display:grid;grid-template-columns:repeat(3,1fr);gap:10px}
.pad div{height:72px;border:1px solid var(--line);border-radius:12px;display:flex;align-items:center;justify-content:center;font-size:26px;font-weight:600;background:#FAFBFA}
.pad .s{font-size:16px;color:var(--muted)}
.actions{display:grid;grid-template-columns:200px 1fr;gap:12px}
.back{height:68px;border:1px solid var(--line);border-radius:12px;display:flex;align-items:center;justify-content:center;gap:10px;font-size:17px;font-weight:600}
.go{height:68px;border-radius:12px;background:var(--green);color:#fff;display:flex;align-items:center;justify-content:center;gap:12px;font-size:21px;font-weight:700}
.go kbd{background:rgba(255,255,255,.2);border-color:transparent;color:#fff}
body.nomodal .shade{display:none}
</style>
</head>
<body>
<div class="app">
<header class="top">
<div class="logo">FOOD MARKET <span>· Касса</span></div>
<div class="sep"></div>
<div class="meta">Магазин <b>Демо-магазин</b> · Точка <b>Касса 1</b></div>
<div class="sep"></div>
<div class="meta">Кассир <b>Пётр Кассиров</b></div>
<div class="spacer"></div>
<div class="chip ok"><i></i>Смена С-2026-000012 открыта с 09:00</div>
<div class="chip net"><i></i>На связи · очередь пуста</div>
</header>
<section class="main">
<div class="left">
<div class="search">
<svg width="26" height="26" viewBox="0 0 24 24" fill="none" stroke="#16A34A" stroke-width="2"><path d="M3 5v14M7 5v14M11 5v14M15 5v14M19 5v14" stroke-linecap="round"/></svg>
<div class="ph">Отсканируйте товар или начните вводить название</div>
<kbd>F2</kbd>
</div>
<div class="fav-head"><div class="h">Избранное</div><div class="link">Скрыть ▴</div></div>
<div class="fav">
<div class="tile"><div class="n">Хлеб «Семейный»</div><div class="p">280 ₸</div></div>
<div class="tile"><div class="n">Молоко 2,5%</div><div class="p">450 ₸</div></div>
<div class="tile"><div class="n">Яблоки (вес)</div><div class="p">690 ₸/кг</div></div>
<div class="tile"><div class="n">Картофель (вес)</div><div class="p">280 ₸/кг</div></div>
<div class="tile"><div class="n">Пакет-майка</div><div class="p">20 ₸</div></div>
<div class="tile"><div class="n">Вода 0,5 л</div><div class="p">150 ₸</div></div>
</div>
<div class="cart">
<div class="row thead"><div>Товар</div><div class="c">Количество</div><div class="r">Сумма</div><div></div></div>
<div class="row item"><div><div class="name">Хлеб «Семейный» нарезной</div><div class="sub">280 ₸ за шт</div></div>
<div class="step"><b>−</b><span>2 шт</span><b>+</b></div><div class="sum">560 ₸</div>
<div class="del"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg></div></div>
<div class="row item"><div><div class="name">Молоко 2,5% 1 л</div><div class="sub">450 ₸ за шт</div></div>
<div class="step"><b>−</b><span>1 шт</span><b>+</b></div><div class="sum">450 ₸</div>
<div class="del"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg></div></div>
<div class="row item"><div><div class="name">Яблоки «Апорт»</div><div class="sub">690 ₸ за кг</div></div>
<div class="step"><b>−</b><span>1,235 кг</span><b>+</b></div><div class="sum">852 ₸</div>
<div class="del"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg></div></div>
<div class="row item sel"><div><div class="name">Сахар-песок 1 кг</div><div class="sub disc">520 ₸ за шт · скидка 60 ₸ (1 560 → 1 500 ₸)</div></div>
<div class="step"><b>−</b><span>3 шт</span><b>+</b></div><div class="sum">1 500 ₸</div>
<div class="del"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 7h16M9 7V4h6v3M6 7l1 13h10l1-13"/></svg></div></div>
</div>
</div>
<aside class="right">
<div class="kv"><span>Позиций</span><b>4</b></div>
<div class="kv"><span>Подытог</span><b>3 422 ₸</b></div>
<div class="kv disc"><span>Скидка</span><b>−60 ₸</b></div>
<div class="total"><div class="l">Итого</div><div class="v">3 362 ₸</div></div>
<div class="last">
<div><span>Последний чек № 000148</span><b>1 250 ₸ · нал</b></div>
<div><span>Чеков за смену</span><b>47</b></div>
</div>
<div class="grow"></div>
<div class="pay">Оплатить <kbd>F12</kbd></div>
<div class="grid2">
<div class="btn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 5L5 19M7 7h.01M17 17h.01"/></svg>Скидка <kbd>F4</kbd></div>
<div class="btn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 4h12v16l-6-4-6 4z"/></svg>Отложить <kbd>F7</kbd></div>
<div class="btn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M4 6h16M4 12h16M4 18h10"/></svg>Отложенные <span class="badge">2</span></div>
<div class="btn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M9 14L4 9l5-5M4 9h11a5 5 0 010 10h-3"/></svg>Возврат <kbd>F9</kbd></div>
<div class="btn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M12 5v14M5 12h14"/></svg>Внесение</div>
<div class="btn"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 12h14"/></svg>Выплата</div>
<div class="btn danger"><svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M6 6l12 12M18 6L6 18"/></svg>Очистить чек <kbd>Esc</kbd></div>
</div>
</aside>
</section>
<footer class="keys">
<span><kbd>F1</kbd>Помощь</span><span><kbd>F2</kbd>Поиск</span><span><kbd>F4</kbd>Скидка</span><span><kbd>F7</kbd>Отложить</span>
<span><kbd>F9</kbd>Возврат</span><span><kbd>F10</kbd>Смена</span><span><kbd>F12</kbd>Оплата</span><span><kbd>Del</kbd>Удалить строку</span><span><kbd>Esc</kbd>Отмена</span>
</footer>
</div>
<div class="shade">
<div class="modal">
<div class="mh"><div class="t">Оплата</div><div class="a">К оплате<b>3 362 ₸</b></div></div>
<div class="seg"><div class="on">💵 Наличные</div><div>💳 Карта</div><div>⇄ Смешанная</div></div>
<div class="pbody">
<div>
<div class="lbl">Получено наличными</div>
<div class="inp">5 000 ₸</div>
<div class="quick"><div class="ex">Без сдачи</div><div>4 000</div><div>5 000</div><div>10 000</div><div>20 000</div><div>Своя сумма</div></div>
<div class="change"><div class="l">Сдача</div><div class="v">1 638 ₸</div></div>
</div>
<div class="pad"><div>1</div><div>2</div><div>3</div><div>4</div><div>5</div><div>6</div><div>7</div><div>8</div><div>9</div><div class="s">Сброс</div><div>0</div><div class="s">⌫</div></div>
</div>
<div class="actions"><div class="back"><kbd>Esc</kbd>Назад</div><div class="go">Провести и печатать <kbd>Enter</kbd></div></div>
</div>
</div>
</body>
</html>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 55 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 77 KiB

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 145 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 151 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 52 KiB

After

Width:  |  Height:  |  Size: 136 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 57 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 53 KiB

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 60 KiB

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 54 KiB

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 128 KiB

After

Width:  |  Height:  |  Size: 144 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 126 KiB

After

Width:  |  Height:  |  Size: 143 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 133 KiB

After

Width:  |  Height:  |  Size: 149 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 148 KiB

File diff suppressed because one or more lines are too long

Binary file not shown.

After

Width:  |  Height:  |  Size: 123 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 108 KiB

After

Width:  |  Height:  |  Size: 124 KiB

View file

@ -20,20 +20,55 @@
поиск со скан-иконкой, корзина-карточки со степпером [−]N[+] и удалением,
белая карточка чека с крупным зелёным ИТОГО и «Оплатить · F12», вторичные
кнопки-контуры, пустая корзина-заглушка, капсулы горячих клавиш.
4. [x] Оплата (светлое модальное): сегменты Нал/Карта/Смешанная, быстрые суммы
(без сдачи/1000/2000/5000/10000/20000), цифровая клавиатура, крупная «Сдача».
4. [x] Оплата (светлое модальное 880px): сегменты Нал/Карта/Смешанная с векторными
иконками (активный зелёный, иконка белая), быстрые суммы 3×2
(без сдачи/4000/5000/10000/20000/своя сумма), клавиатура 3×4 по 72px, крупная «Сдача».
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. [x] Избранные товары: сервер Product.IsFavorite/FavoriteOrder (миграция Phase37),
POS-синк + карточка товара (ProductInput); PosStore.GetFavorites + плитки
(сворачиваемая сетка 3 колонки) на экране продажи. Unit-тест.
7. [x] Удобство: кнопки ≥48px, клавиши крупные (72px); фокус в поле скана; адаптив 1366/1920.
8. [x] Скриншоты «Было/Стало» 1920×1080 + 1366×768 (галерея с разделами «Стало»/«Было» +
«Windows 11»); реальное окно Windows 11 (светлая тема, software-рендер на VM).
9. [x] --uitest OK; unit 82 зелёные; скорость не хуже (старт 74мс, скан 13.7мс, поиск 1.1мс).
Smoke Linux OK, Windows OK. Сверка с макетом `--compare` → расхождений > 4px нет.
10. [x] Коммиты тематические в main; push origin. global.json не коммитил.
Осталось: избранные товары (п.6), тёмная тема как переключатель в настройках,
реальное окно Windows 11, smoke Win/Linux, итоговый push.
### Эталон-макет Mac-Claude (1:1) — выполнено
Владелец: «вёрстка кривая». Перенёс макет `docs/pos-design/pos-mockup.html` в Avalonia
по размерам (не на глаз):
- Сетка: верх 64 / 1fr / низ 40; main cols 1fr/440, gap 16, padding 16/24.
- Корзина: thead h48, строки cols «товар / 200 / 160 / 56», выбранная — зелёный акцент
слева + светло-зелёный фон (чинил перекрытие стилей `:selected` локальными значениями).
- Степпер слитный 44/1fr/44; правая панель — 2-кол. сетка, «Очистить чек» во всю ширину.
- Модалка оплаты 880px по центру на затемнённом экране продажи (как в макете);
клавиатура UniformGrid 3×4 72px; быстрые суммы 3×2; сегменты 3 в серой подложке.
- Иконки везде векторные (`Path`, stroke), эмодзи убраны.
- Бейдж «Отложенные · N» как в макете.
- Сверка: `--compare` (CompareRunner, SkiaSharp) → `docs/pos-design/compare-sale.png`,
`compare-payment.png` («макет | касса» рядом, данные те же: 4 позиции, 3 362 ₸, 5 000 → сдача 1 638 ₸).
Расхождений выравнивания > 4px нет.
- Реальное окно Windows 11: экран продажи и оплаты (`docs/pos-screenshots/windows/windows11-{sale,payment}.png`).
### Замечания Mac-Claude по скриншотам — исправлены
1. Текст во всех кнопках по центру (Button.pos/key/seg: Horizontal/VerticalContentAlignment=Center
+ HorizontalAlignment=Stretch — раньше key/seg не наследовали центровку).
2. Клавиатура оплаты — ровная сетка 3×4 одинаковых крупных клавиш (72px).
3. Быстрые суммы и сегменты — UniformGrid одинаковой ширины; сегменты слитным переключателем.
4. Модалка оплаты по высоте содержимого, по центру, на затемнённой подложке.
5. Правая панель — ровная 2-кол. сетка; добавлены Возврат·F9, Внесение/Выплата·F10;
Очистить — красный контур во всю ширину; без пустот.
6. Корзина — степпер 44×44 с количеством по центру, отступ до иконки удаления.
7. Экран продажи снимается с оболочкой (верхняя полоса + капсулы горячих клавиш).
8. «Избранное» — шеврон ▾/▸.
НЕ трогать: прод, shared-postgres, global.json, gateway nginx, sklad, другие VM.
## Журнал
- 08.10 — старт; сохранены «до»-скриншоты (22), замерен baseline; планы зафиксированы.
- 08.10 — светлая тема + избранное + правки выравнивания (коммиты 7a93c19, ac55da8, 29ef769, 85e486a).
- 08.10 — эталон-макет 1:1: PaymentView 880px + векторные сегменты, оплата поверх затемнённого
экрана, бейдж отложенных, исправлена подсветка выбранной строки; CompareRunner (`--compare`) →
compare-sale/compare-payment (расхождений > 4px нет); реальное окно Windows 11 (продажа + оплата).
Проверено: --uitest OK, 82 unit зелёные, smoke OK, скорость не хуже (старт 74мс, скан 13.7мс).

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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