426 lines
15 KiB
TypeScript
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>
|
|
);
|
|
}
|