180 lines
8.1 KiB
TypeScript
180 lines
8.1 KiB
TypeScript
import { trpc } from "@/lib/trpc";
|
|
import { useAuth } from "@/_core/hooks/useAuth";
|
|
import SonumLayout from "@/components/SonumLayout";
|
|
import { ChevronDown, ChevronRight, Building2, Package, Search, Filter } from "lucide-react";
|
|
import { useState, useMemo } from "react";
|
|
import { EtatBadge } from "@/components/EtatBadge";
|
|
|
|
export default function MesSolutions() {
|
|
const { isAuthenticated } = useAuth();
|
|
const [openIds, setOpenIds] = useState<Set<number>>(new Set());
|
|
const [search, setSearch] = useState("");
|
|
const [filterBloc, setFilterBloc] = useState("");
|
|
|
|
const { data: solutions, isLoading } = trpc.logiciels.mesSolutions.useQuery(undefined, {
|
|
enabled: isAuthenticated,
|
|
});
|
|
const { data: blocs } = trpc.referentiel.blocsFonctionnels.useQuery();
|
|
|
|
const toggle = (id: number) => {
|
|
setOpenIds((prev) => {
|
|
const next = new Set(prev);
|
|
if (next.has(id)) next.delete(id);
|
|
else next.add(id);
|
|
return next;
|
|
});
|
|
};
|
|
|
|
const filtered = useMemo(() => {
|
|
if (!solutions) return [];
|
|
return solutions.filter((s) => {
|
|
const matchSearch =
|
|
!search ||
|
|
s.solutionNom.toLowerCase().includes(search.toLowerCase()) ||
|
|
s.editeurNom.toLowerCase().includes(search.toLowerCase());
|
|
const matchBloc = !filterBloc || s.blocFonctionnelNom === filterBloc;
|
|
return matchSearch && matchBloc;
|
|
});
|
|
}, [solutions, search, filterBloc]);
|
|
|
|
const blocsUniques = useMemo(() => {
|
|
if (!solutions) return [];
|
|
const set = new Set(solutions.map((s) => s.blocFonctionnelNom).filter(Boolean) as string[]);
|
|
return Array.from(set);
|
|
}, [solutions]);
|
|
|
|
return (
|
|
<SonumLayout>
|
|
<div className="max-w-5xl mx-auto px-4 py-8">
|
|
{/* En-tête */}
|
|
<div className="mb-8">
|
|
<div className="flex items-center gap-3 mb-2">
|
|
<div className="w-10 h-10 rounded-xl bg-primary/10 flex items-center justify-center">
|
|
<Package size={20} className="text-primary" />
|
|
</div>
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-foreground">Mes Solutions Numériques</h1>
|
|
<p className="text-sm text-muted-foreground">
|
|
{solutions ? `${solutions.length} solution${solutions.length > 1 ? "s" : ""} référencée${solutions.length > 1 ? "s" : ""}` : "Chargement…"}
|
|
</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Filtres */}
|
|
<div className="flex flex-col sm:flex-row gap-3 mb-6">
|
|
<div className="relative flex-1">
|
|
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
<input
|
|
type="text"
|
|
placeholder="Rechercher par solution ou éditeur…"
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="w-full pl-9 pr-4 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 focus:border-primary"
|
|
/>
|
|
</div>
|
|
<div className="relative">
|
|
<Filter size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
|
<select
|
|
value={filterBloc}
|
|
onChange={(e) => setFilterBloc(e.target.value)}
|
|
className="pl-9 pr-8 py-2.5 text-sm bg-background border border-border rounded-lg focus:outline-none focus:ring-2 focus:ring-primary/30 appearance-none min-w-[200px]"
|
|
>
|
|
<option value="">Tous les blocs fonctionnels</option>
|
|
{blocsUniques.map((b) => (
|
|
<option key={b} value={b}>{b}</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Liste */}
|
|
{isLoading ? (
|
|
<div className="space-y-3">
|
|
{[...Array(4)].map((_, i) => (
|
|
<div key={i} className="h-16 bg-muted/30 rounded-xl animate-pulse" />
|
|
))}
|
|
</div>
|
|
) : filtered.length === 0 ? (
|
|
<div className="text-center py-16 text-muted-foreground">
|
|
<Package size={40} className="mx-auto mb-3 opacity-30" />
|
|
<p className="font-medium">Aucune solution trouvée</p>
|
|
<p className="text-sm mt-1">Rattachez des solutions à vos établissements pour les voir apparaître ici.</p>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-2">
|
|
{filtered.map((sol) => {
|
|
const isOpen = openIds.has(sol.solutionId);
|
|
return (
|
|
<div
|
|
key={sol.solutionId}
|
|
className="border border-border rounded-xl overflow-hidden bg-card shadow-sm hover:shadow-md transition-shadow"
|
|
>
|
|
{/* En-tête accordéon */}
|
|
<button
|
|
onClick={() => toggle(sol.solutionId)}
|
|
className="w-full flex items-center justify-between px-5 py-4 text-left hover:bg-muted/30 transition-colors"
|
|
>
|
|
<div className="flex items-center gap-4 min-w-0">
|
|
<div className="w-9 h-9 rounded-lg bg-primary/10 flex items-center justify-center flex-shrink-0">
|
|
<Package size={16} className="text-primary" />
|
|
</div>
|
|
<div className="min-w-0">
|
|
<div className="font-semibold text-foreground truncate">{sol.solutionNom}</div>
|
|
<div className="text-xs text-muted-foreground">{sol.editeurNom}</div>
|
|
</div>
|
|
{sol.blocFonctionnelNom && (
|
|
<span className="hidden sm:inline-flex text-xs bg-secondary text-secondary-foreground px-2.5 py-1 rounded-full border border-border flex-shrink-0">
|
|
{sol.blocFonctionnelNom}
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="flex items-center gap-3 flex-shrink-0 ml-3">
|
|
<span className="text-xs text-muted-foreground bg-muted px-2.5 py-1 rounded-full">
|
|
{sol.etablissements.length} établissement{sol.etablissements.length > 1 ? "s" : ""}
|
|
</span>
|
|
{isOpen ? (
|
|
<ChevronDown size={16} className="text-muted-foreground" />
|
|
) : (
|
|
<ChevronRight size={16} className="text-muted-foreground" />
|
|
)}
|
|
</div>
|
|
</button>
|
|
|
|
{/* Contenu accordéon */}
|
|
{isOpen && (
|
|
<div className="border-t border-border bg-muted/10">
|
|
<div className="px-5 py-3">
|
|
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">
|
|
Établissements équipés
|
|
</div>
|
|
<div className="space-y-2">
|
|
{sol.etablissements.map((etab) => (
|
|
<div
|
|
key={etab.id}
|
|
className="flex items-center justify-between py-2 px-3 bg-background rounded-lg border border-border"
|
|
>
|
|
<div className="flex items-center gap-2 min-w-0">
|
|
<Building2 size={14} className="text-muted-foreground flex-shrink-0" />
|
|
<span className="text-sm font-medium text-foreground truncate">{etab.nom}</span>
|
|
{etab.region && (
|
|
<span className="text-xs text-muted-foreground hidden sm:block">— {etab.region}</span>
|
|
)}
|
|
</div>
|
|
<EtatBadge etat={etab.etatDeploiement} />
|
|
</div>
|
|
))}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
})}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</SonumLayout>
|
|
);
|
|
}
|