diff --git a/src/food-market.web/src/components/AppLayout.tsx b/src/food-market.web/src/components/AppLayout.tsx index a7958a5..8ee5b71 100644 --- a/src/food-market.web/src/components/AppLayout.tsx +++ b/src/food-market.web/src/components/AppLayout.tsx @@ -11,6 +11,7 @@ import { } from 'lucide-react' import { Logo } from './Logo' import { SuperAdminAsOrgBanner } from './SuperAdminAsOrgBanner' +import { ShortcutsOverlay } from './ShortcutsOverlay' interface MeResponse { sub: string @@ -269,6 +270,9 @@ export function AppLayout() { + {/* Глобальный «?»-оверлей со списком горячих клавиш. Сам обрабатывает + 'Esc' и '?', не блокирует ввод в инпутах. */} + ) } diff --git a/src/food-market.web/src/components/SearchBar.tsx b/src/food-market.web/src/components/SearchBar.tsx index d3df95a..0e7e225 100644 --- a/src/food-market.web/src/components/SearchBar.tsx +++ b/src/food-market.web/src/components/SearchBar.tsx @@ -1,3 +1,4 @@ +import { forwardRef } from 'react' import { Search } from 'lucide-react' interface SearchBarProps { @@ -6,11 +7,19 @@ interface SearchBarProps { placeholder?: string } -export function SearchBar({ value, onChange, placeholder = 'Поиск…' }: SearchBarProps) { +/** + * Поиск с иконкой. forwardRef нужен, чтобы list-страница могла фокуснуть + * input по хоткею `/` (см. useShortcuts на ProductsPage и др.). + */ +export const SearchBar = forwardRef(function SearchBar( + { value, onChange, placeholder = 'Поиск…' }, + ref, +) { return (
onChange(e.target.value)} @@ -19,4 +28,4 @@ export function SearchBar({ value, onChange, placeholder = 'Поиск…' }: Se />
) -} +}) diff --git a/src/food-market.web/src/components/ShortcutsOverlay.tsx b/src/food-market.web/src/components/ShortcutsOverlay.tsx new file mode 100644 index 0000000..21a77b7 --- /dev/null +++ b/src/food-market.web/src/components/ShortcutsOverlay.tsx @@ -0,0 +1,107 @@ +import { useEffect, useState } from 'react' +import { Keyboard, X } from 'lucide-react' +import { useShortcuts } from '@/lib/useShortcuts' + +/** + * Глобальный «?» overlay — открывается клавишей `?`. Показывает текущие + * горячие клавиши приложения: ничего hierarchical, просто плоский список + * по разделам (Edit-страницы / List-страницы / Глобально). + * + * Маунтим один раз в AppLayout, чтобы был доступен везде внутри tenant'a. + */ +export function ShortcutsOverlay() { + const [open, setOpen] = useState(false) + useShortcuts({ + '?': () => setOpen(o => !o), + 'Escape': () => { if (open) setOpen(false) }, + }) + + // Закрытие по клику на оверлей + useEffect(() => { + if (!open) return + const onKey = (e: KeyboardEvent) => { if (e.key === 'Escape') setOpen(false) } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [open]) + + if (!open) return null + + return ( +
setOpen(false)} + role="dialog" + aria-modal="true" + aria-labelledby="shortcuts-title" + > +
e.stopPropagation()} + > +
+
+ +

Горячие клавиши

