Checkpoint: Ajout des badges de couleur pour le public cible (bleu pour Directeurs, vert pour Chefs de service, gris pour Autre) dans les tableaux des séquences et création d'un rapport d'analyse comparant le public cible des séquences avec la fonction des apprenants inscrits pour identifier les écarts et optimiser la planification des formations
This commit is contained in:
@@ -5,6 +5,7 @@ import { Route, Switch } from "wouter";
|
|||||||
import ErrorBoundary from "./components/ErrorBoundary";
|
import ErrorBoundary from "./components/ErrorBoundary";
|
||||||
import { ThemeProvider } from "./contexts/ThemeContext";
|
import { ThemeProvider } from "./contexts/ThemeContext";
|
||||||
import Home from "./pages/Home";
|
import Home from "./pages/Home";
|
||||||
|
import AdminRapportPublicCible from "./pages/AdminRapportPublicCible";
|
||||||
import Admin from "./pages/Admin";
|
import Admin from "./pages/Admin";
|
||||||
import AdminFormations from "./pages/AdminFormations";
|
import AdminFormations from "./pages/AdminFormations";
|
||||||
import AdminSequences from "./pages/AdminSequences";
|
import AdminSequences from "./pages/AdminSequences";
|
||||||
@@ -16,6 +17,7 @@ function Router() {
|
|||||||
return (
|
return (
|
||||||
<Switch>
|
<Switch>
|
||||||
<Route path={"/"} component={Home} />
|
<Route path={"/"} component={Home} />
|
||||||
|
<Route path={"/admin/rapport-public-cible"} component={AdminRapportPublicCible} />
|
||||||
<Route path={"/inscription/:lien"} component={Inscription} />
|
<Route path={"/inscription/:lien"} component={Inscription} />
|
||||||
<Route path={"/admin"} component={Admin} />
|
<Route path={"/admin"} component={Admin} />
|
||||||
<Route path={"/admin/formations"} component={AdminFormations} />
|
<Route path={"/admin/formations"} component={AdminFormations} />
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {
|
|||||||
} from "@/components/ui/sidebar";
|
} from "@/components/ui/sidebar";
|
||||||
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
import { APP_LOGO, APP_TITLE, getLoginUrl } from "@/const";
|
||||||
import { useIsMobile } from "@/hooks/useMobile";
|
import { useIsMobile } from "@/hooks/useMobile";
|
||||||
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar } from "lucide-react";
|
import { LayoutDashboard, LogOut, PanelLeft, Users, GraduationCap, Calendar, BarChart3 } from "lucide-react";
|
||||||
import { CSSProperties, useEffect, useRef, useState } from "react";
|
import { CSSProperties, useEffect, useRef, useState } from "react";
|
||||||
import { useLocation } from "wouter";
|
import { useLocation } from "wouter";
|
||||||
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
import { DashboardLayoutSkeleton } from './DashboardLayoutSkeleton';
|
||||||
@@ -32,6 +32,7 @@ const menuItems = [
|
|||||||
{ icon: GraduationCap, label: "Formations", path: "/admin/formations" },
|
{ icon: GraduationCap, label: "Formations", path: "/admin/formations" },
|
||||||
{ icon: Calendar, label: "Séquences", path: "/admin/sequences" },
|
{ icon: Calendar, label: "Séquences", path: "/admin/sequences" },
|
||||||
{ icon: Users, label: "Apprenants", path: "/admin/apprenants" },
|
{ icon: Users, label: "Apprenants", path: "/admin/apprenants" },
|
||||||
|
{ icon: BarChart3, label: "Rapport Public Cible", path: "/admin/rapport-public-cible" },
|
||||||
];
|
];
|
||||||
|
|
||||||
const SIDEBAR_WIDTH_KEY = "sidebar-width";
|
const SIDEBAR_WIDTH_KEY = "sidebar-width";
|
||||||
|
|||||||
280
client/src/pages/AdminRapportPublicCible.tsx
Normal file
280
client/src/pages/AdminRapportPublicCible.tsx
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
import DashboardLayout from "@/components/DashboardLayout";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
|
import { trpc } from "@/lib/trpc";
|
||||||
|
import { BarChart3, TrendingUp, AlertCircle } from "lucide-react";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export default function AdminRapportPublicCible() {
|
||||||
|
const { data: sequences, isLoading: loadingSequences } = trpc.sequences.list.useQuery();
|
||||||
|
const { data: inscriptions, isLoading: loadingInscriptions } = trpc.inscriptions.listAll.useQuery();
|
||||||
|
|
||||||
|
// Calculer les statistiques d'écart entre public cible et fonction des apprenants
|
||||||
|
const stats = useMemo(() => {
|
||||||
|
if (!sequences || !inscriptions) return null;
|
||||||
|
|
||||||
|
const sequencesWithMismatch = sequences.map((sequence) => {
|
||||||
|
const sequenceInscriptions = inscriptions.filter(
|
||||||
|
(insc: any) => insc.inscription.sequenceId === sequence.id && insc.inscription.statut === "confirmee"
|
||||||
|
);
|
||||||
|
|
||||||
|
const totalInscrits = sequenceInscriptions.length;
|
||||||
|
const mismatchCount = sequenceInscriptions.filter((insc: any) => {
|
||||||
|
const apprenantFonction = insc.apprenant.fonction;
|
||||||
|
const sequencePublicCible = sequence.publicCible;
|
||||||
|
|
||||||
|
// Mapper les fonctions aux publics cibles
|
||||||
|
const fonctionToPublicCible: Record<string, string> = {
|
||||||
|
directeur: "directeur",
|
||||||
|
chef_service: "chef_service",
|
||||||
|
autre: "autre",
|
||||||
|
};
|
||||||
|
|
||||||
|
return fonctionToPublicCible[apprenantFonction] !== sequencePublicCible;
|
||||||
|
}).length;
|
||||||
|
|
||||||
|
const matchRate = totalInscrits > 0 ? ((totalInscrits - mismatchCount) / totalInscrits) * 100 : 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sequence,
|
||||||
|
totalInscrits,
|
||||||
|
mismatchCount,
|
||||||
|
matchCount: totalInscrits - mismatchCount,
|
||||||
|
matchRate,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Statistiques globales
|
||||||
|
const totalInscrits = sequencesWithMismatch.reduce((sum, s) => sum + s.totalInscrits, 0);
|
||||||
|
const totalMismatches = sequencesWithMismatch.reduce((sum, s) => sum + s.mismatchCount, 0);
|
||||||
|
const globalMatchRate = totalInscrits > 0 ? ((totalInscrits - totalMismatches) / totalInscrits) * 100 : 100;
|
||||||
|
|
||||||
|
return {
|
||||||
|
sequencesWithMismatch: sequencesWithMismatch.filter((s) => s.totalInscrits > 0),
|
||||||
|
totalInscrits,
|
||||||
|
totalMismatches,
|
||||||
|
totalMatches: totalInscrits - totalMismatches,
|
||||||
|
globalMatchRate,
|
||||||
|
};
|
||||||
|
}, [sequences, inscriptions]);
|
||||||
|
|
||||||
|
const getPublicCibleLabel = (publicCible: string) => {
|
||||||
|
const labels = {
|
||||||
|
directeur: "Directeur",
|
||||||
|
chef_service: "Chef de service",
|
||||||
|
autre: "Autre",
|
||||||
|
};
|
||||||
|
return labels[publicCible as keyof typeof labels] || "Autre";
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicCibleBadge = (publicCible: string) => {
|
||||||
|
const colors = {
|
||||||
|
directeur: "bg-blue-100 text-blue-800",
|
||||||
|
chef_service: "bg-green-100 text-green-800",
|
||||||
|
autre: "bg-gray-100 text-gray-800",
|
||||||
|
};
|
||||||
|
return colors[publicCible as keyof typeof colors] || colors.autre;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMatchRateBadge = (matchRate: number) => {
|
||||||
|
if (matchRate >= 80) return "bg-green-100 text-green-800";
|
||||||
|
if (matchRate >= 50) return "bg-orange-100 text-orange-800";
|
||||||
|
return "bg-red-100 text-red-800";
|
||||||
|
};
|
||||||
|
|
||||||
|
if (loadingSequences || loadingInscriptions) {
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="container mx-auto py-8">
|
||||||
|
<div className="text-center py-8 text-muted-foreground">Chargement...</div>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!stats) {
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="container mx-auto py-8">
|
||||||
|
<div className="text-center py-8 text-muted-foreground">Aucune donnée disponible</div>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DashboardLayout>
|
||||||
|
<div className="container mx-auto py-8 space-y-6">
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Rapport d'analyse Public Cible</h1>
|
||||||
|
<p className="text-muted-foreground mt-1">
|
||||||
|
Comparaison entre le public cible des séquences et la fonction des apprenants inscrits
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Statistiques globales */}
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Total Inscrits</CardTitle>
|
||||||
|
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.totalInscrits}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Inscrits confirmés</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Correspondances</CardTitle>
|
||||||
|
<TrendingUp className="h-4 w-4 text-green-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-green-600">{stats.totalMatches}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{stats.globalMatchRate.toFixed(1)}% de correspondance
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Écarts</CardTitle>
|
||||||
|
<AlertCircle className="h-4 w-4 text-orange-600" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold text-orange-600">{stats.totalMismatches}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">
|
||||||
|
{(100 - stats.globalMatchRate).toFixed(1)}% d'écart
|
||||||
|
</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
|
||||||
|
<CardTitle className="text-sm font-medium">Séquences Analysées</CardTitle>
|
||||||
|
<BarChart3 className="h-4 w-4 text-muted-foreground" />
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="text-2xl font-bold">{stats.sequencesWithMismatch.length}</div>
|
||||||
|
<p className="text-xs text-muted-foreground">Avec inscrits confirmés</p>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Tableau détaillé par séquence */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Analyse détaillée par séquence</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Taux de correspondance entre le public cible et la fonction des apprenants inscrits
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
{stats.sequencesWithMismatch.length === 0 ? (
|
||||||
|
<div className="text-center py-8 text-muted-foreground">
|
||||||
|
Aucune séquence avec des inscrits confirmés
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="overflow-x-auto">
|
||||||
|
<Table>
|
||||||
|
<TableHeader>
|
||||||
|
<TableRow>
|
||||||
|
<TableHead>Séquence</TableHead>
|
||||||
|
<TableHead>Public cible</TableHead>
|
||||||
|
<TableHead className="text-right">Total Inscrits</TableHead>
|
||||||
|
<TableHead className="text-right">Correspondances</TableHead>
|
||||||
|
<TableHead className="text-right">Écarts</TableHead>
|
||||||
|
<TableHead className="text-right">Taux de correspondance</TableHead>
|
||||||
|
</TableRow>
|
||||||
|
</TableHeader>
|
||||||
|
<TableBody>
|
||||||
|
{stats.sequencesWithMismatch
|
||||||
|
.sort((a, b) => a.matchRate - b.matchRate)
|
||||||
|
.map((stat) => (
|
||||||
|
<TableRow key={stat.sequence.id}>
|
||||||
|
<TableCell className="font-medium">{stat.sequence.nom}</TableCell>
|
||||||
|
<TableCell>
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPublicCibleBadge(stat.sequence.publicCible)}`}
|
||||||
|
>
|
||||||
|
{getPublicCibleLabel(stat.sequence.publicCible)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">{stat.totalInscrits}</TableCell>
|
||||||
|
<TableCell className="text-right text-green-600 font-medium">
|
||||||
|
{stat.matchCount}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-orange-600 font-medium">
|
||||||
|
{stat.mismatchCount}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right">
|
||||||
|
<span
|
||||||
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getMatchRateBadge(stat.matchRate)}`}
|
||||||
|
>
|
||||||
|
{stat.matchRate.toFixed(1)}%
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
|
</TableRow>
|
||||||
|
))}
|
||||||
|
</TableBody>
|
||||||
|
</Table>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
|
||||||
|
{/* Recommandations */}
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>Recommandations</CardTitle>
|
||||||
|
<CardDescription>Actions suggérées pour optimiser la planification des formations</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<div className="space-y-4">
|
||||||
|
{stats.globalMatchRate < 70 && (
|
||||||
|
<div className="flex gap-3 p-4 bg-red-50 border border-red-200 rounded-lg">
|
||||||
|
<AlertCircle className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-red-900">Taux de correspondance faible</p>
|
||||||
|
<p className="text-sm text-red-700 mt-1">
|
||||||
|
Le taux de correspondance global est inférieur à 70%. Considérez la création de séquences
|
||||||
|
spécifiques pour chaque public cible.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stats.sequencesWithMismatch.some((s) => s.matchRate < 50) && (
|
||||||
|
<div className="flex gap-3 p-4 bg-orange-50 border border-orange-200 rounded-lg">
|
||||||
|
<AlertCircle className="h-5 w-5 text-orange-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-orange-900">Séquences à revoir</p>
|
||||||
|
<p className="text-sm text-orange-700 mt-1">
|
||||||
|
Certaines séquences ont un taux de correspondance inférieur à 50%. Vérifiez si le public
|
||||||
|
cible est correctement défini ou si les apprenants sont inscrits aux bonnes séquences.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{stats.globalMatchRate >= 80 && (
|
||||||
|
<div className="flex gap-3 p-4 bg-green-50 border border-green-200 rounded-lg">
|
||||||
|
<TrendingUp className="h-5 w-5 text-green-600 flex-shrink-0 mt-0.5" />
|
||||||
|
<div>
|
||||||
|
<p className="font-medium text-green-900">Excellente correspondance</p>
|
||||||
|
<p className="text-sm text-green-700 mt-1">
|
||||||
|
Le taux de correspondance global est excellent (≥ 80%). La planification des formations
|
||||||
|
est bien alignée avec les publics cibles.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -17,6 +17,24 @@ export default function AdminSequenceInscrits() {
|
|||||||
const [, params] = useRoute("/admin/sequences/:id/inscrits");
|
const [, params] = useRoute("/admin/sequences/:id/inscrits");
|
||||||
const sequenceId = params?.id ? parseInt(params.id) : 0;
|
const sequenceId = params?.id ? parseInt(params.id) : 0;
|
||||||
|
|
||||||
|
const getPublicCibleBadge = (publicCible: string) => {
|
||||||
|
const colors = {
|
||||||
|
directeur: "bg-blue-100 text-blue-800",
|
||||||
|
chef_service: "bg-green-100 text-green-800",
|
||||||
|
autre: "bg-gray-100 text-gray-800",
|
||||||
|
};
|
||||||
|
return colors[publicCible as keyof typeof colors] || colors.autre;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicCibleLabel = (publicCible: string) => {
|
||||||
|
const labels = {
|
||||||
|
directeur: "Directeur",
|
||||||
|
chef_service: "Chef de service",
|
||||||
|
autre: "Autre",
|
||||||
|
};
|
||||||
|
return labels[publicCible as keyof typeof labels] || "Autre";
|
||||||
|
};
|
||||||
|
|
||||||
// États pour les filtres
|
// États pour les filtres
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
const [searchTerm, setSearchTerm] = useState("");
|
||||||
const [filterStatut, setFilterStatut] = useState<string>("all");
|
const [filterStatut, setFilterStatut] = useState<string>("all");
|
||||||
@@ -300,6 +318,12 @@ export default function AdminSequenceInscrits() {
|
|||||||
{nbConfirmes} / {sequence.capaciteMax} inscrits confirmés
|
{nbConfirmes} / {sequence.capaciteMax} inscrits confirmés
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<p className="text-sm font-medium text-muted-foreground">Public cible</p>
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPublicCibleBadge(sequence.publicCible)}`}>
|
||||||
|
{getPublicCibleLabel(sequence.publicCible)}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -288,6 +288,24 @@ export default function AdminSequences() {
|
|||||||
return colors[statut as keyof typeof colors] || colors.ouverte;
|
return colors[statut as keyof typeof colors] || colors.ouverte;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getPublicCibleBadge = (publicCible: string) => {
|
||||||
|
const colors = {
|
||||||
|
directeur: "bg-blue-100 text-blue-800",
|
||||||
|
chef_service: "bg-green-100 text-green-800",
|
||||||
|
autre: "bg-gray-100 text-gray-800",
|
||||||
|
};
|
||||||
|
return colors[publicCible as keyof typeof colors] || colors.autre;
|
||||||
|
};
|
||||||
|
|
||||||
|
const getPublicCibleLabel = (publicCible: string) => {
|
||||||
|
const labels = {
|
||||||
|
directeur: "Directeur",
|
||||||
|
chef_service: "Chef de service",
|
||||||
|
autre: "Autre",
|
||||||
|
};
|
||||||
|
return labels[publicCible as keyof typeof labels] || "Autre";
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="container mx-auto py-8 space-y-6">
|
<div className="container mx-auto py-8 space-y-6">
|
||||||
@@ -620,7 +638,11 @@ export default function AdminSequences() {
|
|||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="max-w-xs truncate">{sequence.lieu}</TableCell>
|
<TableCell className="max-w-xs truncate">{sequence.lieu}</TableCell>
|
||||||
<TableCell>{sequence.publicCible === "directeur" ? "Directeur" : sequence.publicCible === "chef_service" ? "Chef de service" : "Autre"}</TableCell>
|
<TableCell>
|
||||||
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getPublicCibleBadge(sequence.publicCible)}`}>
|
||||||
|
{getPublicCibleLabel(sequence.publicCible)}
|
||||||
|
</span>
|
||||||
|
</TableCell>
|
||||||
<TableCell>{sequence.capaciteMax}</TableCell>
|
<TableCell>{sequence.capaciteMax}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatutBadge(sequence.statut)}`}>
|
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatutBadge(sequence.statut)}`}>
|
||||||
|
|||||||
14
server/db.ts
14
server/db.ts
@@ -285,6 +285,20 @@ export async function createInscription(data: InsertInscription) {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function getAllInscriptions() {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
const results = await db.select({
|
||||||
|
inscription: inscriptions,
|
||||||
|
apprenant: apprenants,
|
||||||
|
})
|
||||||
|
.from(inscriptions)
|
||||||
|
.leftJoin(apprenants, eq(inscriptions.apprenantId, apprenants.id));
|
||||||
|
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
export async function getInscriptionsBySequence(sequenceId: number) {
|
export async function getInscriptionsBySequence(sequenceId: number) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return [];
|
if (!db) return [];
|
||||||
|
|||||||
@@ -247,6 +247,10 @@ export const appRouter = router({
|
|||||||
|
|
||||||
// ===== INSCRIPTIONS =====
|
// ===== INSCRIPTIONS =====
|
||||||
inscriptions: router({
|
inscriptions: router({
|
||||||
|
listAll: adminProcedure.query(async () => {
|
||||||
|
return db.getAllInscriptions();
|
||||||
|
}),
|
||||||
|
|
||||||
listBySequence: adminProcedure.input(z.object({ sequenceId: z.number() })).query(async ({ input }) => {
|
listBySequence: adminProcedure.input(z.object({ sequenceId: z.number() })).query(async ({ input }) => {
|
||||||
return db.getInscriptionsBySequence(input.sequenceId);
|
return db.getInscriptionsBySequence(input.sequenceId);
|
||||||
}),
|
}),
|
||||||
|
|||||||
14
todo.md
14
todo.md
@@ -160,3 +160,17 @@
|
|||||||
- [x] Ajouter le composant Select pour le filtre publicCible dans l'interface
|
- [x] Ajouter le composant Select pour le filtre publicCible dans l'interface
|
||||||
- [x] Implémenter la logique de filtrage par public cible
|
- [x] Implémenter la logique de filtrage par public cible
|
||||||
- [x] Tester le filtre avec différentes valeurs
|
- [x] Tester le filtre avec différentes valeurs
|
||||||
|
|
||||||
|
## Badges de couleur pour le public cible
|
||||||
|
|
||||||
|
- [x] Créer un composant Badge pour afficher le public cible avec des couleurs distinctes
|
||||||
|
- [x] Intégrer le Badge dans le tableau des séquences (AdminSequences)
|
||||||
|
- [x] Intégrer le Badge dans la page des inscrits (AdminSequenceInscrits)
|
||||||
|
- [x] Tester l'affichage des badges avec différents publics cibles
|
||||||
|
|
||||||
|
## Rapport d'analyse public cible vs fonction appre- [x] Créer une nouvelle page AdminRapportPublicCible
|
||||||
|
- [x] Ajouter la route dans App.tsx
|
||||||
|
- [x] Ajouter un lien dans le menu de navigation (DashboardLayout- [x] Créer une procédure tRPC pour récupérer les données d'analyse
|
||||||
|
- [x] Implémenter la logique de comparaison public cible vs fonction apprenants
|
||||||
|
- [x] Afficher les statistiques et écarts dans l'interface
|
||||||
|
- [x] Tester le rapport avec différentes données
|
||||||
|
|||||||
Reference in New Issue
Block a user