Vendaweb-portal/components/FilialSelector.tsx

325 lines
11 KiB
TypeScript
Raw Normal View History

2026-01-08 12:09:16 +00:00
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;