Files
formation-manager-itinova/client/src/pages/AdminRappels.tsx

426 lines
15 KiB
TypeScript

import { useState } from "react";
import DashboardLayout from "@/components/DashboardLayout";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { trpc } from "@/lib/trpc";
import { Bell, Edit, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
// Types de templates disponibles pour les rappels
const TEMPLATE_TYPES = [
{ value: "rappel", label: "Rappel J-7", joursAvant: 7 },
{ value: "rappelJ1", label: "Rappel J-1", joursAvant: 1 },
];
export default function AdminRappels() {
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false);
const [editingRappel, setEditingRappel] = useState<any>(null);
const [formData, setFormData] = useState({
nom: "",
templateType: "",
joursAvant: 7,
heureEnvoi: "09:00",
actif: true,
});
const { data: rappels, refetch } = trpc.rappels.list.useQuery();
const createMutation = trpc.rappels.create.useMutation({
onSuccess: () => {
toast.success("Rappel créé avec succès");
refetch();
setIsCreateOpen(false);
resetForm();
},
onError: (error) => {
toast.error(`Erreur: ${error.message}`);
},
});
const updateMutation = trpc.rappels.update.useMutation({
onSuccess: () => {
toast.success("Rappel modifié avec succès");
refetch();
setIsEditOpen(false);
setEditingRappel(null);
resetForm();
},
onError: (error) => {
toast.error(`Erreur: ${error.message}`);
},
});
const deleteMutation = trpc.rappels.delete.useMutation({
onSuccess: () => {
toast.success("Rappel supprimé avec succès");
refetch();
},
onError: (error) => {
toast.error(`Erreur: ${error.message}`);
},
});
const toggleActifMutation = trpc.rappels.toggleActif.useMutation({
onSuccess: () => {
toast.success("Statut modifié avec succès");
refetch();
},
onError: (error) => {
toast.error(`Erreur: ${error.message}`);
},
});
const resetForm = () => {
setFormData({
nom: "",
templateType: "",
joursAvant: 7,
heureEnvoi: "09:00",
actif: true,
});
};
const handleCreate = () => {
if (!formData.nom || !formData.templateType) {
toast.error("Veuillez remplir tous les champs obligatoires");
return;
}
createMutation.mutate(formData);
};
const handleEdit = (rappel: any) => {
setEditingRappel(rappel);
setFormData({
nom: rappel.nom,
templateType: rappel.templateType,
joursAvant: rappel.joursAvant,
heureEnvoi: rappel.heureEnvoi,
actif: rappel.actif,
});
setIsEditOpen(true);
};
const handleUpdate = () => {
if (!editingRappel) return;
if (!formData.nom || !formData.templateType) {
toast.error("Veuillez remplir tous les champs obligatoires");
return;
}
updateMutation.mutate({
id: editingRappel.id,
...formData,
});
};
const handleDelete = (id: number) => {
if (confirm("Êtes-vous sûr de vouloir supprimer ce rappel ?")) {
deleteMutation.mutate({ id });
}
};
const handleToggleActif = (id: number, actif: boolean) => {
toggleActifMutation.mutate({ id, actif: !actif });
};
const handleTemplateTypeChange = (value: string) => {
const template = TEMPLATE_TYPES.find(t => t.value === value);
setFormData({
...formData,
templateType: value,
joursAvant: template?.joursAvant || 7,
nom: template?.label || "",
});
};
const getTemplateName = (type: string) => {
return TEMPLATE_TYPES.find(t => t.value === type)?.label || type;
};
return (
<DashboardLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="text-3xl font-bold tracking-tight">Rappels automatiques</h1>
<p className="text-muted-foreground">
Configurez les rappels envoyés automatiquement avant les séquences
</p>
</div>
<Button onClick={() => setIsCreateOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Nouveau rappel
</Button>
</div>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Bell className="h-5 w-5" />
Liste des rappels configurés
</CardTitle>
<CardDescription>
Les rappels actifs seront envoyés automatiquement aux apprenants inscrits
</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Nom</TableHead>
<TableHead>Type</TableHead>
<TableHead>Délai</TableHead>
<TableHead>Heure</TableHead>
<TableHead>Statut</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rappels && rappels.length > 0 ? (
rappels.map((rappel) => (
<TableRow key={rappel.id}>
<TableCell className="font-medium">{rappel.nom}</TableCell>
<TableCell>{getTemplateName(rappel.templateType)}</TableCell>
<TableCell>J-{rappel.joursAvant}</TableCell>
<TableCell>{rappel.heureEnvoi}</TableCell>
<TableCell>
<button
onClick={() => handleToggleActif(rappel.id, rappel.actif)}
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium cursor-pointer ${
rappel.actif
? "bg-green-100 text-green-800 hover:bg-green-200"
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
}`}
>
{rappel.actif ? "Actif" : "Inactif"}
</button>
</TableCell>
<TableCell className="text-right">
<div className="flex justify-end gap-2">
<Button
variant="ghost"
size="icon"
onClick={() => handleEdit(rappel)}
>
<Edit className="h-4 w-4" />
</Button>
<Button
variant="ghost"
size="icon"
onClick={() => handleDelete(rappel.id)}
>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={6} className="text-center text-muted-foreground">
Aucun rappel configuré
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Dialog de création */}
<Dialog open={isCreateOpen} onOpenChange={setIsCreateOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Créer un nouveau rappel</DialogTitle>
<DialogDescription>
Configurez un rappel automatique à envoyer avant les séquences
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="templateType">Type de rappel *</Label>
<Select
value={formData.templateType}
onValueChange={handleTemplateTypeChange}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionnez un type de rappel" />
</SelectTrigger>
<SelectContent>
{TEMPLATE_TYPES.map((template) => (
<SelectItem key={template.value} value={template.value}>
{template.label}
</SelectItem>
))}
</SelectContent>
</Select>
<p className="text-sm text-muted-foreground">
Le template d'email correspondant sera utilisé pour l'envoi
</p>
</div>
<div className="space-y-2">
<Label htmlFor="nom">Nom du rappel *</Label>
<Input
id="nom"
value={formData.nom}
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
placeholder="Ex: Rappel automatique J-7"
/>
</div>
<div className="space-y-2">
<Label htmlFor="joursAvant">Nombre de jours avant la séquence *</Label>
<Input
id="joursAvant"
type="number"
min="0"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
}
placeholder="Ex: 7"
/>
<p className="text-sm text-muted-foreground">
Le rappel sera envoyé ce nombre de jours avant le début de la séquence
</p>
</div>
<div className="space-y-2">
<Label htmlFor="heureEnvoi">Heure d'envoi *</Label>
<Input
id="heureEnvoi"
type="time"
value={formData.heureEnvoi}
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
/>
<p className="text-sm text-muted-foreground">
L'heure à laquelle le rappel sera envoyé chaque jour
</p>
</div>
<div className="flex items-center space-x-2">
<Switch
id="actif"
checked={formData.actif}
onCheckedChange={(checked) => setFormData({ ...formData, actif: checked })}
/>
<Label htmlFor="actif">Rappel actif</Label>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsCreateOpen(false)}>
Annuler
</Button>
<Button onClick={handleCreate} disabled={createMutation.isPending}>
{createMutation.isPending ? "Création..." : "Créer"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Dialog de modification */}
<Dialog open={isEditOpen} onOpenChange={setIsEditOpen}>
<DialogContent className="max-w-2xl">
<DialogHeader>
<DialogTitle>Modifier le rappel</DialogTitle>
<DialogDescription>
Modifiez la configuration du rappel automatique
</DialogDescription>
</DialogHeader>
<div className="space-y-4">
<div className="space-y-2">
<Label htmlFor="edit-templateType">Type de rappel *</Label>
<Select
value={formData.templateType}
onValueChange={handleTemplateTypeChange}
>
<SelectTrigger>
<SelectValue placeholder="Sélectionnez un type de rappel" />
</SelectTrigger>
<SelectContent>
{TEMPLATE_TYPES.map((template) => (
<SelectItem key={template.value} value={template.value}>
{template.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="edit-nom">Nom du rappel *</Label>
<Input
id="edit-nom"
value={formData.nom}
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
placeholder="Ex: Rappel automatique J-7"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-joursAvant">Nombre de jours avant la séquence *</Label>
<Input
id="edit-joursAvant"
type="number"
min="0"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
}
placeholder="Ex: 7"
/>
</div>
<div className="space-y-2">
<Label htmlFor="edit-heureEnvoi">Heure d'envoi *</Label>
<Input
id="edit-heureEnvoi"
type="time"
value={formData.heureEnvoi}
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
/>
</div>
<div className="flex items-center space-x-2">
<Switch
id="edit-actif"
checked={formData.actif}
onCheckedChange={(checked) => setFormData({ ...formData, actif: checked })}
/>
<Label htmlFor="edit-actif">Rappel actif</Label>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setIsEditOpen(false)}>
Annuler
</Button>
<Button onClick={handleUpdate} disabled={updateMutation.isPending}>
{updateMutation.isPending ? "Modification..." : "Modifier"}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
</DashboardLayout>
);
}