From 8c4a9c4e5e61a4c5108b40dc106e037e12d97752 Mon Sep 17 00:00:00 2001 From: nns Date: Fri, 9 Oct 2026 07:22:14 +0500 Subject: [PATCH] =?UTF-8?q?feat(pos):=20=D0=B1=D1=8B=D1=81=D1=82=D1=80?= =?UTF-8?q?=D0=BE=D0=B5=20=D1=81=D0=BE=D0=B7=D0=B4=D0=B0=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D1=82=D0=BE=D0=B2=D0=B0=D1=80=D0=B0=20=D0=BF=D0=BE=20=D0=BD?= =?UTF-8?q?=D0=B5=D0=B8=D0=B7=D0=B2=D0=B5=D1=81=D1=82=D0=BD=D0=BE=D0=BC?= =?UTF-8?q?=D1=83=20=D1=88=D1=82=D1=80=D0=B8=D1=85=D0=BA=D0=BE=D0=B4=D1=83?= =?UTF-8?q?=20=D0=B8=D0=B7=20=D0=BA=D0=B0=D1=81=D1=81=D1=8B=20(=D0=BF.4)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Скан неизвестного штрихкода (онлайн) → диалог «Новый товар» (наименование + опц. цена) → POST /products/quick → товар сразу в чеке; попадёт в локальный каталог на ближайшем синке. Офлайн — как было («не найден»). Smoke/uitest зелёные. Co-Authored-By: Claude Opus 4.8 --- src/food-market.pos.core/Sync/IServerApi.cs | 12 +++++ src/food-market.pos/ViewModels/ViewModels.cs | 55 +++++++++++++++++++- src/food-market.pos/Views/SaleView.axaml | 18 +++++++ 3 files changed, 84 insertions(+), 1 deletion(-) diff --git a/src/food-market.pos.core/Sync/IServerApi.cs b/src/food-market.pos.core/Sync/IServerApi.cs index b872f93..fc32afe 100644 --- a/src/food-market.pos.core/Sync/IServerApi.cs +++ b/src/food-market.pos.core/Sync/IServerApi.cs @@ -24,6 +24,11 @@ public interface IServerApi [Get("/api/me")] Task MeAsync(CancellationToken ct = default); + /// Быстрое создание товара по неизвестному штрихкоду прямо из кассы + /// (имя + опц. цена). Только онлайн. + [Post("/api/catalog/products/quick")] + Task QuickCreateProductAsync([Body] QuickCreateProductBody body, CancellationToken ct = default); + [Get("/api/catalog/retail-points")] Task> RetailPointsAsync([Query] int pageSize = 200, CancellationToken ct = default); @@ -46,6 +51,13 @@ public interface IServerApi Task 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? barcodes, + System.Collections.Generic.IReadOnlyList? prices); + public record MeDto(Guid? orgId, string[] roles, bool advancedMode, string? name, string? email, string sub, Dictionary? permissions = null, Guid[]? retailPointIds = null); public record PagedDto(IReadOnlyList items, int total); diff --git a/src/food-market.pos/ViewModels/ViewModels.cs b/src/food-market.pos/ViewModels/ViewModels.cs index 6795fa4..60708cd 100644 --- a/src/food-market.pos/ViewModels/ViewModels.cs +++ b/src/food-market.pos/ViewModels/ViewModels.cs @@ -524,6 +524,13 @@ public partial class SaleViewModel : ViewModelBase partial void OnItemCountChanged(int value) => OnPropertyChanged(nameof(IsCartEmpty)); 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; + /// Поднять модальную оплату (обрабатывает оболочка). public Action? PaymentRequested; /// Чек проведён — оболочка обновляет индикатор очереди. @@ -594,12 +601,58 @@ private void Scan() if (raw.Length == 0) return; var parsed = BarcodeParser.Parse(raw); 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 price = parsed.IsWeighted && parsed.EmbeddedPrice is { } pr ? pr / Math.Max(0.001m, qty) : (decimal?)null; AddProduct(p, qty, price); } + [RelayCommand] private void QuickCreateCancel() { IsQuickCreateOpen = false; QuickCreateName = ""; QuickCreatePrice = null; } + + /// Создать товар по неизвестному штрихкоду на сервере и сразу добавить в чек. + [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] private void RemoveSelected() { diff --git a/src/food-market.pos/Views/SaleView.axaml b/src/food-market.pos/Views/SaleView.axaml index 38da232..61b8834 100644 --- a/src/food-market.pos/Views/SaleView.axaml +++ b/src/food-market.pos/Views/SaleView.axaml @@ -202,5 +202,23 @@ + + + + + + + + + + +