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>
|
Before Width: | Height: | Size: 79 KiB After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 151 KiB After Width: | Height: | Size: 150 KiB |
|
Before Width: | Height: | Size: 131 KiB After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 171 KiB After Width: | Height: | Size: 170 KiB |
|
Before Width: | Height: | Size: 124 KiB After Width: | Height: | Size: 124 KiB |
|
Before Width: | Height: | Size: 172 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 139 KiB |
|
Before Width: | Height: | Size: 149 KiB After Width: | Height: | Size: 148 KiB |
|
Before Width: | Height: | Size: 140 KiB After Width: | Height: | Size: 138 KiB |
|
Before Width: | Height: | Size: 148 KiB After Width: | Height: | Size: 147 KiB |
20
docs/ui-review-fixes-progress.md
Normal 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 — старт: план, разбор замечаний.
|
||||||
|
|
@ -45,7 +45,7 @@ public class ProductGroupsController : ControllerBase
|
||||||
};
|
};
|
||||||
var items = await q
|
var items = await q
|
||||||
.Skip(req.Skip).Take(req.Take)
|
.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);
|
.ToListAsync(ct);
|
||||||
return new PagedResult<ProductGroupDto> { Items = items, Total = total, Page = req.Page, PageSize = req.Take };
|
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)
|
public async Task<ActionResult<ProductGroupDto>> Get(Guid id, CancellationToken ct)
|
||||||
{
|
{
|
||||||
var g = await _db.ProductGroups.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, 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")]
|
[HttpPost, RequiresPermission("ProductGroupsManage")]
|
||||||
|
|
|
||||||
|
|
@ -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 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 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 retailPrice = await _db.PriceTypes.IgnoreQueryFilters().FirstAsync(p => p.OrganizationId == orgId && p.IsRetail, ct);
|
||||||
var currency = await _db.Currencies.FirstAsync(c => c.Code == "KZT", 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;
|
long bcSeq = 487_600_000_001L;
|
||||||
for (int g = 0; g < groups.Count; g++)
|
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++)
|
for (int i = 0; i < 25; i++)
|
||||||
{
|
{
|
||||||
var name = YearDemoSeeder.ProductNames[g][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 raw = priceMin + (decimal)_rng.NextDouble() * (priceMax - priceMin);
|
||||||
var price = Math.Round(raw / 10m, 0) * 10m; // до 10 тенге
|
var price = Math.Round(raw / 10m, 0) * 10m; // до 10 тенге
|
||||||
var p = new Product
|
var p = new Product
|
||||||
|
|
@ -101,7 +106,7 @@ public async Task<SeedSummary> SeedAsync(Guid orgId, CancellationToken ct = defa
|
||||||
OrganizationId = orgId,
|
OrganizationId = orgId,
|
||||||
Name = name,
|
Name = name,
|
||||||
Article = null,
|
Article = null,
|
||||||
UnitOfMeasureId = grpUnit.Id,
|
UnitOfMeasureId = unitObj.Id,
|
||||||
Vat = 12m, VatEnabled = true,
|
Vat = 12m, VatEnabled = true,
|
||||||
ProductGroupId = groups[g].Id,
|
ProductGroupId = groups[g].Id,
|
||||||
Packaging = packaging,
|
Packaging = packaging,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ public record RetailPointDto(
|
||||||
|
|
||||||
public record ProductGroupDto(
|
public record ProductGroupDto(
|
||||||
Guid Id, string Name, Guid? ParentId, string Path, int SortOrder,
|
Guid Id, string Name, Guid? ParentId, string Path, int SortOrder,
|
||||||
decimal? MarkupPercent, Guid? OrganizationId);
|
decimal? MarkupPercent, Guid? OrganizationId, bool IsSystem = false);
|
||||||
|
|
||||||
public record CounterpartyDto(
|
public record CounterpartyDto(
|
||||||
Guid Id, string Name, string? LegalName, CounterpartyType Type,
|
Guid Id, string Name, string? LegalName, CounterpartyType Type,
|
||||||
|
|
|
||||||
|
|
@ -47,7 +47,7 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||||
partial void OnBadgesCompactChanged(bool value) { OnPropertyChanged(nameof(ShiftBadge)); OnPropertyChanged(nameof(QueueBadge)); }
|
partial void OnBadgesCompactChanged(bool value) { OnPropertyChanged(nameof(ShiftBadge)); OnPropertyChanged(nameof(QueueBadge)); }
|
||||||
|
|
||||||
/// <summary>Конструктор для дизайнера/скриншотов (демо-данные).</summary>
|
/// <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(); }
|
public MainWindowViewModel(AppServices svc) { _svc = svc; if (svc.Session is not null) StartSession(); else ShowLogin(); }
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="24" VerticalAlignment="Center" Margin="24,0,0,0">
|
<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">
|
<StackPanel x:Name="StoreMeta" Orientation="Horizontal" Spacing="24" VerticalAlignment="Center">
|
||||||
<TextBlock VerticalAlignment="Center" FontSize="15" Foreground="{DynamicResource PosTextMuted}">
|
<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>
|
</TextBlock>
|
||||||
<Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/>
|
<Border Width="1" Height="28" Background="{DynamicResource PosBorder}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
|
||||||
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 42 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 40 KiB |
|
Before Width: | Height: | Size: 64 KiB After Width: | Height: | Size: 56 KiB |
|
|
@ -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>
|
|
||||||
|
|
@ -3,7 +3,7 @@
|
||||||
// Снимает СТЕНД (test.admin.food-market.kz) под витринной организацией
|
// Снимает СТЕНД (test.admin.food-market.kz) под витринной организацией
|
||||||
// «Магазин у дома» (создаётся deploy/showcase-seed.sh — чистые данные, без
|
// «Магазин у дома» (создаётся deploy/showcase-seed.sh — чистые данные, без
|
||||||
// e2e/test-мусора). Светлая тема, 1440×900 @2x. Плюс рендерит макет новой
|
// e2e/test-мусора). Светлая тема, 1440×900 @2x. Плюс рендерит макет новой
|
||||||
// светлой кассы (scripts/pos-mockup/pos-mockup.html) → продажа + оплата.
|
// Касса (продажа/оплата) снимается отдельно — настоящий Avalonia-экран.
|
||||||
//
|
//
|
||||||
// Предусловие: на стенде поднят API с ShowcaseSeeder и выполнен showcase-seed.
|
// Предусловие: на стенде поднят API с ShowcaseSeeder и выполнен showcase-seed.
|
||||||
// bash deploy/showcase-seed.sh
|
// bash deploy/showcase-seed.sh
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
// CHROME — путь к chrome (по умолч. /usr/bin/google-chrome)
|
// CHROME — путь к chrome (по умолч. /usr/bin/google-chrome)
|
||||||
import { chromium } from 'playwright'
|
import { chromium } from 'playwright'
|
||||||
import path from 'node:path'
|
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 __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||||
const OUT = path.resolve(__dirname, '..', 'public', 'screenshots')
|
const OUT = path.resolve(__dirname, '..', 'public', 'screenshots')
|
||||||
|
|
@ -166,26 +166,12 @@ for (const s of shots) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Макет новой светлой кассы (локальный HTML) ──────────────────────────────
|
// POS (касса): скриншоты НЕ здесь. Это настоящий экран Avalonia-кассы, он
|
||||||
const posHtml = pathToFileURL(path.resolve(__dirname, 'pos-mockup', 'pos-mockup.html')).href
|
// рендерится отдельно (headless Skia) и кладётся в docs/pos-screenshots/:
|
||||||
const posCtx = await browser.newContext({
|
// dotnet run --project src/food-market.pos -c Release -- --screenshots
|
||||||
viewport: { width: 1920, height: 1080 },
|
// На сайт берём 04-sale-items (продажа) и 05-payment-cash (оплата) →
|
||||||
deviceScaleFactor: 1,
|
// public/screenshots/pos-sale.png / pos-pay.png → optimize-screenshots.mjs.
|
||||||
colorScheme: 'light',
|
// HTML-макет кассы на сайте НЕ используется.
|
||||||
})
|
|
||||||
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()
|
|
||||||
|
|
||||||
await browser.close()
|
await browser.close()
|
||||||
console.log('done')
|
console.log('done')
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ export function LowStockWidget({ limit = 10 }: { limit?: number }) {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WidgetCard
|
<WidgetCard
|
||||||
title={t('dashboard.lowStock.title', { defaultValue: 'Low-stock alerts' })}
|
title={t('dashboard.lowStock.title', { defaultValue: 'Низкий остаток' })}
|
||||||
hint={t('dashboard.lowStock.hint', { defaultValue: 'Остаток ≤ минимума' })}
|
hint={t('dashboard.lowStock.hint', { defaultValue: 'Остаток ≤ минимума' })}
|
||||||
icon={AlertTriangle}
|
icon={AlertTriangle}
|
||||||
footer={(
|
footer={(
|
||||||
|
|
@ -169,7 +169,7 @@ export function RecentSalesWidget({ limit = 10 }: { limit?: number }) {
|
||||||
return (
|
return (
|
||||||
<WidgetCard
|
<WidgetCard
|
||||||
title={t('dashboard.recentSales.title', { defaultValue: 'Последние продажи' })}
|
title={t('dashboard.recentSales.title', { defaultValue: 'Последние продажи' })}
|
||||||
hint={t('dashboard.recentSales.hint', { defaultValue: 'Live-обновление' })}
|
hint={t('dashboard.recentSales.hint', { defaultValue: 'Обновляется в реальном времени' })}
|
||||||
icon={ShoppingCart}
|
icon={ShoppingCart}
|
||||||
footer={(
|
footer={(
|
||||||
<Link to="/sales/retail" className="text-xs text-emerald-700 dark:text-emerald-400 hover:underline">
|
<Link to="/sales/retail" className="text-xs text-emerald-700 dark:text-emerald-400 hover:underline">
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,10 @@ interface Props {
|
||||||
|
|
||||||
export function ProductGroupTree({ selectedId, onSelect }: Props) {
|
export function ProductGroupTree({ selectedId, onSelect }: Props) {
|
||||||
const { data: groups, isLoading } = useProductGroups()
|
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 [expanded, setExpanded] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
const toggle = (id: string) =>
|
const toggle = (id: string) =>
|
||||||
|
|
|
||||||
|
|
@ -41,7 +41,7 @@ export interface RetailPoint {
|
||||||
id: string; name: string; code: string | null; storeId: string; storeName: string | null;
|
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
|
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 {
|
export interface Counterparty {
|
||||||
id: string; name: string; legalName: string | null; type: CounterpartyType;
|
id: string; name: string; legalName: string | null; type: CounterpartyType;
|
||||||
bin: string | null; iin: string | null; taxNumber: string | null; countryId: string | null; countryName: string | null;
|
bin: string | null; iin: string | null; taxNumber: string | null; countryId: string | null; countryName: string | null;
|
||||||
|
|
|
||||||
|
|
@ -38,8 +38,8 @@
|
||||||
"yesterday": "Вчера",
|
"yesterday": "Вчера",
|
||||||
"today": "Сегодня",
|
"today": "Сегодня",
|
||||||
"month": "Месяц",
|
"month": "Месяц",
|
||||||
"live": "live",
|
"live": "онлайн",
|
||||||
"offline": "offline"
|
"offline": "офлайн"
|
||||||
},
|
},
|
||||||
"nav": {
|
"nav": {
|
||||||
"section_main": "Главное",
|
"section_main": "Главное",
|
||||||
|
|
@ -98,7 +98,7 @@
|
||||||
"supplierDebts": "Долги поставщикам"
|
"supplierDebts": "Долги поставщикам"
|
||||||
},
|
},
|
||||||
"dashboard": {
|
"dashboard": {
|
||||||
"title": "Dashboard",
|
"title": "Сводка",
|
||||||
"welcome": "Добро пожаловать, {{name}}",
|
"welcome": "Добро пожаловать, {{name}}",
|
||||||
"fallbackDescription": "Сводка по продажам и каталогу",
|
"fallbackDescription": "Сводка по продажам и каталогу",
|
||||||
"revenueToday": "Выручка сегодня",
|
"revenueToday": "Выручка сегодня",
|
||||||
|
|
|
||||||
|
|
@ -231,7 +231,7 @@ export function ProductsPage() {
|
||||||
fractional
|
fractional
|
||||||
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
? { minimumFractionDigits: 2, maximumFractionDigits: 2 }
|
||||||
: { maximumFractionDigits: 0 })
|
: { 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;
|
// Sprint 19: dblclick → inline-edit. priceTypeId=systemPriceType.id;
|
||||||
// если PriceType ещё нет в БД (priceTypes только подгружаются)
|
// если PriceType ещё нет в БД (priceTypes только подгружаются)
|
||||||
|
|
|
||||||