feat(pos): быстрое создание товара по неизвестному штрихкоду из кассы (п.4)
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

Скан неизвестного штрихкода (онлайн) → диалог «Новый товар» (наименование + опц.
цена) → POST /products/quick → товар сразу в чеке; попадёт в локальный каталог на
ближайшем синке. Офлайн — как было («не найден»). Smoke/uitest зелёные.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nns 2026-10-09 07:22:14 +05:00
parent cfc335e911
commit 8c4a9c4e5e
3 changed files with 84 additions and 1 deletions

View file

@ -24,6 +24,11 @@ public interface IServerApi
[Get("/api/me")] [Get("/api/me")]
Task<MeDto> MeAsync(CancellationToken ct = default); Task<MeDto> MeAsync(CancellationToken ct = default);
/// <summary>Быстрое создание товара по неизвестному штрихкоду прямо из кассы
/// (имя + опц. цена). Только онлайн.</summary>
[Post("/api/catalog/products/quick")]
Task<QuickCreatedProductDto> QuickCreateProductAsync([Body] QuickCreateProductBody body, CancellationToken ct = default);
[Get("/api/catalog/retail-points")] [Get("/api/catalog/retail-points")]
Task<PagedDto<RetailPointDto>> RetailPointsAsync([Query] int pageSize = 200, CancellationToken ct = default); Task<PagedDto<RetailPointDto>> RetailPointsAsync([Query] int pageSize = 200, CancellationToken ct = default);
@ -46,6 +51,13 @@ public interface IServerApi
Task<ShiftReportDto> ShiftReportAsync(Guid id, CancellationToken ct = default); Task<ShiftReportDto> ShiftReportAsync(Guid id, CancellationToken ct = default);
} }
public record QuickCreateProductBody(string Name, string? Barcode, decimal? SalePrice, bool Weighed = false);
public record QuickBarcodeDto(string code);
public record QuickPriceDto(decimal amount);
public record QuickCreatedProductDto(Guid id, string name, string unitName, int packaging,
System.Collections.Generic.IReadOnlyList<QuickBarcodeDto>? barcodes,
System.Collections.Generic.IReadOnlyList<QuickPriceDto>? prices);
public record MeDto(Guid? orgId, string[] roles, bool advancedMode, string? name, string? email, string sub, public record MeDto(Guid? orgId, string[] roles, bool advancedMode, string? name, string? email, string sub,
Dictionary<string, bool>? permissions = null, Guid[]? retailPointIds = null); Dictionary<string, bool>? permissions = null, Guid[]? retailPointIds = null);
public record PagedDto<T>(IReadOnlyList<T> items, int total); public record PagedDto<T>(IReadOnlyList<T> items, int total);

View file

