Commit graph

7 commits

Author SHA1 Message Date
nns b073e99ca7 feat(roles): три системные роли — Admin/Cashier/Storekeeper
Менеджер/Закупщик/Бухгалтер сидились как кастомные шаблоны вместе с
организацией, но при этом числились системными в DevDataSeeder
(IsSystem=false), что путало UI (где-то нельзя было менять, где-то
можно было). Юзер хочет: при создании новой орги только три
системные роли (Admin, Storekeeper, Cashier), все остальные роли
администратор создаёт сам.

— SystemRoles.Manager убран. Identity-роли сидируются: SuperAdmin,
  Admin, Cashier, Storekeeper.
— EmployeeRoles tenant-сидер создаёт только три записи (все IsSystem=true,
  все три не редактируются и не удаляются обычным юзером — это правило
  уже работало для Админа/Кассира, теперь покрывает Кладовщика).
— Authorize(Roles = ".. Manager ..") убрано из всех контроллеров (13 файлов):
  Sales/RetailSales, Catalog/{Products,ProductImages,ProductGroups,
  Counterparties,UnitsOfMeasure,RetailPoints,PriceTypes,Stores},
  Purchases/Supplies, Organizations/{Employees,EmployeeRoles,
  OrganizationSettings}.

Существующие организации с уже созданными «Менеджер/Закупщик/
Бухгалтер» записями НЕ затрагиваются — сидер пропускает org если в ней
уже есть роли (anyRole short-circuit). При желании админ может удалить
эти кастомные роли через UI.
2026-05-06 11:31:15 +05:00
nns 4f4a751d26 feat(org-settings): AllowFractionalPrices — переключатель дробных цен
Новая галка в настройках магазина «Разрешить дробные цены (с копейками)»
(default false). Когда выключено — все денежные поля принимают и
сохраняют только целые числа.

- Organization.AllowFractionalPrices + миграция Phase5h.
- OrgSettings DTO/Input + UI настроек (галка с подсказкой).
- MoneyInput получил prop allowFractional: при false запрещает ввод
  точки/запятой и форматирует целым числом, при true — две цифры
  после запятой как раньше.
- ProductEditPage / SupplyEditPage / RetailSaleEditPage передают
  org.allowFractionalPrices во все MoneyInput.
- Списки Products / Supplies / RetailSales форматируют суммы по
  настройке (с .00 или без).
- Сервер защищён от обхода UI: ProductsController / SuppliesController /
  RetailSalesController при сохранении округляют purchasePrice /
  price.amount / unitPrice / discount / paidCash / paidCard до целого
  если флаг выключен.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 12:21:04 +05:00
nns 23e29be21b feat(forms): MoneyInput/NumberInput + select-пагинация + Range на бэкенде
UI:
- Pagination: ввод страницы заменён на select (option 1..totalPages),
  по выбору сразу setPage. Стрелки ← → остаются.
- Field.tsx: добавлены MoneyInput (decimal + суффикс ₸/$/€) и
  NumberInput (decimal без валюты). Оба фильтруют ввод регулярно
  (только цифры + точка/запятая→точка), при focus — выделяют значение.
- ProductEditPage: purchasePrice / vat / minStock / maxStock / amount
  в ценах продаж переведены на новые компоненты; символ валюты —
  из выбранной валюты позиции/закупки или из defaultCurrencySymbol орг.
- SupplyEditPage / RetailSaleEditPage: quantity/unitPrice/discount
  в строках, paidCash/paidCard в шапке — на NumberInput/MoneyInput
  с символом из form.currencyId.
- CountriesPage: vatRate — NumberInput.

API:
- ProductInput / ProductPriceInput / SupplyLineInput /
  RetailSaleLineInput / RetailSaleInput — добавлены [Range(0,1e10)]
  на денежные/количественные поля и [Range(0,100)] на проценты.
  ASP.NET автоматически валидирует и возвращает 400 при выходе.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-25 11:17:32 +05:00
nns 5a06e15924 feat(tables): server-side sort by column header click
Во всех таблицах можно сортировать по клику на заголовок столбца:
первый клик — по возрастанию (↑), второй — по убыванию (↓),
смена колонки сбрасывает предыдущую. Без активной сортировки —
серверный default (обычно по Name ASC).

Реализация:
- PagedRequest: добавлены Sort (ключ колонки) и Order ("asc"/"desc"),
  плюс удобное свойство Desc.
- DataTable: Column.sortKey + props sortKey/sortOrder/onSortChange,
  в заголовке появляется иконка (ArrowUpDown/ArrowUp/ArrowDown).
- useCatalogList: хранит sortKey/sortOrder, отдаёт setSort, шлёт
  ?sort=&order= в query-string.
- Все 10 List-эндпоинтов (Countries, Currencies, UnitsOfMeasure,
  PriceTypes, Stores, RetailPoints, Counterparties, ProductGroups,
  Products, Supplies, RetailSales + Stock/Movements) принимают
  параметры и применяют switch-based OrderBy по whitelisted ключам.
- Все страницы со списками прокидывают sort state и sortKey на
  колонках, где сортировка имеет смысл (тексты/числа/даты).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-24 12:24:05 +05:00
nurdotnet 654a8ba87d feat: strict OtherSystem schema — реплика потерянного f7087e9
Main расходился с БД стейджа (Phase2c3_MsStrict в history, но код ещё ссылался на VatRate etc.) — деплой ломался. Реплицирую удаление сущностей вручную, чтобы код совпадал с таблицами.

