feat(tyres): по умолчанию только подходящая резина, «Показать все» разворачивает

Кладовщик у подъёмника выбирал размер из девятнадцати позиций глазами. Теперь
экран показывает только подходящее выбранной машине (по подтверждённой
применимости), с счётчиком и кнопкой-переключателем на полный список.

Если по модели применимость не отмечена, фильтровать нечем — показываются все
размеры и объясняется, почему: прятать правильную резину по незнанию хуже, чем
показать лишнюю.

Плашка на строке различает «подходит» (применимость) и «штатный» (справочник
размеров), и предвыбор позиции теперь учитывает оба признака.
This commit is contained in:
2026-08-13 20:10:39 +10:00
parent 064b02e735
commit 2a58c5ca94
2 changed files with 43 additions and 12 deletions
@@ -375,6 +375,8 @@ export interface TyreStockRow {
by_grade: Partial<Record<TyreGrade, number>>;
places: string[];
suggested: boolean;
/** Подтверждённая применимость к выбранной машине. */
fits: boolean;
}
export interface TyreSet {
@@ -410,6 +412,8 @@ export interface TyreContext {
suggested_size: string | null;
open_set: TyreSet | null;
stock: TyreStockRow[];
/** Известно ли, что подходит машине. false — фильтровать нечем, показываем всё. */
fit_known: boolean;
}
export interface TyreClose {
@@ -179,14 +179,19 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
// Подставляем подсказанный размер, как только пришёл контекст.
useEffect(() => {
if (!ctx || partId !== null) return;
const suggested = ctx.stock.find((s) => s.suggested && s.qty > 0);
if (suggested) setPartId(suggested.part_id);
const best = ctx.stock.find((s) => (s.fits || s.suggested) && s.qty > 0);
if (best) setPartId(best.part_id);
}, [ctx, partId]);
const visibleStock = useMemo(() => {
if (!ctx) return [];
return showAll ? ctx.stock : ctx.stock.slice(0, 6);
}, [ctx, showAll]);
// По умолчанию показываем ТОЛЬКО подходящее этой машине — иначе кладовщик
// выбирает размер из девятнадцати позиций глазами. Если применимость по
// машине не заполнена, фильтровать нечем: прятать правильную резину по
// незнанию хуже, чем показать лишнюю (то же правило, что у подбора запчастей).
const fitting = useMemo(
() => (ctx ? ctx.stock.filter((s) => s.fits || s.suggested) : []),
[ctx],
);
const visibleStock = showAll || !ctx?.fit_known ? (ctx?.stock ?? []) : fitting;
if (ctxQuery.isLoading) {
return <main className="p-4 text-sm text-muted-foreground">Загрузка</main>;
@@ -290,7 +295,20 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
{/* Что выдаём */}
<section className="p-3 border-b">
<h2 className="text-sm font-semibold mb-2">Что выдаём</h2>
<div className="flex items-baseline justify-between mb-2">
<h2 className="text-sm font-semibold">Что выдаём</h2>
{ctx.fit_known && (
<span className="text-xs text-muted-foreground">
{showAll ? `все размеры (${ctx.stock.length})` : `подходит: ${fitting.length}`}
</span>
)}
</div>
{!ctx.fit_known && (
<p className="text-xs text-muted-foreground mb-2">
Для этой модели не отмечено, какая резина подходит, показаны все размеры.
Отметить можно в карточке позиции на складе.
</p>
)}
<div className="space-y-1">
{visibleStock.length === 0 && (
<p className="text-sm text-muted-foreground">На складе нет резины.</p>
@@ -304,9 +322,16 @@ function IssueForm({ carId, onDone }: { carId: number; onDone: () => void }) {
/>
))}
</div>
{!showAll && ctx.stock.length > 6 && (
<Button variant="ghost" size="sm" className="mt-1" onClick={() => setShowAll(true)}>
Показать все размеры ({ctx.stock.length})
{ctx.fit_known && (
<Button
variant="ghost"
size="sm"
className="mt-1"
onClick={() => setShowAll((v) => !v)}
>
{showAll
? `Только подходящие (${fitting.length})`
: `Показать все размеры (${ctx.stock.length})`}
</Button>
)}
{chosen && chosen.qty < qty && (
@@ -400,8 +425,10 @@ function StockRow({
<span className="flex-1 min-w-0">
<span className="flex items-baseline gap-2 flex-wrap">
<span className="font-semibold">{row.size ?? row.name}</span>
{row.suggested && (
<span className="text-xs rounded px-1.5 py-0.5 bg-primary/10 text-primary">штатный</span>
{(row.fits || row.suggested) && (
<span className="text-xs rounded px-1.5 py-0.5 bg-primary/10 text-primary">
{row.fits ? "подходит" : "штатный"}
</span>
)}
<span className={`text-sm ${row.qty > 0 ? "text-muted-foreground" : "text-destructive"}`}>
{row.qty} шт