Checkpoint: Ajout des types de rappels J-7 et J-1 dans l'interface de gestion des rappels. Les templates d'emails ont été corrigés pour afficher toutes les dates des séquences. Bug connu : l'insertion des rappels échoue avec une erreur SQL sur le champ derniereExecution (à corriger après ce checkpoint).
This commit is contained in:
9
.manus/db/db-query-1764540522642.json
Normal file
9
.manus/db/db-query-1764540522642.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"query": "CREATE TABLE IF NOT EXISTS `rappels` (\n `id` int AUTO_INCREMENT NOT NULL,\n `nom` varchar(255) NOT NULL,\n `templateType` varchar(50) NOT NULL,\n `joursAvant` int NOT NULL,\n `heureEnvoi` varchar(5) NOT NULL DEFAULT '09:00',\n `actif` boolean NOT NULL DEFAULT true,\n `derniereExecution` timestamp NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `rappels_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 `rappels` (\n `id` int AUTO_INCREMENT NOT NULL,\n `nom` varchar(255) NOT NULL,\n `templateType` varchar(50) NOT NULL,\n `joursAvant` int NOT NULL,\n `heureEnvoi` varchar(5) NOT NULL DEFAULT '09:00',\n `actif` boolean NOT NULL DEFAULT true,\n `derniereExecution` timestamp NULL,\n `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `rappels_id` PRIMARY KEY(`id`)\n)",
|
||||||
|
"rows": [],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 112
|
||||||
|
}
|
||||||
@@ -12,22 +12,34 @@ import {
|
|||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { Label } from "@/components/ui/label";
|
import { Label } from "@/components/ui/label";
|
||||||
import { Textarea } from "@/components/ui/textarea";
|
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
|
||||||
import { trpc } from "@/lib/trpc";
|
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";
|
||||||
|
|
||||||
|
// Types de templates disponibles pour les rappels
|
||||||
|
const TEMPLATE_TYPES = [
|
||||||
|
{ value: "rappel", label: "Rappel J-7", joursAvant: 7 },
|
||||||
|
{ value: "rappelJ1", label: "Rappel J-1", joursAvant: 1 },
|
||||||
|
];
|
||||||
|
|
||||||
export default function AdminRappels() {
|
export default function AdminRappels() {
|
||||||
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
const [isCreateOpen, setIsCreateOpen] = useState(false);
|
||||||
const [isEditOpen, setIsEditOpen] = useState(false);
|
const [isEditOpen, setIsEditOpen] = useState(false);
|
||||||
const [editingRappel, setEditingRappel] = useState<any>(null);
|
const [editingRappel, setEditingRappel] = useState<any>(null);
|
||||||
const [formData, setFormData] = useState({
|
const [formData, setFormData] = useState({
|
||||||
nom: "",
|
nom: "",
|
||||||
joursAvant: 0,
|
templateType: "",
|
||||||
sujet: "",
|
joursAvant: 7,
|
||||||
contenu: "",
|
heureEnvoi: "09:00",
|
||||||
actif: true,
|
actif: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -67,18 +79,28 @@ export default function AdminRappels() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const toggleActifMutation = trpc.rappels.toggleActif.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Statut modifié avec succès");
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(`Erreur: ${error.message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const resetForm = () => {
|
const resetForm = () => {
|
||||||
setFormData({
|
setFormData({
|
||||||
nom: "",
|
nom: "",
|
||||||
joursAvant: 0,
|
templateType: "",
|
||||||
sujet: "",
|
joursAvant: 7,
|
||||||
contenu: "",
|
heureEnvoi: "09:00",
|
||||||
actif: true,
|
actif: true,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreate = () => {
|
const handleCreate = () => {
|
||||||
if (!formData.nom || !formData.sujet || !formData.contenu) {
|
if (!formData.nom || !formData.templateType) {
|
||||||
toast.error("Veuillez remplir tous les champs obligatoires");
|
toast.error("Veuillez remplir tous les champs obligatoires");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -89,9 +111,9 @@ export default function AdminRappels() {
|
|||||||
setEditingRappel(rappel);
|
setEditingRappel(rappel);
|
||||||
setFormData({
|
setFormData({
|
||||||
nom: rappel.nom,
|
nom: rappel.nom,
|
||||||
|
templateType: rappel.templateType,
|
||||||
joursAvant: rappel.joursAvant,
|
joursAvant: rappel.joursAvant,
|
||||||
sujet: rappel.sujet,
|
heureEnvoi: rappel.heureEnvoi,
|
||||||
contenu: rappel.contenu,
|
|
||||||
actif: rappel.actif,
|
actif: rappel.actif,
|
||||||
});
|
});
|
||||||
setIsEditOpen(true);
|
setIsEditOpen(true);
|
||||||
@@ -99,7 +121,7 @@ export default function AdminRappels() {
|
|||||||
|
|
||||||
const handleUpdate = () => {
|
const handleUpdate = () => {
|
||||||
if (!editingRappel) return;
|
if (!editingRappel) return;
|
||||||
if (!formData.nom || !formData.sujet || !formData.contenu) {
|
if (!formData.nom || !formData.templateType) {
|
||||||
toast.error("Veuillez remplir tous les champs obligatoires");
|
toast.error("Veuillez remplir tous les champs obligatoires");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -115,6 +137,24 @@ export default function AdminRappels() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleToggleActif = (id: number, actif: boolean) => {
|
||||||
|
toggleActifMutation.mutate({ id, actif: !actif });
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleTemplateTypeChange = (value: string) => {
|
||||||
|
const template = TEMPLATE_TYPES.find(t => t.value === value);
|
||||||
|
setFormData({
|
||||||
|
...formData,
|
||||||
|
templateType: value,
|
||||||
|
joursAvant: template?.joursAvant || 7,
|
||||||
|
nom: template?.label || "",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const getTemplateName = (type: string) => {
|
||||||
|
return TEMPLATE_TYPES.find(t => t.value === type)?.label || type;
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DashboardLayout>
|
<DashboardLayout>
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
@@ -146,8 +186,9 @@ export default function AdminRappels() {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead>Nom</TableHead>
|
<TableHead>Nom</TableHead>
|
||||||
|
<TableHead>Type</TableHead>
|
||||||
<TableHead>Délai</TableHead>
|
<TableHead>Délai</TableHead>
|
||||||
<TableHead>Sujet</TableHead>
|
<TableHead>Heure</TableHead>
|
||||||
<TableHead>Statut</TableHead>
|
<TableHead>Statut</TableHead>
|
||||||
<TableHead className="text-right">Actions</TableHead>
|
<TableHead className="text-right">Actions</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -157,18 +198,20 @@ export default function AdminRappels() {
|
|||||||
rappels.map((rappel) => (
|
rappels.map((rappel) => (
|
||||||
<TableRow key={rappel.id}>
|
<TableRow key={rappel.id}>
|
||||||
<TableCell className="font-medium">{rappel.nom}</TableCell>
|
<TableCell className="font-medium">{rappel.nom}</TableCell>
|
||||||
|
<TableCell>{getTemplateName(rappel.templateType)}</TableCell>
|
||||||
<TableCell>J-{rappel.joursAvant}</TableCell>
|
<TableCell>J-{rappel.joursAvant}</TableCell>
|
||||||
<TableCell>{rappel.sujet}</TableCell>
|
<TableCell>{rappel.heureEnvoi}</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<span
|
<button
|
||||||
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${
|
onClick={() => handleToggleActif(rappel.id, rappel.actif)}
|
||||||
|
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium cursor-pointer ${
|
||||||
rappel.actif
|
rappel.actif
|
||||||
? "bg-green-100 text-green-800"
|
? "bg-green-100 text-green-800 hover:bg-green-200"
|
||||||
: "bg-gray-100 text-gray-800"
|
: "bg-gray-100 text-gray-800 hover:bg-gray-200"
|
||||||
}`}
|
}`}
|
||||||
>
|
>
|
||||||
{rappel.actif ? "Actif" : "Inactif"}
|
{rappel.actif ? "Actif" : "Inactif"}
|
||||||
</span>
|
</button>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-right">
|
<TableCell className="text-right">
|
||||||
<div className="flex justify-end gap-2">
|
<div className="flex justify-end gap-2">
|
||||||
@@ -192,7 +235,7 @@ export default function AdminRappels() {
|
|||||||
))
|
))
|
||||||
) : (
|
) : (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={5} className="text-center text-muted-foreground">
|
<TableCell colSpan={6} className="text-center text-muted-foreground">
|
||||||
Aucun rappel configuré
|
Aucun rappel configuré
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
@@ -212,13 +255,35 @@ export default function AdminRappels() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="templateType">Type de rappel *</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.templateType}
|
||||||
|
onValueChange={handleTemplateTypeChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Sélectionnez un type de rappel" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TEMPLATE_TYPES.map((template) => (
|
||||||
|
<SelectItem key={template.value} value={template.value}>
|
||||||
|
{template.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Le template d'email correspondant sera utilisé pour l'envoi
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="nom">Nom du rappel *</Label>
|
<Label htmlFor="nom">Nom du rappel *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="nom"
|
id="nom"
|
||||||
value={formData.nom}
|
value={formData.nom}
|
||||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
||||||
placeholder="Ex: Rappel J-15"
|
placeholder="Ex: Rappel automatique J-7"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -232,7 +297,7 @@ export default function AdminRappels() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
|
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
|
||||||
}
|
}
|
||||||
placeholder="Ex: 15"
|
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
|
Le rappel sera envoyé ce nombre de jours avant le début de la séquence
|
||||||
@@ -240,26 +305,15 @@ export default function AdminRappels() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="sujet">Sujet de l'email *</Label>
|
<Label htmlFor="heureEnvoi">Heure d'envoi *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="sujet"
|
id="heureEnvoi"
|
||||||
value={formData.sujet}
|
type="time"
|
||||||
onChange={(e) => setFormData({ ...formData, sujet: e.target.value })}
|
value={formData.heureEnvoi}
|
||||||
placeholder="Ex: Rappel - Formation dans 15 jours"
|
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="contenu">Contenu de l'email *</Label>
|
|
||||||
<Textarea
|
|
||||||
id="contenu"
|
|
||||||
value={formData.contenu}
|
|
||||||
onChange={(e) => setFormData({ ...formData, contenu: e.target.value })}
|
|
||||||
placeholder="Utilisez {{nom_apprenant}}, {{titre_formation}}, {{date_debut}}, {{lieu}} comme variables"
|
|
||||||
rows={8}
|
|
||||||
/>
|
/>
|
||||||
<p className="text-sm text-muted-foreground">
|
<p className="text-sm text-muted-foreground">
|
||||||
Variables disponibles : {"{"}{"{"} nom_apprenant {"}"}{"}"}, {"{"}{"{"} titre_formation {"}"}{"}"}, {"{"}{"{"} date_debut {"}"}{"}"}, {"{"}{"{"} lieu {"}"}{"}"}, {"{"}{"{"} formateur {"}"}{"}"}
|
L'heure à laquelle le rappel sera envoyé chaque jour
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -293,13 +347,32 @@ export default function AdminRappels() {
|
|||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="edit-templateType">Type de rappel *</Label>
|
||||||
|
<Select
|
||||||
|
value={formData.templateType}
|
||||||
|
onValueChange={handleTemplateTypeChange}
|
||||||
|
>
|
||||||
|
<SelectTrigger>
|
||||||
|
<SelectValue placeholder="Sélectionnez un type de rappel" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{TEMPLATE_TYPES.map((template) => (
|
||||||
|
<SelectItem key={template.value} value={template.value}>
|
||||||
|
{template.label}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-nom">Nom du rappel *</Label>
|
<Label htmlFor="edit-nom">Nom du rappel *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-nom"
|
id="edit-nom"
|
||||||
value={formData.nom}
|
value={formData.nom}
|
||||||
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
onChange={(e) => setFormData({ ...formData, nom: e.target.value })}
|
||||||
placeholder="Ex: Rappel J-15"
|
placeholder="Ex: Rappel automatique J-7"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -313,34 +386,20 @@ export default function AdminRappels() {
|
|||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
|
setFormData({ ...formData, joursAvant: parseInt(e.target.value) || 0 })
|
||||||
}
|
}
|
||||||
placeholder="Ex: 15"
|
placeholder="Ex: 7"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<Label htmlFor="edit-sujet">Sujet de l'email *</Label>
|
<Label htmlFor="edit-heureEnvoi">Heure d'envoi *</Label>
|
||||||
<Input
|
<Input
|
||||||
id="edit-sujet"
|
id="edit-heureEnvoi"
|
||||||
value={formData.sujet}
|
type="time"
|
||||||
onChange={(e) => setFormData({ ...formData, sujet: e.target.value })}
|
value={formData.heureEnvoi}
|
||||||
placeholder="Ex: Rappel - Formation dans 15 jours"
|
onChange={(e) => setFormData({ ...formData, heureEnvoi: e.target.value })}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-2">
|
|
||||||
<Label htmlFor="edit-contenu">Contenu de l'email *</Label>
|
|
||||||
<Textarea
|
|
||||||
id="edit-contenu"
|
|
||||||
value={formData.contenu}
|
|
||||||
onChange={(e) => setFormData({ ...formData, contenu: e.target.value })}
|
|
||||||
placeholder="Utilisez {{nom_apprenant}}, {{titre_formation}}, {{date_debut}}, {{lieu}} comme variables"
|
|
||||||
rows={8}
|
|
||||||
/>
|
|
||||||
<p className="text-sm text-muted-foreground">
|
|
||||||
Variables disponibles : {"{"}{"{"} nom_apprenant {"}"}{"}"}, {"{"}{"{"} titre_formation {"}"}{"}"}, {"{"}{"{"} date_debut {"}"}{"}"}, {"{"}{"{"} lieu {"}"}{"}"}, {"{"}{"{"} formateur {"}"}{"}"}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Switch
|
<Switch
|
||||||
id="edit-actif"
|
id="edit-actif"
|
||||||
|
|||||||
12
drizzle/0017_unique_skrulls.sql
Normal file
12
drizzle/0017_unique_skrulls.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
CREATE TABLE `rappels` (
|
||||||
|
`id` int AUTO_INCREMENT NOT NULL,
|
||||||
|
`nom` varchar(255) NOT NULL,
|
||||||
|
`templateType` varchar(50) NOT NULL,
|
||||||
|
`joursAvant` int NOT NULL,
|
||||||
|
`heureEnvoi` varchar(5) NOT NULL DEFAULT '09:00',
|
||||||
|
`actif` boolean NOT NULL DEFAULT true,
|
||||||
|
`derniereExecution` timestamp,
|
||||||
|
`createdAt` timestamp NOT NULL DEFAULT (now()),
|
||||||
|
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
|
||||||
|
CONSTRAINT `rappels_id` PRIMARY KEY(`id`)
|
||||||
|
);
|
||||||
1087
drizzle/meta/0017_snapshot.json
Normal file
1087
drizzle/meta/0017_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -120,6 +120,13 @@
|
|||||||
"when": 1764329348207,
|
"when": 1764329348207,
|
||||||
"tag": "0016_romantic_sinister_six",
|
"tag": "0016_romantic_sinister_six",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 17,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1764540514199,
|
||||||
|
"tag": "0017_unique_skrulls",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -263,3 +263,28 @@ export const alertes = mysqlTable("alertes", {
|
|||||||
|
|
||||||
export type Alerte = typeof alertes.$inferSelect;
|
export type Alerte = typeof alertes.$inferSelect;
|
||||||
export type InsertAlerte = typeof alertes.$inferInsert;
|
export type InsertAlerte = typeof alertes.$inferInsert;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table des rappels automatiques
|
||||||
|
* Stocke les règles d'envoi automatique de rappels avant les séquences
|
||||||
|
*/
|
||||||
|
export const rappels = mysqlTable("rappels", {
|
||||||
|
id: int("id").autoincrement().primaryKey(),
|
||||||
|
/** Nom du rappel */
|
||||||
|
nom: varchar("nom", { length: 255 }).notNull(),
|
||||||
|
/** Type de template à utiliser (rappel, rappelJ1) */
|
||||||
|
templateType: varchar("templateType", { length: 50 }).notNull(),
|
||||||
|
/** Nombre de jours avant la première date de la séquence */
|
||||||
|
joursAvant: int("joursAvant").notNull(),
|
||||||
|
/** Heure d'envoi (format HH:MM) */
|
||||||
|
heureEnvoi: varchar("heureEnvoi", { length: 5 }).default("09:00").notNull(),
|
||||||
|
/** Actif ou non */
|
||||||
|
actif: boolean("actif").default(true).notNull(),
|
||||||
|
/** Dernière exécution */
|
||||||
|
derniereExecution: timestamp("derniereExecution"),
|
||||||
|
createdAt: timestamp("createdAt").defaultNow().notNull(),
|
||||||
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Rappel = typeof rappels.$inferSelect;
|
||||||
|
export type InsertRappel = typeof rappels.$inferInsert;
|
||||||
|
|||||||
72
server/db.ts
72
server/db.ts
@@ -26,7 +26,10 @@ import {
|
|||||||
InsertEmailTemplate,
|
InsertEmailTemplate,
|
||||||
formateurs,
|
formateurs,
|
||||||
InsertFormateur,
|
InsertFormateur,
|
||||||
Formateur
|
Formateur,
|
||||||
|
rappels,
|
||||||
|
InsertRappel,
|
||||||
|
Rappel
|
||||||
} from "../drizzle/schema";
|
} from "../drizzle/schema";
|
||||||
import { ENV } from './_core/env';
|
import { ENV } from './_core/env';
|
||||||
|
|
||||||
@@ -816,3 +819,70 @@ export async function deleteFormateur(id: number) {
|
|||||||
|
|
||||||
await db.delete(formateurs).where(eq(formateurs.id, id));
|
await db.delete(formateurs).where(eq(formateurs.id, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ==================== RAPPELS ====================
|
||||||
|
|
||||||
|
export async function createRappel(data: InsertRappel) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
// Construire un objet d'insertion explicite sans derniereExecution
|
||||||
|
const insertData: Partial<InsertRappel> = {
|
||||||
|
nom: data.nom,
|
||||||
|
templateType: data.templateType,
|
||||||
|
joursAvant: data.joursAvant,
|
||||||
|
heureEnvoi: data.heureEnvoi,
|
||||||
|
actif: data.actif,
|
||||||
|
};
|
||||||
|
const result = await db.insert(rappels).values(insertData as InsertRappel);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRappels() {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
return await db.select().from(rappels).orderBy(rappels.joursAvant);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getRappelById(id: number): Promise<Rappel | undefined> {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return undefined;
|
||||||
|
|
||||||
|
const result = await db.select().from(rappels).where(eq(rappels.id, id)).limit(1);
|
||||||
|
return result.length > 0 ? result[0] : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getActiveRappels(): Promise<Rappel[]> {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return [];
|
||||||
|
|
||||||
|
return await db.select().from(rappels).where(eq(rappels.actif, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRappel(id: number, data: Partial<InsertRappel>) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
await db.update(rappels).set({
|
||||||
|
...data,
|
||||||
|
updatedAt: new Date(),
|
||||||
|
}).where(eq(rappels.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteRappel(id: number) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
await db.delete(rappels).where(eq(rappels.id, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function updateRappelExecution(id: number) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) return;
|
||||||
|
|
||||||
|
await db.update(rappels).set({
|
||||||
|
derniereExecution: new Date(),
|
||||||
|
}).where(eq(rappels.id, id));
|
||||||
|
}
|
||||||
|
|||||||
@@ -829,15 +829,15 @@ export const appRouter = router({
|
|||||||
// ===== GESTION DES RAPPELS =====
|
// ===== GESTION DES RAPPELS =====
|
||||||
rappels: router({
|
rappels: router({
|
||||||
list: adminProcedure.query(async () => {
|
list: adminProcedure.query(async () => {
|
||||||
return db.getAllRappels();
|
return db.getRappels();
|
||||||
}),
|
}),
|
||||||
|
|
||||||
create: adminProcedure
|
create: adminProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
nom: z.string(),
|
nom: z.string(),
|
||||||
|
templateType: z.string(),
|
||||||
joursAvant: z.number(),
|
joursAvant: z.number(),
|
||||||
sujet: z.string(),
|
heureEnvoi: z.string().default("09:00"),
|
||||||
contenu: z.string(),
|
|
||||||
actif: z.boolean(),
|
actif: z.boolean(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -847,14 +847,16 @@ export const appRouter = router({
|
|||||||
update: adminProcedure
|
update: adminProcedure
|
||||||
.input(z.object({
|
.input(z.object({
|
||||||
id: z.number(),
|
id: z.number(),
|
||||||
nom: z.string(),
|
nom: z.string().optional(),
|
||||||
joursAvant: z.number(),
|
templateType: z.string().optional(),
|
||||||
sujet: z.string(),
|
joursAvant: z.number().optional(),
|
||||||
contenu: z.string(),
|
heureEnvoi: z.string().optional(),
|
||||||
actif: z.boolean(),
|
actif: z.boolean().optional(),
|
||||||
}))
|
}))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
return db.updateRappel(input);
|
const { id, ...data } = input;
|
||||||
|
await db.updateRappel(id, data);
|
||||||
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
delete: adminProcedure
|
delete: adminProcedure
|
||||||
@@ -867,7 +869,7 @@ export const appRouter = router({
|
|||||||
toggleActif: adminProcedure
|
toggleActif: adminProcedure
|
||||||
.input(z.object({ id: z.number(), actif: z.boolean() }))
|
.input(z.object({ id: z.number(), actif: z.boolean() }))
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
await db.updateRappel({ id: input.id, actif: input.actif });
|
await db.updateRappel(input.id, { actif: input.actif });
|
||||||
return { success: true };
|
return { success: true };
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
34
todo.md
34
todo.md
@@ -869,3 +869,37 @@
|
|||||||
- [x] Analyser le code d'envoi d'emails pour identifier pourquoi le mode simulation persiste
|
- [x] Analyser le code d'envoi d'emails pour identifier pourquoi le mode simulation persiste
|
||||||
- [x] Corriger le double envoi (email réel + notification simulée)
|
- [x] Corriger le double envoi (email réel + notification simulée)
|
||||||
- [ ] Tester l'envoi réel d'emails après correction
|
- [ ] Tester l'envoi réel d'emails après correction
|
||||||
|
|
||||||
|
## Bug Rappels - Types J-7 et J-1 manquants
|
||||||
|
|
||||||
|
- [x] Analyser la page des rappels pour identifier pourquoi les types J-7 et J-1 n'apparaissent pas
|
||||||
|
- [x] Ajouter les types J-7 et J-1 dans l'interface de gestion des rappels
|
||||||
|
- [ ] Tester la création de rappels J-7 et J-1
|
||||||
|
|
||||||
|
## Correction template email rappel J-1
|
||||||
|
|
||||||
|
- [x] Ajouter le template de rappel J-1 dans la page de gestion des templates d'emails
|
||||||
|
|
||||||
|
## Correction template email J-7
|
||||||
|
|
||||||
|
- [x] Analyser le template J-7 dans la page des templates d'emails
|
||||||
|
- [x] Analyser l'aperçu J-7 depuis les séquences
|
||||||
|
- [x] Identifier les différences entre les deux
|
||||||
|
- [x] Corriger le template pour qu'il corresponde à l'aperçu des séquences
|
||||||
|
|
||||||
|
## Amélioration template J-7
|
||||||
|
|
||||||
|
- [x] Modifier le template J-7 pour afficher toutes les dates de la séquence au lieu d'une seule date
|
||||||
|
|
||||||
|
## Bug SMTP - Mode simulation
|
||||||
|
|
||||||
|
- [x] Analyser le code d'envoi d'emails pour identifier pourquoi le mode simulation persiste
|
||||||
|
- [x] Corriger le double envoi (email réel + notification simulée)
|
||||||
|
- [x] Tester l'envoi réel d'emails après correction
|
||||||
|
|
||||||
|
## Bug Rappels - Types J-7 et J-1 manquants
|
||||||
|
|
||||||
|
- [x] Analyser la page des rappels pour identifier pourquoi les types J-7 et J-1 n'apparaissent pas
|
||||||
|
- [x] Ajouter les types J-7 et J-1 dans l'interface de gestion des rappels
|
||||||
|
- [ ] Corriger le bug d'insertion SQL (erreur sur derniereExecution)
|
||||||
|
- [ ] Tester la création de rappels J-7 et J-1
|
||||||
|
|||||||
Reference in New Issue
Block a user