Checkpoint: Ajout de la page Annuaire du personnel — 24 agents fictifs répartis sur 14 services, barre de recherche full-text, filtres par service (select + pastilles rapides) et par disponibilité, vue grille et vue liste, modale fiche agent avec coordonnées complètes. Accessible depuis la sidebar admin (/annuaire) et depuis le bouton "Ouvrir l'annuaire complet" dans la vue publique de l'intranet.

This commit is contained in:
Manus 2026-05-21 20:29:58 +00:00
parent 78228569a1
commit b37e19fb01
4 changed files with 516 additions and 1 deletions

View file

@ -17,6 +17,7 @@ import Notifications from "./pages/Notifications";
import Roles from "./pages/Roles";
import Parametres from "./pages/Parametres";
import EditeurPage from "./pages/EditeurPage";
import Annuaire from "./pages/Annuaire";
// Pages publiques (sans AdminLayout)
import Connexion from "./pages/Connexion";
@ -34,6 +35,7 @@ function AdminRouter() {
<Route path="/widgets" component={Widgets} />
<Route path="/applications" component={Applications} />
<Route path="/utilisateurs" component={Utilisateurs} />
<Route path="/annuaire" component={Annuaire} />
<Route path="/statistiques" component={Statistiques} />
<Route path="/notifications" component={Notifications} />
<Route path="/roles" component={Roles} />

View file

@ -22,6 +22,7 @@ import {
LogOut,
ExternalLink,
User,
BookUser,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
@ -41,6 +42,7 @@ const NAV_ITEMS = [
{ icon: Puzzle, label: "Widgets", path: "/widgets" },
{ icon: Globe, label: "Applications", path: "/applications" },
{ icon: Users, label: "Utilisateurs", path: "/utilisateurs" },
{ icon: BookUser, label: "Annuaire", path: "/annuaire" },
{ icon: BarChart3, label: "Statistiques", path: "/statistiques" },
{ icon: Bell, label: "Notifications", path: "/notifications" },
{ icon: Shield, label: "Rôles et droits", path: "/roles" },

View file

@ -0,0 +1,511 @@
/**
* Page Annuaire du personnel Intranet CHK
* Recherche par nom/prénom/poste, filtres par service, vue carte et liste
*/
import { useState, useMemo } from "react";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import { Card, CardContent } from "@/components/ui/card";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import {
Search,
Phone,
Mail,
MapPin,
LayoutGrid,
List,
Users,
Filter,
X,
ChevronRight,
Building2,
Stethoscope,
Wrench,
BookOpen,
Shield,
HeartPulse,
FlaskConical,
Pill,
Baby,
Ambulance,
Scan,
Utensils,
Landmark,
} from "lucide-react";
import { toast } from "sonner";
// ─── Données du personnel ────────────────────────────────────────────────────
const SERVICES = [
{ id: "tous", label: "Tous les services", icone: Users, couleur: "bg-slate-100 text-slate-700" },
{ id: "direction", label: "Direction", icone: Landmark, couleur: "bg-violet-100 text-violet-700" },
{ id: "urgences", label: "Urgences", icone: Ambulance, couleur: "bg-red-100 text-red-700" },
{ id: "medecine", label: "Médecine générale", icone: Stethoscope, couleur: "bg-blue-100 text-blue-700" },
{ id: "chirurgie", label: "Chirurgie", icone: HeartPulse, couleur: "bg-pink-100 text-pink-700" },
{ id: "pediatrie", label: "Pédiatrie", icone: Baby, couleur: "bg-yellow-100 text-yellow-700" },
{ id: "imagerie", label: "Imagerie médicale", icone: Scan, couleur: "bg-cyan-100 text-cyan-700" },
{ id: "laboratoire", label: "Laboratoire", icone: FlaskConical,couleur: "bg-emerald-100 text-emerald-700" },
{ id: "pharmacie", label: "Pharmacie", icone: Pill, couleur: "bg-orange-100 text-orange-700" },
{ id: "dsi", label: "DSI / Informatique", icone: Wrench, couleur: "bg-indigo-100 text-indigo-700" },
{ id: "rh", label: "Ressources humaines", icone: Shield, couleur: "bg-teal-100 text-teal-700" },
{ id: "formation", label: "Formation", icone: BookOpen, couleur: "bg-lime-100 text-lime-700" },
{ id: "logistique", label: "Logistique", icone: Building2, couleur: "bg-amber-100 text-amber-700" },
{ id: "restauration",label: "Restauration", icone: Utensils, couleur: "bg-rose-100 text-rose-700" },
];
interface Agent {
id: number;
prenom: string;
nom: string;
poste: string;
service: string;
telephone: string;
email: string;
bureau: string;
disponible: boolean;
initiales: string;
couleurAvatar: string;
}
const PERSONNEL: Agent[] = [
{ id: 1, prenom: "Pierre", nom: "Boursiquot", poste: "Directeur des systèmes d'information", service: "dsi", telephone: "4242", email: "p.boursiquot@chk.gf", bureau: "Bât. Admin — Bureau 201", disponible: true, initiales: "PB", couleurAvatar: "#1a5276" },
{ id: 2, prenom: "Marie", nom: "Dupont", poste: "Directrice générale", service: "direction", telephone: "4000", email: "m.dupont@chk.gf", bureau: "Bât. Admin — Bureau 100", disponible: true, initiales: "MD", couleurAvatar: "#6c3483" },
{ id: 3, prenom: "Jean-Luc", nom: "Martin", poste: "Chef de service — Urgences", service: "urgences", telephone: "4100", email: "jl.martin@chk.gf", bureau: "Urgences — Poste central",disponible: true, initiales: "JM", couleurAvatar: "#c0392b" },
{ id: 4, prenom: "Sophie", nom: "Lebrun", poste: "Infirmière coordinatrice", service: "urgences", telephone: "4101", email: "s.lebrun@chk.gf", bureau: "Urgences — Salle de soins",disponible: false, initiales: "SL", couleurAvatar: "#e74c3c" },
{ id: 5, prenom: "Thierry", nom: "Cazal", poste: "Médecin généraliste", service: "medecine", telephone: "4200", email: "t.cazal@chk.gf", bureau: "Médecine — Bureau 12", disponible: true, initiales: "TC", couleurAvatar: "#2980b9" },
{ id: 6, prenom: "Amandine", nom: "Rivière", poste: "Chirurgien orthopédiste", service: "chirurgie", telephone: "4300", email: "a.riviere@chk.gf", bureau: "Chirurgie — Bloc A", disponible: true, initiales: "AR", couleurAvatar: "#c0392b" },
{ id: 7, prenom: "Karim", nom: "Benmoussa", poste: "Pédiatre", service: "pediatrie", telephone: "4400", email: "k.benmoussa@chk.gf", bureau: "Pédiatrie — Bureau 5", disponible: true, initiales: "KB", couleurAvatar: "#f39c12" },
{ id: 8, prenom: "Lucie", nom: "Fontaine", poste: "Infirmière puéricultrice", service: "pediatrie", telephone: "4401", email: "l.fontaine@chk.gf", bureau: "Pédiatrie — Salle 3", disponible: false, initiales: "LF", couleurAvatar: "#e67e22" },
{ id: 9, prenom: "Éric", nom: "Moreau", poste: "Radiologue", service: "imagerie", telephone: "4500", email: "e.moreau@chk.gf", bureau: "Imagerie — Salle IRM", disponible: true, initiales: "EM", couleurAvatar: "#0e6655" },
{ id: 10, prenom: "Nathalie", nom: "Guérin", poste: "Technicienne de radiologie", service: "imagerie", telephone: "4501", email: "n.guerin@chk.gf", bureau: "Imagerie — Scanner", disponible: true, initiales: "NG", couleurAvatar: "#117a65" },
{ id: 11, prenom: "Pascal", nom: "Théodore", poste: "Biologiste médical", service: "laboratoire", telephone: "4600", email: "p.theodore@chk.gf", bureau: "Labo — Bureau principal", disponible: true, initiales: "PT", couleurAvatar: "#1e8449" },
{ id: 12, prenom: "Isabelle", nom: "Noël", poste: "Technicienne de laboratoire", service: "laboratoire", telephone: "4601", email: "i.noel@chk.gf", bureau: "Labo — Salle d'analyses", disponible: false, initiales: "IN", couleurAvatar: "#27ae60" },
{ id: 13, prenom: "Franck", nom: "Delorme", poste: "Pharmacien hospitalier", service: "pharmacie", telephone: "4700", email: "f.delorme@chk.gf", bureau: "Pharmacie — Comptoir", disponible: true, initiales: "FD", couleurAvatar: "#d35400" },
{ id: 14, prenom: "Céline", nom: "Aubert", poste: "Préparatrice en pharmacie", service: "pharmacie", telephone: "4701", email: "c.aubert@chk.gf", bureau: "Pharmacie — Réserve", disponible: true, initiales: "CA", couleurAvatar: "#e67e22" },
{ id: 15, prenom: "Marc", nom: "Loiseau", poste: "Technicien informatique", service: "dsi", telephone: "4243", email: "m.loiseau@chk.gf", bureau: "DSI — Salle serveurs", disponible: true, initiales: "ML", couleurAvatar: "#2471a3" },
{ id: 16, prenom: "Valérie", nom: "Petit", poste: "Responsable RH", service: "rh", telephone: "4800", email: "v.petit@chk.gf", bureau: "RH — Bureau 302", disponible: true, initiales: "VP", couleurAvatar: "#148f77" },
{ id: 17, prenom: "Olivier", nom: "Charron", poste: "Chargé de formation", service: "formation", telephone: "4900", email: "o.charron@chk.gf", bureau: "Formation — Salle 1", disponible: false, initiales: "OC", couleurAvatar: "#7d6608" },
{ id: 18, prenom: "Sandra", nom: "Beaumont", poste: "Responsable logistique", service: "logistique", telephone: "5000", email: "s.beaumont@chk.gf", bureau: "Logistique — Entrepôt", disponible: true, initiales: "SB", couleurAvatar: "#784212" },
{ id: 19, prenom: "René", nom: "Chabrier", poste: "Chef cuisinier", service: "restauration", telephone: "5100", email: "r.chabrier@chk.gf", bureau: "Restauration — Cuisine", disponible: true, initiales: "RC", couleurAvatar: "#922b21" },
{ id: 20, prenom: "Hélène", nom: "Voisin", poste: "Directrice des soins infirmiers", service: "direction", telephone: "4001", email: "h.voisin@chk.gf", bureau: "Bât. Admin — Bureau 102", disponible: true, initiales: "HV", couleurAvatar: "#7d3c98" },
{ id: 21, prenom: "Antoine", nom: "Sainte-Rose", poste: "Médecin urgentiste", service: "urgences", telephone: "4102", email: "a.sainteRose@chk.gf", bureau: "Urgences — SAMU", disponible: true, initiales: "AS", couleurAvatar: "#a93226" },
{ id: 22, prenom: "Marlène", nom: "Joachim", poste: "Aide-soignante", service: "medecine", telephone: "4201", email: "m.joachim@chk.gf", bureau: "Médecine — Couloir B", disponible: false, initiales: "MJ", couleurAvatar: "#1f618d" },
{ id: 23, prenom: "Bertrand", nom: "Quentin", poste: "Anesthésiste-réanimateur", service: "chirurgie", telephone: "4301", email: "b.quentin@chk.gf", bureau: "Chirurgie — Bloc B", disponible: true, initiales: "BQ", couleurAvatar: "#943126" },
{ id: 24, prenom: "Laure", nom: "Ménil", poste: "Manipulatrice en électroradiologie", service: "imagerie", telephone: "4502", email: "l.menil@chk.gf", bureau: "Imagerie — Radiologie", disponible: true, initiales: "LM", couleurAvatar: "#0b6e4f" },
];
// ─── Composant fiche agent ────────────────────────────────────────────────────
function CarteAgent({ agent, onClick }: { agent: Agent; onClick: () => void }) {
const service = SERVICES.find((s) => s.id === agent.service);
return (
<Card
className="shadow-sm hover:shadow-md transition-all duration-200 cursor-pointer group border-slate-200 hover:border-accent/40"
onClick={onClick}
>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className="relative shrink-0">
<Avatar className="w-12 h-12">
<AvatarFallback
className="text-sm font-bold text-white"
style={{ backgroundColor: agent.couleurAvatar }}
>
{agent.initiales}
</AvatarFallback>
</Avatar>
<span
className={`absolute -bottom-0.5 -right-0.5 w-3.5 h-3.5 rounded-full border-2 border-white ${
agent.disponible ? "bg-emerald-400" : "bg-slate-300"
}`}
/>
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-semibold text-slate-800 truncate">
{agent.prenom} {agent.nom}
</p>
<p className="text-xs text-slate-500 truncate mt-0.5">{agent.poste}</p>
{service && (
<Badge
variant="secondary"
className={`text-[10px] px-1.5 py-0 mt-1.5 ${service.couleur} border-0`}
>
{service.label}
</Badge>
)}
</div>
<ChevronRight className="w-4 h-4 text-slate-300 group-hover:text-accent transition-colors shrink-0 mt-1" />
</div>
<div className="mt-3 pt-3 border-t border-slate-100 flex items-center gap-3">
<button
className="flex items-center gap-1.5 text-[11px] text-slate-500 hover:text-accent transition-colors"
onClick={(e) => { e.stopPropagation(); toast.info(`Appel du poste ${agent.telephone}`); }}
>
<Phone className="w-3 h-3" /> {agent.telephone}
</button>
<button
className="flex items-center gap-1.5 text-[11px] text-slate-500 hover:text-accent transition-colors truncate"
onClick={(e) => { e.stopPropagation(); toast.info(`E-mail : ${agent.email}`); }}
>
<Mail className="w-3 h-3 shrink-0" />
<span className="truncate">{agent.email}</span>
</button>
</div>
</CardContent>
</Card>
);
}
function LigneAgent({ agent, onClick }: { agent: Agent; onClick: () => void }) {
const service = SERVICES.find((s) => s.id === agent.service);
return (
<tr
className="border-b border-slate-100 hover:bg-slate-50 cursor-pointer transition-colors group"
onClick={onClick}
>
<td className="py-3 px-4">
<div className="flex items-center gap-3">
<div className="relative shrink-0">
<Avatar className="w-8 h-8">
<AvatarFallback
className="text-xs font-bold text-white"
style={{ backgroundColor: agent.couleurAvatar }}
>
{agent.initiales}
</AvatarFallback>
</Avatar>
<span
className={`absolute -bottom-0.5 -right-0.5 w-2.5 h-2.5 rounded-full border-2 border-white ${
agent.disponible ? "bg-emerald-400" : "bg-slate-300"
}`}
/>
</div>
<div>
<p className="text-sm font-semibold text-slate-800">
{agent.prenom} {agent.nom}
</p>
<p className="text-xs text-slate-500">{agent.poste}</p>
</div>
</div>
</td>
<td className="py-3 px-4 hidden md:table-cell">
{service && (
<Badge variant="secondary" className={`text-[10px] ${service.couleur} border-0`}>
{service.label}
</Badge>
)}
</td>
<td className="py-3 px-4 hidden lg:table-cell">
<button
className="text-xs text-slate-600 hover:text-accent flex items-center gap-1.5"
onClick={(e) => { e.stopPropagation(); toast.info(`Appel du poste ${agent.telephone}`); }}
>
<Phone className="w-3 h-3" /> {agent.telephone}
</button>
</td>
<td className="py-3 px-4 hidden xl:table-cell">
<button
className="text-xs text-slate-600 hover:text-accent flex items-center gap-1.5 truncate max-w-[200px]"
onClick={(e) => { e.stopPropagation(); toast.info(`E-mail : ${agent.email}`); }}
>
<Mail className="w-3 h-3 shrink-0" />
<span className="truncate">{agent.email}</span>
</button>
</td>
<td className="py-3 px-4 hidden lg:table-cell">
<span className="text-xs text-slate-500 flex items-center gap-1">
<MapPin className="w-3 h-3" /> {agent.bureau}
</span>
</td>
<td className="py-3 px-4">
<span className={`inline-flex items-center gap-1 text-[11px] font-medium ${agent.disponible ? "text-emerald-600" : "text-slate-400"}`}>
<span className={`w-1.5 h-1.5 rounded-full ${agent.disponible ? "bg-emerald-500" : "bg-slate-300"}`} />
{agent.disponible ? "Disponible" : "Absent(e)"}
</span>
</td>
</tr>
);
}
// ─── Composant principal ─────────────────────────────────────────────────────
export default function Annuaire() {
const [recherche, setRecherche] = useState("");
const [serviceActif, setServiceActif] = useState("tous");
const [disponibilite, setDisponibilite] = useState("tous");
const [vue, setVue] = useState<"grille" | "liste">("grille");
const [agentSelectionne, setAgentSelectionne] = useState<Agent | null>(null);
const personnelFiltre = useMemo(() => {
return PERSONNEL.filter((agent) => {
const terme = recherche.toLowerCase();
const correspondRecherche =
!terme ||
agent.prenom.toLowerCase().includes(terme) ||
agent.nom.toLowerCase().includes(terme) ||
agent.poste.toLowerCase().includes(terme) ||
agent.email.toLowerCase().includes(terme);
const correspondService = serviceActif === "tous" || agent.service === serviceActif;
const correspondDispo =
disponibilite === "tous" ||
(disponibilite === "disponible" && agent.disponible) ||
(disponibilite === "absent" && !agent.disponible);
return correspondRecherche && correspondService && correspondDispo;
});
}, [recherche, serviceActif, disponibilite]);
const serviceInfo = SERVICES.find((s) => s.id === serviceActif);
const nbDisponibles = personnelFiltre.filter((a) => a.disponible).length;
return (
<div className="space-y-5">
{/* En-tête */}
<div className="flex flex-col sm:flex-row sm:items-center justify-between gap-3">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Annuaire du personnel</h1>
<p className="text-sm text-muted-foreground mt-0.5">
{personnelFiltre.length} agent{personnelFiltre.length > 1 ? "s" : ""} trouvé{personnelFiltre.length > 1 ? "s" : ""}
{" "} <span className="text-emerald-600 font-medium">{nbDisponibles} disponible{nbDisponibles > 1 ? "s" : ""}</span>
</p>
</div>
<div className="flex items-center gap-2">
<Button
variant={vue === "grille" ? "default" : "outline"}
size="sm"
onClick={() => setVue("grille")}
className="gap-1.5"
>
<LayoutGrid className="w-4 h-4" />
<span className="hidden sm:inline">Grille</span>
</Button>
<Button
variant={vue === "liste" ? "default" : "outline"}
size="sm"
onClick={() => setVue("liste")}
className="gap-1.5"
>
<List className="w-4 h-4" />
<span className="hidden sm:inline">Liste</span>
</Button>
</div>
</div>
{/* Barre de recherche + filtres */}
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm space-y-3">
{/* Recherche */}
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Rechercher par nom, prénom, poste ou e-mail…"
className="pl-9 h-10"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
/>
{recherche && (
<button
className="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground hover:text-foreground"
onClick={() => setRecherche("")}
>
<X className="w-4 h-4" />
</button>
)}
</div>
{/* Filtres */}
<div className="flex flex-wrap items-center gap-2">
<Filter className="w-4 h-4 text-muted-foreground shrink-0" />
<Select value={serviceActif} onValueChange={setServiceActif}>
<SelectTrigger className="h-8 w-auto min-w-[180px] text-xs">
<SelectValue placeholder="Tous les services" />
</SelectTrigger>
<SelectContent>
{SERVICES.map((s) => (
<SelectItem key={s.id} value={s.id} className="text-xs">
{s.label}
</SelectItem>
))}
</SelectContent>
</Select>
<Select value={disponibilite} onValueChange={setDisponibilite}>
<SelectTrigger className="h-8 w-auto min-w-[140px] text-xs">
<SelectValue placeholder="Disponibilité" />
</SelectTrigger>
<SelectContent>
<SelectItem value="tous" className="text-xs">Toutes disponibilités</SelectItem>
<SelectItem value="disponible" className="text-xs">Disponible</SelectItem>
<SelectItem value="absent" className="text-xs">Absent(e)</SelectItem>
</SelectContent>
</Select>
{(serviceActif !== "tous" || disponibilite !== "tous" || recherche) && (
<Button
variant="ghost"
size="sm"
className="h-8 text-xs text-muted-foreground hover:text-foreground gap-1"
onClick={() => { setServiceActif("tous"); setDisponibilite("tous"); setRecherche(""); }}
>
<X className="w-3 h-3" /> Réinitialiser
</Button>
)}
</div>
{/* Pastilles de services rapides */}
<div className="flex flex-wrap gap-1.5 pt-1">
{SERVICES.slice(1, 9).map((s) => (
<button
key={s.id}
onClick={() => setServiceActif(serviceActif === s.id ? "tous" : s.id)}
className={`inline-flex items-center gap-1 text-[11px] font-medium px-2.5 py-1 rounded-full border transition-all duration-150 ${
serviceActif === s.id
? `${s.couleur} border-current shadow-sm`
: "bg-slate-50 text-slate-500 border-slate-200 hover:border-slate-300"
}`}
>
<s.icone className="w-3 h-3" />
{s.label}
</button>
))}
</div>
</div>
{/* Résultats */}
{personnelFiltre.length === 0 ? (
<div className="text-center py-16 text-muted-foreground">
<Users className="w-12 h-12 mx-auto mb-3 opacity-30" />
<p className="font-medium">Aucun agent trouvé</p>
<p className="text-sm mt-1">Essayez de modifier vos critères de recherche.</p>
</div>
) : vue === "grille" ? (
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-3">
{personnelFiltre.map((agent) => (
<CarteAgent key={agent.id} agent={agent} onClick={() => setAgentSelectionne(agent)} />
))}
</div>
) : (
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<table className="w-full">
<thead>
<tr className="bg-slate-50 border-b border-slate-200">
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wide py-3 px-4">Agent</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wide py-3 px-4 hidden md:table-cell">Service</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wide py-3 px-4 hidden lg:table-cell">Poste</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wide py-3 px-4 hidden xl:table-cell">E-mail</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wide py-3 px-4 hidden lg:table-cell">Bureau</th>
<th className="text-left text-xs font-semibold text-slate-500 uppercase tracking-wide py-3 px-4">Statut</th>
</tr>
</thead>
<tbody>
{personnelFiltre.map((agent) => (
<LigneAgent key={agent.id} agent={agent} onClick={() => setAgentSelectionne(agent)} />
))}
</tbody>
</table>
</div>
)}
{/* Modale fiche agent */}
<Dialog open={!!agentSelectionne} onOpenChange={() => setAgentSelectionne(null)}>
{agentSelectionne && (
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle className="sr-only">Fiche agent</DialogTitle>
</DialogHeader>
<div className="space-y-4">
{/* En-tête fiche */}
<div className="flex items-center gap-4">
<div className="relative">
<Avatar className="w-16 h-16">
<AvatarFallback
className="text-xl font-bold text-white"
style={{ backgroundColor: agentSelectionne.couleurAvatar }}
>
{agentSelectionne.initiales}
</AvatarFallback>
</Avatar>
<span
className={`absolute -bottom-0.5 -right-0.5 w-4 h-4 rounded-full border-2 border-white ${
agentSelectionne.disponible ? "bg-emerald-400" : "bg-slate-300"
}`}
/>
</div>
<div>
<h2 className="text-lg font-[Outfit] font-bold text-foreground">
{agentSelectionne.prenom} {agentSelectionne.nom}
</h2>
<p className="text-sm text-muted-foreground">{agentSelectionne.poste}</p>
<span className={`inline-flex items-center gap-1 text-xs font-medium mt-1 ${agentSelectionne.disponible ? "text-emerald-600" : "text-slate-400"}`}>
<span className={`w-1.5 h-1.5 rounded-full ${agentSelectionne.disponible ? "bg-emerald-500" : "bg-slate-300"}`} />
{agentSelectionne.disponible ? "Disponible" : "Absent(e)"}
</span>
</div>
</div>
{/* Service */}
{(() => {
const s = SERVICES.find((sv) => sv.id === agentSelectionne.service);
return s ? (
<div className={`flex items-center gap-2 px-3 py-2 rounded-lg ${s.couleur}`}>
<s.icone className="w-4 h-4" />
<span className="text-sm font-medium">{s.label}</span>
</div>
) : null;
})()}
{/* Coordonnées */}
<div className="space-y-2 pt-1">
<button
className="w-full flex items-center gap-3 p-3 rounded-lg border border-slate-200 hover:border-accent/40 hover:bg-slate-50 transition-colors text-left"
onClick={() => toast.info(`Appel du poste ${agentSelectionne.telephone}`)}
>
<div className="w-8 h-8 rounded-lg bg-blue-50 flex items-center justify-center shrink-0">
<Phone className="w-4 h-4 text-blue-600" />
</div>
<div>
<p className="text-[11px] text-muted-foreground">Poste interne</p>
<p className="text-sm font-semibold text-foreground">{agentSelectionne.telephone}</p>
</div>
</button>
<button
className="w-full flex items-center gap-3 p-3 rounded-lg border border-slate-200 hover:border-accent/40 hover:bg-slate-50 transition-colors text-left"
onClick={() => toast.info(`Rédiger un e-mail à ${agentSelectionne.email}`)}
>
<div className="w-8 h-8 rounded-lg bg-violet-50 flex items-center justify-center shrink-0">
<Mail className="w-4 h-4 text-violet-600" />
</div>
<div>
<p className="text-[11px] text-muted-foreground">Adresse e-mail</p>
<p className="text-sm font-semibold text-foreground">{agentSelectionne.email}</p>
</div>
</button>
<div className="flex items-center gap-3 p-3 rounded-lg border border-slate-200">
<div className="w-8 h-8 rounded-lg bg-amber-50 flex items-center justify-center shrink-0">
<MapPin className="w-4 h-4 text-amber-600" />
</div>
<div>
<p className="text-[11px] text-muted-foreground">Bureau</p>
<p className="text-sm font-semibold text-foreground">{agentSelectionne.bureau}</p>
</div>
</div>
</div>
</div>
</DialogContent>
)}
</Dialog>
</div>
);
}

View file

@ -342,7 +342,7 @@ export default function IntranetPublic() {
</button>
</div>
))}
<button className="w-full text-xs text-accent hover:underline text-center pt-1" onClick={() => toast.info("Annuaire complet — Fonctionnalité à venir")}>
<button className="w-full text-xs text-accent hover:underline text-center pt-1" onClick={() => { window.location.href = "/annuaire"; }}>
Ouvrir l'annuaire complet
</button>
</CardContent>