Files
turnosxpress/txclient/src/app/admin/assistant/components/ClientPicker/ClientPicker.tsx
T

210 lines
8.6 KiB
TypeScript

"use client";
import React, { useEffect, useMemo, useRef, useState } from "react";
import style from "./ClientPicker.module.css";
import CloseIcon from "@mui/icons-material/Close";
import SearchIcon from "@mui/icons-material/Search";
import CheckIcon from "@mui/icons-material/Check";
import PersonOffOutlined from "@mui/icons-material/PersonOffOutlined";
import { OrganizationClientView } from "@models/Clients.model";
export interface ClientPickerProps {
open: boolean;
clients: OrganizationClientView[];
selectedId: string | null;
onSelect: (id: string) => void;
onClose: () => void;
loading?: boolean;
}
function formatPhone(c: OrganizationClientView): string {
const parts = [c.phoneCountryCode, c.phoneAreaCode, c.phoneNumber]
.filter(p => p != null && String(p).trim() !== "");
if (parts.length === 0) return "";
const joined = parts.map(p => String(p).trim()).join(" ");
return joined.startsWith("+") ? joined : `+${joined}`;
}
function getInitials(c: OrganizationClientView): string {
const f = (c.firstName?.[0] || "").toUpperCase();
const l = (c.lastName?.[0] || "").toUpperCase();
return `${f}${l}` || "?";
}
export default function ClientPicker({
open,
clients,
selectedId,
onSelect,
onClose,
loading,
}: ClientPickerProps) {
const [query, setQuery] = useState("");
const inputRef = useRef<HTMLInputElement | null>(null);
useEffect(() => {
if (!open) return;
const onKey = (e: KeyboardEvent) => {
if (e.key === "Escape") onClose();
};
window.addEventListener("keydown", onKey);
setQuery("");
const t = setTimeout(() => {
inputRef.current?.focus();
inputRef.current?.select();
}, 80);
return () => {
window.removeEventListener("keydown", onKey);
clearTimeout(t);
};
}, [open, onClose]);
const filtered = useMemo(() => {
const q = query.trim().toLowerCase();
if (!q) return clients;
return clients.filter(c =>
(c.firstName || "").toLowerCase().includes(q) ||
(c.lastName || "").toLowerCase().includes(q) ||
(c.fullName || "").toLowerCase().includes(q)
);
}, [clients, query]);
if (!open) return null;
const hasClients = clients.length > 0;
const noResults = hasClients && filtered.length === 0 && query.trim() !== "";
const searching = query.trim() !== "";
return (
<div
className={style.overlay}
onClick={onClose}
role="dialog"
aria-modal="true"
aria-label="Seleccionar cliente"
>
<div
className={style.sheet}
onClick={(e) => e.stopPropagation()}
>
<header className={style.header}>
<h2>Selecciona un cliente</h2>
<button
type="button"
className={style.closeButton}
onClick={onClose}
aria-label="Cerrar"
>
<CloseIcon style={{ fontSize: 20 }} />
</button>
</header>
<div className={style.searchWrap}>
<SearchIcon
style={{
fontSize: 22,
position: "absolute",
left: "14px",
top: "50%",
transform: "translateY(-50%)",
color: "rgba(255,255,255,0.5)",
pointerEvents: "none",
}}
/>
<input
ref={inputRef}
type="text"
className={style.searchInput}
placeholder="Buscar por nombre o apellido..."
value={query}
onChange={(e) => setQuery(e.target.value)}
aria-label="Buscar cliente"
autoComplete="off"
spellCheck={false}
/>
{searching && (
<button
type="button"
className={style.clearButton}
onClick={() => setQuery("")}
aria-label="Limpiar búsqueda"
>
<CloseIcon style={{ fontSize: 16 }} />
</button>
)}
</div>
<div className={style.results}>
{loading ? (
<div className={style.centerState}>
<div className={style.spinner} />
<p className={style.emptyText}>Cargando clientes</p>
</div>
) : !hasClients ? (
<div className={style.centerState}>
<PersonOffOutlined style={{ fontSize: 46, color: "rgba(255,255,255,0.35)" }} />
<p className={style.emptyTitle}>Sin clientes todavía</p>
<p className={style.emptyText}>
No hay clientes para esta organización todavía. Cuando alguien reserve un turno, aparecerá acá.
</p>
</div>
) : noResults ? (
<div className={style.centerState}>
<p className={style.emptyTitle}>No encontramos clientes con ese nombre.</p>
<button
type="button"
className={style.clearCta}
onClick={() => setQuery("")}
>
Limpiar búsqueda
</button>
</div>
) : (
filtered.map(c => {
const isSelected = c.id === selectedId;
const sub = (c.email && c.email.trim() !== "") ? c.email : formatPhone(c);
const avatar = c.avatar && c.avatar.trim() !== "";
return (
<button
key={c.id}
type="button"
className={`${style.clientCard} ${isSelected ? style.clientCardSelected : ""}`}
onClick={() => onSelect(c.id)}
>
<div className={style.avatarWrap}>
<div className={style.avatarFallback}>{getInitials(c)}</div>
{avatar && (
<img
src={c.avatar}
alt={c.fullName}
className={style.avatarImg}
onError={(e) => {
(e.currentTarget as HTMLImageElement).style.opacity = "0";
}}
/>
)}
</div>
<div className={style.cardBody}>
<div className={style.cardTitle}>
<span className={style.fullName}>{c.fullName}</span>
{c.banned && (
<span className={style.bannedChip}>Suspendido</span>
)}
</div>
{sub && <span className={style.subLine}>{sub}</span>}
</div>
{isSelected && (
<span className={style.checkWrap} aria-label="Seleccionado">
<CheckIcon style={{ fontSize: 18 }} />
</span>
)}
</button>
);
})
)}
</div>
</div>
</div>
);
}