rebrand: касса ALL MARKET (лого, иконка, заголовок, чек)

- Верхняя полоса и экран входа: логотип-изображение ALL MARKET из ресурсов
  (avares Assets/), color на светлой теме и white на тёмной (Brand.ApplyLogo).
- Заголовок окна «ALL MARKET · Касса»; иконка окна и ApplicationIcon = allmarket.ico
  (собрана из фирменных PNG 32/180/192).
- Чек: в шапке — название точки/магазина (резолвится из RetailPoint, PosSession.StoreName),
  внизу мелко «ALL MARKET — система учёта торговли» (был хардкод «Food Market» в шапке).
- AppImage .desktop: Name «ALL MARKET Касса» + фирменная иконка.
- Эталон-макет pos-mockup.html — логотип «all market».
- AssemblyName/exe (foodmarket.Pos) и имена артефактов не менялись (инфраструктура).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-08 17:35:05 +05:00
parent d1176e0fb1
commit d0d6ebcea1
15 changed files with 101 additions and 21 deletions

View file

@ -42,14 +42,18 @@ build_appimage() {
cp -r "$src/." "$appdir/usr/bin/" cp -r "$src/." "$appdir/usr/bin/"
cat > "$appdir/foodmarket-pos.desktop" <<DESK cat > "$appdir/foodmarket-pos.desktop" <<DESK
[Desktop Entry] [Desktop Entry]
Name=Food Market Касса Name=ALL MARKET Касса
Exec=foodmarket.Pos Exec=foodmarket.Pos
Icon=foodmarket-pos Icon=foodmarket-pos
Type=Application Type=Application
Categories=Office; Categories=Office;
DESK DESK
# Минимальная иконка-заглушка (1x1 PNG), если нет фирменной. # Фирменная иконка ALL MARKET (зелёный квадрат 512). Фоллбэк — 1x1 PNG.
if [ -f src/food-market.pos/Assets/allmarket-icon-512.png ]; then
cp src/food-market.pos/Assets/allmarket-icon-512.png "$appdir/foodmarket-pos.png"
else
printf '\x89PNG\r\n\x1a\n' > "$appdir/foodmarket-pos.png" printf '\x89PNG\r\n\x1a\n' > "$appdir/foodmarket-pos.png"
fi
cat > "$appdir/AppRun" <<'RUN' cat > "$appdir/AppRun" <<'RUN'
#!/bin/sh #!/bin/sh
HERE="$(dirname "$(readlink -f "$0")")" HERE="$(dirname "$(readlink -f "$0")")"

View file

@ -18,8 +18,9 @@
/* верхняя полоса */ /* верхняя полоса */
.top{background:var(--card);border-bottom:1px solid var(--line);display:flex;align-items:center;gap:24px;padding:0 24px} .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{font-weight:500;font-size:22px;color:var(--green);letter-spacing:-.5px}
.logo span{color:var(--text);font-weight:600} .logo b{color:#4C4F54;font-weight:300}
.logo span{color:var(--muted);font-weight:600;font-size:17px;margin-left:4px}
.sep{width:1px;height:28px;background:var(--line)} .sep{width:1px;height:28px;background:var(--line)}
.meta{color:var(--muted);font-size:15px} .meta{color:var(--muted);font-size:15px}
.meta b{color:var(--text);font-weight:600} .meta b{color:var(--text);font-weight:600}
@ -125,7 +126,7 @@
<body> <body>
<div class="app"> <div class="app">
<header class="top"> <header class="top">
<div class="logo">FOOD MARKET <span>· Касса</span></div> <div class="logo">all<b> market</b><span>· Касса</span></div>
<div class="sep"></div> <div class="sep"></div>
<div class="meta">Магазин <b>Демо-магазин</b> · Точка <b>Касса 1</b></div> <div class="meta">Магазин <b>Демо-магазин</b> · Точка <b>Касса 1</b></div>
<div class="sep"></div> <div class="sep"></div>

View file

@ -9,6 +9,8 @@ public sealed class PosSession
public required string AccessToken { get; set; } public required string AccessToken { get; set; }
public required string RefreshToken { get; set; } public required string RefreshToken { get; set; }
public string? CashierName { get; set; } public string? CashierName { get; set; }
/// <summary>Название точки/магазина — для шапки чека (резолвится из RetailPoint).</summary>
public string? StoreName { get; set; }
public Guid? CashierUserId { get; set; } public Guid? CashierUserId { get; set; }
public string[]? Roles { get; set; } public string[]? Roles { get; set; }
public Guid? RetailPointId { get; set; } public Guid? RetailPointId { get; set; }

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -96,7 +96,7 @@ public sealed record CommitResult(Guid ClientSaleId, bool PushedNow, int Pending
IReadOnlyList<(string Name, string Unit)> display, decimal refundCash, decimal refundCard) IReadOnlyList<(string Name, string Unit)> display, decimal refundCash, decimal refundCard)
{ {
var doc = new ReceiptDoc { Title = "ВОЗВРАТ" }; var doc = new ReceiptDoc { Title = "ВОЗВРАТ" };
doc.Add("Food Market", center: true, bold: true); doc.Add(_svc.Session?.StoreName ?? "Магазин", center: true, bold: true);
if (CurrentShift?.Number is { } sn) doc.Add($"Смена {sn}", center: true); if (CurrentShift?.Number is { } sn) doc.Add($"Смена {sn}", center: true);
if (_svc.Session?.CashierName is { } c) doc.Add($"Кассир: {c}"); if (_svc.Session?.CashierName is { } c) doc.Add($"Кассир: {c}");
doc.Add(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss")); doc.Add(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
@ -117,6 +117,7 @@ public sealed record CommitResult(Guid ClientSaleId, bool PushedNow, int Pending
if (refundCash > 0m) doc.Add("Наличными", $"{refundCash:N0}"); if (refundCash > 0m) doc.Add("Наличными", $"{refundCash:N0}");
if (refundCard > 0m) doc.Add("На карту", $"{refundCard:N0}"); if (refundCard > 0m) doc.Add("На карту", $"{refundCard:N0}");
doc.Separator(); doc.Separator();
doc.Add("ALL MARKET — система учёта торговли", center: true);
return doc; return doc;
} }
@ -124,7 +125,7 @@ private ReceiptDoc BuildReceipt(Cart cart, decimal paidCash, decimal paidCard, d
{ {
var t = cart.Totals(); var t = cart.Totals();
var doc = new ReceiptDoc { Title = isReturn ? "ВОЗВРАТ" : "ТОВАРНЫЙ ЧЕК" }; var doc = new ReceiptDoc { Title = isReturn ? "ВОЗВРАТ" : "ТОВАРНЫЙ ЧЕК" };
doc.Add("Food Market", center: true, bold: true); doc.Add(_svc.Session?.StoreName ?? "Магазин", center: true, bold: true);
if (CurrentShift?.Number is { } sn) doc.Add($"Смена {sn}", center: true); if (CurrentShift?.Number is { } sn) doc.Add($"Смена {sn}", center: true);
if (_svc.Session?.CashierName is { } c) doc.Add($"Кассир: {c}"); if (_svc.Session?.CashierName is { } c) doc.Add($"Кассир: {c}");
doc.Add(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss")); doc.Add(DateTime.Now.ToString("dd.MM.yyyy HH:mm:ss"));
@ -145,6 +146,7 @@ private ReceiptDoc BuildReceipt(Cart cart, decimal paidCash, decimal paidCard, d
if (t.Vat > 0m) doc.Add("в т.ч. НДС", $"{t.Vat:N0}"); if (t.Vat > 0m) doc.Add("в т.ч. НДС", $"{t.Vat:N0}");
doc.Separator(); doc.Separator();
doc.Add("Спасибо за покупку!", center: true); doc.Add("Спасибо за покупку!", center: true);
doc.Add("ALL MARKET — система учёта торговли", center: true);
return doc; return doc;
} }
} }

View file

@ -61,6 +61,7 @@ private void StartSession()
{ {
CashierName = _svc!.Session?.CashierName ?? "кассир"; CashierName = _svc!.Session?.CashierName ?? "кассир";
QueueCount = _svc.Store.PendingCount(); QueueCount = _svc.Store.PendingCount();
_ = ResolvePointNameAsync();
_sale = new SaleViewModel(_svc) _sale = new SaleViewModel(_svc)
{ {
PaymentRequested = OpenPayment, SaleCommitted = _ => RefreshStatus(), PaymentRequested = OpenPayment, SaleCommitted = _ => RefreshStatus(),
@ -72,6 +73,25 @@ private void StartSession()
_ = CheckForUpdateAsync(); _ = CheckForUpdateAsync();
} }
/// <summary>Резолвит название точки продаж (для шапки чека и верхней полосы).</summary>
private async Task ResolvePointNameAsync()
{
var pid = _svc?.Session?.RetailPointId;
if (_svc?.Api is null || pid is null) return;
try
{
var points = await _svc.Api.RetailPointsAsync();
var name = points.items.FirstOrDefault(p => p.id == pid.Value)?.name;
if (!string.IsNullOrWhiteSpace(name))
{
PointName = name;
StoreName = name;
if (_svc.Session is not null) _svc.Session.StoreName = name;
}
}
catch { /* офлайн — оставляем значения по умолчанию */ }
}
private async Task CheckForUpdateAsync() private async Task CheckForUpdateAsync()
{ {
if (_svc?.Api is null) return; if (_svc?.Api is null) return;

View file

@ -0,0 +1,42 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
using Avalonia.Styling;
namespace foodmarket.Pos.Views;
/// <summary>Логотип и иконка ALL MARKET из ресурсов (avares). Логотип подбирается
/// под тему: color на светлой, white на тёмной. Файлы готовые — не перерисовывать.</summary>
public static class Brand
{
private const string Pkg = "avares://foodmarket.Pos/Assets/";
private static bool IsDark =>
Application.Current?.ActualThemeVariant == ThemeVariant.Dark;
/// <summary>Загрузить логотип (под текущую тему) в переданный Image.</summary>
public static void ApplyLogo(Image? target)
{
if (target is null) return;
var file = IsDark ? "allmarket-logo-white.png" : "allmarket-logo-color.png";
try
{
using var s = AssetLoader.Open(new Uri(Pkg + file));
target.Source = new Bitmap(s);
}
catch { /* ресурс недоступен — оставить пустым */ }
}
/// <summary>Поставить иконку окна/приложения.</summary>
public static void ApplyWindowIcon(Window window)
{
try
{
using var s = AssetLoader.Open(new Uri(Pkg + "allmarket.ico"));
window.Icon = new WindowIcon(s);
}
catch { /* ресурс недоступен */ }
}
}

View file

@ -6,12 +6,9 @@
<Grid Background="{DynamicResource PosBg}"> <Grid Background="{DynamicResource PosBg}">
<Border Classes="card" Width="440" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center" CornerRadius="18"> <Border Classes="card" Width="440" Padding="28" VerticalAlignment="Center" HorizontalAlignment="Center" CornerRadius="18">
<StackPanel Spacing="16"> <StackPanel Spacing="16">
<StackPanel Spacing="4" HorizontalAlignment="Center"> <StackPanel Spacing="10" HorizontalAlignment="Center">
<Border Background="{DynamicResource PosBrand}" CornerRadius="12" Width="48" Height="48" HorizontalAlignment="Center"> <Image x:Name="BrandLogo" Height="48" HorizontalAlignment="Center" Margin="0,4,0,0"/>
<TextBlock Text="FM" Foreground="White" FontWeight="Bold" FontSize="20" HorizontalAlignment="Center" VerticalAlignment="Center"/> <TextBlock Text="Касса · Вход кассира" Foreground="{DynamicResource PosTextMuted}" FontSize="14" HorizontalAlignment="Center"/>
</Border>
<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> </StackPanel>
<!-- PIN --> <!-- PIN -->

View file

@ -4,5 +4,9 @@ namespace foodmarket.Pos.Views;
public partial class LoginView : UserControl public partial class LoginView : UserControl
{ {
public LoginView() => InitializeComponent(); public LoginView()
{
InitializeComponent();
Brand.ApplyLogo(this.FindControl<Image>("BrandLogo"));
}
} }

View file

@ -4,7 +4,7 @@
x:Class="foodmarket.Pos.Views.MainWindow" x:Class="foodmarket.Pos.Views.MainWindow"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
Width="1366" Height="768" MinWidth="1100" MinHeight="680" Width="1366" Height="768" MinWidth="1100" MinHeight="680"
Background="{DynamicResource PosBg}" Title="Касса food-market" Background="{DynamicResource PosBg}" Title="ALL MARKET · Касса"
WindowStartupLocation="CenterScreen"> WindowStartupLocation="CenterScreen">
<Panel> <Panel>
<Grid RowDefinitions="64,*,40"> <Grid RowDefinitions="64,*,40">
@ -12,10 +12,11 @@
<!-- Верхняя полоса --> <!-- Верхняя полоса -->
<Border Grid.Row="0" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1" Padding="24,0"> <Border Grid.Row="0" Background="{DynamicResource PosCard}" BorderBrush="{DynamicResource PosBorder}" BorderThickness="0,0,0,1" Padding="24,0">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" VerticalAlignment="Center"> <Grid ColumnDefinitions="Auto,Auto,*,Auto" VerticalAlignment="Center">
<!-- Лого (всегда) --> <!-- Лого ALL MARKET (готовый файл; color на светлой теме, white на тёмной) -->
<TextBlock Grid.Column="0" VerticalAlignment="Center" FontSize="20"> <StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="12" VerticalAlignment="Center">
<Run Text="FOOD MARKET" Foreground="{DynamicResource PosBrand}" FontWeight="ExtraBold"/><Run Text=" · Касса" Foreground="{DynamicResource PosText}" FontWeight="SemiBold"/> <Image x:Name="BrandLogo" Height="38" VerticalAlignment="Center"/>
</TextBlock> <TextBlock Text="· Касса" FontSize="18" Foreground="{DynamicResource PosTextMuted}" FontWeight="SemiBold" VerticalAlignment="Center"/>
</StackPanel>
<!-- Мета (сворачивается по ширине) --> <!-- Мета (сворачивается по ширине) -->
<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">

View file

@ -11,6 +11,8 @@ public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
AddHandler(KeyDownEvent, OnKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel); AddHandler(KeyDownEvent, OnKeyDown, Avalonia.Interactivity.RoutingStrategies.Tunnel);
Brand.ApplyWindowIcon(this);
Brand.ApplyLogo(this.FindControl<Image>("BrandLogo"));
// Адаптив шапки по ширине: при нехватке места сокращаем тексты чипов и // Адаптив шапки по ширине: при нехватке места сокращаем тексты чипов и
// прячем «Магазин …», но чипы статуса никогда не обрезаем. // прячем «Магазин …», но чипы статуса никогда не обрезаем.
this.GetObservable(BoundsProperty).Subscribe(new BoundsObserver(_ => ApplyTopBarDensity())); this.GetObservable(BoundsProperty).Subscribe(new BoundsObserver(_ => ApplyTopBarDensity()));

View file

@ -7,9 +7,14 @@
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault> <AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<ApplicationIcon></ApplicationIcon> <ApplicationIcon>Assets\allmarket.ico</ApplicationIcon>
</PropertyGroup> </PropertyGroup>
<ItemGroup>
<!-- Логотипы/иконка ALL MARKET для окна и экранов (avares://foodmarket.Pos/Assets/...) -->
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\food-market.pos.core\food-market.pos.core.csproj" /> <ProjectReference Include="..\food-market.pos.core\food-market.pos.core.csproj" />
<ProjectReference Include="..\food-market.shared\food-market.shared.csproj" /> <ProjectReference Include="..\food-market.shared\food-market.shared.csproj" />