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:
122
client/src/components/SequenceMultiSelect.tsx
Normal file
122
client/src/components/SequenceMultiSelect.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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 })}
|
||||
|
||||
Reference in New Issue
Block a user