queue-med/client/src/components/PractitionerManager.tsx

233 lines
7.8 KiB
TypeScript

import { useState } from "react";
import { Loader2, Plus, Trash2, UserPlus, Users } from "lucide-react";
import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { toast } from "sonner";
const PRESET_COLORS = [
"#10b981", // emerald
"#06b6d4", // cyan
"#0d9488", // teal
"#8b5cf6", // violet
"#f97316", // orange
"#ec4899", // pink
"#3b82f6", // blue
"#eab308", // yellow
];
export default function PractitionerManager({ clinicId }: { clinicId: number }) {
const utils = trpc.useUtils();
const membersQuery = trpc.clinic.listMembers.useQuery(
{ clinicId },
{ enabled: clinicId > 0 }
);
const [adding, setAdding] = useState(false);
const [email, setEmail] = useState("");
const [displayName, setDisplayName] = useState("");
const [color, setColor] = useState(PRESET_COLORS[0]);
const addMember = trpc.clinic.addMember.useMutation({
onSuccess: () => {
toast.success("Praticien ajouté");
utils.clinic.listMembers.invalidate({ clinicId });
utils.clinic.listMembersPublic.invalidate({ clinicId });
setEmail("");
setDisplayName("");
setColor(PRESET_COLORS[0]);
setAdding(false);
},
onError: (e) => toast.error(e.message),
});
const removeMember = trpc.clinic.removeMember.useMutation({
onSuccess: () => {
toast.success("Praticien retiré");
utils.clinic.listMembers.invalidate({ clinicId });
utils.clinic.listMembersPublic.invalidate({ clinicId });
},
onError: (e) => toast.error(e.message),
});
const updateMember = trpc.clinic.updateMember.useMutation({
onSuccess: () => {
utils.clinic.listMembers.invalidate({ clinicId });
utils.clinic.listMembersPublic.invalidate({ clinicId });
},
onError: (e) => toast.error(e.message),
});
const handleAdd = () => {
if (!email.trim()) {
toast.error("Email requis");
return;
}
addMember.mutate({
clinicId,
email: email.trim(),
displayName: displayName.trim() || undefined,
color,
});
};
const members = membersQuery.data ?? [];
return (
<div className="glass-card rounded-2xl p-5 space-y-4">
<div className="flex items-center justify-between gap-3">
<div className="flex items-center gap-2">
<div className="w-8 h-8 rounded-lg bg-emerald-100 flex items-center justify-center">
<Users className="w-4 h-4 text-emerald-700" />
</div>
<div>
<h3 className="font-semibold text-slate-900">Praticiens du cabinet</h3>
<p className="text-xs text-slate-500">
Multi-médecins · couleur d'identification
</p>
</div>
</div>
<Button
size="sm"
variant={adding ? "ghost" : "outline"}
onClick={() => setAdding((v) => !v)}
>
{adding ? (
"Annuler"
) : (
<>
<Plus className="w-3.5 h-3.5 mr-1" /> Ajouter
</>
)}
</Button>
</div>
{adding && (
<div className="rounded-xl border border-emerald-200/70 bg-emerald-50/40 p-4 space-y-3">
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">
Email du praticien
</label>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="docteur@example.com"
className="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white text-sm focus:outline-none focus:border-emerald-400"
/>
<p className="text-xs text-slate-500 mt-1">
Le praticien doit déjà avoir un compte QueueMed.
</p>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-1">
Nom affiché (optionnel)
</label>
<input
type="text"
value={displayName}
onChange={(e) => setDisplayName(e.target.value)}
placeholder="Dr. Martin"
className="w-full px-3 py-2 rounded-lg border border-slate-200 bg-white text-sm focus:outline-none focus:border-emerald-400"
/>
</div>
<div>
<label className="block text-xs font-bold text-slate-600 mb-2">
Couleur d'identification
</label>
<div className="flex flex-wrap gap-2">
{PRESET_COLORS.map((c) => (
<button
key={c}
onClick={() => setColor(c)}
className={`w-8 h-8 rounded-lg border-2 transition-all ${
color === c
? "border-slate-900 scale-110 shadow-md"
: "border-white"
}`}
style={{ background: c }}
aria-label={`Couleur ${c}`}
/>
))}
<input
type="color"
value={color}
onChange={(e) => setColor(e.target.value)}
className="w-8 h-8 rounded-lg cursor-pointer"
aria-label="Couleur personnalisée"
/>
</div>
</div>
<Button
variant="gradient"
size="sm"
className="w-full"
onClick={handleAdd}
disabled={addMember.isPending}
>
{addMember.isPending ? (
<Loader2 className="w-3.5 h-3.5 mr-1 animate-spin" />
) : (
<UserPlus className="w-3.5 h-3.5 mr-1" />
)}
Ajouter le praticien
</Button>
</div>
)}
{membersQuery.isLoading ? (
<div className="flex items-center justify-center py-6">
<Loader2 className="w-5 h-5 text-emerald-500 animate-spin" />
</div>
) : members.length === 0 ? (
<p className="text-center text-sm text-slate-500 py-6">
Aucun praticien associé. Ajoutez-en un pour activer l'attribution.
</p>
) : (
<ul className="divide-y divide-slate-100">
{members.map((m) => (
<li
key={m.id}
className="flex items-center gap-3 py-3 first:pt-0 last:pb-0"
>
<input
type="color"
value={m.color ?? "#10b981"}
onChange={(e) =>
updateMember.mutate({
clinicId,
memberId: m.id,
color: e.target.value,
})
}
className="w-9 h-9 rounded-lg border border-slate-200 cursor-pointer flex-shrink-0"
aria-label="Modifier la couleur"
/>
<div className="flex-1 min-w-0">
<div className="font-semibold text-slate-900 text-sm truncate">
{m.displayName ?? m.name ?? m.email ?? "—"}
</div>
<div className="text-xs text-slate-500 truncate">
{m.email ?? "—"} · {m.role}
</div>
</div>
<button
onClick={() =>
removeMember.mutate({ clinicId, memberId: m.id })
}
disabled={removeMember.isPending || m.role === "owner"}
className="w-8 h-8 rounded-lg flex items-center justify-center text-red-600 hover:bg-red-50 disabled:opacity-30"
title={
m.role === "owner"
? "Le propriétaire ne peut être retiré"
: "Retirer ce praticien"
}
>
<Trash2 className="w-4 h-4" />
</button>
</li>
))}
</ul>
)}
</div>
);
}