refactor(retail-points): rename «Точка продаж» → «Касса» + перенос

складов и касс в раздел «Настройки организации»; useOrgInfra хук

UI-переименование:
- RetailPointsPage: title «Кассы», description обновлён, лейблы
  «Новая касса» / «Удалить кассу?»; доменная сущность RetailPoint
  и URL /api/catalog/retail-points сохранены — DTO/БД не трогаем.
- В сайдбаре пункты «Склады» и «Кассы» перенесены из бывшей
  группы «Склады» в группу «Настройки организации» (рядом с
  «Общие»). Старые пункты верхнего уровня убраны.

useOrgInfra() — общий хук:
- возвращает stores, cashRegisters, defaultStoreId, defaultCashId
- showStorePicker / showCashPicker = length > 1 (умное скрытие
  селекторов в формах документов когда инфра одна).
В SupplyEditPage скрытие склада уже работало через
(stores.data?.length ?? 0) > 1 — оставил как есть, новый хук
для будущих документов (продажи, инвентаризации).

Сидер default Store + RetailPoint per Organization уже есть в
DevDataSeeder.cs (Основной склад MAIN + Касса 1 POS-1) —
дополнять не нужно.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
nns 2026-04-26 11:53:49 +05:00
parent dc4b5360b9
commit cec76ecaaf
3 changed files with 42 additions and 10 deletions

View file

@ -37,10 +37,6 @@ function buildNav(): NavSection[] {
{ group: 'Контрагенты', items: [
{ to: '/catalog/counterparties', icon: Users, label: 'Контрагенты' },
]},
{ group: 'Склады', items: [
{ to: '/catalog/stores', icon: Warehouse, label: 'Склады' },
{ to: '/catalog/retail-points', icon: StoreIcon, label: 'Точки продаж' },
]},
{ group: 'Остатки', items: [
{ to: '/inventory/stock', icon: Boxes, label: 'Остатки' },
{ to: '/inventory/movements', icon: History, label: 'Движения' },
@ -57,8 +53,10 @@ function buildNav(): NavSection[] {
{ group: 'Импорт', items: [
{ to: '/admin/import/moysklad', icon: Download, label: 'МойСклад' },
]},
{ group: 'Настройки', items: [
{ to: '/settings/organization', icon: Settings, label: 'Организация' },
{ group: 'Настройки организации', items: [
{ to: '/settings/organization', icon: Settings, label: 'Общие' },
{ to: '/catalog/stores', icon: Warehouse, label: 'Склады' },
{ to: '/catalog/retail-points', icon: StoreIcon, label: 'Кассы' },
]},
]
}

View file

@ -0,0 +1,34 @@
import { useStores } from '@/lib/useLookups'
import { useQuery } from '@tanstack/react-query'
import { api } from '@/lib/api'
import type { PagedResult, RetailPoint } from '@/lib/types'
/** Инфраструктура организации (склады, кассы) для умного скрытия селекторов
* в формах документов: если у организации только 1 склад/касса выбор не
* показывается, форма автоматически подставляет единственное значение.
* Появится 2-й пикер вернётся. */
export function useOrgInfra() {
const stores = useStores()
const cashRegisters = useQuery({
queryKey: ['lookup:retail-points'],
queryFn: async () =>
(await api.get<PagedResult<RetailPoint>>('/api/catalog/retail-points?pageSize=500')).data.items,
staleTime: 0,
refetchOnMount: 'always',
refetchOnWindowFocus: true,
})
const storesList = stores.data ?? []
const cashList = cashRegisters.data ?? []
const mainStore = storesList.find((s) => s.isMain) ?? storesList[0]
return {
stores: storesList,
cashRegisters: cashList,
isLoading: stores.isLoading || cashRegisters.isLoading,
showStorePicker: storesList.length > 1,
showCashPicker: cashList.length > 1,
defaultStoreId: mainStore?.id ?? null,
defaultCashId: cashList[0]?.id ?? null,
}
}

View file

@ -55,8 +55,8 @@ export function RetailPointsPage() {
return (
<>
<ListPageShell
title="Точки продаж"
description="Кассовые точки. Привязаны к складу, с которого идут продажи."
title="Кассы"
description="Кассовые точки продаж. Каждая привязана к складу, с которого списывается товар."
actions={
<>
<SearchBar value={search} onChange={setSearch} />
@ -96,12 +96,12 @@ export function RetailPointsPage() {
<Modal
open={!!form}
onClose={() => setForm(null)}
title={form?.id ? 'Редактировать точку продаж' : 'Новая точка продаж'}
title={form?.id ? 'Редактировать кассу' : 'Новая касса'}
footer={
<>
{form?.id && (
<Button variant="danger" size="sm" onClick={async () => {
if (confirm('Удалить точку продаж?')) {
if (confirm('Удалить кассу?')) {
await remove.mutateAsync(form.id!)
setForm(null)
}