Add searchable consumption reference selectors
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m31s
This commit is contained in:
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user