Add searchable consumption reference selectors
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s

This commit is contained in:
Cauê Faleiros
2026-08-26 14:40:07 -03:00
parent 1c0025ed17
commit fb243cdd20

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { Link, useSearchParams } from 'react-router-dom';
import { ClipboardList, CloudDownload, Loader2, Package, Pencil, RefreshCw, Ruler, Save, Tags, Trash2, Upload } from 'lucide-react';
import { ChevronDown, ClipboardList, CloudDownload, Loader2, Package, Pencil, RefreshCw, Ruler, Save, Search, Tags, Trash2, Upload } from 'lucide-react';
import {
deleteCatalogCategory,
deleteCatalogProduct,
@@ -101,6 +101,84 @@ const getAverageYield = (reference: ConsumptionReference) => {
return reference.generalYield;
};
const CatalogProductPicker = ({
value,
products,
emptyLabel,
searchPlaceholder,
onChange,
}: {
value: string;
products: CatalogProduct[];
emptyLabel: string;
searchPlaceholder: string;
onChange: (value: string) => void;
}) => {
const [isOpen, setIsOpen] = useState(false);
const [query, setQuery] = useState('');
const selectedProduct = products.find(product => String(product.id) === value);
const normalizedQuery = query.trim().toLocaleLowerCase('pt-BR');
const visibleProducts = useMemo(() => products
.filter(product => !normalizedQuery || `${product.name} ${product.sku} ${product.color || ''}`.toLocaleLowerCase('pt-BR').includes(normalizedQuery))
.slice(0, 60), [products, normalizedQuery]);
return (
<div className="relative mt-1">
<button
type="button"
aria-expanded={isOpen}
onClick={() => {
setIsOpen(current => !current);
setQuery('');
}}
className={`${inputClassName} flex items-center justify-between gap-2 text-left cursor-pointer`}
>
<span className={selectedProduct ? 'truncate text-dark-text' : 'truncate text-dark-muted'}>
{selectedProduct ? `${selectedProduct.name}${selectedProduct.sku ? ` (${selectedProduct.sku})` : ''}` : emptyLabel}
</span>
<ChevronDown className={`h-4 w-4 shrink-0 text-dark-muted transition-transform ${isOpen ? 'rotate-180' : ''}`} />
</button>
{isOpen && (
<div className="absolute z-30 mt-2 w-full overflow-hidden rounded-xl border border-dark-border bg-dark-card shadow-2xl">
<div className="border-b border-dark-border p-2">
<label className="relative block">
<Search className="pointer-events-none absolute left-3 top-3 h-4 w-4 text-dark-muted" />
<input
autoFocus
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={searchPlaceholder}
className={`${inputClassName} pl-9`}
/>
</label>
</div>
<div className="max-h-60 overflow-y-auto p-1">
<button
type="button"
onClick={() => { onChange(''); setIsOpen(false); }}
className="flex w-full items-center rounded-lg px-3 py-2 text-left text-sm font-semibold text-dark-muted transition-colors hover:bg-dark-input hover:text-dark-text cursor-pointer"
>
{emptyLabel}
</button>
{visibleProducts.map(product => (
<button
key={product.id}
type="button"
onClick={() => { onChange(String(product.id)); setIsOpen(false); }}
className={`flex w-full flex-col rounded-lg px-3 py-2 text-left transition-colors hover:bg-dark-input cursor-pointer ${String(product.id) === value ? 'bg-brand-primary/10' : ''}`}
>
<span className="text-sm font-bold text-dark-text">{product.name}</span>
<span className="mt-0.5 text-xs font-semibold text-dark-muted">{[product.sku, product.color ? formatColorLabel(product.color) : ''].filter(Boolean).join(' · ')}</span>
</button>
))}
{!visibleProducts.length && <p className="px-3 py-4 text-sm font-semibold text-dark-muted">Nenhum item encontrado.</p>}
</div>
</div>
)}
</div>
);
};
const getReferenceSourceLabel = (reference: ConsumptionReference) => (
reference.source === 'tiny_op'
? 'Tiny OP'
@@ -1011,17 +1089,23 @@ const Registrations = () => {
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
<label className={labelClassName}>
Produto
<select value={referenceForm.productId} onChange={(event) => setReferenceForm(current => ({ ...current, productId: event.target.value }))} className={inputClassName}>
<option value="">Selecione...</option>
{finishedProducts.map(product => <option key={product.id} value={product.id}>{product.name} ({product.sku})</option>)}
</select>
<CatalogProductPicker
value={referenceForm.productId}
products={finishedProducts}
emptyLabel="Selecione o produto"
searchPlaceholder="Buscar por nome ou SKU..."
onChange={(productId) => setReferenceForm(current => ({ ...current, productId }))}
/>
</label>
<label className={labelClassName}>
Malha
<select value={referenceForm.materialProductId} onChange={(event) => setReferenceForm(current => ({ ...current, materialProductId: event.target.value }))} className={inputClassName}>
<option value="">Qualquer</option>
{rawMaterials.map(product => <option key={product.id} value={product.id}>{product.name}</option>)}
</select>
<CatalogProductPicker
value={referenceForm.materialProductId}
products={rawMaterials}
emptyLabel="Qualquer malha"
searchPlaceholder="Buscar malha por nome ou SKU..."
onChange={(materialProductId) => setReferenceForm(current => ({ ...current, materialProductId }))}
/>
</label>
</div>
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">