Checkpoint: Refonte complète du système de rappels pour permettre une gestion granulaire par séquence

Fonctionnalités ajoutées :
- Table de liaison rappelSequences pour associer rappels et séquences
- Composant SequenceMultiSelect avec recherche et multi-sélection
- Gestion des associations dans les procédures create/update
- Modification du scheduler pour filtrer les séquences selon les associations
- Si aucune séquence n'est sélectionnée, le rappel s'applique à toutes les séquences (comportement par défaut)

Le système permet maintenant de configurer des rappels spécifiques pour certaines dates/séquences particulières, offrant une flexibilité complète dans la gestion des rappels automatiques.
This commit is contained in:
Manus
2026-01-08 08:27:20 -05:00
parent 5de295a18c
commit 6ee87f9b22
12 changed files with 2462 additions and 15 deletions

View File

@@ -0,0 +1,122 @@
import { useState } from "react";
import { Check, ChevronsUpDown } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { Badge } from "@/components/ui/badge";
interface Sequence {
id: number;
nom: string;
formationNom?: string;
}
interface SequenceMultiSelectProps {
sequences: Sequence[];
selectedIds: number[];
onChange: (selectedIds: number[]) => void;
placeholder?: string;
}
export function SequenceMultiSelect({
sequences,
selectedIds,
onChange,
placeholder = "Sélectionnez des séquences...",
}: SequenceMultiSelectProps) {
const [open, setOpen] = useState(false);
const toggleSequence = (sequenceId: number) => {
const newSelection = selectedIds.includes(sequenceId)
? selectedIds.filter((id) => id !== sequenceId)
: [...selectedIds, sequenceId];
onChange(newSelection);
};
const selectedSequences = sequences.filter((seq) =>
selectedIds.includes(seq.id)
);
return (
<div className="space-y-2">
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{selectedIds.length === 0 ? (
<span className="text-muted-foreground">{placeholder}</span>
) : (
<span>
{selectedIds.length} séquence(s) sélectionnée(s)
</span>
)}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0" align="start">
<Command>
<CommandInput placeholder="Rechercher une séquence..." />
<CommandEmpty>Aucune séquence trouvée.</CommandEmpty>
<CommandGroup className="max-h-64 overflow-auto">
{sequences.map((sequence) => (
<CommandItem
key={sequence.id}
onSelect={() => toggleSequence(sequence.id)}
className="cursor-pointer"
>
<Check
className={cn(
"mr-2 h-4 w-4",
selectedIds.includes(sequence.id)
? "opacity-100"
: "opacity-0"
)}
/>
<div className="flex flex-col">
<span>{sequence.nom}</span>
{sequence.formationNom && (
<span className="text-xs text-muted-foreground">
{sequence.formationNom}
</span>
)}
</div>
</CommandItem>
))}
</CommandGroup>
</Command>
</PopoverContent>
</Popover>
{selectedSequences.length > 0 && (
<div className="flex flex-wrap gap-2">
{selectedSequences.map((sequence) => (
<Badge
key={sequence.id}
variant="secondary"
className="cursor-pointer hover:bg-destructive hover:text-destructive-foreground"
onClick={() => toggleSequence(sequence.id)}
>
{sequence.nom}
<span className="ml-1">×</span>
</Badge>
))}
</div>
)}
</div>
);
}

View File

@@ -25,6 +25,7 @@ import { trpc } from "@/lib/trpc";
import { Bell, Edit, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { FileUpload } from "@/components/FileUpload";
import { SequenceMultiSelect } from "@/components/SequenceMultiSelect";
// Types de templates disponibles pour les rappels
const TEMPLATE_TYPES = [
@@ -47,6 +48,7 @@ export default function AdminRappels() {
joursAvant: 7,
heureEnvoi: "09:00",
actif: true,
sequenceIds: [] as number[],
fichier: null as {
nomFichier: string;
urlFichier: string;
@@ -57,6 +59,7 @@ export default function AdminRappels() {
});
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");
@@ -111,6 +114,7 @@ export default function AdminRappels() {
joursAvant: 7,
heureEnvoi: "09:00",
actif: true,
sequenceIds: [],
});
};
@@ -131,6 +135,7 @@ export default function AdminRappels() {
joursAvant: rappel.joursAvant,
heureEnvoi: rappel.heureEnvoi,
actif: rappel.actif,
sequenceIds: rappel.sequenceIds || [],
fichier: rappel.nomFichier ? {
nomFichier: rappel.nomFichier,
urlFichier: rappel.urlFichier,
@@ -376,6 +381,23 @@ export default function AdminRappels() {
</p>
</div>
<div className="space-y-2">
<Label>Séquences concernées (optionnel)</Label>
<SequenceMultiSelect
sequences={sequences?.map(seq => ({
id: seq.id,
nom: seq.nom,
formationNom: seq.formation?.nom
})) || []}
selectedIds={formData.sequenceIds}
onChange={(sequenceIds) => setFormData({ ...formData, sequenceIds })}
placeholder="Toutes les séquences (par défaut)"
/>
<p className="text-sm text-muted-foreground">
Si aucune séquence n'est sélectionnée, le rappel s'appliquera à toutes les séquences
</p>
</div>
<FileUpload
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}
@@ -503,6 +525,23 @@ export default function AdminRappels() {
/>
</div>
<div className="space-y-2">
<Label>Séquences concernées (optionnel)</Label>
<SequenceMultiSelect
sequences={sequences?.map(seq => ({
id: seq.id,
nom: seq.nom,
formationNom: seq.formation?.nom
})) || []}
selectedIds={formData.sequenceIds}
onChange={(sequenceIds) => setFormData({ ...formData, sequenceIds })}
placeholder="Toutes les séquences (par défaut)"
/>
<p className="text-sm text-muted-foreground">
Si aucune séquence n'est sélectionnée, le rappel s'appliquera à toutes les séquences
</p>
</div>
<FileUpload
value={formData.fichier}
onChange={(fichier) => setFormData({ ...formData, fichier })}