325 lines
11 KiB
TypeScript
325 lines
11 KiB
TypeScript
|
|
import React, { useState, useRef, useEffect } from "react";
|
||
|
|
|
||
|
|
interface Stock {
|
||
|
|
store: string;
|
||
|
|
storeName: string;
|
||
|
|
quantity: number;
|
||
|
|
work: boolean;
|
||
|
|
blocked: string;
|
||
|
|
breakdown: number;
|
||
|
|
transfer: number;
|
||
|
|
allowDelivery: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface FilialSelectorProps {
|
||
|
|
stocks: Stock[];
|
||
|
|
value?: string;
|
||
|
|
onValueChange?: (value: string) => void;
|
||
|
|
placeholder?: string;
|
||
|
|
label?: string;
|
||
|
|
className?: string;
|
||
|
|
}
|
||
|
|
|
||
|
|
const FilialSelector: React.FC<FilialSelectorProps> = ({
|
||
|
|
stocks,
|
||
|
|
value,
|
||
|
|
onValueChange,
|
||
|
|
placeholder = "Selecione a filial retira",
|
||
|
|
label = "Filial Retira",
|
||
|
|
className,
|
||
|
|
}) => {
|
||
|
|
const [isOpen, setIsOpen] = useState(false);
|
||
|
|
const [searchTerm, setSearchTerm] = useState("");
|
||
|
|
const [selectedStore, setSelectedStore] = useState<string>(value || "");
|
||
|
|
const [displayValue, setDisplayValue] = useState<string>("");
|
||
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
||
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
||
|
|
const tableRef = useRef<HTMLDivElement>(null);
|
||
|
|
|
||
|
|
// Atualizar selectedStore e displayValue quando value mudar externamente
|
||
|
|
useEffect(() => {
|
||
|
|
if (value !== undefined) {
|
||
|
|
setSelectedStore(value);
|
||
|
|
const selected = stocks.find((s) => s.store === value);
|
||
|
|
if (selected) {
|
||
|
|
setDisplayValue(selected.storeName);
|
||
|
|
setSearchTerm(selected.storeName);
|
||
|
|
} else {
|
||
|
|
setDisplayValue("");
|
||
|
|
setSearchTerm("");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}, [value, stocks]);
|
||
|
|
|
||
|
|
// Fechar dropdown ao clicar fora
|
||
|
|
useEffect(() => {
|
||
|
|
const handleClickOutside = (event: MouseEvent) => {
|
||
|
|
if (
|
||
|
|
containerRef.current &&
|
||
|
|
!containerRef.current.contains(event.target as Node)
|
||
|
|
) {
|
||
|
|
setIsOpen(false);
|
||
|
|
// Restaurar displayValue quando fechar sem selecionar
|
||
|
|
if (selectedStore) {
|
||
|
|
const selected = stocks.find((s) => s.store === selectedStore);
|
||
|
|
if (selected) {
|
||
|
|
setSearchTerm(selected.storeName);
|
||
|
|
setDisplayValue(selected.storeName);
|
||
|
|
}
|
||
|
|
} else {
|
||
|
|
setSearchTerm("");
|
||
|
|
setDisplayValue("");
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
if (isOpen) {
|
||
|
|
document.addEventListener("mousedown", handleClickOutside);
|
||
|
|
return () => {
|
||
|
|
document.removeEventListener("mousedown", handleClickOutside);
|
||
|
|
};
|
||
|
|
}
|
||
|
|
}, [isOpen, selectedStore, stocks]);
|
||
|
|
|
||
|
|
// Filtrar estoques baseado no termo de busca (busca em store e storeName)
|
||
|
|
const filteredStocks = stocks.filter((stock) => {
|
||
|
|
if (!searchTerm) return true;
|
||
|
|
const search = searchTerm.toLowerCase();
|
||
|
|
return (
|
||
|
|
stock.store.toLowerCase().includes(search) ||
|
||
|
|
stock.storeName.toLowerCase().includes(search)
|
||
|
|
);
|
||
|
|
});
|
||
|
|
|
||
|
|
const handleSelect = (store: string) => {
|
||
|
|
setSelectedStore(store);
|
||
|
|
const stock = stocks.find((s) => s.store === store);
|
||
|
|
if (stock) {
|
||
|
|
setDisplayValue(stock.storeName);
|
||
|
|
setSearchTerm(stock.storeName);
|
||
|
|
}
|
||
|
|
onValueChange?.(store);
|
||
|
|
setIsOpen(false);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleClear = () => {
|
||
|
|
setSearchTerm("");
|
||
|
|
setDisplayValue("");
|
||
|
|
setSelectedStore("");
|
||
|
|
onValueChange?.("");
|
||
|
|
if (inputRef.current) {
|
||
|
|
inputRef.current.focus();
|
||
|
|
}
|
||
|
|
setIsOpen(true);
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||
|
|
const newValue = e.target.value;
|
||
|
|
setSearchTerm(newValue);
|
||
|
|
setDisplayValue(newValue);
|
||
|
|
setIsOpen(true);
|
||
|
|
// Se limpar o input, limpar também a seleção
|
||
|
|
if (!newValue) {
|
||
|
|
setSelectedStore("");
|
||
|
|
onValueChange?.("");
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleInputFocus = () => {
|
||
|
|
setIsOpen(true);
|
||
|
|
// Quando focar, mostrar o termo de busca atual
|
||
|
|
if (selectedStore) {
|
||
|
|
const selected = stocks.find((s) => s.store === selectedStore);
|
||
|
|
if (selected) {
|
||
|
|
setSearchTerm(selected.storeName);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const handleToggleDropdown = () => {
|
||
|
|
setIsOpen(!isOpen);
|
||
|
|
if (!isOpen && inputRef.current) {
|
||
|
|
inputRef.current.focus();
|
||
|
|
}
|
||
|
|
};
|
||
|
|
|
||
|
|
const formatNumber = (num: number): string => {
|
||
|
|
return num.toLocaleString("pt-BR", {
|
||
|
|
minimumFractionDigits: 3,
|
||
|
|
maximumFractionDigits: 3,
|
||
|
|
});
|
||
|
|
};
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div className={className}>
|
||
|
|
{label && (
|
||
|
|
<label className="block text-xs font-bold text-slate-700 uppercase tracking-wide mb-2">
|
||
|
|
{label}
|
||
|
|
</label>
|
||
|
|
)}
|
||
|
|
<div ref={containerRef} className="relative">
|
||
|
|
{/* Input */}
|
||
|
|
<div className="relative">
|
||
|
|
<input
|
||
|
|
ref={inputRef}
|
||
|
|
type="text"
|
||
|
|
value={isOpen ? searchTerm : displayValue}
|
||
|
|
onChange={handleInputChange}
|
||
|
|
onFocus={handleInputFocus}
|
||
|
|
placeholder={placeholder}
|
||
|
|
className="w-full px-4 py-3 pr-20 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-orange-500/20 focus:border-orange-500 text-sm font-medium"
|
||
|
|
/>
|
||
|
|
{/* Botões de ação */}
|
||
|
|
<div className="absolute right-2 top-1/2 -translate-y-1/2 flex items-center gap-1">
|
||
|
|
{(searchTerm || displayValue) && (
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={handleClear}
|
||
|
|
className="p-1.5 hover:bg-slate-100 rounded transition-colors"
|
||
|
|
title="Limpar"
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className="w-4 h-4 text-slate-500"
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2"
|
||
|
|
d="M6 18L18 6M6 6l12 12"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
)}
|
||
|
|
<button
|
||
|
|
type="button"
|
||
|
|
onClick={handleToggleDropdown}
|
||
|
|
className="p-1.5 hover:bg-slate-100 rounded transition-colors"
|
||
|
|
title={isOpen ? "Fechar" : "Abrir"}
|
||
|
|
>
|
||
|
|
<svg
|
||
|
|
className={`w-4 h-4 text-slate-500 transition-transform ${
|
||
|
|
isOpen ? "rotate-180" : ""
|
||
|
|
}`}
|
||
|
|
fill="none"
|
||
|
|
stroke="currentColor"
|
||
|
|
viewBox="0 0 24 24"
|
||
|
|
>
|
||
|
|
<path
|
||
|
|
strokeLinecap="round"
|
||
|
|
strokeLinejoin="round"
|
||
|
|
strokeWidth="2"
|
||
|
|
d="M19 9l-7 7-7-7"
|
||
|
|
/>
|
||
|
|
</svg>
|
||
|
|
</button>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
{/* Dropdown com Tabela */}
|
||
|
|
{isOpen && (
|
||
|
|
<div
|
||
|
|
ref={tableRef}
|
||
|
|
className="absolute z-50 w-full mt-1 bg-white border border-slate-300 rounded-lg shadow-xl max-h-[400px] flex flex-col overflow-hidden"
|
||
|
|
>
|
||
|
|
{/* Tabela */}
|
||
|
|
<div className="overflow-y-auto flex-1">
|
||
|
|
<table className="w-full">
|
||
|
|
<thead className="bg-slate-50 sticky top-0 z-10">
|
||
|
|
<tr>
|
||
|
|
<th className="px-3 py-2 text-left text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-200" style={{ width: "50px" }}>
|
||
|
|
Loja
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-left text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-200" style={{ width: "200px" }}>
|
||
|
|
Nome da Loja
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-center text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-200" style={{ width: "60px" }}>
|
||
|
|
Entrega
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-right text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-200" style={{ width: "80px" }}>
|
||
|
|
Estoque
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-center text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-200" style={{ width: "60px" }}>
|
||
|
|
Pertence
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-right text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-200" style={{ width: "70px" }}>
|
||
|
|
Bloq
|
||
|
|
</th>
|
||
|
|
<th className="px-3 py-2 text-right text-[9px] font-black text-slate-400 uppercase tracking-widest border-b border-slate-200" style={{ width: "70px" }}>
|
||
|
|
Transf
|
||
|
|
</th>
|
||
|
|
</tr>
|
||
|
|
</thead>
|
||
|
|
<tbody className="divide-y divide-slate-100">
|
||
|
|
{filteredStocks.length === 0 ? (
|
||
|
|
<tr>
|
||
|
|
<td
|
||
|
|
colSpan={7}
|
||
|
|
className="px-4 py-8 text-center text-sm text-slate-500"
|
||
|
|
>
|
||
|
|
Nenhuma filial encontrada
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
) : (
|
||
|
|
filteredStocks.map((stock) => {
|
||
|
|
const isSelected = stock.store === selectedStore;
|
||
|
|
return (
|
||
|
|
<tr
|
||
|
|
key={stock.store}
|
||
|
|
onClick={() => handleSelect(stock.store)}
|
||
|
|
className={`cursor-pointer transition-colors ${
|
||
|
|
isSelected
|
||
|
|
? "bg-blue-50 hover:bg-blue-100"
|
||
|
|
: "hover:bg-slate-50"
|
||
|
|
}`}
|
||
|
|
>
|
||
|
|
<td className="px-3 py-2 text-xs text-slate-700 font-medium">
|
||
|
|
{stock.store}
|
||
|
|
</td>
|
||
|
|
<td className="px-3 py-2 text-xs text-slate-700 font-medium">
|
||
|
|
{stock.storeName}
|
||
|
|
</td>
|
||
|
|
<td className="px-3 py-2 text-center">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={stock.allowDelivery === 1}
|
||
|
|
readOnly
|
||
|
|
className="w-4 h-4 text-orange-500 border-slate-300 rounded focus:ring-orange-500 focus:ring-2 cursor-default pointer-events-none"
|
||
|
|
/>
|
||
|
|
</td>
|
||
|
|
<td className="px-3 py-2 text-xs text-slate-700 text-right font-medium">
|
||
|
|
{formatNumber(stock.quantity)}
|
||
|
|
</td>
|
||
|
|
<td className="px-3 py-2 text-center">
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
checked={stock.work}
|
||
|
|
readOnly
|
||
|
|
className="w-4 h-4 text-orange-500 border-slate-300 rounded focus:ring-orange-500 focus:ring-2 cursor-default pointer-events-none"
|
||
|
|
/>
|
||
|
|
</td>
|
||
|
|
<td className="px-3 py-2 text-xs text-slate-700 text-right font-medium">
|
||
|
|
{stock.blocked || "0"}
|
||
|
|
</td>
|
||
|
|
<td className="px-3 py-2 text-xs text-slate-700 text-right font-medium">
|
||
|
|
{stock.transfer || 0}
|
||
|
|
</td>
|
||
|
|
</tr>
|
||
|
|
);
|
||
|
|
})
|
||
|
|
)}
|
||
|
|
</tbody>
|
||
|
|
</table>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export default FilialSelector;
|
||
|
|
|