+
+ +
+
+
+ + +
+
+ + +
+
+ + +
+
+
+ Нажми ? в любой момент, чтобы открыть эту шпаргалку. +
+
+
+ ) +} + +function Section({ title, children }: { title: string; children: React.ReactNode }) { + return ( +
+

{title}

+
{children}
+
+ ) +} + +function Row({ label, keys }: { label: string; keys: string[] }) { + return ( +
+ {label} + + {keys.map((k, i) => ( + + {i > 0 && +} + {k} + + ))} + +
+ ) +} + +function Kbd({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ) +} diff --git a/src/food-market.web/src/lib/useShortcuts.ts b/src/food-market.web/src/lib/useShortcuts.ts new file mode 100644 index 0000000..fd14a74 --- /dev/null +++ b/src/food-market.web/src/lib/useShortcuts.ts @@ -0,0 +1,77 @@ +import { useEffect, useRef } from 'react' + +/** + * Регистрирует глобальные клавиатурные сокращения. Поддерживаемые формы: + * - 'Escape' | 'Enter' | 'F1' … + * - 'mod+s' — Ctrl+S на Linux/Win, Cmd+S на Mac + * - 'mod+shift+s' + * - 'n', '/', '?' (одиночные клавиши БЕЗ модификаторов) + * + * Особенности: + * - Одиночные клавиши срабатывают только если фокус НЕ в input/textarea/ + * contenteditable (чтобы не ломать набор текста). 'Escape' и mod+* + * срабатывают всегда. + * - preventDefault() вызывается автоматически — handler решает только что + * делать, без боли с e.preventDefault(). + * - При unmount хук снимает listener. + * + * Использование: + * useShortcuts({ + * 'mod+s': () => save.mutate(), + * 'Escape': () => navigate(-1), + * '/': () => searchRef.current?.focus(), + * 'n': () => navigate('/catalog/products/new'), + * }) + */ +type Handler = (e: KeyboardEvent) => void +type Map = Record + +const isTypingTarget = (el: EventTarget | null): boolean => { + if (!(el instanceof HTMLElement)) return false + const tag = el.tagName + if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true + if (el.isContentEditable) return true + return false +} + +const matches = (e: KeyboardEvent, spec: string): boolean => { + const parts = spec.toLowerCase().split('+').map(s => s.trim()) + const wantMod = parts.includes('mod') + const wantShift = parts.includes('shift') + const wantAlt = parts.includes('alt') + const key = parts[parts.length - 1] + + const ctrlOrMeta = e.ctrlKey || e.metaKey + if (wantMod !== ctrlOrMeta) return false + if (wantShift !== e.shiftKey) return false + if (wantAlt !== e.altKey) return false + + // 'mod+s' → e.key === 's' (lowercase) + return e.key.toLowerCase() === key +} + +export function useShortcuts(map: Map, enabled = true) { + // Держим map в ref, чтобы не перерегистрировать listener на каждый рендер + // (handlers часто захватывают свежие props/state — но useEffect deps + // делает их стабильными). + const ref = useRef(map) + ref.current = map + + useEffect(() => { + if (!enabled) return + const onKey = (e: KeyboardEvent) => { + for (const [spec, handler] of Object.entries(ref.current)) { + const isBareKey = !spec.includes('+') && spec.length === 1 + // Бэр-клавиши ('n', '/', '?') не должны срабатывать в input'ах. + if (isBareKey && isTypingTarget(e.target)) continue + if (matches(e, spec)) { + e.preventDefault() + handler(e) + return + } + } + } + document.addEventListener('keydown', onKey) + return () => document.removeEventListener('keydown', onKey) + }, [enabled]) +} diff --git a/src/food-market.web/src/pages/CounterpartiesPage.tsx b/src/food-market.web/src/pages/CounterpartiesPage.tsx index a28ea1e..a150aad 100644 --- a/src/food-market.web/src/pages/CounterpartiesPage.tsx +++ b/src/food-market.web/src/pages/CounterpartiesPage.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { validateEmail, validatePhone } from '@/lib/validation' +import { useShortcuts } from '@/lib/useShortcuts' import { Plus, Trash2, Users } from 'lucide-react' import { api } from '@/lib/api' import { ListPageShell } from '@/components/ListPageShell' @@ -57,6 +58,13 @@ export function CounterpartiesPage() { const [form, setForm] = useState
(null) const [fieldErrors, setFieldErrors] = useState>>({}) const { confirm, dialogProps } = useConfirm() + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — открыть модалку «новый контрагент». + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => { setForm(blankForm); setFieldErrors({}) }, + }) const countries = useQuery({ queryKey: ['countries-lookup'], @@ -80,7 +88,7 @@ export function CounterpartiesPage() { description="Поставщики и покупатели." actions={ <> - + } diff --git a/src/food-market.web/src/pages/DemandEditPage.tsx b/src/food-market.web/src/pages/DemandEditPage.tsx index d282fb8..a40b639 100644 --- a/src/food-market.web/src/pages/DemandEditPage.tsx +++ b/src/food-market.web/src/pages/DemandEditPage.tsx @@ -11,6 +11,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { useStores, useCurrencies } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { @@ -211,6 +212,13 @@ export function DemandEditPage() { ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить, Esc = назад к списку. + // Esc отключается когда открыт ConfirmDialog. + useShortcuts({ + 'mod+s': () => { if (canSave && !save.isPending) save.mutate() }, + 'Escape': () => navigate('/sales/demands'), + }, !dialogProps.open) + // На редактировании пока тащим документ — показываем скелет. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/DemandsPage.tsx b/src/food-market.web/src/pages/DemandsPage.tsx index 3594692..0fdce4d 100644 --- a/src/food-market.web/src/pages/DemandsPage.tsx +++ b/src/food-market.web/src/pages/DemandsPage.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, Truck } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' @@ -7,6 +8,7 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { type DemandListRow, DemandStatus, demandPaymentLabel } from '@/lib/types' @@ -20,6 +22,13 @@ export function DemandsPage() { const moneyFmt = fractional ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новую отгрузку. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/sales/demands/new'), + }) return ( - + diff --git a/src/food-market.web/src/pages/EnterEditPage.tsx b/src/food-market.web/src/pages/EnterEditPage.tsx index be866fc..7d1f722 100644 --- a/src/food-market.web/src/pages/EnterEditPage.tsx +++ b/src/food-market.web/src/pages/EnterEditPage.tsx @@ -11,6 +11,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { useStores, useCurrencies } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { EnterStatus, type EnterDto, type Product } from '@/lib/types' @@ -191,6 +192,13 @@ export function EnterEditPage() { ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить, Esc = назад к списку. + // Esc отключается когда открыт ConfirmDialog. + useShortcuts({ + 'mod+s': () => { if (canSave && !save.isPending) save.mutate() }, + 'Escape': () => navigate('/inventory/enters'), + }, !dialogProps.open) + // На редактировании пока тащим документ — показываем скелет. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/EntersPage.tsx b/src/food-market.web/src/pages/EntersPage.tsx index 709545d..c26f957 100644 --- a/src/food-market.web/src/pages/EntersPage.tsx +++ b/src/food-market.web/src/pages/EntersPage.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, PackagePlus } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' @@ -7,6 +8,7 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { type EnterListRow, EnterStatus } from '@/lib/types' @@ -20,6 +22,13 @@ export function EntersPage() { const moneyFmt = fractional ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новое оприходование. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/inventory/enters/new'), + }) return ( - + diff --git a/src/food-market.web/src/pages/InventoriesPage.tsx b/src/food-market.web/src/pages/InventoriesPage.tsx index 45426a3..45ce6f1 100644 --- a/src/food-market.web/src/pages/InventoriesPage.tsx +++ b/src/food-market.web/src/pages/InventoriesPage.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, ClipboardList } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' @@ -7,6 +8,7 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { type InventoryListRow, InventoryStatus } from '@/lib/types' @@ -20,6 +22,13 @@ export function InventoriesPage() { const moneyFmt = fractional ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новую инвентаризацию. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/inventory/inventories/new'), + }) return ( - + diff --git a/src/food-market.web/src/pages/InventoryEditPage.tsx b/src/food-market.web/src/pages/InventoryEditPage.tsx index 3ac4a19..e252956 100644 --- a/src/food-market.web/src/pages/InventoryEditPage.tsx +++ b/src/food-market.web/src/pages/InventoryEditPage.tsx @@ -10,6 +10,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { useStores } from '@/lib/useLookups' import { InventoryStatus, type InventoryDto } from '@/lib/types' @@ -207,6 +208,18 @@ export function InventoryEditPage() { const canPost = isDraft && form.lines.some((l) => l.diff !== 0) && !isNew + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить (через create на новой, save на + // существующей), Esc = назад к списку. Esc отключается когда открыт ConfirmDialog. + const canSubmit = isDraft && !!form.storeId && !create.isPending && !save.isPending + useShortcuts({ + 'mod+s': () => { + if (!canSubmit) return + if (isNew) create.mutate() + else save.mutate() + }, + 'Escape': () => navigate('/inventory/inventories'), + }, !dialogProps.open) + // На редактировании пока тащим документ — показываем скелет. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/LossEditPage.tsx b/src/food-market.web/src/pages/LossEditPage.tsx index 55cdb9b..e671331 100644 --- a/src/food-market.web/src/pages/LossEditPage.tsx +++ b/src/food-market.web/src/pages/LossEditPage.tsx @@ -11,6 +11,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { useStores, useCurrencies } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { LossStatus, LossReason, lossReasonLabel, type LossDto, type Product } from '@/lib/types' @@ -194,6 +195,13 @@ export function LossEditPage() { ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить, Esc = назад к списку. + // Esc отключается когда открыт ConfirmDialog. + useShortcuts({ + 'mod+s': () => { if (canSave && !save.isPending) save.mutate() }, + 'Escape': () => navigate('/inventory/losses'), + }, !dialogProps.open) + // На редактировании пока тащим документ — показываем скелет. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/LossesPage.tsx b/src/food-market.web/src/pages/LossesPage.tsx index e0cec03..548fa3e 100644 --- a/src/food-market.web/src/pages/LossesPage.tsx +++ b/src/food-market.web/src/pages/LossesPage.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, Trash2 } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' @@ -7,6 +8,7 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { type LossListRow, LossStatus, lossReasonLabel } from '@/lib/types' @@ -20,6 +22,13 @@ export function LossesPage() { const moneyFmt = fractional ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новое списание. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/inventory/losses/new'), + }) return ( - + diff --git a/src/food-market.web/src/pages/ProductEditPage.tsx b/src/food-market.web/src/pages/ProductEditPage.tsx index 11fbc68..f3b5d2c 100644 --- a/src/food-market.web/src/pages/ProductEditPage.tsx +++ b/src/food-market.web/src/pages/ProductEditPage.tsx @@ -15,6 +15,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { generateEan13InternalPrefix2, generateBarcode, generateArticle } from '@/lib/barcode' interface PriceRow { id?: string; priceTypeId: string; amount: number; currencyId: string } @@ -222,6 +223,15 @@ export function ProductEditPage() { && form.barcodes.length > 0 && missingRequiredPrices.length === 0 + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить, Esc = назад к списку. + // Срабатывают даже из инпутов (mod+s) и из любого фокуса (Escape). + // enabled=false когда открыт ConfirmDialog — иначе Esc уйдёт двум хендлерам + // и навигация перевесит закрытие диалога. + useShortcuts({ + 'mod+s': () => { if (canSave && !save.isPending) save.mutate() }, + 'Escape': () => navigate('/catalog/products'), + }, !dialogProps.open) + // На редактировании пока тащим существующий товар — показываем скелет // вместо пустых полей формы, чтобы не путать пользователя. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/ProductsPage.tsx b/src/food-market.web/src/pages/ProductsPage.tsx index db49f5b..a117aeb 100644 --- a/src/food-market.web/src/pages/ProductsPage.tsx +++ b/src/food-market.web/src/pages/ProductsPage.tsx @@ -1,4 +1,4 @@ -import { useState } from 'react' +import { useRef, useState } from 'react' import { Link, useNavigate } from 'react-router-dom' import { DataTable } from '@/components/DataTable' import { EmptyState } from '@/components/EmptyState' @@ -7,6 +7,7 @@ import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { Plus, Filter, X, FolderTree, Package } from 'lucide-react' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { usePriceTypes } from '@/lib/useLookups' import { ProductGroupTree } from '@/components/ProductGroupTree' @@ -108,6 +109,13 @@ export function ProductsPage() { const showMarked = org.data?.showMarkedOnProduct ?? false const activeCount = activeFilterCount(filters) const [groupsOpen, setGroupsOpen] = useState(false) + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новый товар. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/catalog/products/new'), + }) type Col = { header: string @@ -213,7 +221,7 @@ export function ProductsPage() {

- + diff --git a/src/food-market.web/src/pages/SupplierReturnEditPage.tsx b/src/food-market.web/src/pages/SupplierReturnEditPage.tsx index ddb051c..19e00f9 100644 --- a/src/food-market.web/src/pages/SupplierReturnEditPage.tsx +++ b/src/food-market.web/src/pages/SupplierReturnEditPage.tsx @@ -11,6 +11,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { useStores, useCurrencies } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { SupplierReturnStatus, type SupplierReturnDto, type Product } from '@/lib/types' @@ -197,6 +198,13 @@ export function SupplierReturnEditPage() { ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить, Esc = назад к списку. + // Esc отключается когда открыт ConfirmDialog. + useShortcuts({ + 'mod+s': () => { if (canSave && !save.isPending) save.mutate() }, + 'Escape': () => navigate('/purchases/supplier-returns'), + }, !dialogProps.open) + // На редактировании пока тащим документ — показываем скелет. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/SupplierReturnsPage.tsx b/src/food-market.web/src/pages/SupplierReturnsPage.tsx index 7f82b31..c0101fd 100644 --- a/src/food-market.web/src/pages/SupplierReturnsPage.tsx +++ b/src/food-market.web/src/pages/SupplierReturnsPage.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, Undo2 } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' @@ -7,6 +8,7 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { type SupplierReturnListRow, SupplierReturnStatus } from '@/lib/types' @@ -20,6 +22,13 @@ export function SupplierReturnsPage() { const moneyFmt = fractional ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новый возврат поставщику. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/purchases/supplier-returns/new'), + }) return ( - + diff --git a/src/food-market.web/src/pages/SuppliesPage.tsx b/src/food-market.web/src/pages/SuppliesPage.tsx index 77ec3e2..a7ed70d 100644 --- a/src/food-market.web/src/pages/SuppliesPage.tsx +++ b/src/food-market.web/src/pages/SuppliesPage.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, PackagePlus } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' @@ -7,6 +8,7 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { type SupplyListRow, SupplyStatus } from '@/lib/types' @@ -20,6 +22,13 @@ export function SuppliesPage() { const moneyFmt = fractional ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новую приёмку. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/purchases/supplies/new'), + }) return ( - + diff --git a/src/food-market.web/src/pages/SupplyEditPage.tsx b/src/food-market.web/src/pages/SupplyEditPage.tsx index 1dbfbac..d26c004 100644 --- a/src/food-market.web/src/pages/SupplyEditPage.tsx +++ b/src/food-market.web/src/pages/SupplyEditPage.tsx @@ -12,6 +12,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { useStores, useCurrencies, usePriceTypes } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { SupplyStatus, type SupplyDto, type Product } from '@/lib/types' @@ -270,6 +271,14 @@ export function SupplyEditPage() { && form.lines.length > 0 && isDraft + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить, Esc = назад к списку. + // Esc отключается когда открыт ConfirmDialog, чтобы не закрыть диалог + // и одновременно не уйти на список. + useShortcuts({ + 'mod+s': () => { if (canSave && !save.isPending) save.mutate() }, + 'Escape': () => navigate('/purchases/supplies'), + }, !dialogProps.open) + // На редактировании пока тащим документ — показываем скелет. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/TransferEditPage.tsx b/src/food-market.web/src/pages/TransferEditPage.tsx index 09ca7c2..ac3f390 100644 --- a/src/food-market.web/src/pages/TransferEditPage.tsx +++ b/src/food-market.web/src/pages/TransferEditPage.tsx @@ -11,6 +11,7 @@ import { ConfirmDialog } from '@/components/ConfirmDialog' import { FormSkeleton } from '@/components/Skeleton' import { Breadcrumbs } from '@/components/Breadcrumbs' import { useConfirm } from '@/lib/useConfirm' +import { useShortcuts } from '@/lib/useShortcuts' import { useStores } from '@/lib/useLookups' import { useOrgSettings } from '@/lib/useOrgSettings' import { TransferStatus, type TransferDto, type Product } from '@/lib/types' @@ -179,6 +180,13 @@ export function TransferEditPage() { ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + // Хоткеи edit-страницы: Ctrl/Cmd+S = сохранить, Esc = назад к списку. + // Esc отключается когда открыт ConfirmDialog. + useShortcuts({ + 'mod+s': () => { if (canSave && !save.isPending) save.mutate() }, + 'Escape': () => navigate('/inventory/transfers'), + }, !dialogProps.open) + // На редактировании пока тащим документ — показываем скелет. if (!isNew && existing.isLoading) return diff --git a/src/food-market.web/src/pages/TransfersPage.tsx b/src/food-market.web/src/pages/TransfersPage.tsx index cac9ed6..99cef10 100644 --- a/src/food-market.web/src/pages/TransfersPage.tsx +++ b/src/food-market.web/src/pages/TransfersPage.tsx @@ -1,3 +1,4 @@ +import { useRef } from 'react' import { Link, useNavigate } from 'react-router-dom' import { Plus, ArrowRight, ArrowLeftRight } from 'lucide-react' import { ListPageShell } from '@/components/ListPageShell' @@ -7,6 +8,7 @@ import { Pagination } from '@/components/Pagination' import { SearchBar } from '@/components/SearchBar' import { Button } from '@/components/Button' import { useCatalogList } from '@/lib/useCatalog' +import { useShortcuts } from '@/lib/useShortcuts' import { useOrgSettings } from '@/lib/useOrgSettings' import { type TransferListRow, TransferStatus } from '@/lib/types' @@ -20,6 +22,13 @@ export function TransfersPage() { const moneyFmt = fractional ? { minimumFractionDigits: 2, maximumFractionDigits: 2 } : { maximumFractionDigits: 0 } + const searchRef = useRef(null) + + // Хоткеи list-страницы: `/` фокусит поиск, `n` — создать новое перемещение. + useShortcuts({ + '/': () => searchRef.current?.focus(), + 'n': () => navigate('/inventory/transfers/new'), + }) return ( - +