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:
9
.manus/db/db-query-1767877951620.json
Normal file
9
.manus/db/db-query-1767877951620.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"query": "CREATE TABLE IF NOT EXISTS `rappelSequences` (\n `id` int AUTO_INCREMENT NOT NULL,\n `rappelId` int NOT NULL,\n `sequenceId` int NOT NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n CONSTRAINT `rappelSequences_id` PRIMARY KEY(`id`)\n);",
|
||||||
|
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute CREATE TABLE IF NOT EXISTS `rappelSequences` (\n `id` int AUTO_INCREMENT NOT NULL,\n `rappelId` int NOT NULL,\n `sequenceId` int NOT NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n CONSTRAINT `rappelSequences_id` PRIMARY KEY(`id`)\n);",
|
||||||
|
"rows": [],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 652
|
||||||
|
}
|
||||||
9
.manus/db/db-query-1767878502013.json
Normal file
9
.manus/db/db-query-1767878502013.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"query": "SELECT * FROM rappelSequences;",
|
||||||
|
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT * FROM rappelSequences;",
|
||||||
|
"rows": [],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 119
|
||||||
|
}
|
||||||
16
.manus/db/db-query-1767878680556.json
Normal file
16
.manus/db/db-query-1767878680556.json
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
{
|
||||||
|
"query": "SELECT r.id, r.nom, rs.sequenceId, s.nom as sequenceNom FROM rappels r LEFT JOIN rappelSequences rs ON r.id = rs.rappelId LEFT JOIN sequences s ON rs.sequenceId = s.id WHERE r.nom = 'Rappel 2' ORDER BY r.id DESC LIMIT 5;",
|
||||||
|
"command": "mysql --batch --raw --column-names --default-character-set=utf8mb4 --host gateway02.us-east-1.prod.aws.tidbcloud.com --port 4000 --user 4CrrYuB5tme73Qo.root --database 7PAT67UmWoxv8vwp8Bbcv6 --execute SELECT r.id, r.nom, rs.sequenceId, s.nom as sequenceNom FROM rappels r LEFT JOIN rappelSequences rs ON r.id = rs.rappelId LEFT JOIN sequences s ON rs.sequenceId = s.id WHERE r.nom = 'Rappel 2' ORDER BY r.id DESC LIMIT 5;",
|
||||||
|
"rows": [
|
||||||
|
{
|
||||||
|
"id": "60002",
|
||||||
|
"nom": "Rappel 2",
|
||||||
|
"sequenceId": "90002",
|
||||||
|
"sequenceNom": "Groupe B"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "id\tnom\tsequenceId\tsequenceNom\n60002\tRappel 2\t90002\tGroupe B\n",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 58
|
||||||
|
}
|
||||||
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 { Bell, Edit, Plus, Trash2 } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { FileUpload } from "@/components/FileUpload";
|
import { FileUpload } from "@/components/FileUpload";
|
||||||
|
import { SequenceMultiSelect } from "@/components/SequenceMultiSelect";
|
||||||
|
|
||||||
// Types de templates disponibles pour les rappels
|
// Types de templates disponibles pour les rappels
|
||||||
const TEMPLATE_TYPES = [
|
const TEMPLATE_TYPES = [
|
||||||
@@ -47,6 +48,7 @@ export default function AdminRappels() {
|
|||||||
joursAvant: 7,
|
joursAvant: 7,
|
||||||
heureEnvoi: "09:00",
|
heureEnvoi: "09:00",
|
||||||
actif: true,
|
actif: true,
|
||||||
|
sequenceIds: [] as number[],
|
||||||
fichier: null as {
|
fichier: null as {
|
||||||
nomFichier: string;
|
nomFichier: string;
|
||||||
urlFichier: string;
|
urlFichier: string;
|
||||||
@@ -57,6 +59,7 @@ export default function AdminRappels() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { data: rappels, refetch } = trpc.rappels.list.useQuery();
|
const { data: rappels, refetch } = trpc.rappels.list.useQuery();
|
||||||
|
const { data: sequences } = trpc.sequences.list.useQuery();
|
||||||
const createMutation = trpc.rappels.create.useMutation({
|
const createMutation = trpc.rappels.create.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
toast.success("Rappel créé avec succès");
|
toast.success("Rappel créé avec succès");
|
||||||
@@ -111,6 +114,7 @@ export default function AdminRappels() {
|
|||||||
joursAvant: 7,
|
joursAvant: 7,
|
||||||
heureEnvoi: "09:00",
|
heureEnvoi: "09:00",
|
||||||
actif: true,
|
actif: true,
|
||||||
|
sequenceIds: [],
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -131,6 +135,7 @@ export default function AdminRappels() {
|
|||||||
joursAvant: rappel.joursAvant,
|
joursAvant: rappel.joursAvant,
|
||||||
heureEnvoi: rappel.heureEnvoi,
|
heureEnvoi: rappel.heureEnvoi,
|
||||||
actif: rappel.actif,
|
actif: rappel.actif,
|
||||||
|
sequenceIds: rappel.sequenceIds || [],
|
||||||
fichier: rappel.nomFichier ? {
|
fichier: rappel.nomFichier ? {
|
||||||
nomFichier: rappel.nomFichier,
|
nomFichier: rappel.nomFichier,
|
||||||
urlFichier: rappel.urlFichier,
|
urlFichier: rappel.urlFichier,
|
||||||
@@ -376,6 +381,23 @@ export default function AdminRappels() {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</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
|
<FileUpload
|
||||||
value={formData.fichier}
|
value={formData.fichier}
|
||||||
onChange={(fichier) => setFormData({ ...formData, fichier })}
|
onChange={(fichier) => setFormData({ ...formData, fichier })}
|
||||||
@@ -503,6 +525,23 @@ export default function AdminRappels() {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</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
|
<FileUpload
|
||||||
value={formData.fichier}
|
value={formData.fichier}
|
||||||
onChange={(fichier) => setFormData({ ...formData, fichier })}
|
onChange={(fichier) => setFormData({ ...formData, fichier })}
|
||||||
|
|||||||
2124
drizzle/meta/0030_snapshot.json
Normal file
2124
drizzle/meta/0030_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -211,6 +211,13 @@
|
|||||||
"when": 1767869539717,
|
"when": 1767869539717,
|
||||||
"tag": "0029_classy_warbound",
|
"tag": "0029_classy_warbound",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 30,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1767877943207,
|
||||||
|
"tag": "0030_cultured_korg",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -305,6 +305,23 @@ export const rappels = mysqlTable("rappels", {
|
|||||||
export type Rappel = typeof rappels.$inferSelect;
|
export type Rappel = typeof rappels.$inferSelect;
|
||||||
export type InsertRappel = typeof rappels.$inferInsert;
|
export type InsertRappel = typeof rappels.$inferInsert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table de liaison entre rappels et séquences (many-to-many)
|
||||||
|
* Permet de définir quelles séquences sont concernées par chaque rappel
|
||||||
|
* Si aucune association n'existe pour un rappel, il s'applique à toutes les séquences
|
||||||
|
*/
|
||||||
|
export const rappelSequences = mysqlTable("rappelSequences", {
|
||||||
|
id: int("id").autoincrement().primaryKey(),
|
||||||
|
/** ID du rappel */
|
||||||
|
rappelId: int("rappelId").notNull(),
|
||||||
|
/** ID de la séquence concernée */
|
||||||
|
sequenceId: int("sequenceId").notNull(),
|
||||||
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type RappelSequence = typeof rappelSequences.$inferSelect;
|
||||||
|
export type InsertRappelSequence = typeof rappelSequences.$inferInsert;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Table des questionnaires
|
* Table des questionnaires
|
||||||
* Stocke les questionnaires de satisfaction et d'évaluation des acquis
|
* Stocke les questionnaires de satisfaction et d'évaluation des acquis
|
||||||
|
|||||||
52
server/db.ts
52
server/db.ts
@@ -29,7 +29,8 @@ import {
|
|||||||
Formateur,
|
Formateur,
|
||||||
rappels,
|
rappels,
|
||||||
InsertRappel,
|
InsertRappel,
|
||||||
Rappel
|
Rappel,
|
||||||
|
rappelSequences
|
||||||
} from "../drizzle/schema";
|
} from "../drizzle/schema";
|
||||||
import { ENV } from './_core/env';
|
import { ENV } from './_core/env';
|
||||||
|
|
||||||
@@ -969,7 +970,20 @@ export async function getRappels() {
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return [];
|
if (!db) return [];
|
||||||
|
|
||||||
return await db.select().from(rappels).orderBy(rappels.joursAvant);
|
const allRappels = await db.select().from(rappels).orderBy(rappels.joursAvant);
|
||||||
|
|
||||||
|
// Pour chaque rappel, récupérer les séquences associées
|
||||||
|
const rappelsWithSequences = await Promise.all(
|
||||||
|
allRappels.map(async (rappel) => {
|
||||||
|
const sequenceIds = await getRappelSequences(rappel.id);
|
||||||
|
return {
|
||||||
|
...rappel,
|
||||||
|
sequenceIds,
|
||||||
|
};
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
return rappelsWithSequences;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getRappelById(id: number): Promise<Rappel | undefined> {
|
export async function getRappelById(id: number): Promise<Rappel | undefined> {
|
||||||
@@ -1022,9 +1036,43 @@ export async function deleteRappel(id: number) {
|
|||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) throw new Error("Database not available");
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
// Supprimer d'abord les associations
|
||||||
|
await db.delete(rappelSequences).where(eq(rappelSequences.rappelId, id));
|
||||||
|
// Puis le rappel
|
||||||
await db.delete(rappels).where(eq(rappels.id, id));
|
await db.delete(rappels).where(eq(rappels.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== RAPPEL SEQUENCES ====================
|
||||||
|
|
||||||
|
export async function createRappelSequences(rappelId: number, sequenceIds: number[]) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
if (sequenceIds.length === 0) return;
|
||||||
|
|
||||||
|
const values = sequenceIds.map(sequenceId => ({
|
||||||
|
rappelId,
|
||||||
|
sequenceId,
|
||||||
|
}));
|
||||||
|
|
||||||
|
await db.insert(rappelSequences).values(values);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRappelSequences(rappelId: number) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
await db.delete(rappelSequences).where(eq(rappelSequences.rappelId, rappelId));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRappelSequences(rappelId: number): Promise<number[]> {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
const results = await db.select().from(rappelSequences).where(eq(rappelSequences.rappelId, rappelId));
|
||||||
|
return results.map(r => r.sequenceId);
|
||||||
|
}
|
||||||
|
|
||||||
export async function updateRappelExecution(id: number) {
|
export async function updateRappelExecution(id: number) {
|
||||||
const db = await getDb();
|
const db = await getDb();
|
||||||
if (!db) return;
|
if (!db) return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { getDb } from "./db";
|
import { getDb } from "./db";
|
||||||
import { rappels, sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema";
|
import { rappels, sequences, inscriptions, apprenants, formations, datesFormation, formateurs } from "../drizzle/schema";
|
||||||
import { eq, and, gte, lte, sql } from "drizzle-orm";
|
import { eq, and, gte, lte, sql, inArray } from "drizzle-orm";
|
||||||
import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService";
|
import { sendRappelJ7Email, sendRappelJ1Email } from "./emailService";
|
||||||
import { logRappelEnvoye, rappelDejaEnvoye } from "./rappelDb";
|
import { logRappelEnvoye, rappelDejaEnvoye } from "./rappelDb";
|
||||||
import { notifyOwner } from "./_core/notification";
|
import { notifyOwner } from "./_core/notification";
|
||||||
@@ -55,16 +55,38 @@ async function processRappel(rappel: any) {
|
|||||||
|
|
||||||
console.log(`[Rappels] Recherche des séquences débutant le ${dateCible.toLocaleDateString()}`);
|
console.log(`[Rappels] Recherche des séquences débutant le ${dateCible.toLocaleDateString()}`);
|
||||||
|
|
||||||
// CORRECTION DU BUG: Récupérer d'abord toutes les séquences
|
// Vérifier si ce rappel est associé à des séquences spécifiques
|
||||||
const toutesSequences = await db
|
const sequencesAssociees = rappel.sequenceIds || [];
|
||||||
.select({
|
const filtreSequences = sequencesAssociees.length > 0;
|
||||||
sequence: sequences,
|
|
||||||
formation: formations,
|
console.log(`[Rappels] ${filtreSequences ? `Filtré sur ${sequencesAssociees.length} séquence(s) spécifique(s)` : 'Toutes les séquences'}`);
|
||||||
formateur: formateurs,
|
|
||||||
})
|
// Récupérer toutes les séquences (ou uniquement celles associées)
|
||||||
.from(sequences)
|
let toutesSequences;
|
||||||
.leftJoin(formations, eq(sequences.formationId, formations.id))
|
if (filtreSequences) {
|
||||||
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id));
|
// Seulement les séquences associées à ce rappel
|
||||||
|
toutesSequences = await db
|
||||||
|
.select({
|
||||||
|
sequence: sequences,
|
||||||
|
formation: formations,
|
||||||
|
formateur: formateurs,
|
||||||
|
})
|
||||||
|
.from(sequences)
|
||||||
|
.leftJoin(formations, eq(sequences.formationId, formations.id))
|
||||||
|
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id))
|
||||||
|
.where(inArray(sequences.id, sequencesAssociees));
|
||||||
|
} else {
|
||||||
|
// Toutes les séquences
|
||||||
|
toutesSequences = await db
|
||||||
|
.select({
|
||||||
|
sequence: sequences,
|
||||||
|
formation: formations,
|
||||||
|
formateur: formateurs,
|
||||||
|
})
|
||||||
|
.from(sequences)
|
||||||
|
.leftJoin(formations, eq(sequences.formationId, formations.id))
|
||||||
|
.leftJoin(formateurs, eq(sequences.formateurId, formateurs.id));
|
||||||
|
}
|
||||||
|
|
||||||
// Filtrer les séquences dont la première date correspond à la date cible
|
// Filtrer les séquences dont la première date correspond à la date cible
|
||||||
const sequencesConcernees = [];
|
const sequencesConcernees = [];
|
||||||
|
|||||||
@@ -1070,6 +1070,7 @@ export const appRouter = router({
|
|||||||
joursAvant: z.number(),
|
joursAvant: z.number(),
|
||||||
heureEnvoi: z.string().default("09:00"),
|
heureEnvoi: z.string().default("09:00"),
|
||||||
actif: z.boolean(),
|
actif: z.boolean(),
|
||||||
|
sequenceIds: z.array(z.number()).optional(),
|
||||||
fichier: z.object({
|
fichier: z.object({
|
||||||
nomFichier: z.string(),
|
nomFichier: z.string(),
|
||||||
urlFichier: z.string(),
|
urlFichier: z.string(),
|
||||||
@@ -1079,7 +1080,18 @@ export const appRouter = router({
|
|||||||
}).nullable().optional(),
|
}).nullable().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return db.createRappel(input);
|
const { sequenceIds, ...rappelData } = input;
|
||||||
|
const result: any = await db.createRappel(rappelData);
|
||||||
|
|
||||||
|
// Créer les associations rappel-séquence si des séquences sont spécifiées
|
||||||
|
if (sequenceIds && sequenceIds.length > 0) {
|
||||||
|
const rappelId = result.insertId || result[0]?.insertId;
|
||||||
|
if (rappelId) {
|
||||||
|
await db.createRappelSequences(Number(rappelId), sequenceIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
|
|
||||||
update: adminProcedure
|
update: adminProcedure
|
||||||
@@ -1091,6 +1103,7 @@ export const appRouter = router({
|
|||||||
joursAvant: z.number().optional(),
|
joursAvant: z.number().optional(),
|
||||||
heureEnvoi: z.string().optional(),
|
heureEnvoi: z.string().optional(),
|
||||||
actif: z.boolean().optional(),
|
actif: z.boolean().optional(),
|
||||||
|
sequenceIds: z.array(z.number()).optional(),
|
||||||
fichier: z.object({
|
fichier: z.object({
|
||||||
nomFichier: z.string(),
|
nomFichier: z.string(),
|
||||||
urlFichier: z.string(),
|
urlFichier: z.string(),
|
||||||
@@ -1100,8 +1113,17 @@ export const appRouter = router({
|
|||||||
}).nullable().optional(),
|
}).nullable().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
const { id, ...data } = input;
|
const { id, sequenceIds, ...data } = input;
|
||||||
await db.updateRappel(id, data);
|
await db.updateRappel(id, data);
|
||||||
|
|
||||||
|
// Mettre à jour les associations rappel-séquence si spécifiées
|
||||||
|
if (sequenceIds !== undefined) {
|
||||||
|
await db.deleteRappelSequences(id);
|
||||||
|
if (sequenceIds.length > 0) {
|
||||||
|
await db.createRappelSequences(id, sequenceIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
12
todo.md
12
todo.md
@@ -537,3 +537,15 @@
|
|||||||
## Correction affichage délai rappels post-formation
|
## Correction affichage délai rappels post-formation
|
||||||
- [x] Corriger l.affichage de la colonne "Délai" pour afficher J+X pour les rappels post-formation
|
- [x] Corriger l.affichage de la colonne "Délai" pour afficher J+X pour les rappels post-formation
|
||||||
- [x] Tester l.affichage avec un rappel pré-formation et un rappel post-formation
|
- [x] Tester l.affichage avec un rappel pré-formation et un rappel post-formation
|
||||||
|
|
||||||
|
## Refonte système de rappels - Gestion par séquence
|
||||||
|
- [x] Créer la table rappelSequences (relation many-to-many entre rappels et séquences)
|
||||||
|
- [x] Appliquer la migration en base de données
|
||||||
|
- [x] Créer un composant de sélection multi-séquences
|
||||||
|
- [x] Modifier le formulaire AdminRappels pour intégrer le sélecteur de séquences
|
||||||
|
- [x] Créer les fonctions DB pour gérer les associations rappel-séquence
|
||||||
|
- [x] Mettre à jour les procédures tRPC create et update pour gérer les associations
|
||||||
|
- [x] Modifier le scheduler pour ne traiter que les séquences associées à chaque rappel
|
||||||
|
- [ ] Adapter l'affichage du tableau pour montrer les séquences associées
|
||||||
|
- [ ] Tester la création d'un rappel avec sélection de séquences spécifiques
|
||||||
|
- [ ] Tester l'envoi automatique avec le nouveau système
|
||||||
|
|||||||
Reference in New Issue
Block a user