fix(supply-quick-add): sticky input at viewport bottom + auto-scroll on add
Quick-add bar теперь не sticky-внутри-Section, а отдельный flex-sibling
формы — всегда прибит к нижнему краю viewport независимо от высоты
содержимого и overflow-hidden у Section'а:
<form flex flex-col h-full>
<topbar />
<body flex-1 overflow-auto> ← скроллится
<quick-add bar flex-shrink-0> ← всегда виден
</form>
После каждого добавления строки скролл-контейнер тела документа
автоскроллится к низу (smooth, через requestAnimationFrame чтобы
дождаться рендера новой строки) — новая строка всегда появляется
прямо над input'ом и пользователь видит подтверждение скана.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b56c499b45
commit
970a9baec3
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState, useEffect, type FormEvent, type ReactNode } from 'react'
|
import { useState, useEffect, useRef, type FormEvent, type ReactNode } from 'react'
|
||||||
import { useNavigate, useParams, Link } from 'react-router-dom'
|
import { useNavigate, useParams, Link } from 'react-router-dom'
|
||||||
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
import { useQuery, useQueryClient, useMutation } from '@tanstack/react-query'
|
||||||
import { ArrowLeft, Plus, Trash2, Save, CheckCircle } from 'lucide-react'
|
import { ArrowLeft, Plus, Trash2, Save, CheckCircle } from 'lucide-react'
|
||||||
|
|
@ -65,6 +65,16 @@ export function SupplyEditPage() {
|
||||||
const [form, setForm] = useState<Form>(emptyForm)
|
const [form, setForm] = useState<Form>(emptyForm)
|
||||||
const [pickerOpen, setPickerOpen] = useState(false)
|
const [pickerOpen, setPickerOpen] = useState(false)
|
||||||
const [error, setError] = useState<string | null>(null)
|
const [error, setError] = useState<string | null>(null)
|
||||||
|
// Скролл-контейнер тела документа: после каждого добавления строки
|
||||||
|
// автоскроллим к низу, чтобы новая строка и input оказались в зоне видимости.
|
||||||
|
const scrollBodyRef = useRef<HTMLDivElement>(null)
|
||||||
|
const scrollToBottom = () => {
|
||||||
|
const el = scrollBodyRef.current
|
||||||
|
if (!el) return
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
el.scrollTo({ top: el.scrollHeight, behavior: 'smooth' })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const existing = useQuery({
|
const existing = useQuery({
|
||||||
queryKey: ['/api/purchases/supplies', id],
|
queryKey: ['/api/purchases/supplies', id],
|
||||||
|
|
@ -211,6 +221,7 @@ export function SupplyEditPage() {
|
||||||
...form,
|
...form,
|
||||||
lines: form.lines.map((l, ix) => ix === idx ? { ...l, quantity: l.quantity + 1 } : l),
|
lines: form.lines.map((l, ix) => ix === idx ? { ...l, quantity: l.quantity + 1 } : l),
|
||||||
})
|
})
|
||||||
|
scrollToBottom()
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
const defaultRetail = p.prices?.[0]?.amount ?? null
|
const defaultRetail = p.prices?.[0]?.amount ?? null
|
||||||
|
|
@ -228,6 +239,7 @@ export function SupplyEditPage() {
|
||||||
retailPriceOverride: null,
|
retailPriceOverride: null,
|
||||||
}],
|
}],
|
||||||
})
|
})
|
||||||
|
scrollToBottom()
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
const updateLine = (i: number, patch: Partial<LineRow>) =>
|
const updateLine = (i: number, patch: Partial<LineRow>) =>
|
||||||
|
|
@ -273,7 +285,7 @@ export function SupplyEditPage() {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable body */}
|
{/* Scrollable body */}
|
||||||
<div className="flex-1 overflow-auto">
|
<div ref={scrollBodyRef} className="flex-1 overflow-auto">
|
||||||
<div className="max-w-6xl mx-auto p-3 sm:p-6 space-y-4 sm:space-y-5">
|
<div className="max-w-6xl mx-auto p-3 sm:p-6 space-y-4 sm:space-y-5">
|
||||||
{error && (
|
{error && (
|
||||||
<div className="p-3 rounded-md bg-red-50 text-red-700 text-sm border border-red-200">{error}</div>
|
<div className="p-3 rounded-md bg-red-50 text-red-700 text-sm border border-red-200">{error}</div>
|
||||||
|
|
@ -448,23 +460,25 @@ export function SupplyEditPage() {
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
{!isPosted && (
|
|
||||||
// Sticky-bottom вне Section (overflow-hidden родителя ломает sticky):
|
|
||||||
// пользователь может подряд сканировать партию штрихкодов — после
|
|
||||||
// каждого скана строка добавляется в таблицу выше, а input остаётся
|
|
||||||
// прибит к низу скроллируемого тела документа и принимает следующий ввод.
|
|
||||||
<div className="sticky bottom-0 -mx-3 sm:-mx-6 px-3 sm:px-6 py-3 bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 z-10 shadow-[0_-4px_12px_rgba(0,0,0,0.04)]">
|
|
||||||
<SupplyLineQuickAdd
|
|
||||||
storeId={form.storeId}
|
|
||||||
onPick={addOrIncrementLine}
|
|
||||||
linesCount={form.lines.length}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Quick-add bar — flex-sibling формы, всегда у нижнего края viewport.
|
||||||
|
* Не sticky внутри scroll-body, чтобы overflow родителей и высота
|
||||||
|
* содержимого не влияли на видимость. После каждого добавления
|
||||||
|
* строки тело документа автоскроллится к низу. */}
|
||||||
|
{!isPosted && (
|
||||||
|
<div className="flex-shrink-0 bg-white dark:bg-slate-900 border-t border-slate-200 dark:border-slate-800 px-3 sm:px-6 py-3 shadow-[0_-4px_12px_rgba(0,0,0,0.04)]">
|
||||||
|
<div className="max-w-6xl mx-auto">
|
||||||
|
<SupplyLineQuickAdd
|
||||||
|
storeId={form.storeId}
|
||||||
|
onPick={addOrIncrementLine}
|
||||||
|
linesCount={form.lines.length}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
<ProductPicker open={pickerOpen} onClose={() => setPickerOpen(false)} onPick={addLineFromProduct} />
|
||||||
</form>
|
</form>
|
||||||
)
|
)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue