906 lines
36 KiB
TypeScript
906 lines
36 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 { 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>
|
||
);
|
||
}
|