235 lines
9.7 KiB
JavaScript
235 lines
9.7 KiB
JavaScript
import React, { useMemo, useState } from 'react';
|
|
import { Building2, Check, ChevronDown, Search } from 'lucide-react';
|
|
import { cn } from '@/lib/utils';
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuLabel,
|
|
DropdownMenuTrigger,
|
|
} from '@/components/ui/dropdown-menu';
|
|
|
|
const locationDisplayName = (loc) =>
|
|
(loc?.locationname && loc.locationname.trim()) || loc?.address || 'Unnamed location';
|
|
|
|
const isValidLocation = (loc) => {
|
|
const name = String(loc?.locationname || '').toLowerCase();
|
|
return !name.includes('retired') && !name.includes('do not use') && !name.includes('test row');
|
|
};
|
|
|
|
export function HubLocationSelect({
|
|
locations = [],
|
|
selectedLocationId = '',
|
|
onSelectLocation,
|
|
disabled = false,
|
|
placeholder = 'Select Hub Location',
|
|
variant = 'header', // 'header' | 'field'
|
|
className,
|
|
}) {
|
|
const [search, setSearch] = useState('');
|
|
|
|
const validLocations = useMemo(() => {
|
|
return (locations || [])
|
|
.filter(isValidLocation)
|
|
.sort((a, b) => {
|
|
if (Boolean(a.isprimary) !== Boolean(b.isprimary)) return a.isprimary ? -1 : 1;
|
|
return locationDisplayName(a).localeCompare(locationDisplayName(b));
|
|
});
|
|
}, [locations]);
|
|
|
|
const filtered = useMemo(() => {
|
|
if (!search.trim()) return validLocations;
|
|
const q = search.toLowerCase();
|
|
return validLocations.filter(
|
|
(loc) =>
|
|
locationDisplayName(loc).toLowerCase().includes(q) ||
|
|
loc.address?.toLowerCase().includes(q) ||
|
|
loc.city?.toLowerCase().includes(q) ||
|
|
loc.pincode?.includes(q)
|
|
);
|
|
}, [validLocations, search]);
|
|
|
|
const selectedLoc = useMemo(() => {
|
|
return validLocations.find(
|
|
(l) => String(l.tenantlocationid || l.locationid) === String(selectedLocationId)
|
|
);
|
|
}, [validLocations, selectedLocationId]);
|
|
|
|
const displayLabel = selectedLoc ? locationDisplayName(selectedLoc) : placeholder;
|
|
|
|
if (variant === 'field') {
|
|
return (
|
|
<DropdownMenu onOpenChange={(open) => !open && setSearch('')}>
|
|
<DropdownMenuTrigger asChild disabled={disabled}>
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
'flex h-10 w-full items-center justify-between gap-2 rounded-xl border border-slate-200 bg-white px-3 text-xs font-medium text-slate-900 transition-colors',
|
|
'hover:border-slate-300 focus:outline-none focus:ring-2 focus:ring-black/10 focus:border-black cursor-pointer shadow-2xs',
|
|
disabled && 'bg-slate-50 opacity-60 cursor-not-allowed hover:border-slate-200',
|
|
className
|
|
)}
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<Building2 className="h-4 w-4 shrink-0 text-slate-400" />
|
|
<span className={cn('truncate', !selectedLoc && 'text-slate-400 font-normal')}>
|
|
{displayLabel}
|
|
</span>
|
|
{selectedLoc?.isprimary && (
|
|
<span className="shrink-0 rounded-full bg-blue-50 px-2 py-0.5 text-[10px] font-bold text-blue-700 border border-blue-200">
|
|
PRIMARY
|
|
</span>
|
|
)}
|
|
</div>
|
|
<ChevronDown className="h-4 w-4 shrink-0 text-slate-400" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent align="start" className="w-[320px] sm:w-[380px] p-1 shadow-xl">
|
|
<DropdownMenuLabel className="px-2.5 py-1.5 text-xs font-semibold text-slate-500">
|
|
Select Pickup Hub / Kitchen Location
|
|
</DropdownMenuLabel>
|
|
|
|
<div className="relative mb-1 px-1">
|
|
<Search className="pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400" />
|
|
<input
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Search location or address…"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50 pl-8 pr-2 text-xs text-slate-900 placeholder:text-slate-400 outline-none focus:border-slate-400 focus:bg-white"
|
|
/>
|
|
</div>
|
|
|
|
<div className="max-h-60 overflow-y-auto">
|
|
{filtered.map((loc) => {
|
|
const id = String(loc.tenantlocationid || loc.locationid);
|
|
const isSelected = String(selectedLocationId) === id;
|
|
|
|
return (
|
|
<DropdownMenuItem
|
|
key={id}
|
|
onClick={() => onSelectLocation(id)}
|
|
className={cn(
|
|
'flex items-start justify-between gap-2 px-2.5 py-2 text-xs cursor-pointer rounded-lg mb-0.5',
|
|
isSelected ? 'bg-blue-50 text-blue-900 font-semibold' : 'hover:bg-slate-50'
|
|
)}
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<p className="truncate text-xs font-semibold text-slate-900">
|
|
{locationDisplayName(loc)}
|
|
</p>
|
|
{loc.isprimary && (
|
|
<span className="shrink-0 rounded bg-blue-100/80 px-1.5 py-0.2 text-[9px] font-bold text-blue-800">
|
|
PRIMARY
|
|
</span>
|
|
)}
|
|
</div>
|
|
{(loc.address || loc.city) && (
|
|
<p className="mt-0.5 truncate text-[11px] text-slate-500">
|
|
{[loc.address, loc.city, loc.pincode].filter(Boolean).join(' · ')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{isSelected && <Check className="h-4 w-4 text-blue-600 shrink-0 mt-0.5" />}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
|
|
{filtered.length === 0 && (
|
|
<p className="py-4 text-center text-xs text-slate-400">No matching locations found</p>
|
|
)}
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|
|
|
|
// Header quick picker variant
|
|
return (
|
|
<DropdownMenu onOpenChange={(open) => !open && setSearch('')}>
|
|
<DropdownMenuTrigger asChild disabled={disabled}>
|
|
<button
|
|
type="button"
|
|
className={cn(
|
|
'flex h-9 items-center gap-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-1.5 text-xs font-semibold text-slate-900 transition-colors shadow-2xs',
|
|
'hover:bg-slate-100 hover:border-slate-300 focus:outline-none focus:ring-2 focus:ring-black/10 cursor-pointer',
|
|
disabled && 'opacity-60 cursor-not-allowed hover:bg-slate-50',
|
|
className
|
|
)}
|
|
>
|
|
<Building2 className="w-4 h-4 text-slate-400 shrink-0" />
|
|
<span className="text-[11px] font-semibold text-slate-500 shrink-0">Hub:</span>
|
|
<span className="max-w-[180px] sm:max-w-[220px] truncate text-xs font-bold text-slate-900">
|
|
{displayLabel}
|
|
</span>
|
|
<ChevronDown className="w-3.5 h-3.5 text-slate-400 shrink-0 ml-0.5" />
|
|
</button>
|
|
</DropdownMenuTrigger>
|
|
|
|
<DropdownMenuContent align="start" className="w-[300px] sm:w-[360px] p-1 shadow-xl">
|
|
<DropdownMenuLabel className="px-2.5 py-1.5 text-xs font-semibold text-slate-500">
|
|
Switch Pickup Hub / Kitchen
|
|
</DropdownMenuLabel>
|
|
|
|
<div className="relative mb-1 px-1">
|
|
<Search className="pointer-events-none absolute left-3 top-2.5 h-3.5 w-3.5 text-slate-400" />
|
|
<input
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
placeholder="Search kitchen or hub…"
|
|
onClick={(e) => e.stopPropagation()}
|
|
onKeyDown={(e) => e.stopPropagation()}
|
|
className="h-8 w-full rounded-lg border border-slate-200 bg-slate-50 pl-8 pr-2 text-xs text-slate-900 placeholder:text-slate-400 outline-none focus:border-slate-400 focus:bg-white"
|
|
/>
|
|
</div>
|
|
|
|
<div className="max-h-60 overflow-y-auto">
|
|
{filtered.map((loc) => {
|
|
const id = String(loc.tenantlocationid || loc.locationid);
|
|
const isSelected = String(selectedLocationId) === id;
|
|
|
|
return (
|
|
<DropdownMenuItem
|
|
key={id}
|
|
onClick={() => onSelectLocation(id)}
|
|
className={cn(
|
|
'flex items-start justify-between gap-2 px-2.5 py-2 text-xs cursor-pointer rounded-lg mb-0.5',
|
|
isSelected ? 'bg-blue-50 text-blue-900 font-semibold' : 'hover:bg-slate-50'
|
|
)}
|
|
>
|
|
<div className="min-w-0 flex-1">
|
|
<div className="flex items-center gap-1.5">
|
|
<p className="truncate text-xs font-semibold text-slate-900">
|
|
{locationDisplayName(loc)}
|
|
</p>
|
|
{loc.isprimary && (
|
|
<span className="shrink-0 rounded bg-blue-100/80 px-1.5 py-0.2 text-[9px] font-bold text-blue-800">
|
|
PRIMARY
|
|
</span>
|
|
)}
|
|
</div>
|
|
{(loc.address || loc.city) && (
|
|
<p className="mt-0.5 truncate text-[11px] text-slate-500">
|
|
{[loc.address, loc.city].filter(Boolean).join(' · ')}
|
|
</p>
|
|
)}
|
|
</div>
|
|
|
|
{isSelected && <Check className="h-4 w-4 text-blue-600 shrink-0 mt-0.5" />}
|
|
</DropdownMenuItem>
|
|
);
|
|
})}
|
|
|
|
{filtered.length === 0 && (
|
|
<p className="py-4 text-center text-xs text-slate-400">No matching locations found</p>
|
|
)}
|
|
</div>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
);
|
|
}
|