Commit graph

15 commits

Author SHA1 Message Date
nns 195dfb1a2c feat(pos): весовой товар на кассе — вес с весов/вручную + автопоиск весов
Часть 3 + автопоиск (данные sysadmin: касса МойСклад спамит «Порт не выбран»):
- Весовой товар при сканировании/плитке → вес с весов (если на связи), иначе
  модалка ручного ввода веса. Сумма = цена/кг × вес; подпись «1,235 кг × 690 ₸/кг».
- ScaleMonitor (pos.core): опрос ТОЛЬКО по требованию (при весовом товаре), мягкая
  деградация на ручной ввод, запись о потере/восстановлении связи РОВНО ОДИН раз
  (не сотни «Таймаут»). Юнит-тесты (100 опросов → 1 запись).
- ScaleDiscovery (pos): автоскан всех COM-портов (вкл. USB-COM CH340/Prolific) ×
  протоколы Масса-К/CAS × скорости; кнопка «Найти весы» в верхней панели; найденный
  порт запоминается (kv) и восстанавливается при старте.
- UnitShort («шт»/«кг»/«л») протянут через sync→SQLite→PosProduct/CartLine; цена
  весового показывается «690 ₸/кг» вместо кода ОКЕИ «690 ₸/796».
- Разбор этикеток в кассе — по настройкам орг (по умолчанию ВЫКЛ, точный штрихкод).
- Юнит-тесты 321/321 (обновлены PosBarcodeTests под дефолт-off).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-09 11:17:35 +05:00
nns 4181bfe8ad feat(pos): Z-отчёт по товарам на печать + вопрос «размен?» при автооткрытии (п.1,2)
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
П.2 (POS): закрытие смены печатает Z-отчёт с разбивкой по товарам (продано/возврат)
через ESC/POS; кнопка «Печать Z-отчёта (по товарам)». ShiftReportDto += byProduct/overLimit24h.
П.1 (POS): первая продажа без открытой смены (онлайн) — диалог «Открыть смену. Размен?
(можно пропустить)» → автооткрытие и переход к оплате; офлайн — как было.
П.10: full-chain обновлён — продажа без смены теперь автооткрытие (не 409) + проверка
byProduct в Z-отчёте; прогон на стенде 16 ✓ / 0 ✗.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 07:38:07 +05:00
nns 8c4a9c4e5e 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>
2026-10-09 07:22:14 +05:00
nns 63da7a9334 perf(pos): мгновенный старт — gzip-каталог, дельта остатков, синк вне UI-потока
Касса работает из локальной базы сразу; вся синхронизация — в фоне, UI не ждёт.
- HttpClient с AutomaticDecompression: сервер уже жмёт JSON (gzip), но касса не
  слала Accept-Encoding → каталог 30k ехал 22 МБ; теперь 3 МБ (×7.3).
- ApplySync: убран O(n²) скан цен (HashSet product-id) — запись 30k 12.2с → 1.8с.
- Остатки дельтой: PosSyncResponse.StocksFullSnapshot — первичная загрузка = полный
  снимок, далее только изменённые (ушедшие в 0 удаляются). Рекуррентный тик:
  остатков 30000→0, 806мс→189мс.
- Синк вне UI-потока (Task.Run) + маршалинг обновлений через Dispatcher: тяжёлая
  запись каталога/дельты никогда не фризит окно. Флаг анти-перекрытия тиков.
- Индикатор в шапке «обновлено N мин назад / обновляется…»; ненавязчивая полоса
  первичной загрузки (только когда каталог пуст), не модалка.
- Сервер: since нормализуется к UTC (был 500 от Npgsql на Kind=Unspecified).
- Тест POS-дельты остатков (upsert/zero-remove); oversell-тест кассы приведён к
  решению владельца (продажа в минус разрешена → Accepted + предупреждение).

Замеры (стенд, 30 000 товаров): первичная 14.9с→5.1с (сеть 3.2с + запись 1.8с),
тёплый старт 141мс, дельта без изменений 189мс, дельта 500 товаров ~0.35с.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 04:59:01 +05:00
nns 0fd04d4f5c feat(pos): предупреждения о минусе на кассе — в строке + индикатор в шапке (офлайн)
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
Сервер уже слал остатки в sync (StockSyncDto), но касса их отбрасывала. Теперь:
- PosStore хранит снимок остатков (таблица stock, перезапись каждый синк — сервер
  шлёт полный снимок ненулевых); GetStock/NegativeCount/NegativeStocks.
- В строке чека при количестве больше остатка — «на остатке N шт — уйдёт в минус»
  (жёлтое, НЕ блокирует продажу — решение владельца). Работает офлайн по снимку.
- В шапке — постоянный жёлтый индикатор «N в минусе — нужна инвентаризация» по
  складу кассы, по тапу — список товаров (Flyout). Обновляется при старте, после
  чека и после синка.
