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

906 lines
36 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 { Badge } from "@/components/ui/badge";
import { trpc } from "@/lib/trpc";
import { Bell, Download, Edit, Plus, Trash2, GraduationCap, Calendar, CalendarDays } from "lucide-react";
import { toast } from "sonner";
import { FileUpload } from "@/components/FileUpload";
import { DateFormationMultiSelect } from "@/components/DateFormationMultiSelect";
// Types de templates disponibles pour les rappels
const TEMPLATE_TYPES = [
{ value: "rappel1", label: "Rappel 1", joursAvant: 7 },
{ value: "rappel2", label: "Rappel 2", joursAvant: 1 },
{ value: "rappel3", label: "Rappel 3", joursAvant: 14 },
{ value: "rappel4", label: "Rappel 4", joursAvant: 21 },
{ value: "rappel5", label: "Rappel 5", joursAvant: 30 },
{ value: "rappel6", label: "Rappel 6", joursAvant: 60 },
];
export default function AdminRappels() {
const [isCreateOpen, setIsCreateOpen] = useState(false);
const [isEditOpen, setIsEditOpen] = useState(false);
const [editingRappel, setEditingRappel] = useState<any>(null);
const [filterFormation, setFilterFormation] = useState<string>("");
const [filterSequence, setFilterSequence] = useState<string>("");
const [formData, setFormData] = useState({
nom: "",
templateType: "",
timing: "pre_formation" as "pre_formation" | "post_formation",
joursAvant: 7,
heureEnvoi: "09:00",
actif: true,
dateFormationIds: [] as number[],
fichier: null as {
nomFichier: string;
urlFichier: string;
s3Key: string;
typeFichier: string;
tailleFichier: number;
} | null,
fichier2: null as {
nomFichier: string;
urlFichier: string;
s3Key: string;
typeFichier: string;
tailleFichier: number;
} | null,
});
const { data: rappels, refetch } = trpc.rappels.list.useQuery();
const { data: sequences } = trpc.sequences.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: "",
fichier: null,
fichier2: null,
timing: "pre_formation",
joursAvant: 7,
heureEnvoi: "09:00",
actif: true,
dateFormationIds: [],
});
};
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,
timing: rappel.timing || "pre_formation",
joursAvant: rappel.joursAvant,
heureEnvoi: rappel.heureEnvoi,
actif: rappel.actif,
dateFormationIds: rappel.dateFormationIds || [],
fichier: rappel.nomFichier ? {
nomFichier: rappel.nomFichier,
urlFichier: rappel.urlFichier,
s3Key: rappel.s3Key,
typeFichier: rappel.typeFichier,
tailleFichier: rappel.tailleFichier,
} : null,
fichier2: rappel.nomFichier2 ? {
nomFichier: rappel.nomFichier2,
urlFichier: rappel.urlFichier2,
s3Key: rappel.s3Key2,
typeFichier: rappel.typeFichier2,
tailleFichier: rappel.tailleFichier2,
} : null,
});
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;
};
const exportToCSV = () => {
if (!rappels || rappels.length === 0) {
toast.error("Aucun rappel à exporter");
return;
}
// Appliquer les mêmes filtres que le tableau
const filteredRappels = rappels.filter((rappel) => {
if (filterFormation && filterFormation !== "all") {
if (!rappel.datesDetails || rappel.datesDetails.length === 0) {
return true;
}
const hasFormation = rappel.datesDetails.some((d: any) => d.formationNom === filterFormation);
if (!hasFormation) return false;
}
if (filterSequence && filterSequence !== "all") {
if (!rappel.datesDetails || rappel.datesDetails.length === 0) {
return true;
}
const hasSequence = rappel.datesDetails.some((d: any) => d.sequenceNom === filterSequence);
if (!hasSequence) return false;
}
return true;
});
// Créer le CSV
const headers = ["Nom", "Type", "Template", "Délai", "Heure", "Formations", "Séquences", "Dates", "Statut"];
const rows = filteredRappels.map((rappel) => {
const type = rappel.datesDetails && rappel.datesDetails.length > 0 ? "Ciblé" : "Global";
const templateName = getTemplateName(rappel.templateType);
const delai = rappel.timing === "post_formation" ? `J+${rappel.joursAvant}` : `J-${rappel.joursAvant}`;
const statut = rappel.actif ? "Actif" : "Inactif";
let formations = "Toutes";
let sequences = "Toutes";
let dates = "Toutes";
if (rappel.datesDetails && rappel.datesDetails.length > 0) {
const formationNames = Array.from(new Set(rappel.datesDetails.map((d: any) => d.formationNom)));
const sequenceNames = Array.from(new Set(rappel.datesDetails.map((d: any) => d.sequenceNom)));
const dateOrdres = rappel.datesDetails.map((d: any) => `Date ${d.ordre}`).join(", ");
formations = formationNames.join("; ");
sequences = sequenceNames.join("; ");
dates = dateOrdres;
}
return [rappel.nom, type, templateName, delai, rappel.heureEnvoi, formations, sequences, dates, statut];
});
// Convertir en CSV
const csvContent = [
headers.join(","),
...rows.map(row => row.map(cell => `\"${cell}\"`).join(","))
].join("\n");
// Télécharger le fichier
const blob = new Blob(["\ufeff" + csvContent], { type: "text/csv;charset=utf-8;" });
const link = document.createElement("a");
const url = URL.createObjectURL(blob);
link.setAttribute("href", url);
link.setAttribute("download", `rappels_${new Date().toISOString().split('T')[0]}.csv`);
link.style.visibility = "hidden";
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
toast.success(`Export réussi : ${filteredRappels.length} rappel(s)`);
};
const formatDatesInfo = (rappel: any) => {
if (!rappel.datesDetails || rappel.datesDetails.length === 0) {
return (
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-gray-50 text-gray-600 border-gray-300">
<Calendar className="w-3 h-3 mr-1" />
Toutes les dates
</Badge>
</div>
);
}
// Grouper par formation et séquence
const grouped = rappel.datesDetails.reduce((acc: any, date: any) => {
const key = `${date.formationNom}|||${date.sequenceNom}`;
if (!acc[key]) {
acc[key] = {
formationNom: date.formationNom,
sequenceNom: date.sequenceNom,
dates: []
};
}
acc[key].dates.push(date);
return acc;
}, {});
return (
<div className="space-y-1.5">
{Object.values(grouped).map((group: any, groupIdx: number) => (
<div key={groupIdx} className="flex items-center gap-1.5 flex-wrap">
<Badge className="bg-blue-100 text-blue-800 border-blue-300 hover:bg-blue-100 text-xs">
<GraduationCap className="w-3 h-3 mr-1" />
{group.formationNom}
</Badge>
<span className="text-gray-400 text-sm"></span>
<Badge className="bg-green-100 text-green-800 border-green-300 hover:bg-green-100 text-xs">
<CalendarDays className="w-3 h-3 mr-1" />
{group.sequenceNom}
</Badge>
<span className="text-gray-400 text-sm">:</span>
{group.dates.map((d: any, idx: number) => (
<Badge
key={idx}
variant="outline"
className="bg-orange-50 text-orange-700 border-orange-300 text-xs"
>
<Calendar className="w-3 h-3 mr-1" />
Date {d.ordre}
</Badge>
))}
</div>
))}
</div>
);
};
const formatDatesInfoOLD = (rappel: any) => {
if (!rappel.datesDetails || rappel.datesDetails.length === 0) {
return (
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-gray-50 text-gray-600 border-gray-300">
<Calendar className="w-3 h-3 mr-1" />
Toutes les dates
</Badge>
</div>
);
}
// Grouper par formation et séquence
const grouped = rappel.datesDetails.reduce((acc: any, date: any) => {
const key = `${date.formationNom}|||${date.sequenceNom}`;
if (!acc[key]) {
acc[key] = {
formationNom: date.formationNom,
sequenceNom: date.sequenceNom,
dates: []
};
}
acc[key].dates.push(date);
return acc;
}, {});
return (
<div className="space-y-2">
{Object.values(grouped).map((group: any, groupIdx: number) => (
<div key={groupIdx} className="space-y-1">
<div className="flex items-start gap-1 flex-wrap">
<Badge className="bg-blue-100 text-blue-800 border-blue-300 hover:bg-blue-100">
<GraduationCap className="w-3 h-3 mr-1" />
{group.formationNom}
</Badge>
<Badge className="bg-green-100 text-green-800 border-green-300 hover:bg-green-100">
<CalendarDays className="w-3 h-3 mr-1" />
{group.sequenceNom}
</Badge>
</div>
<div className="flex items-center gap-1 flex-wrap ml-1">
{group.dates.map((d: any, idx: number) => (
<Badge
key={d.dateFormationId}
variant="outline"
className="bg-orange-50 text-orange-700 border-orange-300 text-xs"
>
Date {d.ordre}
</Badge>
))}
</div>
</div>
))}
</div>
);
};
return (
<DashboardLayout>
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h1 className="page-title">Rappels automatiques</h1>
<p className="text-muted-foreground">
Configurez les rappels envoyés automatiquement avant les séquences
</p>
</div>
<div className="flex gap-2">
<Button variant="outline" onClick={exportToCSV}>
<Download className="mr-2 h-4 w-4" />
Export CSV
</Button>
<Button onClick={() => setIsCreateOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
Nouveau rappel
</Button>
</div>
</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>
{/* Filtres */}
<div className="mb-4 flex gap-4">
<div className="flex-1">
<Label htmlFor="filter-formation">Filtrer par formation</Label>
<Select value={filterFormation} onValueChange={setFilterFormation}>
<SelectTrigger id="filter-formation">
<SelectValue placeholder="Toutes les formations" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes les formations</SelectItem>
{sequences && Array.from(new Set(sequences.map(s => s.formation?.nom).filter(Boolean))).map((formationNom) => (
<SelectItem key={formationNom} value={formationNom as string}>
{formationNom}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex-1">
<Label htmlFor="filter-sequence">Filtrer par séquence</Label>
<Select value={filterSequence} onValueChange={setFilterSequence}>
<SelectTrigger id="filter-sequence">
<SelectValue placeholder="Toutes les séquences" />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">Toutes les séquences</SelectItem>
{sequences && sequences.map((seq) => (
<SelectItem key={seq.id} value={seq.nom}>
{seq.nom}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div>
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[180px]">Nom</TableHead>
<TableHead className="w-[120px]">Template</TableHead>
<TableHead className="w-[70px]">Délai</TableHead>
<TableHead className="w-[70px]">Heure</TableHead>
<TableHead>Dates concernées</TableHead>
<TableHead className="w-[100px]">Pièce jointe</TableHead>
<TableHead className="w-[80px]">Statut</TableHead>
<TableHead className="w-[100px] text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{rappels && rappels.length > 0 ? (
rappels
.filter((rappel) => {
// Filtrer par formation
if (filterFormation && filterFormation !== "all") {
if (!rappel.datesDetails || rappel.datesDetails.length === 0) {
return true; // Les rappels globaux passent tous les filtres
}
const hasFormation = rappel.datesDetails.some((d: any) => d.formationNom === filterFormation);
if (!hasFormation) return false;
}
// Filtrer par séquence
if (filterSequence && filterSequence !== "all") {
if (!rappel.datesDetails || rappel.datesDetails.length === 0) {
return true; // Les rappels globaux passent tous les filtres
}
const hasSequence = rappel.datesDetails.some((d: any) => d.sequenceNom === filterSequence);
if (!hasSequence) return false;
}
return true;
})
.map((rappel, index) => (
<TableRow key={rappel.id} className={index % 2 === 0 ? "bg-white" : "bg-blue-50/70"}>
<TableCell className="font-medium">
<div className="flex items-center gap-2">
{rappel.datesDetails && rappel.datesDetails.length > 0 ? (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-800">
🎯 Ciblé
</span>
) : (
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-800">
🌍 Global
</span>
)}
<span>{rappel.nom}</span>
</div>
</TableCell>
<TableCell>{getTemplateName(rappel.templateType)}</TableCell>
<TableCell>
{rappel.timing === "post_formation" ? `J+${rappel.joursAvant}` : `J-${rappel.joursAvant}`}
</TableCell>
<TableCell>{rappel.heureEnvoi}</TableCell>
<TableCell>
{formatDatesInfo(rappel)}
</TableCell>
<TableCell>
{rappel.nomFichier && rappel.urlFichier ? (
<a
href={rappel.urlFichier}
download={rappel.nomFichier}
target="_blank"
rel="noopener noreferrer"
title={`${rappel.nomFichier} (${rappel.tailleFichier ? (rappel.tailleFichier / 1024).toFixed(1) + ' Ko' : 'Taille inconnue'})`}
className="inline-block"
>
<Badge variant="outline" className="bg-green-50 text-green-700 border-green-200 flex items-center gap-1.5 w-fit cursor-pointer hover:bg-green-100 transition-colors">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-check-circle">
<path d="M22 11.08V12a10 10 0 1 1-5.93-9.14"/>
<polyline points="22 4 12 14.01 9 11.01"/>
</svg>
Oui
</Badge>
</a>
) : (
<Badge variant="outline" className="bg-red-50 text-red-700 border-red-200 flex items-center gap-1.5 w-fit">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" className="lucide lucide-x-circle">
<circle cx="12" cy="12" r="10"/>
<path d="m15 9-6 6"/>
<path d="m9 9 6 6"/>
</svg>
Non
</Badge>
)}
</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 text-red-600" />
</Button>
</div>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={8} 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">Template d'email utilisé *</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>Timing d'envoi *</Label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="timing"
value="pre_formation"
checked={formData.timing === "pre_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Pré-formation</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="timing"
value="post_formation"
checked={formData.timing === "post_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Post-formation</span>
</label>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="joursAvant">
{formData.timing === "pre_formation"
? "Nombre de jours avant la séquence *"
: "Nombre de jours après la séquence *"}
</Label>
<Input
id="joursAvant"
type="number"
min="1"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
}
placeholder="Ex: 7"
/>
<p className="text-sm text-muted-foreground">
{formData.timing === "pre_formation"
? "Le rappel sera envoyé ce nombre de jours avant le début de la séquence"
: "Le rappel sera envoyé ce nombre de jours après la fin 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="space-y-2">
<Label>Dates de formation concernées (optionnel)</Label>
<DateFormationMultiSelect
sequences={sequences?.map(seq => ({
id: seq.id,
nom: seq.nom,
formationNom: seq.formation?.nom || '',
dates: seq.dates?.map((d: any) => ({
id: d.id,
date: d.dateDebut ? new Date(d.dateDebut).toISOString() : '',
heureDebut: d.dateDebut ? new Date(d.dateDebut).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '',
heureFin: d.dateFin ? new Date(d.dateFin).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '',
})) || [],
})) || []}
selectedDateIds={formData.dateFormationIds}
onSelectionChange={(dateFormationIds) => setFormData({ ...formData, dateFormationIds })}
/>
</div>
<FileUpload
label="Pièce jointe 1 (optionnel)"
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<FileUpload
label="Pièce jointe 2 (optionnel)"
value={formData.fichier2}
onChange={(fichier2) => setFormData({ ...formData, fichier2 })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<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">Template d'email utilisé *</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>Timing d'envoi *</Label>
<div className="flex gap-4">
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="edit-timing"
value="pre_formation"
checked={formData.timing === "pre_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Pré-formation</span>
</label>
<label className="flex items-center gap-2 cursor-pointer">
<input
type="radio"
name="edit-timing"
value="post_formation"
checked={formData.timing === "post_formation"}
onChange={(e) => setFormData({ ...formData, timing: e.target.value as "pre_formation" | "post_formation" })}
className="w-4 h-4"
/>
<span>Post-formation</span>
</label>
</div>
</div>
<div className="space-y-2">
<Label htmlFor="edit-joursAvant">
{formData.timing === "pre_formation"
? "Nombre de jours avant la séquence *"
: "Nombre de jours après la séquence *"}
</Label>
<Input
id="edit-joursAvant"
type="number"
min="1"
value={formData.joursAvant}
onChange={(e) =>
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
}
placeholder="Ex: 7"
/>
<p className="text-sm text-muted-foreground">
{formData.timing === "pre_formation"
? "Le rappel sera envoyé ce nombre de jours avant le début de la séquence"
: "Le rappel sera envoyé ce nombre de jours après la fin de la séquence"}
</p>
</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="space-y-2">
<Label>Dates de formation concernées (optionnel)</Label>
<DateFormationMultiSelect
sequences={sequences?.map(seq => ({
id: seq.id,
nom: seq.nom,
formationNom: seq.formation?.nom || '',
dates: seq.dates?.map(d => ({
id: d.id,
date: d.dateDebut ? new Date(d.dateDebut).toISOString() : '',
heureDebut: d.dateDebut ? new Date(d.dateDebut).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '',
heureFin: d.dateFin ? new Date(d.dateFin).toLocaleTimeString('fr-FR', { hour: '2-digit', minute: '2-digit' }) : '',
})) || [],
})) || []}
selectedDateIds={formData.dateFormationIds}
onSelectionChange={(dateFormationIds) => setFormData({ ...formData, dateFormationIds })}
/>
</div>
<FileUpload
label="Pièce jointe 1 (optionnel)"
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<FileUpload
label="Pièce jointe 2 (optionnel)"
value={formData.fichier2}
onChange={(fichier2) => setFormData({ ...formData, fichier2 })}
accept=".pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.jpg,.jpeg,.png"
maxSize={10}
/>
<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>
);
}