@ -524,6 +524,13 @@ public partial class SaleViewModel : ViewModelBase
partial void OnItemCountChanged(int value) => OnPropertyChanged(nameof(IsCartEmpty)); partial void OnItemCountChanged(int value) => OnPropertyChanged(nameof(IsCartEmpty));
partial void OnDiscountChanged(decimal value) => OnPropertyChanged(nameof(HasAnyDiscount)); partial void OnDiscountChanged(decimal value) => OnPropertyChanged(nameof(HasAnyDiscount));
// ── Быстрое создание товара по неизвестному штрихкоду (онлайн) ────────────
[ObservableProperty] private bool _isQuickCreateOpen;
[ObservableProperty] private string _quickCreateBarcode = "";
[ObservableProperty] private string _quickCreateName = "";
[ObservableProperty] private decimal? _quickCreatePrice;
[ObservableProperty] private bool _quickCreateBusy;
/// <summary>Поднять модальную оплату (обрабатывает оболочка).</summary> /// <summary>Поднять модальную оплату (обрабатывает оболочка).</summary>
public Action<PaymentViewModel>? PaymentRequested; public Action<PaymentViewModel>? PaymentRequested;
/// <summary>Чек проведён — оболочка обновляет индикатор очереди.</summary> /// <summary>Чек проведён — оболочка обновляет индикатор очереди.</summary>
@ -594,12 +601,58 @@ private void Scan()
if (raw.Length == 0) return; if (raw.Length == 0) return;
var parsed = BarcodeParser.Parse(raw); var parsed = BarcodeParser.Parse(raw);
var p = _svc?.Store.FindByBarcode(parsed.Lookup); var p = _svc?.Store.FindByBarcode(parsed.Lookup);
if (p is null) { Message = $"Штрихкод {parsed.Lookup} не найден"; return; } if (p is null)
{
// Неизвестный штрихкод: онлайн — предлагаем быстро создать товар
// (1 поле + цена), офлайн — просто сообщаем.
if (_svc?.Api is not null)
{
QuickCreateBarcode = parsed.Lookup;
QuickCreateName = "";
QuickCreatePrice = null;
IsQuickCreateOpen = true;
Message = $"Штрихкод {parsed.Lookup} не найден — создайте товар";
}
else Message = $"Штрихкод {parsed.Lookup} не найден";
return;
}
var qty = parsed.IsWeighted && parsed.EmbeddedWeight is { } w ? w : 1m; var qty = parsed.IsWeighted && parsed.EmbeddedWeight is { } w ? w : 1m;
var price = parsed.IsWeighted && parsed.EmbeddedPrice is { } pr ? pr / Math.Max(0.001m, qty) : (decimal?)null; var price = parsed.IsWeighted && parsed.EmbeddedPrice is { } pr ? pr / Math.Max(0.001m, qty) : (decimal?)null;
AddProduct(p, qty, price); AddProduct(p, qty, price);
} }
[RelayCommand] private void QuickCreateCancel() { IsQuickCreateOpen = false; QuickCreateName = ""; QuickCreatePrice = null; }
/// <summary>Создать товар по неизвестному штрихкоду на сервере и сразу добавить в чек.</summary>
[RelayCommand]
private async Task QuickCreateConfirmAsync()
{
if (_svc?.Api is null || QuickCreateBusy) return;
if (string.IsNullOrWhiteSpace(QuickCreateName)) { Message = "Укажите название товара"; return; }
QuickCreateBusy = true;
try
{
var dto = await _svc.Api.QuickCreateProductAsync(
new QuickCreateProductBody(QuickCreateName.Trim(), QuickCreateBarcode, QuickCreatePrice));
var np = new PosProduct
{
Id = dto.id, Name = dto.name, Barcodes = new[] { QuickCreateBarcode },
UnitCode = string.IsNullOrWhiteSpace(dto.unitName) ? "шт" : dto.unitName,
Packaging = (PosPackaging)(dto.packaging == 0 ? 1 : dto.packaging),
VatPercent = 12m, VatEnabled = true,
Price = dto.prices is { Count: > 0 } pr ? pr[0].amount : (QuickCreatePrice ?? 0m),
};
IsQuickCreateOpen = false;
AddProduct(np, 1m);
// Товар появится в локальном каталоге при ближайшем синке.
}
catch (Exception ex)
{
Message = "Не удалось создать товар: " + ex.Message;
}
finally { QuickCreateBusy = false; }
}
[RelayCommand] [RelayCommand]
private void RemoveSelected() private void RemoveSelected()
{ {

View file

@ -202,5 +202,23 @@
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Border>
<!-- Быстрое создание товара по неизвестному штрихкоду (онлайн) -->
<Border Grid.ColumnSpan="2" Background="#99000000" IsVisible="{Binding IsQuickCreateOpen}">
<Border Width="440" Background="{DynamicResource PosCard}" CornerRadius="14" Padding="24"
VerticalAlignment="Center" HorizontalAlignment="Center" BorderBrush="{DynamicResource PosBorder}" BorderThickness="1">
<StackPanel Spacing="12">
<TextBlock Text="Новый товар" FontSize="20" FontWeight="SemiBold" Foreground="{DynamicResource PosText}"/>
<TextBlock Text="{Binding QuickCreateBarcode, StringFormat='Штрихкод: {0}'}" Foreground="{DynamicResource PosTextMuted}"/>
<TextBox Watermark="Наименование" FontSize="16" Text="{Binding QuickCreateName}"/>
<TextBox Watermark="Цена продажи (можно позже)" FontSize="16" Text="{Binding QuickCreatePrice}"/>
<StackPanel Orientation="Horizontal" Spacing="12" HorizontalAlignment="Right">
<Button Content="Отмена" Padding="18,10" Command="{Binding QuickCreateCancelCommand}"/>
<Button Content="Создать и добавить" Padding="18,10" Background="{DynamicResource PosBrand}" Foreground="White"
Command="{Binding QuickCreateConfirmCommand}" IsEnabled="{Binding !QuickCreateBusy}"/>
</StackPanel>
</StackPanel>
</Border>
</Border>
</Grid> </Grid>
</UserControl> </UserControl>