241 lines
11 KiB
TypeScript
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>
|
|
);
|
|
}
|