Files
sonum/client/src/pages/FicheEtablissement.tsx

241 lines
11 KiB
TypeScript

import { useAuth } from "@/_core/hooks/useAuth";
import { EtatBadge, HebergementBadge, FacturationBadge } from "@/components/EtatBadge";
import SonumLayout from "@/components/SonumLayout";
import ContactModal from "@/components/ContactModal";
import { trpc } from "@/lib/trpc";
import {
ArrowLeft,
Building2,
Eye,
Mail,
MapPin,
Phone,
Server,
Tag,
Users,
} from "lucide-react";
import { useEffect, useState } from "react";
import { useLocation } from "wouter";
import { INTEROPERABILITE } from "../../../shared/referentiel";
interface Props {
params: { id: string };
}
export default function FicheEtablissement({ params }: Props) {
const id = Number(params.id);
const { user } = useAuth();
const [, navigate] = useLocation();
const [showContact, setShowContact] = useState(false);
const etabQuery = trpc.etablissements.byId.useQuery({ id }, { enabled: !!id });
const logicielsQuery = trpc.logiciels.byEtablissement.useQuery({ etablissementId: id }, { enabled: !!id });
const recordConsultation = trpc.tracabilite.enregistrerConsultation.useMutation();
const isGestionnaire = user?.sonumRole === "gestionnaire" || user?.role === "admin";
const isReferent = etabQuery.data?.referentId === user?.id;
const canSeeCounter = isReferent || isGestionnaire;
const compteurQuery = trpc.tracabilite.compteur.useQuery(
{ etablissementId: id },
{ enabled: !!id && canSeeCounter }
);
useEffect(() => {
if (id) {
recordConsultation.mutate({ etablissementId: id });
}
}, [id]);
if (etabQuery.isLoading) {
return (
<SonumLayout>
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-2 border-primary border-t-transparent" />
</div>
</SonumLayout>
);
}
if (!etabQuery.data) {
return (
<SonumLayout>
<div className="p-8 text-center text-muted-foreground">Établissement introuvable.</div>
</SonumLayout>
);
}
const etab = etabQuery.data;
// Grouper les logiciels par bloc fonctionnel
const logicielsByBloc = (logicielsQuery.data ?? []).reduce((acc, l) => {
const bloc = l.blocFonctionnelNom ?? "Autres";
if (!acc[bloc]) acc[bloc] = [];
acc[bloc].push(l);
return acc;
}, {} as Record<string, typeof logicielsQuery.data>);
return (
<SonumLayout>
<div className="p-6 lg:p-8 max-w-5xl mx-auto">
{/* Retour */}
<button
onClick={() => navigate(-1 as any)}
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors mb-6"
>
<ArrowLeft size={16} />
Retour
</button>
{/* En-tête fiche */}
<div className="bg-card rounded-xl border border-border shadow-sm p-6 mb-6">
<div className="flex items-start justify-between flex-wrap gap-4">
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-xl bg-primary/10 flex items-center justify-center flex-shrink-0">
<Building2 size={28} className="text-primary" />
</div>
<div>
<h1 className="text-xl font-bold text-foreground mb-1">{etab.nom}</h1>
<div className="flex flex-wrap items-center gap-3 text-sm text-muted-foreground">
{etab.finess && (
<span className="flex items-center gap-1">
<Tag size={13} />
FINESS : {etab.finess}
</span>
)}
{etab.region && (
<span className="flex items-center gap-1">
<MapPin size={13} />
{etab.region}
</span>
)}
{etab.typeActivite && (
<span className="bg-secondary text-secondary-foreground px-2.5 py-0.5 rounded-full text-xs font-medium border border-border">
{etab.typeActivite}
</span>
)}
{etab.tailleEffectifs && (
<span className="flex items-center gap-1">
<Users size={13} />
{etab.tailleEffectifs}
</span>
)}
</div>
</div>
</div>
<div className="flex items-center gap-3">
{/* Compteur de consultation (visible référent + gestionnaire) */}
{canSeeCounter && compteurQuery.data !== undefined && (
<div className="flex items-center gap-2 px-3 py-2 bg-muted rounded-lg border border-border text-sm">
<Eye size={15} className="text-muted-foreground" />
<span className="text-muted-foreground">Consultations :</span>
<span className="font-bold text-foreground">{compteurQuery.data.count}</span>
</div>
)}
{/* Bouton de contact */}
{etab.accepteMiseEnRelation && (
<button
onClick={() => setShowContact(true)}
className="flex items-center gap-2 px-4 py-2 bg-primary text-white rounded-lg text-sm font-medium hover:bg-primary/90 transition-colors shadow-sm"
>
<Mail size={15} />
Prendre contact
</button>
)}
</div>
</div>
</div>
{/* Logiciels par bloc fonctionnel */}
<div>
<h2 className="text-lg font-bold text-foreground mb-4">Solutions numériques</h2>
{logicielsQuery.isLoading ? (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-7 w-7 border-2 border-primary border-t-transparent" />
</div>
) : Object.keys(logicielsByBloc).length === 0 ? (
<div className="text-center py-12 bg-card rounded-xl border border-border">
<Server size={40} className="mx-auto text-muted-foreground/30 mb-3" />
<p className="text-muted-foreground">Aucun logiciel renseigné pour cet établissement.</p>
</div>
) : (
<div className="space-y-4">
{Object.entries(logicielsByBloc).map(([bloc, logiciels]) => (
<div key={bloc} className="bg-card rounded-xl border border-border shadow-sm overflow-hidden">
{/* En-tête bloc */}
<div className="px-5 py-3.5 bg-muted/30 border-b border-border">
<h3 className="text-sm font-semibold text-foreground flex items-center gap-2">
<span className="w-2 h-2 rounded-full bg-primary inline-block" />
{bloc}
<span className="text-xs text-muted-foreground font-normal ml-1">
({logiciels?.length} solution{(logiciels?.length ?? 0) > 1 ? "s" : ""})
</span>
</h3>
</div>
{/* Tableau logiciels */}
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-border">
<th className="text-left px-5 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider">Solution</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden md:table-cell">Éditeur</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider">État</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden lg:table-cell">Hébergement</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden xl:table-cell">Facturation</th>
<th className="text-left px-4 py-3 font-semibold text-muted-foreground text-xs uppercase tracking-wider hidden xl:table-cell">Interop.</th>
</tr>
</thead>
<tbody className="divide-y divide-border">
{logiciels?.map((l) => (
<tr key={l.id} className="hover:bg-muted/20 transition-colors">
<td className="px-5 py-3.5">
<div className="font-medium text-foreground">{l.solutionNom}</div>
{l.versionMajeure && (
<div className="text-xs text-muted-foreground">v{l.versionMajeure}</div>
)}
{l.commentaire && (
<div className="text-xs text-muted-foreground mt-0.5 italic">{l.commentaire}</div>
)}
</td>
<td className="px-4 py-3.5 hidden md:table-cell text-muted-foreground">{l.editeurNom}</td>
<td className="px-4 py-3.5">
<EtatBadge etat={l.etatDeploiement} />
</td>
<td className="px-4 py-3.5 hidden lg:table-cell">
{l.modeHebergement ? <HebergementBadge mode={l.modeHebergement} /> : <span className="text-muted-foreground text-xs"></span>}
</td>
<td className="px-4 py-3.5 hidden xl:table-cell">
{l.modeFacturation ? <FacturationBadge mode={l.modeFacturation} /> : <span className="text-muted-foreground text-xs"></span>}
</td>
<td className="px-4 py-3.5 hidden xl:table-cell">
{l.interoperabilite ? (
<span className="text-xs text-muted-foreground">{INTEROPERABILITE[l.interoperabilite]}</span>
) : <span className="text-muted-foreground text-xs"></span>}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
))}
</div>
)}
</div>
</div>
{showContact && (
<ContactModal
etablissementId={id}
etablissementNom={etab.nom}
onClose={() => setShowContact(false)}
/>
)}
</SonumLayout>
);
}