Files
doormilxpress_astryx/src/components/doormile/HubLocationSelect.jsx

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>
);
}