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 { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { Link, useSearchParams } from 'react-router-dom';
|
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 {
|
import {
|
||||||
deleteCatalogCategory,
|
deleteCatalogCategory,
|
||||||
deleteCatalogProduct,
|
deleteCatalogProduct,
|
||||||
@@ -101,6 +101,84 @@ const getAverageYield = (reference: ConsumptionReference) => {
|
|||||||
return reference.generalYield;
|
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) => (
|
const getReferenceSourceLabel = (reference: ConsumptionReference) => (
|
||||||
reference.source === 'tiny_op'
|
reference.source === 'tiny_op'
|
||||||
? 'Tiny OP'
|
? 'Tiny OP'
|
||||||
@@ -1011,17 +1089,23 @@ const Registrations = () => {
|
|||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
<label className={labelClassName}>
|
<label className={labelClassName}>
|
||||||
Produto
|
Produto
|
||||||
<select value={referenceForm.productId} onChange={(event) => setReferenceForm(current => ({ ...current, productId: event.target.value }))} className={inputClassName}>
|
<CatalogProductPicker
|
||||||
<option value="">Selecione...</option>
|
value={referenceForm.productId}
|
||||||
{finishedProducts.map(product => <option key={product.id} value={product.id}>{product.name} ({product.sku})</option>)}
|
products={finishedProducts}
|
||||||
</select>
|
emptyLabel="Selecione o produto"
|
||||||
|
searchPlaceholder="Buscar por nome ou SKU..."
|
||||||
|
onChange={(productId) => setReferenceForm(current => ({ ...current, productId }))}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
<label className={labelClassName}>
|
<label className={labelClassName}>
|
||||||
Malha
|
Malha
|
||||||
<select value={referenceForm.materialProductId} onChange={(event) => setReferenceForm(current => ({ ...current, materialProductId: event.target.value }))} className={inputClassName}>
|
<CatalogProductPicker
|
||||||
<option value="">Qualquer</option>
|
value={referenceForm.materialProductId}
|
||||||
{rawMaterials.map(product => <option key={product.id} value={product.id}>{product.name}</option>)}
|
products={rawMaterials}
|
||||||
</select>
|
emptyLabel="Qualquer malha"
|
||||||
|
searchPlaceholder="Buscar malha por nome ou SKU..."
|
||||||
|
onChange={(materialProductId) => setReferenceForm(current => ({ ...current, materialProductId }))}
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
|
||||||
|
|||||||
Reference in New Issue
Block a user