Убрано (нет в OtherSystem — не выдумываем):
- Domain: VatRate сущность целиком; Counterparty.Kind + enum CounterpartyKind; Store.Kind + enum StoreKind; Product.IsAlcohol; UnitOfMeasure.Symbol/DecimalPlaces/IsBase.
- EF: DbSet<VatRate>, ConfigureVatRate, Product.VatRate navigation, индекс Counterparty.Kind.
- DTO/Input: соответствующие поля и VatRateDto/Input.
- API: VatRatesController удалён; references в Products/Counterparties/Stores/UoM/Supplies/Retail/Stock.

Добавлено как в OtherSystem:
- Product.Vat (int) + Product.VatEnabled — OtherSystem держит НДС числом на товаре.
- KZ default VAT 16% — applied в сидерах и в OtherSystemImportService когда товар не принёс свой vat.

OtherSystemImportService:
- ResolveKind убран; CompanyType=entrepreneur→Individual (как и было).
- VatRates lookup → прямой p.Vat ?? 16 + p.Vat > 0 для VatEnabled.
- baseUnit ищется по code="796" вместо IsBase.

Web:
- types.ts: убраны CounterpartyKind/StoreKind/VatRate/Product.vatRateId/vatPercent/isAlcohol/UoM.symbol/decimalPlaces/isBase; добавлено Product.vat/vatEnabled; унифицировано unitSymbol→unitName.
- VatRatesPage удалён, роут из App.tsx тоже.
- CounterpartiesPage/StoresPage/UnitsOfMeasurePage: убраны соответствующие поля в формах.
- ProductEditPage: select "Ставка НДС" теперь с фиксированными 0/10/12/16/20 + чекбокс VatEnabled.
- Stock/RetailSale/Supply pages: unitSymbol → unitName.

deploy-stage unguarded — теперь код соответствует DB, авто-deploy безопасен.
2026-04-23 17:32:02 +05:00
nurdotnet 306e38f2e8 feat(dashboard): sales chart + KPIs (как «Показатели» в сторонняя система)
API: GET /api/sales/retail/stats?days=30 — возвращает:
- revenueToday + transactionsToday
- revenueThisMonth + transactionsThisMonth + avgTicketThisMonth
- revenuePrevMonth (для сравнения месяц-к-месяцу)
- series — массив дневных точек {bucket, revenue, transactions} с заполнением
  пустых дней нулями (чтобы линия графика была непрерывной)
- считает только проведённые чеки (Status == Posted)

Web:
- recharts добавлен (3.8.1)
- SalesChart компонент: AreaChart с градиент-заливкой брендового зелёного,
  ось X — дни (DD.MM), ось Y — выручка, tooltip с числами и валютой
- DashboardPage пересобран под продажи как первичную инфу:
  - 4 KPI-карточки сверху: выручка сегодня, выручка за месяц (с дельтой
    к прошлому месяцу), средний чек, прошлый месяц
  - график за 30 дней с empty-state когда чеков нет
  - Каталог теперь второстепенный (мелкие карточки внизу)

Empty-state: если за 30 дней не было ни одной продажи — показываем
"График появится когда появятся первые продажи" вместо плоской линии.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 09:57:35 +05:00
nurdotnet 61558179e3 phase2c: RetailSale document — посты в stock как минусовые движения
Domain (foodmarket.Domain.Sales):
- RetailSale: Number "ПР-{yyyy}-{NNNNNN}", Date, Status (Draft/Posted),
  Store/RetailPoint/Customer/Currency, Subtotal/DiscountTotal/Total,
  Payment (Cash/Card/BankTransfer/Bonus/Mixed) + PaidCash/PaidCard split,
  CashierUserId, Notes, Lines.
- RetailSaleLine: ProductId, Quantity, UnitPrice, Discount, LineTotal,
  VatPercent (snapshot), SortOrder.
- PaymentMethod enum.

EF: retail_sales + retail_sale_lines, unique index (tenant,Number),
indexes by date/status/cashier. Migration Phase2c_RetailSale.

API /api/sales/retail (Authorize):
- GET list with filters status/store/from/to/search.
- GET {id} with lines joined to products + units, customer/retail-point
  names resolved.
- POST create draft (lines optional, totals computed server-side).
- PUT update — replaces lines wholesale; rejected if Posted.
- DELETE — drafts only.
- POST {id}/post — creates -qty StockMovements via IStockService for each
  line (decreasing stock), Type=RetailSale; flips to Posted, stamps PostedAt.
- POST {id}/unpost — reverses with +qty movements tagged "retail-sale-reversal".
- Auto-numbering scoped per tenant + year.

Web:
- types: RetailSaleStatus, PaymentMethod, RetailSaleListRow, RetailSaleLineDto,
  RetailSaleDto.
- /sales/retail list (number, date+time, status badge, store, cashier point,
  customer (or "аноним"), payment method, line count, total).
- /sales/retail/new + /:id edit page mirrors Supply edit page UX:
  sticky top bar (Back / Save / Post / Unpost / Delete), reqs grid with
  date/store/customer/currency/payment/paid-cash/paid-card, lines table
  with inline qty/price/discount + Subtotal/Discount/К оплате footer.
- ProductPicker reused. On line add, picks retail price from product's
  prices list (matches "розн" in priceTypeName) or first.
- Sidebar new group "Продажи" → "Розничные чеки" (ShoppingCart).

Posting cycle ready: Supply (+stock) → ... → RetailSale (-stock).
В Stock и Движения видно текущее состояние и историю.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 16:07:37 +05:00