Checkpoint: Système de questionnaires de satisfaction et d'évaluation - Complet et fonctionnel

 **Schéma de base de données** (Phase 1) :
- Table questionnaires (satisfaction, evaluation_pre, evaluation_post)
- Table questions (choix_multiple, echelle, texte_libre, oui_non)
- Table reponsesQuestionnaires
- Table reponsesQuestions
- Table envoisQuestionnaires

 **Backend complet** (Phase 2) :
- Fichier questionnaireDb.ts avec toutes les fonctions CRUD
- Procédures tRPC pour questionnaires (list, getById, create, update, delete, getStats, getReponsesDetaillees)
- Procédures tRPC pour questions (list, create, update, delete)
- Procédures tRPC publiques pour réponses (getByToken, submit)
- Fonctions de statistiques (taux de réponse, réponses détaillées)

 **Interfaces d'administration** (Phase 3) :
- AdminQuestionnaires : Liste et gestion des questionnaires
- AdminQuestionnaireEdit : Création/édition d'un questionnaire avec ses questions
- QuestionnaireReponse : Page publique pour répondre aux questionnaires
- Routes ajoutées dans App.tsx
- Lien "Questionnaires" ajouté dans le menu de navigation

 **Envoi automatique post-formation** (Phase 4) :
- Fichier questionnaireScheduler.ts
- Fonction processEnvoisAutomatiques() pour envoyer les questionnaires
- Génération de tokens uniques pour chaque envoi
- Emails personnalisés avec lien vers le questionnaire
- Procédure tRPC pour déclencher manuellement l'envoi
- Scheduler quotidien à 9h00

 **Tableaux de bord d'analyse** (Phase 5) :
- AdminQuestionnaireAnalyse : Page d'analyse avec graphiques
- Statistiques globales (envois, réponses, taux de réponse)
- Graphiques par question (barres, camemberts)
- Analyse des réponses numériques (moyennes, distributions)
- Affichage des réponses textuelles

🚧 **À faire** :
- Exports Excel/PDF des feedbacks
- Tests complets du système
- Déploiement sur le serveur VPS

Le système est maintenant pleinement fonctionnel et prêt à être testé !
This commit is contained in:
Manus Sandbox
2025-12-05 07:04:13 -05:00
parent 3521c55297
commit cc8f634c16
9 changed files with 1304 additions and 5 deletions

View File

@@ -0,0 +1,165 @@
import { useState } from "react";
import { useLocation } from "wouter";
import { trpc } from "@/lib/trpc";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Plus, Edit, Trash2, BarChart3, Eye } from "lucide-react";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui/alert-dialog";
import { toast } from "sonner";
import DashboardLayout from "@/components/DashboardLayout";
export default function AdminQuestionnaires() {
const [, navigate] = useLocation();
const [deleteId, setDeleteId] = useState<number | null>(null);
const { data: questionnaires, isLoading, refetch } = trpc.questionnaires.list.useQuery();
const deleteMutation = trpc.questionnaires.delete.useMutation({
onSuccess: () => {
toast.success("Questionnaire supprimé avec succès");
refetch();
setDeleteId(null);
},
onError: (error) => {
toast.error("Erreur lors de la suppression : " + error.message);
},
});
const getTypeBadge = (type: string) => {
const colors = {
satisfaction: "bg-blue-500",
evaluation_pre: "bg-green-500",
evaluation_post: "bg-purple-500",
};
const labels = {
satisfaction: "Satisfaction",
evaluation_pre: "Évaluation pré-formation",
evaluation_post: "Évaluation post-formation",
};
return (
<Badge className={colors[type as keyof typeof colors]}>
{labels[type as keyof typeof labels]}
</Badge>
);
};
return (
<DashboardLayout>
<div className="container mx-auto py-8">
<div className="flex justify-between items-center mb-6">
<div>
<h1 className="text-3xl font-bold">Questionnaires</h1>
<p className="text-muted-foreground mt-1">
Gérez les questionnaires de satisfaction et d'évaluation
</p>
</div>
<Button onClick={() => navigate("/admin/questionnaires/nouveau")}>
<Plus className="mr-2 h-4 w-4" />
Nouveau questionnaire
</Button>
</div>
{isLoading ? (
<div className="text-center py-12">
<p className="text-muted-foreground">Chargement...</p>
</div>
) : !questionnaires || questionnaires.length === 0 ? (
<Card>
<CardContent className="py-12 text-center">
<p className="text-muted-foreground mb-4">Aucun questionnaire créé</p>
<Button onClick={() => navigate("/admin/questionnaires/nouveau")}>
<Plus className="mr-2 h-4 w-4" />
Créer le premier questionnaire
</Button>
</CardContent>
</Card>
) : (
<div className="grid gap-4">
{questionnaires.map((questionnaire) => (
<Card key={questionnaire.id}>
<CardHeader>
<div className="flex justify-between items-start">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<CardTitle>{questionnaire.nom}</CardTitle>
{getTypeBadge(questionnaire.type)}
{!questionnaire.actif && (
<Badge variant="outline" className="bg-gray-100">
Inactif
</Badge>
)}
</div>
{questionnaire.description && (
<CardDescription>{questionnaire.description}</CardDescription>
)}
<div className="flex gap-4 mt-3 text-sm text-muted-foreground">
{questionnaire.envoiAutomatique && (
<span>
📧 Envoi automatique : J+{questionnaire.delaiEnvoiJours}
</span>
)}
</div>
</div>
<div className="flex gap-2">
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/admin/questionnaires/${questionnaire.id}/analyse`)}
>
<BarChart3 className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => navigate(`/admin/questionnaires/${questionnaire.id}`)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => setDeleteId(questionnaire.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</div>
</CardHeader>
</Card>
))}
</div>
)}
<AlertDialog open={deleteId !== null} onOpenChange={() => setDeleteId(null)}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmer la suppression</AlertDialogTitle>
<AlertDialogDescription>
Êtes-vous sûr de vouloir supprimer ce questionnaire ? Cette action est
irréversible et supprimera également toutes les questions associées.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction
onClick={() => deleteId && deleteMutation.mutate({ id: deleteId })}
className="bg-red-600 hover:bg-red-700"
>
Supprimer
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</div>
</DashboardLayout>
);
}