- Тесты PosStore: снимок/перезапись, GetStock, NegativeCount/список (4/4).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-09 03:38:50 +05:00
nns d0d6ebcea1 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>
2026-10-08 17:35:05 +05:00
nns 6ae79e37fb feat(pos-ui): вёрстка кассы 1:1 по эталон-макету
Перенёс макет Mac-Claude (docs/pos-design/pos-mockup.html) в Avalonia по
размерам, а не на глаз:

- Оплата: модалка 880px поверх затемнённого экрана продажи (как в макете);
  сегменты Нал/Карта/Смешанная с векторными иконками (активный зелёный —
  иконка белая через стиль Button.seg.on Path); быстрые суммы 3×2
  (без сдачи/4000/5000/10000/20000/своя сумма); клавиатура 3×4 по 72px.
- Продажа: исправлена подсветка выбранной строки — локальные
  Background/IsVisible перекрывали сеттеры стиля :selected; теперь зелёный
  акцент слева + светло-зелёный фон работают. Бейдж «Отложенные · N».
  Чип связи «На связи · очередь пуста».
- Демо: --demo-pay открывает модалку оплаты для скриншота на Windows;
  PayScrim в скриншотере рендерит оплату поверх реальной оболочки продажи.
- Инструмент сверки: CompareRunner (--compare, SkiaSharp) складывает
  «макет | касса» рядом → docs/pos-design/compare-*.png.

Скорость не ухудшена (старт 74мс, скан 13.7мс). --uitest OK, 82 unit зелёные.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-08 14:35:18 +05:00
nns ac55da8d06 feat(pos): избранные товары (плитки) + переключатель тёмной темы
- Избранное: Product.IsFavorite/FavoriteOrder (миграция Phase37), в POS-синке
  (ProductSyncDto) и в карточке товара (ProductInput). Касса: PosStore хранит
  флаг (+ALTER для старых БД) и GetFavorites(); на экране продажи — сворачиваемая
  сетка плиток «быстрых товаров» (товары без штрихкода: хлеб, овощи на вес),
  тап добавляет в чек. Unit-тест GetFavorites (порядок/фильтр). 44 POS unit зелёные.
- Тёмная тема — опция: кнопка-переключатель в шапке (ToggleTheme), выбор
  сохраняется на устройстве (kv device.theme), применяется при старте. На тех же
  токенах (ThemeDictionaries Light/Dark). По умолчанию — светлая. Кадр «тёмная тема» в галерее.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 13:35:56 +05:00
nns 6b959ac59d feat(pos): быстрый вход кассира по PIN (grant_type=pin) + lockout
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
- Сервер: Employee.PinHash (PBKDF2, миграция Phase36); POST
  /api/organization/employees/{id}/pin — админ задаёт/снимает PIN (4–6 цифр,
  уникален среди активных сотрудников на пересекающихся точках). Кастомный
  OpenIddict-грант grant_type=pin (retail_point_id + pin): находит кассира,
  привязанного к точке, по PIN и выдаёт токены. Брутфорс-защита — in-memory по
  точке (5 неверных → блок 5 мин). Клиент-сидер досоздаёт gt:pin существующим
  клиентам (без пересоздания). Пароль остаётся запасным.
- Касса: PIN-пад на зарегистрированном устройстве (маскированный ввод, 0–9/⌫/
  Войти), переключение на вход по логину; AuthService.PinLoginAsync.
- Тесты: unit PinHasher (9), integration (3): вход по PIN выдаёт рабочий токен;
  5 неверных → блок; дубль PIN на точке → 409. + скриншот PIN-пада в галерее.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:38:36 +05:00
nns ff69dec527 feat(pos): регистрация устройства на точке продаж + привязка кассира
- Сервер: /api/me отдаёт retailPointIds (точки, к которым привязан сотрудник).
- Касса: DeviceRegistration (чистая логика, 5 unit-тестов) — первый запуск:
  админ выбирает точку из списка активных (сохраняется на устройстве, kv);
  кассир на незарегистрированном устройстве → «обратитесь к администратору»;
  кассир, не привязанный к точке устройства → «вы не привязаны к этой кассе»;
  админ работает на любой точке. Смена точки — кнопка-сброс (новую выберет
  только админ). Экран входа: шаг выбора точки (ComboBox активных точек).
- Заменяет прежний «берётся первая активная» (ошибка при двух точках).
- Тесты: unit (5 решений гейта) + integration (/api/me retailPointIds кассира).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:24:51 +05:00
nns 01b47cc0c8 feat(pos): возврат по чеку — экран выбора позиций + защита от over-return
- Сервер: GET /api/pos/v1/sales/by-number/{number} — чек-основание с позициями
  и доступным к возврату (продано − уже возвращено, по QtyReturned). POS-путь
  возврата теперь валидирует over-return (нельзя вернуть больше остатка) и
  инкрементит QtyReturned на исходных строках (как RetailSalesController).
