Checkpoint: Implémentation de 3 fonctionnalités avancées pour le tableau des rappels : (1) Badges visuels 🌍 Global (bleu) et 🎯 Ciblé (orange) dans la colonne Nom pour identifier rapidement le type de rappel, (2) Filtres par formation et séquence permettant de filtrer les rappels affichés (les rappels globaux passent tous les filtres), (3) Export CSV avec bouton dédié générant un fichier contenant toutes les informations des rappels (nom, type, template, délai, heure, formations, séquences, dates, statut) en respectant les filtres actifs. Interface utilisateur améliorée avec une meilleure organisation visuelle et des outils de gestion plus puissants.
This commit is contained in:
@@ -22,7 +22,7 @@ import {
|
||||
} 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 { Bell, Download, Edit, Plus, Trash2 } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { FileUpload } from "@/components/FileUpload";
|
||||
import { DateFormationMultiSelect } from "@/components/DateFormationMultiSelect";
|
||||
@@ -41,6 +41,8 @@ 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: "",
|
||||
@@ -183,6 +185,76 @@ export default function AdminRappels() {
|
||||
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 <span className="text-muted-foreground italic">Toutes les dates</span>;
|
||||
@@ -227,10 +299,16 @@ export default function AdminRappels() {
|
||||
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 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>
|
||||
@@ -244,6 +322,41 @@ export default function AdminRappels() {
|
||||
</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>
|
||||
@@ -258,9 +371,42 @@ export default function AdminRappels() {
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{rappels && rappels.length > 0 ? (
|
||||
rappels.map((rappel) => (
|
||||
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) => (
|
||||
<TableRow key={rappel.id}>
|
||||
<TableCell className="font-medium">{rappel.nom}</TableCell>
|
||||
<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}`}
|
||||
|
||||
6
todo.md
6
todo.md
@@ -566,3 +566,9 @@
|
||||
- [x] Ajouter une colonne "Dates concernées" avec formations, séquences et dates
|
||||
- [x] Modifier la procédure tRPC pour inclure les informations de dates
|
||||
- [x] Tester l'affichage avec plusieurs rappels
|
||||
|
||||
## Améliorations avancées du tableau des rappels
|
||||
- [x] Ajouter des badges visuels Global/Ciblé dans la colonne Nom
|
||||
- [x] Implémenter les filtres par formation et séquence
|
||||
- [x] Créer la fonction d'export CSV des rappels
|
||||
- [x] Tester toutes les fonctionnalités
|
||||
|
||||
Reference in New Issue
Block a user