Checkpoint: Modification de la fenêtre de création des rappels
## Modifications effectuées :
### Base de données (drizzle/schema.ts)
Ajout d'un champ `timing` dans la table `rappels` :
- Type : ENUM('pre_formation', 'post_formation')
- Valeur par défaut : 'pre_formation'
- Permet de distinguer les rappels avant et après formation
### Interface utilisateur (client/src/pages/AdminRappels.tsx)
**Formulaire de création et d'édition :**
- Ajout de radio buttons pour choisir "Pré-formation" ou "Post-formation"
- Le label du champ "Nombre de jours" change dynamiquement selon le choix :
* "Nombre de jours avant la séquence" (pré-formation)
* "Nombre de jours après la séquence" (post-formation)
- Le texte d'aide s'adapte également au timing choisi
**États et formulaires :**
- Ajout du champ `timing` dans `formData`
- Mise à jour de `resetForm()` et `handleEdit()` pour gérer le nouveau champ
### Backend (server/routers.ts)
Mise à jour des procédures tRPC :
- `rappels.create` : ajout du champ `timing` avec validation Zod
- `rappels.update` : ajout du champ `timing` optionnel
## Fonctionnement
L'utilisateur peut maintenant créer des rappels qui seront envoyés soit avant (pré-formation) soit après (post-formation) une séquence, avec un nombre de jours personnalisable pour chaque cas.
This commit is contained in:
9
.manus/db/db-query-1767868512905.json
Normal file
9
.manus/db/db-query-1767868512905.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"query": "ALTER TABLE rappels ADD COLUMN timing ENUM('pre_formation', 'post_formation') NOT NULL DEFAULT 'pre_formation' AFTER templateType;",
|
||||||
|
"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 ALTER TABLE rappels ADD COLUMN timing ENUM('pre_formation', 'post_formation') NOT NULL DEFAULT 'pre_formation' AFTER templateType;",
|
||||||
|
"rows": [],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 1118
|
||||||
|
}
|
||||||
@@ -42,6 +42,7 @@ export default function AdminRappels() {
|
|||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
nom: "",
|
nom: "",
|
||||||
templateType: "",
|
templateType: "",
|
||||||
|
timing: "pre_formation" as "pre_formation" | "post_formation",
|
||||||
joursAvant: 7,
|
joursAvant: 7,
|
||||||
heureEnvoi: "09:00",
|
heureEnvoi: "09:00",
|
||||||
actif: true,
|
actif: true,
|
||||||
@@ -97,6 +98,7 @@ export default function AdminRappels() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
nom: "",
|
nom: "",
|
||||||
templateType: "",
|
templateType: "",
|
||||||
|
timing: "pre_formation",
|
||||||
joursAvant: 7,
|
joursAvant: 7,
|
||||||
heureEnvoi: "09:00",
|
heureEnvoi: "09:00",
|
||||||
actif: true,
|
actif: true,
|
||||||
@@ -116,6 +118,7 @@ export default function AdminRappels() {
|
|||||||
setFormData({
|
setFormData({
|
||||||
nom: rappel.nom,
|
nom: rappel.nom,
|
||||||
templateType: rappel.templateType,
|
templateType: rappel.templateType,
|
||||||
|
timing: rappel.timing || "pre_formation",
|
||||||
joursAvant: rappel.joursAvant,
|
joursAvant: rappel.joursAvant,
|
||||||
heureEnvoi: rappel.heureEnvoi,
|
heureEnvoi: rappel.heureEnvoi,
|
||||||
actif: rappel.actif,
|
actif: rappel.actif,
|
||||||
@@ -292,19 +295,53 @@ export default function AdminRappels() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="joursAvant">Nombre de jours avant la séquence *</Label>
|
<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
|
<Input
|
||||||
id="joursAvant"
|
id="joursAvant"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="1"
|
||||||
value={formData.joursAvant}
|
value={formData.joursAvant}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
|
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
|
||||||
}
|
}
|
||||||
placeholder="Ex: 7"
|
placeholder="Ex: 7"
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Le rappel sera envoyé ce nombre de jours avant le début de la séquence
|
{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>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -381,17 +418,54 @@ export default function AdminRappels() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-joursAvant">Nombre de jours avant la séquence *</Label>
|
<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
|
<Input
|
||||||
id="edit-joursAvant"
|
id="edit-joursAvant"
|
||||||
type="number"
|
type="number"
|
||||||
min="0"
|
min="1"
|
||||||
value={formData.joursAvant}
|
value={formData.joursAvant}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
|
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 1 })
|
||||||
}
|
}
|
||||||
placeholder="Ex: 7"
|
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>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
|||||||
2043
drizzle/meta/0028_snapshot.json
Normal file
2043
drizzle/meta/0028_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -197,6 +197,13 @@
|
|||||||
"when": 1767799408213,
|
"when": 1767799408213,
|
||||||
"tag": "0027_naive_jackal",
|
"tag": "0027_naive_jackal",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 28,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1767868505577,
|
||||||
|
"tag": "0028_military_pandemic",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -276,9 +276,11 @@ export const rappels = mysqlTable("rappels", {
|
|||||||
id: int("id").autoincrement().primaryKey(),
|
id: int("id").autoincrement().primaryKey(),
|
||||||
/** Nom du rappel */
|
/** Nom du rappel */
|
||||||
nom: varchar("nom", { length: 255 }).notNull(),
|
nom: varchar("nom", { length: 255 }).notNull(),
|
||||||
/** Type de template à utiliser (rappel, rappelJ1) */
|
/** Type de template à utiliser (rappel, rappelJ1, etc.) */
|
||||||
templateType: varchar("templateType", { length: 50 }).notNull(),
|
templateType: varchar("templateType", { length: 50 }).notNull(),
|
||||||
/** Nombre de jours avant la première date de la séquence */
|
/** Timing : 'pre_formation' ou 'post_formation' */
|
||||||
|
timing: mysqlEnum("timing", ["pre_formation", "post_formation"]).default("pre_formation").notNull(),
|
||||||
|
/** Nombre de jours avant (positif) ou après (négatif) la première date de la séquence */
|
||||||
joursAvant: int("joursAvant").notNull(),
|
joursAvant: int("joursAvant").notNull(),
|
||||||
/** Heure d'envoi (format HH:MM) */
|
/** Heure d'envoi (format HH:MM) */
|
||||||
heureEnvoi: varchar("heureEnvoi", { length: 5 }).default("09:00").notNull(),
|
heureEnvoi: varchar("heureEnvoi", { length: 5 }).default("09:00").notNull(),
|
||||||
|
|||||||
@@ -1066,6 +1066,7 @@ export const appRouter = router({
|
|||||||
.input(z.object({
|
.input(z.object({
|
||||||
nom: z.string(),
|
nom: z.string(),
|
||||||
templateType: z.string(),
|
templateType: z.string(),
|
||||||
|
timing: z.enum(["pre_formation", "post_formation"]).default("pre_formation"),
|
||||||
joursAvant: z.number(),
|
joursAvant: z.number(),
|
||||||
heureEnvoi: z.string().default("09:00"),
|
heureEnvoi: z.string().default("09:00"),
|
||||||
actif: z.boolean(),
|
actif: z.boolean(),
|
||||||
@@ -1079,6 +1080,7 @@ export const appRouter = router({
|
|||||||
id: z.number(),
|
id: z.number(),
|
||||||
nom: z.string().optional(),
|
nom: z.string().optional(),
|
||||||
templateType: z.string().optional(),
|
templateType: z.string().optional(),
|
||||||
|
timing: z.enum(["pre_formation", "post_formation"]).optional(),
|
||||||
joursAvant: z.number().optional(),
|
joursAvant: z.number().optional(),
|
||||||
heureEnvoi: z.string().optional(),
|
heureEnvoi: z.string().optional(),
|
||||||
actif: z.boolean().optional(),
|
actif: z.boolean().optional(),
|
||||||
|
|||||||
8
todo.md
8
todo.md
@@ -515,3 +515,11 @@
|
|||||||
|
|
||||||
## Correction erreur emailTemplates.getByType
|
## Correction erreur emailTemplates.getByType
|
||||||
- [x] Modifier getEmailTemplateByType dans db.ts pour retourner null au lieu de undefined
|
- [x] Modifier getEmailTemplateByType dans db.ts pour retourner null au lieu de undefined
|
||||||
|
|
||||||
|
## Modification de la fenêtre de création des rappels
|
||||||
|
- [x] Analyser le schéma actuel de la table rappels
|
||||||
|
- [x] Ajouter un champ "timing" (pre_formation / post_formation) dans le schéma
|
||||||
|
- [x] Appliquer la modification en base de données (ALTER TABLE)
|
||||||
|
- [x] Modifier AdminRappels.tsx pour ajouter les radio buttons pré/post-formation
|
||||||
|
- [x] Ajouter un champ de saisie du nombre de jours avec label dynamique
|
||||||
|
- [x] Mettre à jour les procédures tRPC (create et update) pour gérer timing
|
||||||
|
|||||||
Reference in New Issue
Block a user