Compare commits
2 commits
e9f8da1b82
...
c8a7efde47
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8a7efde47 | ||
|
|
43bf1dc3de |
|
|
@ -3,7 +3,6 @@ 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'
|
||||||
|
|
@ -51,7 +50,6 @@ 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 />} />
|
||||||
|
|
|
||||||
|
|
@ -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, Coins, LogOut, Download,
|
Users, Warehouse, Store as StoreIcon, Globe, 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,7 +53,6 @@ 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: 'МойСклад' },
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
import { useEffect, useMemo, useRef, useState, type KeyboardEvent } from 'react'
|
import { useEffect, useLayoutEffect, 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'
|
||||||
|
|
@ -62,6 +63,28 @@ 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(() => {
|
||||||
|
|
@ -92,11 +115,15 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
||||||
return () => { cancelled = true }
|
return () => { cancelled = true }
|
||||||
}, [debounced, storeId])
|
}, [debounced, storeId])
|
||||||
|
|
||||||
// Outside click — закрыть dropdown
|
// Outside click — закрыть dropdown. Dropdown рендерится в Portal, поэтому
|
||||||
|
// проверяем и wrap (input), и сам dropdown.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!open) return
|
if (!open) return
|
||||||
const onDoc = (e: MouseEvent) => {
|
const onDoc = (e: MouseEvent) => {
|
||||||
if (wrapRef.current && !wrapRef.current.contains(e.target as Node)) setOpen(false)
|
const t = e.target as Node
|
||||||
|
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)
|
||||||
|
|
@ -225,8 +252,12 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
||||||
{hint}
|
{hint}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{open && (query.trim().length > 0 || items.length > 0) && (
|
{open && (query.trim().length > 0 || items.length > 0) && dropdownPos && createPortal(
|
||||||
<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">
|
<div
|
||||||
|
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 ? (
|
||||||
|
|
@ -275,7 +306,8 @@ export function SupplyLineQuickAdd({ storeId, disabled, onPick }: Props) {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>,
|
||||||
|
document.body,
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<ProductQuickCreateModal
|
<ProductQuickCreateModal
|
||||||
|
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue