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:
165
client/src/pages/AdminQuestionnaires.tsx
Normal file
165
client/src/pages/AdminQuestionnaires.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user