fix(ui): правки по ревью скриншотов — i18n, валюта, дубль групп, касса, фасовки
Some checks are pending
Auto-tag / Create date-tag (push) Waiting to run
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Public / Build + push Public (test + latest) (push) Waiting to run
Docker Public / Deploy :test → test.food-market.kz (push) Blocked by required conditions
Docker Web / Build + push Web (push) Waiting to run
Docker Web / Deploy Web on stage (push) Blocked by required conditions

1. Английские строки в админке → русский: ru.json dashboard.title «Dashboard»→
   «Сводка», common.live/offline «live/offline»→«онлайн/офлайн»; дашборд-виджеты
   defaultValue «Low-stock alerts»→«Низкий остаток», «Live-обновление»→«Обновляется
   в реальном времени».
2. Каталог: системная группа «Все товары» больше не дублирует корень дерева
   (ProductGroupDto.IsSystem + ProductGroupTree прячет системные группы).
3. Валюта: розничная цена в каталоге теперь «₸», а не «KZT» (символ вместо кода).
4. Касса на сайте: вместо HTML-макета — НАСТОЯЩИЙ экран Avalonia-кассы
   (--screenshots) с витринными данными «Магазин у дома», кассир Виктория Ким, с
   логотипом; убран префикс «Магазин » в шапке (не было двойного «Магазин Магазин»).
   HTML-макет удалён из публичного пайплайна.
5. Витрина: фасовка/единица товара выводится из названия (кг→весовой, иначе
   штучный) — нет «Авокадо 1шт весовой» / «Бешбармак 1кг штучный».

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-08 22:53:28 +05:00
parent bce417941c
commit d20cf15ca2
31 changed files with 53 additions and 258 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 79 KiB

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 151 KiB

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 131 KiB

After

Width:  |  Height:  |  Size: 130 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 171 KiB

After

Width:  |  Height:  |  Size: 170 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 124 KiB

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 172 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 139 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 149 KiB

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 140 KiB

After

Width:  |  Height:  |  Size: 138 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 148 KiB

After

Width:  |  Height:  |  Size: 147 KiB

View file

