Checkpoint: Page Solutions Logicielles : toggle Accordéon / Liste. La vue Liste affiche toutes les solutions avec leurs établissements directement dans un tableau (solution, éditeur, bloc fonctionnel, établissement, région, état) sans aucune interaction. Les rowSpan groupent les lignes par solution. 0 erreur TypeScript.
This commit is contained in:
@@ -4,15 +4,21 @@ import SonumLayout from "@/components/SonumLayout";
|
|||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from "@/components/ui/dialog";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { ChevronDown, ChevronRight, Building2, Package, Search, Filter, BarChart2, Plus, Pencil, Trash2 } from "lucide-react";
|
import {
|
||||||
|
ChevronDown, ChevronRight, Building2, Package, Search, Filter,
|
||||||
|
BarChart2, Plus, Pencil, Trash2, LayoutList, Rows3
|
||||||
|
} from "lucide-react";
|
||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { EtatBadge } from "@/components/EtatBadge";
|
import { EtatBadge } from "@/components/EtatBadge";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
|
type ViewMode = "accordion" | "flat";
|
||||||
|
|
||||||
export default function SolutionsLogicielles() {
|
export default function SolutionsLogicielles() {
|
||||||
const { user, isAuthenticated } = useAuth();
|
const { user, isAuthenticated } = useAuth();
|
||||||
const isGestionnaire = user?.sonumRole === "gestionnaire" || user?.role === "admin";
|
const isGestionnaire = user?.sonumRole === "gestionnaire" || user?.role === "admin";
|
||||||
|
|
||||||
|
const [viewMode, setViewMode] = useState<ViewMode>("accordion");
|
||||||
const [openIds, setOpenIds] = useState<Set<number>>(new Set());
|
const [openIds, setOpenIds] = useState<Set<number>>(new Set());
|
||||||
const [search, setSearch] = useState("");
|
const [search, setSearch] = useState("");
|
||||||
const [filterBloc, setFilterBloc] = useState("");
|
const [filterBloc, setFilterBloc] = useState("");
|
||||||
@@ -43,7 +49,13 @@ export default function SolutionsLogicielles() {
|
|||||||
const toggle = (id: number) => setOpenIds((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
const toggle = (id: number) => setOpenIds((prev) => { const n = new Set(prev); n.has(id) ? n.delete(id) : n.add(id); return n; });
|
||||||
|
|
||||||
const openAdd = () => { setFormNom(""); setFormEditeurId(""); setFormBlocId(""); setEditTarget(null); setDialogMode("add"); };
|
const openAdd = () => { setFormNom(""); setFormEditeurId(""); setFormBlocId(""); setEditTarget(null); setDialogMode("add"); };
|
||||||
const openEdit = (sol: any) => { setFormNom(sol.solutionNom); setFormEditeurId(sol.editeurId ?? ""); setFormBlocId(sol.blocFonctionnelId ?? ""); setEditTarget(sol); setDialogMode("edit"); };
|
const openEdit = (sol: any) => {
|
||||||
|
setFormNom(sol.solutionNom);
|
||||||
|
setFormEditeurId(sol.editeurId ?? "");
|
||||||
|
setFormBlocId(sol.blocFonctionnelId ?? "");
|
||||||
|
setEditTarget(sol);
|
||||||
|
setDialogMode("edit");
|
||||||
|
};
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!formNom.trim() || !formEditeurId) { toast.error("Nom et éditeur obligatoires"); return; }
|
if (!formNom.trim() || !formEditeurId) { toast.error("Nom et éditeur obligatoires"); return; }
|
||||||
@@ -77,7 +89,8 @@ export default function SolutionsLogicielles() {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<SonumLayout>
|
<SonumLayout>
|
||||||
<div className="max-w-5xl mx-auto px-4 py-8">
|
<div className="max-w-6xl mx-auto px-4 py-8">
|
||||||
|
{/* En-tête */}
|
||||||
<div className="mb-8">
|
<div className="mb-8">
|
||||||
<div className="flex items-center justify-between gap-3 mb-4">
|
<div className="flex items-center justify-between gap-3 mb-4">
|
||||||
<div className="flex items-center gap-3">
|
<div className="flex items-center gap-3">
|
||||||
@@ -97,7 +110,7 @@ export default function SolutionsLogicielles() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{solutions && (
|
{solutions && (
|
||||||
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3 mb-2">
|
<div className="grid grid-cols-3 gap-3 mb-2">
|
||||||
<div className="bg-card border border-border rounded-xl p-4 text-center">
|
<div className="bg-card border border-border rounded-xl p-4 text-center">
|
||||||
<div className="text-2xl font-bold text-primary">{solutions.length}</div>
|
<div className="text-2xl font-bold text-primary">{solutions.length}</div>
|
||||||
<div className="text-xs text-muted-foreground mt-0.5">Solutions référencées</div>
|
<div className="text-xs text-muted-foreground mt-0.5">Solutions référencées</div>
|
||||||
@@ -106,7 +119,7 @@ export default function SolutionsLogicielles() {
|
|||||||
<div className="text-2xl font-bold text-blue-600">{totalEtablissements}</div>
|
<div className="text-2xl font-bold text-blue-600">{totalEtablissements}</div>
|
||||||
<div className="text-xs text-muted-foreground mt-0.5">Établissements équipés</div>
|
<div className="text-xs text-muted-foreground mt-0.5">Établissements équipés</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="bg-card border border-border rounded-xl p-4 text-center col-span-2 sm:col-span-1">
|
<div className="bg-card border border-border rounded-xl p-4 text-center">
|
||||||
<div className="text-2xl font-bold text-green-600">{blocsUniques.length}</div>
|
<div className="text-2xl font-bold text-green-600">{blocsUniques.length}</div>
|
||||||
<div className="text-xs text-muted-foreground mt-0.5">Blocs fonctionnels</div>
|
<div className="text-xs text-muted-foreground mt-0.5">Blocs fonctionnels</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -114,21 +127,53 @@ export default function SolutionsLogicielles() {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Barre de filtres + toggle vue */}
|
||||||
<div className="flex flex-col sm:flex-row gap-3 mb-6">
|
<div className="flex flex-col sm:flex-row gap-3 mb-6">
|
||||||
<div className="relative flex-1">
|
<div className="relative flex-1">
|
||||||
<Search size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
<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" />
|
<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>
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<Filter size={15} className="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground" />
|
<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]">
|
<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>
|
<option value="">Tous les blocs fonctionnels</option>
|
||||||
{blocsUniques.map((b) => <option key={b} value={b}>{b}</option>)}
|
{blocsUniques.map((b) => <option key={b} value={b}>{b}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
{/* Toggle vue */}
|
||||||
|
<div className="flex items-center bg-muted rounded-lg p-1 gap-1 flex-shrink-0">
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("accordion")}
|
||||||
|
title="Vue accordéon"
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${viewMode === "accordion" ? "bg-background shadow text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
||||||
|
>
|
||||||
|
<LayoutList size={14} /> Accordéon
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setViewMode("flat")}
|
||||||
|
title="Vue liste à plat"
|
||||||
|
className={`flex items-center gap-1.5 px-3 py-1.5 rounded-md text-xs font-medium transition-colors ${viewMode === "flat" ? "bg-background shadow text-foreground" : "text-muted-foreground hover:text-foreground"}`}
|
||||||
|
>
|
||||||
|
<Rows3 size={14} /> Liste
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{(search || filterBloc) && <p className="text-xs text-muted-foreground mb-4">{filtered.length} solution{filtered.length > 1 ? "s" : ""} correspondant aux critères</p>}
|
{(search || filterBloc) && (
|
||||||
|
<p className="text-xs text-muted-foreground mb-4">
|
||||||
|
{filtered.length} solution{filtered.length > 1 ? "s" : ""} correspondant aux critères
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="space-y-3">{[...Array(6)].map((_, i) => <div key={i} className="h-16 bg-muted/30 rounded-xl animate-pulse" />)}</div>
|
<div className="space-y-3">{[...Array(6)].map((_, i) => <div key={i} className="h-16 bg-muted/30 rounded-xl animate-pulse" />)}</div>
|
||||||
@@ -137,14 +182,18 @@ export default function SolutionsLogicielles() {
|
|||||||
<Package size={40} className="mx-auto mb-3 opacity-30" />
|
<Package size={40} className="mx-auto mb-3 opacity-30" />
|
||||||
<p className="font-medium">Aucune solution trouvée</p>
|
<p className="font-medium">Aucune solution trouvée</p>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : viewMode === "accordion" ? (
|
||||||
|
/* ── VUE ACCORDÉON ─────────────────────────────────────────── */
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{filtered.map((sol) => {
|
{filtered.map((sol) => {
|
||||||
const isOpen = openIds.has(sol.solutionId);
|
const isOpen = openIds.has(sol.solutionId);
|
||||||
return (
|
return (
|
||||||
<div key={sol.solutionId} className="border border-border rounded-xl overflow-hidden bg-card shadow-sm hover:shadow-md transition-shadow">
|
<div key={sol.solutionId} className="border border-border rounded-xl overflow-hidden bg-card shadow-sm hover:shadow-md transition-shadow">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<button onClick={() => toggle(sol.solutionId)} className="flex-1 flex items-center justify-between px-5 py-4 text-left hover:bg-muted/30 transition-colors">
|
<button
|
||||||
|
onClick={() => toggle(sol.solutionId)}
|
||||||
|
className="flex-1 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="flex items-center gap-4 min-w-0">
|
||||||
<div className="w-9 h-9 rounded-lg bg-blue-500/10 flex items-center justify-center flex-shrink-0">
|
<div className="w-9 h-9 rounded-lg bg-blue-500/10 flex items-center justify-center flex-shrink-0">
|
||||||
<Package size={16} className="text-blue-600" />
|
<Package size={16} className="text-blue-600" />
|
||||||
@@ -154,12 +203,16 @@ export default function SolutionsLogicielles() {
|
|||||||
<div className="text-xs text-muted-foreground">{sol.editeurNom}</div>
|
<div className="text-xs text-muted-foreground">{sol.editeurNom}</div>
|
||||||
</div>
|
</div>
|
||||||
{sol.blocFonctionnelNom && (
|
{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>
|
<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>
|
||||||
<div className="flex items-center gap-3 flex-shrink-0 ml-3">
|
<div className="flex items-center gap-3 flex-shrink-0 ml-3">
|
||||||
{sol.nbEtablissements > 0 ? (
|
{sol.nbEtablissements > 0 ? (
|
||||||
<span className="text-xs text-muted-foreground bg-muted px-2.5 py-1 rounded-full">{sol.nbEtablissements} établissement{sol.nbEtablissements > 1 ? "s" : ""}</span>
|
<span className="text-xs text-muted-foreground bg-muted px-2.5 py-1 rounded-full">
|
||||||
|
{sol.nbEtablissements} établissement{sol.nbEtablissements > 1 ? "s" : ""}
|
||||||
|
</span>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-xs text-muted-foreground/50 bg-muted/50 px-2.5 py-1 rounded-full italic">Non déployée</span>
|
<span className="text-xs text-muted-foreground/50 bg-muted/50 px-2.5 py-1 rounded-full italic">Non déployée</span>
|
||||||
)}
|
)}
|
||||||
@@ -171,45 +224,127 @@ export default function SolutionsLogicielles() {
|
|||||||
<button onClick={(e) => { e.stopPropagation(); openEdit(sol); }} className="p-1.5 rounded bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors" title="Modifier">
|
<button onClick={(e) => { e.stopPropagation(); openEdit(sol); }} className="p-1.5 rounded bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors" title="Modifier">
|
||||||
<Pencil size={13} />
|
<Pencil size={13} />
|
||||||
</button>
|
</button>
|
||||||
<button onClick={(e) => { e.stopPropagation(); if (confirm(`Supprimer "${sol.solutionNom}" ?`)) deleteMutation.mutate({ id: sol.solutionId }); }} className="p-1.5 rounded bg-red-50 text-red-600 hover:bg-red-100 transition-colors" title="Supprimer">
|
<button
|
||||||
|
onClick={(e) => { e.stopPropagation(); if (confirm(`Supprimer "${sol.solutionNom}" ?`)) deleteMutation.mutate({ id: sol.solutionId }); }}
|
||||||
|
className="p-1.5 rounded bg-red-50 text-red-600 hover:bg-red-100 transition-colors"
|
||||||
|
title="Supprimer"
|
||||||
|
>
|
||||||
<Trash2 size={13} />
|
<Trash2 size={13} />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isOpen && (
|
{isOpen && (
|
||||||
<div className="border-t border-border bg-muted/10">
|
<div className="border-t border-border bg-muted/10 px-5 py-3">
|
||||||
<div className="px-5 py-3">
|
{sol.etablissements.length === 0 ? (
|
||||||
{sol.etablissements.length === 0 ? (
|
<p className="text-sm text-muted-foreground italic py-2">Aucun établissement n'utilise encore cette solution.</p>
|
||||||
<p className="text-sm text-muted-foreground italic py-2">Aucun établissement n'utilise encore cette solution.</p>
|
) : (
|
||||||
) : (
|
<>
|
||||||
<>
|
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Établissements équipés</div>
|
||||||
<div className="text-xs font-semibold text-muted-foreground uppercase tracking-wider mb-3">Établissements équipés</div>
|
<div className="space-y-2">
|
||||||
<div className="space-y-2">
|
{sol.etablissements.map((etab) => (
|
||||||
{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 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">
|
||||||
<div className="flex items-center gap-2 min-w-0">
|
<Building2 size={14} className="text-muted-foreground flex-shrink-0" />
|
||||||
<Building2 size={14} className="text-muted-foreground flex-shrink-0" />
|
<span className="text-sm font-medium text-foreground truncate">{etab.nom}</span>
|
||||||
<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>}
|
||||||
{etab.region && <span className="text-xs text-muted-foreground hidden sm:block">— {etab.region}</span>}
|
|
||||||
</div>
|
|
||||||
<EtatBadge etat={etab.etatDeploiement} />
|
|
||||||
</div>
|
</div>
|
||||||
))}
|
<EtatBadge etat={etab.etatDeploiement} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
))}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
/* ── VUE LISTE À PLAT ──────────────────────────────────────── */
|
||||||
|
<div className="border border-border rounded-xl overflow-hidden bg-card shadow-sm">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="bg-muted/50 border-b border-border">
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">Solution</th>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">Éditeur</th>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider hidden sm:table-cell">Bloc fonctionnel</th>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">Établissement</th>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider hidden md:table-cell">Région</th>
|
||||||
|
<th className="text-left px-4 py-3 text-xs font-semibold text-muted-foreground uppercase tracking-wider">État</th>
|
||||||
|
{isGestionnaire && <th className="px-4 py-3 w-16"></th>}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{filtered.map((sol) => {
|
||||||
|
if (sol.etablissements.length === 0) {
|
||||||
|
return (
|
||||||
|
<tr key={`sol-${sol.solutionId}-empty`} className="border-b border-border last:border-0 hover:bg-muted/20 transition-colors">
|
||||||
|
<td className="px-4 py-3 font-medium text-foreground">{sol.solutionNom}</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground">{sol.editeurNom}</td>
|
||||||
|
<td className="px-4 py-3 hidden sm:table-cell">
|
||||||
|
{sol.blocFonctionnelNom && (
|
||||||
|
<span className="text-xs bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border border-border">{sol.blocFonctionnelNom}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground italic text-xs" colSpan={2}>Non déployée</td>
|
||||||
|
<td className="px-4 py-3"></td>
|
||||||
|
{isGestionnaire && (
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button onClick={() => openEdit(sol)} className="p-1.5 rounded bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors" title="Modifier"><Pencil size={12} /></button>
|
||||||
|
<button onClick={() => { if (confirm(`Supprimer "${sol.solutionNom}" ?`)) deleteMutation.mutate({ id: sol.solutionId }); }} className="p-1.5 rounded bg-red-50 text-red-600 hover:bg-red-100 transition-colors" title="Supprimer"><Trash2 size={12} /></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
)}
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return sol.etablissements.map((etab, idx) => (
|
||||||
|
<tr key={`sol-${sol.solutionId}-etab-${etab.id}`} className="border-b border-border last:border-0 hover:bg-muted/20 transition-colors">
|
||||||
|
{idx === 0 ? (
|
||||||
|
<>
|
||||||
|
<td className="px-4 py-3 font-semibold text-foreground align-top" rowSpan={sol.etablissements.length}>
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Package size={13} className="text-blue-500 flex-shrink-0" />
|
||||||
|
{sol.solutionNom}
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground align-top" rowSpan={sol.etablissements.length}>{sol.editeurNom}</td>
|
||||||
|
<td className="px-4 py-3 hidden sm:table-cell align-top" rowSpan={sol.etablissements.length}>
|
||||||
|
{sol.blocFonctionnelNom && (
|
||||||
|
<span className="text-xs bg-secondary text-secondary-foreground px-2 py-0.5 rounded-full border border-border">{sol.blocFonctionnelNom}</span>
|
||||||
|
)}
|
||||||
|
</td>
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<td className="px-4 py-3">
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<Building2 size={13} className="text-muted-foreground flex-shrink-0" />
|
||||||
|
<span className="text-foreground">{etab.nom}</span>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
<td className="px-4 py-3 text-muted-foreground hidden md:table-cell">{etab.region ?? "—"}</td>
|
||||||
|
<td className="px-4 py-3"><EtatBadge etat={etab.etatDeploiement} /></td>
|
||||||
|
{isGestionnaire && idx === 0 ? (
|
||||||
|
<td className="px-4 py-3 align-top" rowSpan={sol.etablissements.length}>
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<button onClick={() => openEdit(sol)} className="p-1.5 rounded bg-blue-50 text-blue-600 hover:bg-blue-100 transition-colors" title="Modifier"><Pencil size={12} /></button>
|
||||||
|
<button onClick={() => { if (confirm(`Supprimer "${sol.solutionNom}" ?`)) deleteMutation.mutate({ id: sol.solutionId }); }} className="p-1.5 rounded bg-red-50 text-red-600 hover:bg-red-100 transition-colors" title="Supprimer"><Trash2 size={12} /></button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
) : isGestionnaire ? <td className="px-4 py-3"></td> : null}
|
||||||
|
</tr>
|
||||||
|
));
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Dialog ajout / modification */}
|
||||||
<Dialog open={dialogMode !== null} onOpenChange={(o) => !o && setDialogMode(null)}>
|
<Dialog open={dialogMode !== null} onOpenChange={(o) => !o && setDialogMode(null)}>
|
||||||
<DialogContent className="max-w-md">
|
<DialogContent className="max-w-md">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
@@ -218,18 +353,31 @@ export default function SolutionsLogicielles() {
|
|||||||
<div className="space-y-4 py-2">
|
<div className="space-y-4 py-2">
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-semibold uppercase text-muted-foreground mb-1.5 block">Nom de la solution *</Label>
|
<Label className="text-xs font-semibold uppercase text-muted-foreground mb-1.5 block">Nom de la solution *</Label>
|
||||||
<input value={formNom} onChange={(e) => setFormNom(e.target.value)} placeholder="Ex : DPI, Logiciel RH…" className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30" />
|
<input
|
||||||
|
value={formNom}
|
||||||
|
onChange={(e) => setFormNom(e.target.value)}
|
||||||
|
placeholder="Ex : DPI, Logiciel RH…"
|
||||||
|
className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background focus:outline-none focus:ring-2 focus:ring-primary/30"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-semibold uppercase text-muted-foreground mb-1.5 block">Éditeur *</Label>
|
<Label className="text-xs font-semibold uppercase text-muted-foreground mb-1.5 block">Éditeur *</Label>
|
||||||
<select value={formEditeurId} onChange={(e) => setFormEditeurId(e.target.value ? Number(e.target.value) : "")} className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background">
|
<select
|
||||||
|
value={formEditeurId}
|
||||||
|
onChange={(e) => setFormEditeurId(e.target.value ? Number(e.target.value) : "")}
|
||||||
|
className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background"
|
||||||
|
>
|
||||||
<option value="">— Sélectionner un éditeur —</option>
|
<option value="">— Sélectionner un éditeur —</option>
|
||||||
{editeursList?.map((e: any) => <option key={e.id} value={e.id}>{e.nom}</option>)}
|
{editeursList?.map((e: any) => <option key={e.id} value={e.id}>{e.nom}</option>)}
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Label className="text-xs font-semibold uppercase text-muted-foreground mb-1.5 block">Bloc fonctionnel</Label>
|
<Label className="text-xs font-semibold uppercase text-muted-foreground mb-1.5 block">Bloc fonctionnel</Label>
|
||||||
<select value={formBlocId} onChange={(e) => setFormBlocId(e.target.value ? Number(e.target.value) : "")} className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background">
|
<select
|
||||||
|
value={formBlocId}
|
||||||
|
onChange={(e) => setFormBlocId(e.target.value ? Number(e.target.value) : "")}
|
||||||
|
className="w-full border border-border rounded-lg px-3 py-2 text-sm bg-background"
|
||||||
|
>
|
||||||
<option value="">— Non renseigné —</option>
|
<option value="">— Non renseigné —</option>
|
||||||
{blocsList?.map((b: any) => <option key={b.id} value={b.id}>{b.nom}</option>)}
|
{blocsList?.map((b: any) => <option key={b.id} value={b.id}>{b.nom}</option>)}
|
||||||
</select>
|
</select>
|
||||||
|
|||||||
4
todo.md
4
todo.md
@@ -70,3 +70,7 @@
|
|||||||
- [x] Page "Solutions Logicielles" : liste globale des solutions avec établissements équipés (accordéon), accessible depuis la sidebar
|
- [x] Page "Solutions Logicielles" : liste globale des solutions avec établissements équipés (accordéon), accessible depuis la sidebar
|
||||||
- [x] Admin : onglet import établissements (CSV/Excel)
|
- [x] Admin : onglet import établissements (CSV/Excel)
|
||||||
- [x] Admin : onglet import contacts (CSV/Excel)
|
- [x] Admin : onglet import contacts (CSV/Excel)
|
||||||
|
|
||||||
|
## Évolution v5
|
||||||
|
|
||||||
|
- [x] Solutions Logicielles : toggle vue accordéon / vue liste à plat (solutions + établissements visibles sans clic)
|
||||||
|
|||||||
Reference in New Issue
Block a user