- Контракт: PosReturnLookupDto/PosReturnLineDto; IServerApi.ReturnLookupAsync.
- Касса: экран возврата — найти чек по номеру/скану QR, степпер количества по
  каждой позиции (0..доступно, кнопка «Всё»), сумма к выдаче, способ (нал/карта
  как в исходном чеке); CheckoutService.CommitReturnAsync пишет возврат в офлайн-
  очередь + печать + выгрузка.
- Тесты: integration — lookup by-number, возврат 2 из 3, over-return в Failed,
  довозврат 1, QtyReturned/остатки корректны; 404 на неизвестный номер. 11/11 PosSyncTests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 09:16:48 +05:00
nns 5173eaa07c feat(catalog): штрихкоды товара — добавление + весовые этикетки
- POST /api/catalog/products/{id}/barcodes {code, kind}: добавить штрихкод к
  товару, уникальность в организации, 409 «штрихкод уже у товара X» (+название),
  идемпотентно при повторе тому же товару, автоопределение типа по длине.
- Весовые этикетки (внутримагазинные EAN-13, как в МойСкладе): общий парсер
  food-market.shared/Barcodes/WeightedBarcode (настраиваемые префиксы веса/цены,
  длина PLU); касса pos.core делегирует туда. Настройки организации
  (WeightBarcode*: enabled/префиксы/длина кода) + миграция Phase35.
- GET /api/catalog/products/by-barcode/{value} разбирает весовую этикетку →
  товар + количество (вес) / встроенная цена; обычный ШК — как прежде.
- GET/PUT /api/organization/settings отдаёт/принимает настройки (блок весовых
  применяется только если передан — round-trip-safe для текущей веб-формы).
- Офлайн-каталог ProductSyncDto: явный isWeighed (packaging==2) для мобильного/кассы.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 05:39:20 +05:00
nns b305cc9b9a feat(pos): отложенный чек (park/resume) + снимок корзины
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
- Cart.Snapshot()/Restore() — независимая копия строк и скидки на чек.
- SaleViewModel: ParkCommand («Отложить» — сохранить чек, начать новый) и
  ResumeCommand («Вернуть» — восстановить отложенный); кнопки на экране продажи.
- Unit-тест независимости снимка (38 POS unit-тестов зелёные). Скриншоты обновлены.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 05:03:18 +05:00
nns 2b63bc9c66 feat(pos-core): логика кассы без UI + 37 unit-тестов
- Модели (товар/корзина/строка/смена), парсер весовых ШК (EAN-13 20–29),
  корзина (штучный инкремент, весовой, скидки строка/чек с клампом), математика
  оплаты и смены (должно/факт/разница).
- PosStore на raw Microsoft.Data.Sqlite (быстрый старт, AOT-дружелюбно): каталог
  + штрихкоды + очередь продаж + kv; индексы по штрихкоду/NameLower (кириллица
  не сворачивается через LIKE) и barcodes.ProductId (дельта-синк O(n), не O(n²)).
- SyncService: дельта-pull каталога + push очереди с детерминированным
  idempotency-ключом (MD5 отсортированных ClientSaleId → один набор = один ключ).
- Auth: password/refresh grant, ITokenStore (FileTokenStore 0600 на Unix,
  InMemory для тестов). Checkout.BuildSale: сборка серверного чека, скидка на чек
  распределяется по строкам (сервер понимает только построчную). UpdateCheck.
- Тесты: штрихкод/корзина/оплата/смена/стор/синк-ключ/checkout/офлайн(5 без
  дублей)/версия — 37 зелёных.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-08 04:58:22 +05:00
nns fd2f5ae4f3 Phase 0: project scaffolding and end-to-end auth
- .NET 8 LTS solution with 7 projects (domain/application/infrastructure/api/shared/pos.core/pos[WPF])
- Central package management (Directory.Packages.props), .editorconfig, global.json pin to 8.0.417
- PostgreSQL 14 dev DB via existing brew service; food_market database created
- ASP.NET Identity + OpenIddict 5 (password + refresh token flows) with ephemeral dev keys
- EF Core 8 + Npgsql; multi-tenant query filter via reflection over ITenantEntity
- Initial migration: 13 tables (Identity + OpenIddict + organizations)
- AuthorizationController implements /connect/token; seeders create demo org + admin
- Protected /api/me endpoint returns current user + org claims
- React 19 + Vite 8 + Tailwind v4 SPA with TanStack Query, React Router 7
- Login flow with dev-admin placeholder, bearer interceptor + refresh token fallback
- docs/architecture.md, CLAUDE.md, README.md

Verified end-to-end: health check, password grant issues JWT with org_id,
web app builds successfully (310 kB gzipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-21 13:59:13 +05:00