@ -0,0 +1,20 @@
# Правки по ревью скриншотов сайта (Mac-Claude)
Замечания к новым скриншотам test.food-market.kz/screenshots/*.webp. Часть — ошибки
самой программы (не только скриншотов).
## План / статус
1. [ ] Английские строки в админке: «Dashboard», «Low-stock alerts», «live» и др. —
перевести; grep по web/src на видимые латинские строки; лингва-проверка в menu-walk.
2. [ ] Каталог: дубль «Все товары» (корень дерева + системная группа). Убрать дубль.
3. [ ] Валюта: везде «₸» и формат «1 300 ₸» (сейчас розничная — «510 KZT»).
4. [ ] Касса на сайте: заменить рендер HTML-макета на НАСТОЯЩИЙ экран Avalonia-кассы
(--screenshots, витринные данные «Магазин у дома», с логотипом). Макет не использовать.
5. [ ] Витрина: packaging по товарам (Авокадо 1шт — штучный, Бешбармак 1кг — весовой и т.п.).
6. [ ] Выложить сайт+стенд, переснять затронутые скриншоты, посмотреть каждый кадр.
## Что НЕ трогаем
Прод, shared-postgres, global.json, gateway nginx, sklad.
## Журнал
- 08.10 — старт: план, разбор замечаний.

View file

@ -45,7 +45,7 @@ public class ProductGroupsController : ControllerBase
};
var items = await q
.Skip(req.Skip).Take(req.Take)
.Select(g => new ProductGroupDto(g.Id, g.Name, g.ParentId, g.Path, g.SortOrder, g.MarkupPercent, g.OrganizationId))
.Select(g => new ProductGroupDto(g.Id, g.Name, g.ParentId, g.Path, g.SortOrder, g.MarkupPercent, g.OrganizationId, g.IsSystem))
.ToListAsync(ct);
return new PagedResult<ProductGroupDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
}
@ -54,7 +54,7 @@ public class ProductGroupsController : ControllerBase
public async Task<ActionResult<ProductGroupDto>> Get(Guid id, CancellationToken ct)
{
var g = await _db.ProductGroups.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, ct);
return g is null ? NotFound() : new ProductGroupDto(g.Id, g.Name, g.ParentId, g.Path, g.SortOrder, g.MarkupPercent, g.OrganizationId);
return g is null ? NotFound() : new ProductGroupDto(g.Id, g.Name, g.ParentId, g.Path, g.SortOrder, g.MarkupPercent, g.OrganizationId, g.IsSystem);
}
[HttpPost, RequiresPermission("ProductGroupsManage")]

View file

@ -60,7 +60,6 @@ public async Task<SeedSummary> SeedAsync(Guid orgId, CancellationToken ct = defa
var unitKg = await _db.UnitsOfMeasure.IgnoreQueryFilters().FirstAsync(u => u.OrganizationId == null && u.Code == "166", ct);
var unitSht = await _db.UnitsOfMeasure.IgnoreQueryFilters().FirstAsync(u => u.OrganizationId == null && u.Code == "796", ct);
var unitL = await _db.UnitsOfMeasure.IgnoreQueryFilters().FirstAsync(u => u.OrganizationId == null && u.Code == "112", ct);
var retailPrice = await _db.PriceTypes.IgnoreQueryFilters().FirstAsync(p => p.OrganizationId == orgId && p.IsRetail, ct);
var currency = await _db.Currencies.FirstAsync(c => c.Code == "KZT", ct);
@ -89,11 +88,17 @@ public async Task<SeedSummary> SeedAsync(Guid orgId, CancellationToken ct = defa
long bcSeq = 487_600_000_001L;
for (int g = 0; g < groups.Count; g++)
{
var (priceMin, priceMax, unit, packaging) = YearDemoSeeder.GroupParams(g);
var grpUnit = unit switch { "kg" => unitKg, "l" => unitL, _ => unitSht };
// Цену берём из диапазона группы; фасовку/единицу — из НАЗВАНИЯ товара,
// чтобы они всегда совпадали (нет «Авокадо 1шт — весовой» и «… 1кг — штучный»).
var (priceMin, priceMax, _, _) = YearDemoSeeder.GroupParams(g);
for (int i = 0; i < 25; i++)
{
var name = YearDemoSeeder.ProductNames[g][i];
// «кг» в названии → весовой товар (кг); всё остальное (штучные
// фасовки Nг/Nмл/Nл/Nшт, бутылки, пачки) → штучный.
var isWeight = name.Contains("кг");
var unitObj = isWeight ? unitKg : unitSht;
var packaging = isWeight ? Packaging.Weight : Packaging.Piece;
var raw = priceMin + (decimal)_rng.NextDouble() * (priceMax - priceMin);
var price = Math.Round(raw / 10m, 0) * 10m; // до 10 тенге
var p = new Product
@ -101,7 +106,7 @@ public async Task<SeedSummary> SeedAsync(Guid orgId, CancellationToken ct = defa
OrganizationId = orgId,
Name = name,
Article = null,
UnitOfMeasureId = grpUnit.Id,
UnitOfMeasureId = unitObj.Id,
Vat = 12m, VatEnabled = true,
ProductGroupId = groups[g].Id,
Packaging = packaging,

View file

@ -29,7 +29,7 @@ public record RetailPointDto(
public record ProductGroupDto(
Guid Id, string Name, Guid? ParentId, string Path, int SortOrder,
decimal? MarkupPercent, Guid? OrganizationId);
decimal? MarkupPercent, Guid? OrganizationId, bool IsSystem = false);
public record CounterpartyDto(
Guid Id, string Name, string? LegalName, CounterpartyType Type,

View file

@ -47,7 +47,7 @@ public partial class MainWindowViewModel : ViewModelBase
partial void OnBadgesCompactChanged(bool value) { OnPropertyChanged(nameof(ShiftBadge)); OnPropertyChanged(nameof(QueueBadge)); }
/// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary>
public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Пётр Кассиров"; ShiftNumber = "С-2026-000012 открыта с 09:00"; IsShiftOpen = true; StoreName = "Демо-магазин"; PointName = "Касса 1"; }
public MainWindowViewModel() { CurrentPage = SaleViewModel.Sample(); CashierName = "Виктория Ким"; ShiftNumber = "С-2026-000012 открыта с 09:00"; IsShiftOpen = true; StoreName = "Магазин у дома"; PointName = "Касса 1"; }
public MainWindowViewModel(AppServices svc) { _svc = svc; if (svc.Session is not null) StartSession(); else ShowLogin(); }

View file

@ -24,7 +24,7 @@
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="24" VerticalAlignment="Center" Margin="24,0,0,0">
<StackPanel x:Name="StoreMeta" Orientation="Horizontal" Spacing="24" VerticalAlignment="Center">
<TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}">
<Run Text="Магазин "/><Run Text="{Binding StoreName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/><Run Text=" · Точка "/><Run Text="{Binding PointName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
<Run Text="{Binding StoreName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/><Run Text=" · Точка "/><Run Text="{Binding PointName}" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/>
</TextBlock>
<Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/>
</StackPanel>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 62 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 64 KiB

After

Width:  |  Height:  |  Size: 56 KiB

View file

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

View file

@ -3,7 +3,7 @@
// Снимает СТЕНД (test.admin.food-market.kz) под витринной организацией
// «Магазин у дома» (создаётся deploy/showcase-seed.sh — чистые данные, без
// e2e/test-мусора). Светлая тема, 1440×900 @2x. Плюс рендерит макет новой
// светлой кассы (scripts/pos-mockup/pos-mockup.html) → продажа + оплата.
// Касса (продажа/оплата) снимается отдельно — настоящий Avalonia-экран.
//
// Предусловие: на стенде поднят API с ShowcaseSeeder и выполнен showcase-seed.
// bash deploy/showcase-seed.sh
@ -18,7 +18,7 @@
// CHROME — путь к chrome (по умолч. /usr/bin/google-chrome)
import { chromium } from 'playwright'
import path from 'node:path'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { fileURLToPath } from 'node:url'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const OUT = path.resolve(__dirname, '..', 'public', 'screenshots')
@ -166,26 +166,12 @@ for (const s of shots) {
}
}
// ── Макет новой светлой кассы (локальный HTML) ──────────────────────────────
const posHtml = pathToFileURL(path.resolve(__dirname, 'pos-mockup', 'pos-mockup.html')).href
const posCtx = await browser.newContext({
viewport: { width: 1920, height: 1080 },
deviceScaleFactor: 1,
colorScheme: 'light',
})
const posPage = await posCtx.newPage()
console.log('pos mockup →', posHtml)
// Экран оплаты (модалка видна).
await posPage.goto(posHtml, { waitUntil: 'networkidle' })
await posPage.waitForTimeout(400)
await posPage.screenshot({ path: path.join(OUT, 'pos-pay.png') })
console.log(' ok → pos-pay.png')
// Экран продажи (прячем модалку через body.nomodal).
await posPage.evaluate(() => document.body.classList.add('nomodal'))
await posPage.waitForTimeout(200)
await posPage.screenshot({ path: path.join(OUT, 'pos-sale.png') })
console.log(' ok → pos-sale.png')
await posCtx.close()
// POS (касса): скриншоты НЕ здесь. Это настоящий экран Avalonia-кассы, он
// рендерится отдельно (headless Skia) и кладётся в docs/pos-screenshots/:
// dotnet run --project src/food-market.pos -c Release -- --screenshots
// На сайт берём 04-sale-items (продажа) и 05-payment-cash (оплата) →
// public/screenshots/pos-sale.png / pos-pay.png → optimize-screenshots.mjs.
// HTML-макет кассы на сайте НЕ используется.
await browser.close()
console.log('done')

View file

@ -109,7 +109,7 @@ export function LowStockWidget({ limit = 10 }: { limit?: number }) {
return (
<WidgetCard
title={t('dashboard.lowStock.title', { defaultValue: 'Low-stock alerts' })}
title={t('dashboard.lowStock.title', { defaultValue: 'Низкий остаток' })}
hint={t('dashboard.lowStock.hint', { defaultValue: 'Остаток ≤ минимума' })}
icon={AlertTriangle}
footer={(
@ -169,7 +169,7 @@ export function RecentSalesWidget({ limit = 10 }: { limit?: number }) {
return (
<WidgetCard
title={t('dashboard.recentSales.title', { defaultValue: 'Последние продажи' })}
hint={t('dashboard.recentSales.hint', { defaultValue: 'Live-обновление' })}
hint={t('dashboard.recentSales.hint', { defaultValue: 'Обновляется в реальном времени' })}
icon={ShoppingCart}
footer={(
<Link to="/sales/retail" className="text-xs text-emerald-700 dark:text-emerald-400 hover:underline">

View file

@ -35,7 +35,10 @@ interface Props {
export function ProductGroupTree({ selectedId, onSelect }: Props) {
const { data: groups, isLoading } = useProductGroups()
const tree = useMemo(() => buildTree(groups ?? []), [groups])
// Системную группу «Все товары» (bootstrap-дефолт для товаров без группы) в
// дереве не показываем — её роль уже играет корневой пункт «Все товары»
// (по модели МойСклада корень = все товары, без отдельной одноимённой группы).
const tree = useMemo(() => buildTree((groups ?? []).filter((g) => !g.isSystem)), [groups])
const [expanded, setExpanded] = useState<Set<string>>(new Set())
const toggle = (id: string) =>

View file

@ -41,7 +41,7 @@ export interface RetailPoint {
id: string; name: string; code: string | null; storeId: string; storeName: string | null;
address: string | null; phone: string | null; fiscalSerial: string | null; fiscalRegNumber: string | null; isActive: boolean
}
export interface ProductGroup { id: string; name: string; parentId: string | null; path: string; sortOrder: number; markupPercent: number | null; organizationId: string | null }
export interface ProductGroup { id: string; name: string; parentId: string | null; path: string; sortOrder: number; markupPercent: number | null; organizationId: string | null; isSystem?: boolean }
export interface Counterparty {
id: string; name: string; legalName: string | null; type: CounterpartyType;
bin: string | null; iin: string | null; taxNumber: string | null; countryId: string | null; countryName: string | null;

View file

@ -38,8 +38,8 @@
"yesterday": "Вчера",
"today": "Сегодня",
"month": "Месяц",
"live": "live",
"offline": "offline"
"live": "онлайн",
"offline": "офлайн"
},
"nav": {
"section_main": "Главное",
@ -98,7 +98,7 @@
"supplierDebts": "Долги поставщикам"
},
"dashboard": {
"title": "Dashboard",
"title": "Сводка",
"welcome": "Добро пожаловать, {{name}}",
"fallbackDescription": "Сводка по продажам и каталогу",
"revenueToday": "Выручка сегодня",

View file

@ -231,7 +231,7 @@ export function ProductsPage() {
fractional
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
: { maximumFractionDigits: 0 })
return `${num} ${pr?.currencyCode ?? org.data?.defaultCurrencySymbol ?? ''}`.trim()
return `${num} ${org.data?.defaultCurrencySymbol ?? pr?.currencyCode ?? ''}`.trim()
}
// Sprint 19: dblclick → inline-edit. priceTypeId=systemPriceType.id;
// если PriceType ещё нет в БД (priceTypes только подгружаются)