Compare commits
4 commits
7437305265
...
85e486abc7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
85e486abc7 | ||
|
|
29ef76990e | ||
|
|
ac55da8d06 | ||
|
|
7a93c1941d |
50
docs/pos-design.md
Normal 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` + подмену словаря. По умолчанию — светлая.
|
||||
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 39 KiB |
BIN
docs/pos-screenshots/01b-login-pin-1366x768.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/pos-screenshots/01b-login-pin-1920x1080.png
Normal file
|
After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 40 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 133 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 52 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 31 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 58 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 60 KiB |
|
Before Width: | Height: | Size: 38 KiB After Width: | Height: | Size: 65 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 59 KiB |
|
Before Width: | Height: | Size: 77 KiB After Width: | Height: | Size: 128 KiB |
|
Before Width: | Height: | Size: 82 KiB After Width: | Height: | Size: 135 KiB |
|
Before Width: | Height: | Size: 76 KiB After Width: | Height: | Size: 126 KiB |
|
Before Width: | Height: | Size: 81 KiB After Width: | Height: | Size: 133 KiB |
BIN
docs/pos-screenshots/11-shell-dark-1366x768.png
Normal file
|
After Width: | Height: | Size: 124 KiB |
BIN
docs/pos-screenshots/11-shell-dark-1920x1080.png
Normal file
|
After Width: | Height: | Size: 130 KiB |
BIN
docs/pos-screenshots/before/01-login-1366x768.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/pos-screenshots/before/01-login-1920x1080.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
docs/pos-screenshots/before/01b-login-pin-1366x768.png
Normal file
|
After Width: | Height: | Size: 24 KiB |
BIN
docs/pos-screenshots/before/01b-login-pin-1920x1080.png
Normal file
|
After Width: | Height: | Size: 28 KiB |
BIN
docs/pos-screenshots/before/02-shift-open-1366x768.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/pos-screenshots/before/02-shift-open-1920x1080.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/pos-screenshots/before/03-sale-empty-1366x768.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/pos-screenshots/before/03-sale-empty-1920x1080.png
Normal file
|
After Width: | Height: | Size: 39 KiB |
BIN
docs/pos-screenshots/before/04-sale-items-1366x768.png
Normal file
|
After Width: | Height: | Size: 62 KiB |
BIN
docs/pos-screenshots/before/04-sale-items-1920x1080.png
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
docs/pos-screenshots/before/05-payment-cash-1366x768.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/pos-screenshots/before/05-payment-cash-1920x1080.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/pos-screenshots/before/06-payment-mixed-1366x768.png
Normal file
|
After Width: | Height: | Size: 31 KiB |
BIN
docs/pos-screenshots/before/06-payment-mixed-1920x1080.png
Normal file
|
After Width: | Height: | Size: 35 KiB |
BIN
docs/pos-screenshots/before/07-return-1366x768.png
Normal file
|
After Width: | Height: | Size: 52 KiB |
BIN
docs/pos-screenshots/before/07-return-1920x1080.png
Normal file
|
After Width: | Height: | Size: 57 KiB |
BIN
docs/pos-screenshots/before/08-shift-z-1366x768.png
Normal file
|
After Width: | Height: | Size: 51 KiB |
BIN
docs/pos-screenshots/before/08-shift-z-1920x1080.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
docs/pos-screenshots/before/09-shell-offline-1366x768.png
Normal file
|
After Width: | Height: | Size: 79 KiB |
BIN
docs/pos-screenshots/before/09-shell-offline-1920x1080.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
BIN
docs/pos-screenshots/before/10-shell-online-1366x768.png
Normal file
|
After Width: | Height: | Size: 78 KiB |
BIN
docs/pos-screenshots/before/10-shell-online-1920x1080.png
Normal file
|
After Width: | Height: | Size: 84 KiB |
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 108 KiB |
39
docs/pos-ui-progress.md
Normal 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; планы зафиксированы.
|
||||
|
|
@ -1300,5 +1300,7 @@ private static void Apply(Product e, ProductInput i)
|
|||
}
|
||||
e.PurchaseCurrencyId = i.PurchaseCurrencyId;
|
||||
e.ImageUrl = i.ImageUrl;
|
||||
e.IsFavorite = i.IsFavorite;
|
||||
e.FavoriteOrder = i.FavoriteOrder;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -145,6 +145,8 @@ orderby l.SortOrder
|
|||
// Product не имеет IsArchived в текущей доменной модели; всегда false.
|
||||
// Когда добавим архив — заполняется здесь, без правки контракта.
|
||||
IsArchived = false,
|
||||
IsFavorite = x.p.IsFavorite,
|
||||
FavoriteOrder = x.p.FavoriteOrder,
|
||||
UpdatedAt = x.stamp,
|
||||
}).ToList();
|
||||
|
||||
|
|
|
|||
|
|
@ -100,4 +100,5 @@ public record ProductInput(
|
|||
[Range(0, 1e10)] decimal? ReferencePrice = null, Guid? PurchaseCurrencyId = null,
|
||||
[StringLength(1000)] string? ImageUrl = null,
|
||||
IReadOnlyList<ProductPriceInput>? Prices = null,
|
||||
IReadOnlyList<ProductBarcodeInput>? Barcodes = null);
|
||||
IReadOnlyList<ProductBarcodeInput>? Barcodes = null,
|
||||
bool IsFavorite = false, int FavoriteOrder = 0);
|
||||
|
|
|
|||
|
|
@ -73,6 +73,12 @@ public class Product : TenantEntity
|
|||
/// вне сезона), архивный — навсегда (списан).</summary>
|
||||
public bool IsAvailableForSale { get; set; } = true;
|
||||
|
||||
/// <summary>Товар в «Избранном» кассы — показывается плиткой на экране продажи
|
||||
/// (обычно товары без штрихкода: хлеб, овощи на вес). Настраивает админ.</summary>
|
||||
public bool IsFavorite { get; set; }
|
||||
/// <summary>Порядок плитки в «Избранном» (меньше — раньше).</summary>
|
||||
public int FavoriteOrder { get; set; }
|
||||
|
||||
public ICollection<ProductPrice> Prices { get; set; } = [];
|
||||
public ICollection<ProductBarcode> Barcodes { get; set; } = [];
|
||||
public ICollection<ProductImage> Images { get; set; } = [];
|
||||
|
|
|
|||
|
|
@ -0,0 +1,36 @@
|
|||
using Microsoft.EntityFrameworkCore.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore.Migrations;
|
||||
using foodmarket.Infrastructure.Persistence;
|
||||
|
||||
#nullable disable
|
||||
|
||||
namespace foodmarket.Infrastructure.Persistence.Migrations
|
||||
{
|
||||
/// <summary>Phase37 — «Избранное» кассы: товары-плитки на экране продажи
|
||||
/// (обычно без штрихкода — хлеб, овощи на вес). Добавляет в <c>products</c>:
|
||||
/// <list type="bullet">
|
||||
/// <item><c>IsFavorite</c> boolean DEFAULT false.</item>
|
||||
/// <item><c>FavoriteOrder</c> integer DEFAULT 0.</item>
|
||||
/// </list>
|
||||
/// Идемпотентно. Отдаётся в POS-синке (ProductSyncDto).</summary>
|
||||
[DbContext(typeof(AppDbContext))]
|
||||
[Migration("20261008120000_Phase37_ProductFavorite")]
|
||||
public partial class Phase37_ProductFavorite : Migration
|
||||
{
|
||||
protected override void Up(MigrationBuilder b)
|
||||
{
|
||||
b.Sql(@"
|
||||
ALTER TABLE public.products ADD COLUMN IF NOT EXISTS ""IsFavorite"" boolean NOT NULL DEFAULT false;
|
||||
ALTER TABLE public.products ADD COLUMN IF NOT EXISTS ""FavoriteOrder"" integer NOT NULL DEFAULT 0;
|
||||
");
|
||||
}
|
||||
|
||||
protected override void Down(MigrationBuilder b)
|
||||
{
|
||||
b.Sql(@"
|
||||
ALTER TABLE public.products DROP COLUMN IF EXISTS ""IsFavorite"";
|
||||
ALTER TABLE public.products DROP COLUMN IF EXISTS ""FavoriteOrder"";
|
||||
");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,7 +34,8 @@ private void InitSchema()
|
|||
CREATE TABLE IF NOT EXISTS products(
|
||||
Id TEXT PRIMARY KEY, Name TEXT NOT NULL, NameLower TEXT NOT NULL DEFAULT '', Article TEXT, UnitCode TEXT NOT NULL,
|
||||
Packaging INTEGER NOT NULL, VatPercent TEXT NOT NULL, VatEnabled INTEGER NOT NULL,
|
||||
Price TEXT NOT NULL DEFAULT '0', IsArchived INTEGER NOT NULL DEFAULT 0, UpdatedAt TEXT);
|
||||
Price TEXT NOT NULL DEFAULT '0', IsArchived INTEGER NOT NULL DEFAULT 0, UpdatedAt TEXT,
|
||||
IsFavorite INTEGER NOT NULL DEFAULT 0, FavoriteOrder INTEGER NOT NULL DEFAULT 0);
|
||||
CREATE TABLE IF NOT EXISTS barcodes(Code TEXT NOT NULL, ProductId TEXT NOT NULL);
|
||||
CREATE INDEX IF NOT EXISTS ix_barcodes_code ON barcodes(Code);
|
||||
CREATE INDEX IF NOT EXISTS ix_barcodes_productid ON barcodes(ProductId);
|
||||
|
|
@ -43,6 +44,15 @@ private void InitSchema()
|
|||
ClientSaleId TEXT PRIMARY KEY, Json TEXT NOT NULL, Status INTEGER NOT NULL DEFAULT 0,
|
||||
CreatedAt TEXT NOT NULL, Error TEXT);
|
||||
CREATE TABLE IF NOT EXISTS kv(Key TEXT PRIMARY KEY, Value TEXT);");
|
||||
// Для БД, созданных до «Избранного» — добавляем колонки (SQLite не умеет
|
||||
// IF NOT EXISTS для ADD COLUMN, поэтому игнорируем ошибку «duplicate column»).
|
||||
TryExec("ALTER TABLE products ADD COLUMN IsFavorite INTEGER NOT NULL DEFAULT 0");
|
||||
TryExec("ALTER TABLE products ADD COLUMN FavoriteOrder INTEGER NOT NULL DEFAULT 0");
|
||||
}
|
||||
|
||||
private void TryExec(string sql)
|
||||
{
|
||||
try { Exec(sql); } catch { /* колонка уже есть */ }
|
||||
}
|
||||
|
||||
// ── каталог ──────────────────────────────────────────────────────────────
|
||||
|
|
@ -57,11 +67,11 @@ public void ApplySync(PosSyncResponse sync)
|
|||
// Подготовленные команды с переиспользуемыми параметрами — массовая
|
||||
// вставка 30k товаров за ~1с вместо десятков секунд (команда на строку).
|
||||
var up = _cn.CreateCommand(); up.Transaction = tx;
|
||||
up.CommandText = @"INSERT INTO products(Id,Name,NameLower,Article,UnitCode,Packaging,VatPercent,VatEnabled,Price,IsArchived,UpdatedAt)
|
||||
VALUES($id,$n,$nl,$a,$u,$pk,$vp,$ve,$pr,$ar,$up)
|
||||
up.CommandText = @"INSERT INTO products(Id,Name,NameLower,Article,UnitCode,Packaging,VatPercent,VatEnabled,Price,IsArchived,UpdatedAt,IsFavorite,FavoriteOrder)
|
||||
VALUES($id,$n,$nl,$a,$u,$pk,$vp,$ve,$pr,$ar,$up,$fav,$favo)
|
||||
ON CONFLICT(Id) DO UPDATE SET Name=$n,NameLower=$nl,Article=$a,UnitCode=$u,Packaging=$pk,VatPercent=$vp,VatEnabled=$ve,
|
||||
Price=CASE WHEN $hasPrice=1 THEN $pr ELSE products.Price END, IsArchived=$ar, UpdatedAt=$up;";
|
||||
var upP = AddParams(up, "$id", "$n", "$nl", "$a", "$u", "$pk", "$vp", "$ve", "$pr", "$hasPrice", "$ar", "$up");
|
||||
Price=CASE WHEN $hasPrice=1 THEN $pr ELSE products.Price END, IsArchived=$ar, UpdatedAt=$up, IsFavorite=$fav, FavoriteOrder=$favo;";
|
||||
var upP = AddParams(up, "$id", "$n", "$nl", "$a", "$u", "$pk", "$vp", "$ve", "$pr", "$hasPrice", "$ar", "$up", "$fav", "$favo");
|
||||
up.Prepare();
|
||||
var delBc = _cn.CreateCommand(); delBc.Transaction = tx; delBc.CommandText = "DELETE FROM barcodes WHERE ProductId=$id";
|
||||
var delP = AddParams(delBc, "$id"); delBc.Prepare();
|
||||
|
|
@ -79,6 +89,7 @@ public void ApplySync(PosSyncResponse sync)
|
|||
upP["$ve"].Value = p.VatEnabled ? 1 : 0;
|
||||
upP["$pr"].Value = price.ToString(System.Globalization.CultureInfo.InvariantCulture);
|
||||
upP["$hasPrice"].Value = hasPrice ? 1 : 0; upP["$ar"].Value = p.IsArchived ? 1 : 0; upP["$up"].Value = p.UpdatedAt.ToString("O");
|
||||
upP["$fav"].Value = p.IsFavorite ? 1 : 0; upP["$favo"].Value = p.FavoriteOrder;
|
||||
up.ExecuteNonQuery();
|
||||
delP["$id"].Value = idStr; delBc.ExecuteNonQuery();
|
||||
foreach (var bc in p.Barcodes)
|
||||
|
|
@ -123,6 +134,18 @@ public int ProductCount()
|
|||
return r.Read() ? Map(r) : null;
|
||||
}
|
||||
|
||||
/// <summary>Избранные товары для плиток «быстрых товаров» на экране продажи.</summary>
|
||||
public IReadOnlyList<PosProduct> GetFavorites(int limit = 12)
|
||||
{
|
||||
using var cmd = _cn.CreateCommand();
|
||||
cmd.CommandText = "SELECT * FROM products WHERE IsFavorite=1 AND IsArchived=0 ORDER BY FavoriteOrder, Name LIMIT $l";
|
||||
Bind(cmd, "$l", limit.ToString());
|
||||
var list = new List<PosProduct>();
|
||||
using var r = cmd.ExecuteReader();
|
||||
while (r.Read()) list.Add(Map(r));
|
||||
return list;
|
||||
}
|
||||
|
||||
public IReadOnlyList<PosProduct> SearchByName(string term, int limit = 50)
|
||||
{
|
||||
term = (term ?? string.Empty).Trim();
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ public sealed class PosProduct
|
|||
public required bool VatEnabled { get; init; }
|
||||
public required decimal Price { get; init; }
|
||||
public bool IsArchived { get; init; }
|
||||
public bool IsFavorite { get; init; }
|
||||
public bool IsWeighed => Packaging == PosPackaging.Weight;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,47 +2,19 @@
|
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:foodmarket.Pos"
|
||||
x:Class="foodmarket.Pos.App"
|
||||
RequestedThemeVariant="Dark">
|
||||
RequestedThemeVariant="Light">
|
||||
<Application.DataTemplates>
|
||||
<local:ViewLocator />
|
||||
</Application.DataTemplates>
|
||||
<Application.Resources>
|
||||
<ResourceDictionary>
|
||||
<ResourceDictionary.MergedDictionaries>
|
||||
<ResourceInclude Source="avares://foodmarket.Pos/Styles/Tokens.axaml"/>
|
||||
</ResourceDictionary.MergedDictionaries>
|
||||
</ResourceDictionary>
|
||||
</Application.Resources>
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<!-- Крупный кассовый стиль: большие кнопки, читаемые цифры, без анимаций. -->
|
||||
<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>
|
||||
<StyleInclude Source="avares://foodmarket.Pos/Styles/PosStyles.axaml"/>
|
||||
</Application.Styles>
|
||||
</Application>
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ public override void OnFrameworkInitializationCompleted()
|
|||
else
|
||||
{
|
||||
var services = new AppServices();
|
||||
// Тема устройства (по умолчанию светлая; тёмная — опция в настройках).
|
||||
try { if (services.Store.GetKv("device.theme") == "Dark") RequestedThemeVariant = Avalonia.Styling.ThemeVariant.Dark; } catch { }
|
||||
desktop.MainWindow = new MainWindow { DataContext = new MainWindowViewModel(services) };
|
||||
}
|
||||
}
|
||||
|
|
|
|||
168
src/food-market.pos/Styles/PosStyles.axaml
Normal file
|
|
@ -0,0 +1,168 @@
|
|||
<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"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
</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"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style Selector="Button.key /template/ ContentPresenter">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</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"/>
|
||||
<Setter Property="HorizontalAlignment" Value="Stretch"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</Style>
|
||||
<Style Selector="Button.seg /template/ ContentPresenter">
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
</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>
|
||||
46
src/food-market.pos/Styles/Tokens.axaml
Normal 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>
|
||||
|
|
@ -33,14 +33,15 @@ public static int Run(string[] args)
|
|||
new("01-login", "Вход кассира (адрес сервера, логин, пароль)", () => new LoginView { DataContext = LoginViewModel.Sample() }),
|
||||
new("01b-login-pin", "Быстрый вход по PIN (зарегистрированное устройство)", () => new LoginView { DataContext = LoginViewModel.SamplePin() }),
|
||||
new("02-shift-open", "Смена: Z-отчёт/состояние смены", () => new ShiftView { DataContext = ShiftViewModel.Sample() }),
|
||||
new("03-sale-empty", "Продажа: пустой чек, ожидание сканирования", () => new SaleView { DataContext = new SaleViewModel() }),
|
||||
new("04-sale-items", "Продажа: чек с товарами (вкл. весовой)", () => new SaleView { DataContext = SaleViewModel.Sample() }),
|
||||
new("05-payment-cash", "Оплата наличными со сдачей", () => new PaymentView { DataContext = PaymentViewModel.SampleCash() }),
|
||||
new("06-payment-mixed", "Оплата смешанная (нал+карта)", () => new PaymentView { DataContext = PaymentViewModel.SampleMixed() }),
|
||||
new("03-sale-empty", "Продажа: пустой чек, ожидание сканирования (с оболочкой)", () => ShellWith(EmptySale())),
|
||||
new("04-sale-items", "Продажа: чек с товарами + избранное (с оболочкой)", () => new MainWindow { DataContext = new MainWindowViewModel() }),
|
||||
new("05-payment-cash", "Оплата наличными со сдачей", () => PayScrim(PaymentViewModel.SampleCash())),
|
||||
new("06-payment-mixed", "Оплата смешанная (нал+карта)", () => PayScrim(PaymentViewModel.SampleMixed())),
|
||||
new("07-return", "Возврат по чеку (выбор позиций)", () => new ReturnView { DataContext = ReturnViewModel.Sample() }),
|
||||
new("08-shift-z", "Z-отчёт при закрытии (должно/факт/разница)", () => new ShiftView { DataContext = ShiftViewModel.SampleClosed() }),
|
||||
new("09-shell-offline", "Статус-бар: нет связи, чеки в очереди", () => ShellOffline()),
|
||||
new("10-shell-online", "Статус-бар: на связи, очередь пуста", () => ShellOnline()),
|
||||
new("11-shell-dark", "Тёмная тема (опция в настройках, не по умолчанию)", () => ShellDark()),
|
||||
};
|
||||
|
||||
var sizes = new (string suffix, int w, int h)[] { ("1920x1080", 1920, 1080), ("1366x768", 1366, 768) };
|
||||
|
|
@ -70,6 +71,26 @@ public static int Run(string[] args)
|
|||
return 0;
|
||||
}
|
||||
|
||||
private static SaleViewModel EmptySale() => new();
|
||||
private static Control ShellWith(SaleViewModel sale)
|
||||
{
|
||||
var vm = new MainWindowViewModel();
|
||||
vm.CurrentPage = sale;
|
||||
return new MainWindow { DataContext = vm };
|
||||
}
|
||||
/// <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 },
|
||||
},
|
||||
};
|
||||
|
||||
private static Control ShellOffline()
|
||||
{
|
||||
var vm = new MainWindowViewModel { IsOnline = false, QueueCount = 3, CashierName = "Пётр Кассиров", ShiftNumber = "С-2026-000001" };
|
||||
|
|
@ -80,6 +101,13 @@ private static Control ShellOnline()
|
|||
var vm = new MainWindowViewModel { IsOnline = true, QueueCount = 0, CashierName = "Пётр Кассиров", ShiftNumber = "С-2026-000001" };
|
||||
return new MainWindow { DataContext = vm };
|
||||
}
|
||||
private static Control ShellDark()
|
||||
{
|
||||
// Переключаем тему на тёмную для этого кадра (последний — не влияет на прочие).
|
||||
if (Avalonia.Application.Current is { } app) app.RequestedThemeVariant = Avalonia.Styling.ThemeVariant.Dark;
|
||||
var vm = new MainWindowViewModel { IsOnline = true, QueueCount = 0, CashierName = "Пётр Кассиров", ShiftNumber = "С-2026-000001" };
|
||||
return new MainWindow { DataContext = vm };
|
||||
}
|
||||
|
||||
private static void WriteGallery(string dir, List<(string file, string caption)> shots)
|
||||
{
|
||||
|
|
@ -90,11 +118,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}" +
|
||||
"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>");
|
||||
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)
|
||||
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>");
|
||||
|
||||
// Раздел «Было» — прежняя тёмная тема (если сохранена в 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 (если есть).
|
||||
var winDir = Path.Combine(dir, "windows");
|
||||
if (Directory.Exists(winDir))
|
||||
|
|
|
|||
|
|
@ -24,19 +24,24 @@ public partial class MainWindowViewModel : ViewModelBase
|
|||
[ObservableProperty] private object? _currentPage;
|
||||
[ObservableProperty] private string _cashierName = "—";
|
||||
[ObservableProperty] private string _shiftNumber = "смена не открыта";
|
||||
[ObservableProperty] private bool _isShiftOpen;
|
||||
[ObservableProperty] private string _pointName = "Магазин";
|
||||
[ObservableProperty] private bool _isOnline = true;
|
||||
[ObservableProperty] private int _queueCount;
|
||||
[ObservableProperty] private PaymentViewModel? _payment;
|
||||
[ObservableProperty] private bool _isPaymentOpen;
|
||||
[ObservableProperty] private string? _updateMessage;
|
||||
|
||||
public string NetBadge => IsOnline ? "● на связи" : "○ нет связи";
|
||||
public string QueueBadge => QueueCount > 0 ? $"{QueueCount} чек(ов) в очереди" : "очередь пуста";
|
||||
public string NetBadge => IsOnline ? "На связи" : "Нет связи";
|
||||
public string ShiftBadge => IsShiftOpen ? $"Смена {ShiftNumber}" : "Смена закрыта";
|
||||
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"; }
|
||||
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(); }
|
||||
|
||||
|
|
@ -50,7 +55,12 @@ private void StartSession()
|
|||
{
|
||||
CashierName = _svc!.Session?.CashierName ?? "кассир";
|
||||
QueueCount = _svc.Store.PendingCount();
|
||||
_sale = new SaleViewModel(_svc) { PaymentRequested = OpenPayment, SaleCommitted = _ => RefreshStatus() };
|
||||
_sale = new SaleViewModel(_svc)
|
||||
{
|
||||
PaymentRequested = OpenPayment, SaleCommitted = _ => RefreshStatus(),
|
||||
NavReturnRequested = () => ShowReturnCommand.Execute(null),
|
||||
NavShiftRequested = () => ShowShiftCommand.Execute(null),
|
||||
};
|
||||
CurrentPage = _sale;
|
||||
StartSyncLoop();
|
||||
_ = CheckForUpdateAsync();
|
||||
|
|
@ -86,9 +96,21 @@ private static string LocalVersion()
|
|||
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 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);
|
||||
|
||||
/// <summary>Переключить светлую/тёмную тему (тёмная — опция, по умолчанию светлая).
|
||||
/// Выбор сохраняется на устройстве.</summary>
|
||||
[RelayCommand]
|
||||
private void ToggleTheme()
|
||||
{
|
||||
var app = Avalonia.Application.Current;
|
||||
if (app is null) return;
|
||||
var dark = app.ActualThemeVariant != Avalonia.Styling.ThemeVariant.Dark;
|
||||
app.RequestedThemeVariant = dark ? Avalonia.Styling.ThemeVariant.Dark : Avalonia.Styling.ThemeVariant.Light;
|
||||
_svc?.Store.SetKv("device.theme", dark ? "Dark" : "Light");
|
||||
}
|
||||
|
||||
private void OpenPayment(PaymentViewModel pvm)
|
||||
{
|
||||
pvm.OnCancel = ClosePayment;
|
||||
|
|
@ -360,6 +382,9 @@ public partial class SaleViewModel : ViewModelBase
|
|||
private readonly Cart _cart = new();
|
||||
private readonly Stack<CartSnapshot> _parked = new();
|
||||
public ObservableCollection<SaleLineVm> Lines { get; } = new();
|
||||
public ObservableCollection<PosProduct> Favorites { get; } = new();
|
||||
[ObservableProperty] private bool _favoritesOpen = true;
|
||||
public bool HasFavorites => Favorites.Count > 0;
|
||||
|
||||
[ObservableProperty] private string _scanInput = "";
|
||||
[ObservableProperty] private string _shiftNumber = "С-2026-000001";
|
||||
|
|
@ -369,14 +394,34 @@ public partial class SaleViewModel : ViewModelBase
|
|||
[ObservableProperty] private decimal _total;
|
||||
[ObservableProperty] private int _itemCount;
|
||||
[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>
|
||||
public Action<PaymentViewModel>? PaymentRequested;
|
||||
/// <summary>Чек проведён — оболочка обновляет индикатор очереди.</summary>
|
||||
public Action<CheckoutService.CommitResult>? SaleCommitted;
|
||||
/// <summary>Переходы к возврату/смене (обрабатывает оболочка — те же, что F9/F7/F10).</summary>
|
||||
public Action? NavReturnRequested;
|
||||
public Action? NavShiftRequested;
|
||||
|
||||
[RelayCommand] private void GoReturn() => NavReturnRequested?.Invoke();
|
||||
[RelayCommand] private void GoShift() => NavShiftRequested?.Invoke();
|
||||
|
||||
public SaleViewModel() { }
|
||||
public SaleViewModel(AppServices svc) { _svc = svc; }
|
||||
public SaleViewModel(AppServices svc) { _svc = svc; LoadFavorites(); }
|
||||
|
||||
private void LoadFavorites()
|
||||
{
|
||||
Favorites.Clear();
|
||||
if (_svc is not null) foreach (var p in _svc.Store.GetFavorites()) Favorites.Add(p);
|
||||
OnPropertyChanged(nameof(HasFavorites));
|
||||
}
|
||||
|
||||
[RelayCommand] private void AddFavorite(PosProduct p) => AddProduct(p, 1m);
|
||||
[RelayCommand] private void ToggleFavorites() => FavoritesOpen = !FavoritesOpen;
|
||||
|
||||
private void Recalc()
|
||||
{
|
||||
|
|
@ -433,6 +478,17 @@ public void SetLineQuantity(SaleLineVm line, decimal qty)
|
|||
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} ₸"; }
|
||||
|
||||
[RelayCommand]
|
||||
|
|
@ -511,6 +567,13 @@ public static SaleViewModel Sample()
|
|||
vm.Total = decimal.Round(vm.Subtotal - vm.Discount, 2);
|
||||
vm.ItemCount = vm.Lines.Count;
|
||||
vm.Message = "Чек готов к оплате — F12 Оплата";
|
||||
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("Яблоки (вес)", "кг", 690m, true), Fav("Картофель (вес)", "кг", 280m, true),
|
||||
Fav("Пакет-майка", "шт", 20m, false), Fav("Вода 0,5л", "шт", 150m, false) })
|
||||
vm.Favorites.Add(f);
|
||||
return vm;
|
||||
}
|
||||
}
|
||||
|
|
@ -524,16 +587,35 @@ public partial class PaymentViewModel : ViewModelBase
|
|||
public decimal Change => PaymentMath.Compute(Total, PaidCash, PaidCard).Change;
|
||||
public decimal Shortfall => PaymentMath.Compute(Total, PaidCash, PaidCard).Shortfall;
|
||||
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 OnPaidCardChanged(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)); }
|
||||
|
||||
/// <summary>Вызывается при подтверждении оплаты (cash, card, change).</summary>
|
||||
public Func<decimal, decimal, decimal, Task>? OnConfirm;
|
||||
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]
|
||||
private async Task Confirm()
|
||||
|
|
@ -544,8 +626,8 @@ private async Task Confirm()
|
|||
|
||||
[RelayCommand] private void Cancel() => OnCancel?.Invoke();
|
||||
|
||||
public static PaymentViewModel SampleCash() => new() { Total = 3365m, PaidCash = 5000m, Method = "Наличные" };
|
||||
public static PaymentViewModel SampleMixed() => new() { Total = 3365m, PaidCash = 1365m, PaidCard = 2000m, Method = "Смешанная" };
|
||||
public static PaymentViewModel SampleCash() => new() { Total = 3362m, PaidCash = 5000m, Method = "Наличные" };
|
||||
public static PaymentViewModel SampleMixed() => new() { Total = 3362m, PaidCash = 1362m, PaidCard = 2000m, Method = "Смешанная" };
|
||||
}
|
||||
|
||||
/// <summary>Смена: открыть (размен) / закрыть (факт → должно/факт/разница),
|
||||
|
|
@ -568,9 +650,12 @@ public partial class ShiftViewModel : ViewModelBase
|
|||
|
||||
public decimal ExpectedCash => ShiftMath.ExpectedCash(OpeningFloat, SalesCash, ReturnsCash, DrawerIn, DrawerOut);
|
||||
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 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>
|
||||
public Action<string?>? OnShiftChanged;
|
||||
|
|
|
|||
|
|
@ -3,63 +3,65 @@
|
|||
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
|
||||
x:Class="foodmarket.Pos.Views.LoginView"
|
||||
x:DataType="vm:LoginViewModel">
|
||||
<Grid Background="#0f172a">
|
||||
<Border Background="#1e293b" CornerRadius="12" Width="440" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock Text="FOOD MARKET" Foreground="#22c55e" FontWeight="Bold" FontSize="22" HorizontalAlignment="Center"/>
|
||||
<TextBlock Text="Вход кассира" Foreground="#f8fafc" FontSize="18" HorizontalAlignment="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"/>
|
||||
<Grid Background="{DynamicResource PosBg}">
|
||||
<Border Classes="card" Width="440" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center" CornerRadius="18">
|
||||
<StackPanel Spacing="16">
|
||||
<StackPanel Spacing="4" 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"/>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Error}" Foreground="#fca5a5" 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="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}"/>
|
||||
<TextBlock Text="Food Market · Касса" Foreground="{DynamicResource PosText}" FontWeight="Bold" FontSize="18" HorizontalAlignment="Center" Margin="0,8,0,0"/>
|
||||
<TextBlock Text="Вход кассира" Foreground="{DynamicResource PosTextMuted}" FontSize="14" HorizontalAlignment="Center"/>
|
||||
</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" HorizontalAlignment="Stretch" Height="40" FontSize="13" Content="Вход по логину и паролю" Command="{Binding UsePasswordCommand}"/>
|
||||
<Button Classes="pos ghost" HorizontalAlignment="Stretch" Height="42" FontSize="13" Content="Вход по логину и паролю" Command="{Binding UsePasswordCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Логин/пароль -->
|
||||
<StackPanel Spacing="14" IsVisible="{Binding !PinMode}">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Адрес сервера" Foreground="#94a3b8"/>
|
||||
<TextBox Classes="pos" Text="{Binding ServerUrl}" FontSize="16"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Логин кассира" Foreground="#94a3b8"/>
|
||||
<TextBox Classes="pos" Text="{Binding Username}" FontSize="18"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="PIN / пароль" Foreground="#94a3b8"/>
|
||||
<TextBox Classes="pos" Text="{Binding Pin}" PasswordChar="●" FontSize="18"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Error}" Foreground="#fca5a5" 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}"/>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Адрес сервера" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
|
||||
<TextBox Classes="pos" Text="{Binding ServerUrl}" FontSize="15"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Логин кассира" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
|
||||
<TextBox Classes="pos" Text="{Binding Username}" FontSize="17"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="PIN / пароль" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
|
||||
<TextBox Classes="pos" Text="{Binding Pin}" PasswordChar="●" FontSize="17"/>
|
||||
</StackPanel>
|
||||
<TextBlock Text="{Binding Error}" Foreground="{DynamicResource PosDanger}" TextWrapping="Wrap" IsVisible="{Binding Error, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
<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}">
|
||||
<TextBlock Text="Регистрация кассы — выберите точку продаж:" Foreground="#f8fafc"/>
|
||||
<ComboBox ItemsSource="{Binding Points}" SelectedItem="{Binding SelectedPoint}" HorizontalAlignment="Stretch" FontSize="16"/>
|
||||
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="52" Content="Зарегистрировать на этой точке" Command="{Binding ConfirmPointCommand}"/>
|
||||
</StackPanel>
|
||||
<StackPanel Spacing="8" IsVisible="{Binding RequirePointSelection}">
|
||||
<TextBlock Text="Регистрация кассы — выберите точку продаж:" Foreground="{DynamicResource PosText}" FontSize="14"/>
|
||||
<ComboBox ItemsSource="{Binding Points}" SelectedItem="{Binding SelectedPoint}" HorizontalAlignment="Stretch" FontSize="15"/>
|
||||
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="50" Content="Зарегистрировать на этой точке" Command="{Binding ConfirmPointCommand}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Button Classes="pos" HorizontalAlignment="Stretch" Height="40" FontSize="13" Content="Быстрый вход по PIN" Command="{Binding UsePinCommand}" IsVisible="{Binding DeviceRegistered}"/>
|
||||
|
||||
<Button Classes="pos" HorizontalAlignment="Stretch" Height="40" FontSize="13" Command="{Binding ResetDeviceCommand}" ToolTip.Tip="Сбросить регистрацию устройства. Новую точку выберет только администратор.">
|
||||
<TextBlock Text="Сменить точку продаж (админ)" TextWrapping="Wrap" TextAlignment="Center"/>
|
||||
</Button>
|
||||
<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="Сбросить регистрацию устройства. Новую точку выберет только администратор."/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
|
|
|||
|
|
@ -3,43 +3,73 @@
|
|||
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
|
||||
x:Class="foodmarket.Pos.Views.MainWindow"
|
||||
x:DataType="vm:MainWindowViewModel"
|
||||
Width="1280" Height="800" MinWidth="1024" MinHeight="680"
|
||||
Background="#0f172a" Title="Касса food-market"
|
||||
Width="1366" Height="768" MinWidth="1100" MinHeight="680"
|
||||
Background="{DynamicResource PosBg}" Title="Касса food-market"
|
||||
WindowStartupLocation="CenterScreen">
|
||||
<Panel>
|
||||
<Grid RowDefinitions="54,*,34">
|
||||
<!-- Статус-бар -->
|
||||
<Border Grid.Row="0" Background="#1e293b" Padding="16,0">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto,Auto">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
|
||||
<TextBlock Text="FOOD MARKET · Касса" FontWeight="Bold" Foreground="#22c55e" FontSize="16" VerticalAlignment="Center"/>
|
||||
<Grid RowDefinitions="64,*,46">
|
||||
|
||||
<!-- Верхняя полоса -->
|
||||
<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>
|
||||
<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 Grid.Column="2" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
|
||||
<Border Classes="chip" Classes.ok="{Binding IsShiftOpen}" Classes.warn="{Binding !IsShiftOpen}">
|
||||
<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>
|
||||
<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="#fde68a" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}" FontSize="12"/>
|
||||
<TextBlock Grid.Column="3" Margin="18,0" Text="{Binding NetBadge}" VerticalAlignment="Center" Foreground="#34d399"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding QueueBadge}" Foreground="#fbbf24" VerticalAlignment="Center"/>
|
||||
|
||||
<TextBlock Grid.Column="1" Text="{Binding UpdateMessage}" Foreground="{DynamicResource PosWarn}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="12.5"
|
||||
IsVisible="{Binding UpdateMessage, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Текущая страница -->
|
||||
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}" Margin="0"/>
|
||||
<ContentControl Grid.Row="1" Content="{Binding CurrentPage}"/>
|
||||
|
||||
<!-- Нижняя панель навигации -->
|
||||
<Border Grid.Row="2" Background="#1e293b">
|
||||
<StackPanel Orientation="Horizontal" Spacing="2" HorizontalAlignment="Center">
|
||||
<TextBlock Text="F1 Помощь · F2 Поиск · F7 Внесение/Выплата · F9 Возврат · F10 Смена · F12 Оплата · Esc Отмена"
|
||||
Foreground="#94a3b8" VerticalAlignment="Center" FontSize="12"/>
|
||||
</StackPanel>
|
||||
<!-- Горячие клавиши -->
|
||||
<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>
|
||||
|
||||
<!-- Модальная оплата поверх экрана продажи -->
|
||||
<Border Background="#cc0b1220" IsVisible="{Binding IsPaymentOpen}">
|
||||
<ContentControl Content="{Binding Payment}"/>
|
||||
<!-- Модальная оплата -->
|
||||
<Border Background="{DynamicResource PosScrim}" IsVisible="{Binding IsPaymentOpen}">
|
||||
<ContentControl Content="{Binding Payment}" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</Panel>
|
||||
</Window>
|
||||
|
|
|
|||
|
|
@ -3,37 +3,76 @@
|
|||
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
|
||||
x:Class="foodmarket.Pos.Views.PaymentView"
|
||||
x:DataType="vm:PaymentViewModel">
|
||||
<Grid Background="#0f172a">
|
||||
<Border Background="#1e293b" CornerRadius="12" Width="560" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock Text="Оплата чека" FontSize="24" FontWeight="Bold" Foreground="#f8fafc" HorizontalAlignment="Center"/>
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="К оплате" Foreground="#94a3b8" FontSize="18" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="#34d399" FontSize="30" FontWeight="Bold"/>
|
||||
</Grid>
|
||||
<Separator Background="#334155"/>
|
||||
<Grid ColumnDefinitions="140,*">
|
||||
<TextBlock Text="Наличные" Foreground="#cbd5e1" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Column="1" Classes="pos" Text="{Binding PaidCash}" FontSize="22"/>
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="140,*">
|
||||
<TextBlock Text="Картой" Foreground="#cbd5e1" VerticalAlignment="Center"/>
|
||||
<TextBox Grid.Column="1" Classes="pos" Text="{Binding PaidCard}" FontSize="22"/>
|
||||
</Grid>
|
||||
<Border Background="#0b1220" CornerRadius="8" Padding="14">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="СДАЧА" Foreground="#f8fafc" FontSize="20" FontWeight="SemiBold" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Change, StringFormat='{}{0:N0} ₸'}" Foreground="#fbbf24" FontSize="30" FontWeight="Bold"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<Grid ColumnDefinitions="*,2*">
|
||||
<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"/>
|
||||
</Button>
|
||||
</Grid>
|
||||
<Button Classes="pos" HorizontalAlignment="Stretch" Height="44" Content="Esc · Отмена" Command="{Binding CancelCommand}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
<Border Classes="card" Width="720" Padding="28" CornerRadius="18" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<StackPanel Spacing="16">
|
||||
<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"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Сегменты способа оплаты (слитный переключатель) -->
|
||||
<Border Background="{DynamicResource PosSubtle}" 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}"/>
|
||||
</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>
|
||||
<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"/>
|
||||
</UniformGrid>
|
||||
|
||||
<!-- Сдача -->
|
||||
<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 Change, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosBrand}" FontSize="36" FontWeight="Bold"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Правая часть: цифровая клавиатура 3×4 (ровные квадраты) -->
|
||||
<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}"/>
|
||||
</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}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -3,54 +3,57 @@
|
|||
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
|
||||
x:Class="foodmarket.Pos.Views.ReturnView"
|
||||
x:DataType="vm:ReturnViewModel">
|
||||
<Grid Background="#0f172a" RowDefinitions="Auto,*,Auto" Margin="16">
|
||||
<StackPanel Grid.Row="0" Spacing="8">
|
||||
<TextBlock Text="Возврат по чеку" FontSize="22" FontWeight="Bold" Foreground="#f8fafc"/>
|
||||
<Grid ColumnDefinitions="*,170">
|
||||
<TextBox Classes="pos" Watermark="Номер чека или скан QR (Enter)" Text="{Binding ReceiptNumber}">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding LookupCommand}"/>
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="1" Classes="pos" Margin="8,0,0,0" HorizontalAlignment="Stretch" Content="Найти" Command="{Binding LookupCommand}"/>
|
||||
</Grid>
|
||||
<TextBlock Text="{Binding Message}" Foreground="#60a5fa" TextWrapping="Wrap"/>
|
||||
<Grid Background="{DynamicResource PosBg}" RowDefinitions="Auto,*,Auto" Margin="20">
|
||||
<StackPanel Grid.Row="0" Spacing="10">
|
||||
<TextBlock Text="Возврат по чеку" FontSize="22" FontWeight="Bold" Foreground="{DynamicResource PosText}"/>
|
||||
<Border Classes="card" Padding="0">
|
||||
<Grid ColumnDefinitions="*,170">
|
||||
<TextBox BorderThickness="0" Background="Transparent" FontSize="18" VerticalContentAlignment="Center" Margin="16,0"
|
||||
Watermark="Номер чека или скан QR (Enter)" Text="{Binding ReceiptNumber}">
|
||||
<TextBox.KeyBindings><KeyBinding Gesture="Enter" Command="{Binding LookupCommand}"/></TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="1" Classes="pos primary" Margin="6" HorizontalAlignment="Stretch" Content="Найти" Command="{Binding LookupCommand}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Message}" Foreground="{DynamicResource PosTextMuted}" TextWrapping="Wrap" FontSize="14"/>
|
||||
</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.Styles><Style Selector="ListBoxItem"><Setter Property="Padding" Value="0"/><Setter Property="Margin" Value="0,3"/></Style></ListBox.Styles>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate x:DataType="vm:ReturnLineVm">
|
||||
<Grid ColumnDefinitions="*,190,110,120" MinHeight="58">
|
||||
<StackPanel VerticalAlignment="Center" Margin="4,6">
|
||||
<TextBlock Text="{Binding Name}" Foreground="#f3f4f6" FontSize="16" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding AvailText}" Foreground="#94a3b8" FontSize="12.5"/>
|
||||
</StackPanel>
|
||||
<!-- Степпер количества к возврату -->
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" HorizontalAlignment="Right" VerticalAlignment="Center">
|
||||
<Button Classes="pos" Width="44" Height="40" Padding="0" FontSize="20" Content="−"
|
||||
Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).DecCommand}" CommandParameter="{Binding}"/>
|
||||
<Border Background="#0b1220" CornerRadius="6" Width="58" Height="40">
|
||||
<TextBlock Text="{Binding QtyText}" Foreground="#f8fafc" FontSize="17" HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<Button Classes="pos" Width="44" Height="40" Padding="0" FontSize="20" Content="+"
|
||||
Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).IncCommand}" CommandParameter="{Binding}"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="2" Classes="pos" Height="40" FontSize="13" Margin="6,0,0,0" Content="Всё"
|
||||
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"/>
|
||||
</Grid>
|
||||
<Border Background="{DynamicResource PosSubtle}" CornerRadius="10" Padding="12,8">
|
||||
<Grid ColumnDefinitions="*,210,110,120">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Name}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="Medium" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding AvailText}" Foreground="{DynamicResource PosTextMuted}" FontSize="13"/>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="6" HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Button Classes="key" Width="42" Height="42" Padding="0" FontSize="22" Content="−"
|
||||
Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).DecCommand}" CommandParameter="{Binding}"/>
|
||||
<TextBlock Text="{Binding QtyText}" Foreground="{DynamicResource PosText}" FontSize="17" FontWeight="SemiBold" MinWidth="54" TextAlignment="Center" VerticalAlignment="Center"/>
|
||||
<Button Classes="key" Width="42" Height="42" Padding="0" FontSize="20" Content="+"
|
||||
Command="{Binding $parent[ItemsControl].((vm:ReturnViewModel)DataContext).IncCommand}" CommandParameter="{Binding}"/>
|
||||
</StackPanel>
|
||||
<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}"/>
|
||||
<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>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Border>
|
||||
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="К возврату" Foreground="#cbd5e1" FontSize="18" VerticalAlignment="Center"/>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="16">
|
||||
<TextBlock Text="{Binding RefundTotal, StringFormat='{}{0:N0} ₸'}" Foreground="#fca5a5" FontSize="26" FontWeight="Bold" VerticalAlignment="Center"/>
|
||||
<Button Classes="pos danger" Height="56" Width="230" Content="Оформить возврат" Command="{Binding SubmitCommand}" IsEnabled="{Binding Found}"/>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
<Border Grid.Row="2" Classes="card" Padding="16,12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center">
|
||||
<TextBlock Text="К возврату" Foreground="{DynamicResource PosTextMuted}" FontSize="14"/>
|
||||
<TextBlock Text="{Binding RefundTotal, StringFormat='{}{0:N0} ₸'}" Foreground="{DynamicResource PosDanger}" FontSize="28" FontWeight="Bold"/>
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" Classes="pos danger" Height="56" Width="240" FontSize="17" Content="Оформить возврат" Command="{Binding SubmitCommand}" IsEnabled="{Binding Found}"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -3,77 +3,135 @@
|
|||
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
|
||||
x:Class="foodmarket.Pos.Views.SaleView"
|
||||
x:DataType="vm:SaleViewModel">
|
||||
<Grid ColumnDefinitions="*,380" Background="#0f172a">
|
||||
<!-- Левая часть: чек -->
|
||||
<Grid Grid.Column="0" RowDefinitions="Auto,*,Auto" Margin="16,12,8,12">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto" Margin="0,0,0,8">
|
||||
<TextBox Grid.Column="0" Classes="pos" Watermark="Штрихкод / поиск товара (Enter)"
|
||||
Text="{Binding ScanInput}" FontSize="22">
|
||||
<TextBox.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{Binding ScanCommand}"/>
|
||||
</TextBox.KeyBindings>
|
||||
</TextBox>
|
||||
<Button Grid.Column="1" Classes="pos" Margin="8,0,0,0" Content="Отложить" Command="{Binding ParkCommand}"/>
|
||||
<Button Grid.Column="2" Classes="pos" Margin="6,0,0,0" Content="Вернуть" Command="{Binding ResumeCommand}"/>
|
||||
</Grid>
|
||||
<Grid ColumnDefinitions="*,400" Background="{DynamicResource PosBg}">
|
||||
|
||||
<Border Grid.Row="1" Background="#111827" CornerRadius="10" Padding="0">
|
||||
<DockPanel>
|
||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="*,110,110,120" Background="#1f2937" Height="38">
|
||||
<TextBlock Text=" Товар" Foreground="#9ca3af" VerticalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="1" Text="Кол-во" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="2" Text="Цена" Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||
<TextBlock Grid.Column="3" Text="Сумма " Foreground="#9ca3af" VerticalAlignment="Center" HorizontalAlignment="Right"/>
|
||||
<!-- ЛЕВО: поиск + избранное + корзина -->
|
||||
<Grid Grid.Column="0" RowDefinitions="Auto,Auto,*,Auto" Margin="20,18,10,18">
|
||||
|
||||
<!-- Поиск / скан -->
|
||||
<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"/>
|
||||
<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>
|
||||
</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>
|
||||
</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>
|
||||
|
||||
<!-- Корзина -->
|
||||
<Border Grid.Row="2" Classes="card" Padding="0">
|
||||
<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>
|
||||
<ListBox ItemsSource="{Binding Lines}" Background="Transparent" BorderThickness="0">
|
||||
|
||||
<!-- Пустая корзина -->
|
||||
<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"/>
|
||||
</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}">
|
||||
<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>
|
||||
<DataTemplate x:DataType="vm:SaleLineVm">
|
||||
<Grid ColumnDefinitions="*,110,110,120" MinHeight="46">
|
||||
<StackPanel VerticalAlignment="Center" Margin="0,4">
|
||||
<TextBlock Text="{Binding Name}" Foreground="#f3f4f6" FontSize="16" TextWrapping="Wrap"/>
|
||||
<TextBlock Text="{Binding DiscountText}" Foreground="#fca5a5" FontSize="12.5"
|
||||
IsVisible="{Binding HasDiscount}"/>
|
||||
</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"/>
|
||||
<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"/>
|
||||
</Grid>
|
||||
<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}"/>
|
||||
</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"
|
||||
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>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</DockPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<TextBlock Grid.Row="2" Text="{Binding Message}" Foreground="#60a5fa" Margin="4,8,0,0" FontSize="14"/>
|
||||
<TextBlock Grid.Row="3" Text="{Binding Message}" Foreground="{DynamicResource PosTextMuted}" Margin="4,10,0,0" FontSize="13.5"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Правая часть: итоги + оплата -->
|
||||
<Border Grid.Column="1" Background="#1e293b" Margin="8,12,16,12" CornerRadius="10" Padding="18">
|
||||
<Grid RowDefinitions="Auto,Auto,Auto,*,Auto">
|
||||
<StackPanel Grid.Row="0" Spacing="6">
|
||||
<Grid ColumnDefinitions="*,Auto"><TextBlock Text="Позиций" Foreground="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding ItemCount}" Foreground="#e5e7eb"/></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="#94a3b8"/><TextBlock Grid.Column="1" Text="{Binding Discount, StringFormat='-{0:N0} ₸'}" Foreground="#fca5a5"/></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>
|
||||
</StackPanel>
|
||||
<Separator Grid.Row="1" Margin="0,12" Background="#334155"/>
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="ИТОГО" Foreground="#f8fafc" FontSize="22" FontWeight="Bold" VerticalAlignment="Bottom"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Total, StringFormat='{}{0:N0} ₸'}" Foreground="#34d399" FontSize="34" FontWeight="Bold" HorizontalAlignment="Right"/>
|
||||
</Grid>
|
||||
<StackPanel Grid.Row="4" Spacing="8">
|
||||
<Button Classes="pos primary" HorizontalAlignment="Stretch" Height="68" FontSize="22" Command="{Binding PayCommand}">
|
||||
<TextBlock Text="F12 · ОПЛАТА" TextAlignment="Center"/>
|
||||
</Button>
|
||||
<Grid ColumnDefinitions="*,*" >
|
||||
<Button Classes="pos" Margin="0,0,4,0" FontSize="16" HorizontalAlignment="Stretch">
|
||||
<TextBlock Text="Скидка" TextAlignment="Center"/>
|
||||
</Button>
|
||||
<Button Grid.Column="1" Classes="pos danger" Margin="4,0,0,0" FontSize="16" HorizontalAlignment="Stretch" Command="{Binding ClearCommand}" ToolTip.Tip="Очистить чек (Esc)">
|
||||
<TextBlock Text="Очистить" TextWrapping="Wrap" TextAlignment="Center"/>
|
||||
</Button>
|
||||
|
||||
<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"/>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</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>
|
||||
</Border>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
|
|
|||
|
|
@ -3,33 +3,56 @@
|
|||
xmlns:vm="clr-namespace:foodmarket.Pos.ViewModels"
|
||||
x:Class="foodmarket.Pos.Views.ShiftView"
|
||||
x:DataType="vm:ShiftViewModel">
|
||||
<Grid Background="#0f172a">
|
||||
<Border Background="#1e293b" CornerRadius="12" Width="620" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<Grid Background="{DynamicResource PosBg}">
|
||||
<Border Classes="card" Width="640" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center">
|
||||
<StackPanel Spacing="12">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<TextBlock Text="Z-отчёт смены" FontSize="24" FontWeight="Bold" Foreground="#f8fafc"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Number}" Foreground="#93c5fd" FontSize="18" VerticalAlignment="Center"/>
|
||||
<TextBlock Text="Смена — Z-отчёт" FontSize="22" FontWeight="Bold" Foreground="{DynamicResource PosText}"/>
|
||||
<Border Grid.Column="1" Classes="chip" Classes.ok="{Binding IsOpen}" Classes.warn="{Binding !IsOpen}" VerticalAlignment="Center">
|
||||
<TextBlock Text="{Binding Number}"/>
|
||||
</Border>
|
||||
</Grid>
|
||||
<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 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="*,*,*">
|
||||
<Button Classes="pos" Margin="0,0,4,0" HorizontalAlignment="Stretch" 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="2" Classes="pos" Margin="4,0,0,0" HorizontalAlignment="Stretch" Content="Выплата" Command="{Binding WithdrawCashCommand}" 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 ghost" Margin="5,0" Content="Внести" Command="{Binding DepositCashCommand}" IsEnabled="{Binding IsOpen}"/>
|
||||
<Button Grid.Column="2" Classes="pos ghost" Margin="5,0,0,0" Content="Выплата" Command="{Binding WithdrawCashCommand}" IsEnabled="{Binding IsOpen}"/>
|
||||
</Grid>
|
||||
<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>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
|
|
|||
|
|
@ -36,6 +36,10 @@ public record ProductSyncDto
|
|||
public required bool IsMarked { get; init; }
|
||||
/// <summary>true — товар архивный, на кассе не продаётся, оставлен для отчётов.</summary>
|
||||
public required bool IsArchived { get; init; }
|
||||
/// <summary>Товар в «Избранном» кассы (плитка на экране продажи).</summary>
|
||||
public bool IsFavorite { get; init; }
|
||||
/// <summary>Порядок плитки в «Избранном».</summary>
|
||||
public int FavoriteOrder { get; init; }
|
||||
public DateTime UpdatedAt { get; init; }
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -436,3 +436,31 @@ public void Cashier_not_assigned_to_device_point_is_blocked()
|
|||
=> DeviceRegistration.Evaluate(A, canManagePoints: false, new[] { B })
|
||||
.Should().Be(DeviceRegistration.Decision.NotAssigned);
|
||||
}
|
||||
|
||||
public class PosFavoritesTests
|
||||
{
|
||||
private static ProductSyncDto P(Guid id, string name, bool fav, int order) => new()
|
||||
{
|
||||
Id = id, Name = name, Barcodes = Array.Empty<string>(), UnitCode = "шт", Packaging = 1,
|
||||
VatPercent = 12m, VatEnabled = true, IsMarked = false, IsArchived = false,
|
||||
IsFavorite = fav, FavoriteOrder = order, UpdatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
[Fact]
|
||||
public void GetFavorites_returns_only_favorites_in_order()
|
||||
{
|
||||
using var s = PosStore.InMemory();
|
||||
var a = Guid.NewGuid(); var b = Guid.NewGuid(); var c = Guid.NewGuid();
|
||||
s.ApplySync(new PosSyncResponse
|
||||
{
|
||||
ServerTime = DateTime.UtcNow,
|
||||
Products = new[] { P(a, "Обычный", false, 0), P(b, "Хлеб", true, 2), P(c, "Молоко", true, 1) },
|
||||
Prices = Array.Empty<PriceSyncDto>(), Stocks = Array.Empty<StockSyncDto>(),
|
||||
Counterparties = Array.Empty<CounterpartySyncDto>(), DeletedProductIds = Array.Empty<Guid>(),
|
||||
});
|
||||
|
||||
var favs = s.GetFavorites();
|
||||
favs.Select(f => f.Name).Should().Equal("Молоко", "Хлеб"); // по FavoriteOrder 1,2
|
||||
favs.Should().OnlyContain(f => f.Name != "Обычный");
|
||||
}
|
||||
}
|
||||
|
|
|
|||