Files
MSP-CAS/src/components/layout/ClientSelector.tsx
MSP Monitor f4491757d9 Initial commit: MSP Monitor Dashboard
- Next.js 14 frontend with dark cyan/navy theme
- tRPC API with Prisma ORM
- MeshCentral, LibreNMS, Headwind MDM integrations
- Multi-tenant architecture
- Alert system with email/SMS/webhook notifications
- Docker Compose deployment
- Complete documentation
2026-01-21 19:29:20 +00:00

136 lines
4.4 KiB
TypeScript

'use client'
import { useState } from 'react'
import { Building2, ChevronDown, Check, Search } from 'lucide-react'
import { cn } from '@/lib/utils'
interface Client {
id: string
nombre: string
codigo: string
}
interface ClientSelectorProps {
clients?: Client[]
selectedId?: string | null
onChange?: (clientId: string | null) => void
showAll?: boolean
}
export default function ClientSelector({
clients = [],
selectedId = null,
onChange,
showAll = true,
}: ClientSelectorProps) {
const [open, setOpen] = useState(false)
const [search, setSearch] = useState('')
const selectedClient = selectedId
? clients.find((c) => c.id === selectedId)
: null
const filteredClients = clients.filter(
(c) =>
c.nombre.toLowerCase().includes(search.toLowerCase()) ||
c.codigo.toLowerCase().includes(search.toLowerCase())
)
const handleSelect = (id: string | null) => {
onChange?.(id)
setOpen(false)
setSearch('')
}
return (
<div className="relative">
<button
onClick={() => setOpen(!open)}
className="flex items-center gap-2 px-3 py-2 bg-dark-300 border border-dark-100 rounded-lg hover:border-primary-500 transition-colors min-w-[200px]"
>
<Building2 className="w-4 h-4 text-gray-500" />
<span className="flex-1 text-left text-sm">
{selectedClient ? selectedClient.nombre : 'Todos los clientes'}
</span>
<ChevronDown
className={cn(
'w-4 h-4 text-gray-500 transition-transform',
open && 'rotate-180'
)}
/>
</button>
{open && (
<>
<div
className="fixed inset-0 z-40"
onClick={() => {
setOpen(false)
setSearch('')
}}
/>
<div className="absolute left-0 mt-2 w-72 bg-dark-200 border border-dark-100 rounded-lg shadow-lg z-50">
{/* Search */}
<div className="p-2 border-b border-dark-100">
<div className="relative">
<Search className="absolute left-2 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-500" />
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="Buscar cliente..."
className="input py-1.5 pl-8 text-sm"
autoFocus
/>
</div>
</div>
{/* Options */}
<div className="max-h-60 overflow-y-auto p-1">
{showAll && (
<button
onClick={() => handleSelect(null)}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm hover:bg-dark-100 transition-colors',
!selectedId && 'bg-primary-900/50 text-primary-400'
)}
>
<Building2 className="w-4 h-4" />
<span className="flex-1 text-left">Todos los clientes</span>
{!selectedId && <Check className="w-4 h-4" />}
</button>
)}
{filteredClients.length === 0 ? (
<div className="px-3 py-4 text-center text-gray-500 text-sm">
No se encontraron clientes
</div>
) : (
filteredClients.map((client) => (
<button
key={client.id}
onClick={() => handleSelect(client.id)}
className={cn(
'w-full flex items-center gap-2 px-3 py-2 rounded-lg text-sm hover:bg-dark-100 transition-colors',
selectedId === client.id && 'bg-primary-900/50 text-primary-400'
)}
>
<div className="w-8 h-8 rounded-lg bg-dark-100 flex items-center justify-center text-xs font-medium text-gray-400">
{client.codigo.substring(0, 2).toUpperCase()}
</div>
<div className="flex-1 text-left">
<div className="font-medium">{client.nombre}</div>
<div className="text-xs text-gray-500">{client.codigo}</div>
</div>
{selectedId === client.id && <Check className="w-4 h-4" />}
</button>
))
)}
</div>
</div>
</>
)}
</div>
)
}