Compare commits

..

No commits in common. "c8a7efde47a7513dd2d9ea0979392d63ade3265b" and "e9f8da1b82a51a7dd4b73fae125dbcc750649057" have entirely different histories.

4 changed files with 45 additions and 39 deletions

View file

@ -3,6 +3,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { LoginPage } from '@/pages/LoginPage' import { LoginPage } from '@/pages/LoginPage'
import { DashboardPage } from '@/pages/DashboardPage' import { DashboardPage } from '@/pages/DashboardPage'
import { CountriesPage } from '@/pages/CountriesPage' import { CountriesPage } from '@/pages/CountriesPage'
import { CurrenciesPage } from '@/pages/CurrenciesPage'
import { UnitsOfMeasurePage } from '@/pages/UnitsOfMeasurePage' import { UnitsOfMeasurePage } from '@/pages/UnitsOfMeasurePage'
import { PriceTypesPage } from '@/pages/PriceTypesPage' import { PriceTypesPage } from '@/pages/PriceTypesPage'
import { StoresPage } from '@/pages/StoresPage' import { StoresPage } from '@/pages/StoresPage'
@ -50,6 +51,7 @@ export default function App() {
<Route path="/catalog/stores" element={<StoresPage />} /> <Route path="/catalog/stores" element={<StoresPage />} />
<Route path="/catalog/retail-points" element={<RetailPointsPage />} /> <Route path="/catalog/retail-points" element={<RetailPointsPage />} />
<Route path="/catalog/countries" element={<CountriesPage />} /> <Route path="/catalog/countries" element={<CountriesPage />} />
<Route path="/catalog/currencies" element={<CurrenciesPage />} />
<Route path="/inventory/stock" element={<StockPage />} /> <Route path="/inventory/stock" element={<StockPage />} />
<Route path="/inventory/movements" element={<StockMovementsPage />} /> <Route path="/inventory/movements" element={<StockMovementsPage />} />
<Route path="/purchases/supplies" element={<SuppliesPage />} /> <Route path="/purchases/supplies" element={<SuppliesPage />} />

View file

@ -6,7 +6,7 @@ import { logout } from '@/lib/auth'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { import {
LayoutDashboard, Package, FolderTree, Ruler, Tag, LayoutDashboard, Package, FolderTree, Ruler, Tag,
Users, Warehouse, Store as StoreIcon, Globe, LogOut, Download, Users, Warehouse, Store as StoreIcon, Globe, Coins, LogOut, Download,
Boxes, History, TruckIcon, ShoppingCart, Settings, Menu, X, Boxes, History, TruckIcon, ShoppingCart, Settings, Menu, X,
} from 'lucide-react' } from 'lucide-react'
import { Logo } from './Logo' import { Logo } from './Logo'
@ -53,6 +53,7 @@ function buildNav(): NavSection[] {
]}, ]},
{ group: 'Справочники', items: [ { group: 'Справочники', items: [
{ to: '/catalog/countries', icon: Globe, label: 'Страны' }, { to: '/catalog/countries', icon: Globe, label: 'Страны' },
{ to: '/catalog/currencies', icon: Coins, label: 'Валюты' },
]}, ]},
{ group: 'Импорт', items: [ { group: 'Импорт', items: [
{ to: '/admin/import/moysklad', icon: Download, label: 'МойСклад' }, { to: '/admin/import/moysklad', icon: Download, label: 'МойСклад' },

View file

@ -1,5 +1,4 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react' import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
import { createPortal } from 'react-dom'
import { useMutation } from '@tanstack/react-query' import { useMutation } from '@tanstack/react-query'
import { Plus } from 'lucide-react' import { Plus } from 'lucide-react'
import { api } from '@/lib/api' import { api } from '@/lib/api'
@ -63,28 +62,6 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
)>(null) )>(null)
const inputRef = useRef<HTMLInputElement>(null) const inputRef = useRef<HTMLInputElement>(null)
const wrapRef = useRef<HTMLDivElement>(null) const wrapRef = useRef<HTMLDivElement>(null)
const dropdownRef = useRef<HTMLDivElement>(null)
const [dropdownPos, setDropdownPos] = useState<{ top: number; left: number; width: number } | null>(null)
// Считаем позицию dropdown'а по rect input'а — без этого
// он рендерится через portal в body и будет в углу.
const recomputePos = () => {
const el = inputRef.current
if (!el) return
const r = el.getBoundingClientRect()
setDropdownPos({ top: r.bottom + 4, left: r.left, width: r.width })
}
useLayoutEffect(() => {
if (!open) return
recomputePos()
const onScrollOrResize = () => recomputePos()
window.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
return () => {
window.removeEventListener('scroll', onScrollOrResize, true)
window.removeEventListener('resize', onScrollOrResize)
}
}, [open])
// Автофокус при монтировании // Автофокус при монтировании
useEffect(() => { useEffect(() => {
@ -115,15 +92,11 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
return () => { cancelled = true } return () => { cancelled = true }
}, [debounced, storeId]) }, [debounced, storeId])
// Outside click — закрыть dropdown. Dropdown рендерится в Portal, поэтому // Outside click — закрыть dropdown
// проверяем и wrap (input), и сам dropdown.
useEffect(() => { useEffect(() => {
if (!open) return if (!open) return
const onDoc = (e: MouseEvent) => { const onDoc = (e: MouseEvent) => {
const t = e.target as Node if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false)
const inWrap = wrapRef.current?.contains(t)
const inDropdown = dropdownRef.current?.contains(t)
if (!inWrap && !inDropdown) setOpen(false)
} }
document.addEventListener('mousedown', onDoc) document.addEventListener('mousedown', onDoc)
return () => document.removeEventListener('mousedown', onDoc) return () => document.removeEventListener('mousedown', onDoc)
@ -252,12 +225,8 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
{hint} {hint}
</div> </div>
)} )}
{open && (query.trim().length > 0 || items.length > 0) && dropdownPos && createPortal( {open && (query.trim().length > 0 || items.length > 0) && (
<div <div className="absolute z-30 mt-1 w-full rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-lg max-h-80 overflow-auto">
ref={dropdownRef}
style={{ position: 'fixed', top: dropdownPos.top, left: dropdownPos.left, width: dropdownPos.width }}
className="z-[100] rounded-md border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-900 shadow-lg max-h-80 overflow-auto"
>
{loading && items.length === 0 ? ( {loading && items.length === 0 ? (
<div className="px-3 py-2 text-sm text-slate-400">Ищу</div> <div className="px-3 py-2 text-sm text-slate-400">Ищу</div>
) : items.length === 0 ? ( ) : items.length === 0 ? (
@ -306,8 +275,7 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
</button> </button>
</div> </div>
)} )}
</div>, </div>
document.body,
)} )}
<ProductQuickCreateModal <ProductQuickCreateModal

View file

@ -0,0 +1,35 @@
import { ListPageShell } from '@/components/ListPageShell'
import { DataTable } from '@/components/DataTable'
import { Pagination } from '@/components/Pagination'
import { SearchBar } from '@/components/SearchBar'
import { useCatalogList } from '@/lib/useCatalog'
import type { Currency } from '@/lib/types'
export function CurrenciesPage() {
const { data, isLoading, page, setPage, search, setSearch, sortKey, sortOrder, setSort } = useCatalogList<Currency>('/api/catalog/currencies')
return (
<ListPageShell
title="Валюты"
description="Доступные валюты для операций. Основная — тенге (KZT)."
actions={<SearchBar value={search} onChange={setSearch} />}
footer={data && data.total > 0 && (
<Pagination page={page} pageSize={data.pageSize} total={data.total} onPageChange={setPage} />
)}
>
<DataTable
rows={data?.items ?? []}
isLoading={isLoading}
rowKey={(r) => r.id}
sortKey={sortKey}
sortOrder={sortOrder}
onSortChange={setSort}
columns={[
{ header: 'Код', width: '90px', sortKey: 'code', cell: (r) => <span className="font-mono">{r.code}</span> },
{ header: 'Название', sortKey: 'name', cell: (r) => r.name },
{ header: 'Символ', width: '100px', sortKey: 'symbol', cell: (r) => <span className="text-lg">{r.symbol}</span> },
]}
/>
</ListPageShell>
)
}