Checkpoint: Trois améliorations majeures du champ formateur :

 **1. Affichage dans le tableau des séquences**
- Nouvelle colonne "Formateur" ajoutée au tableau de gestion
- Affiche le nom du formateur ou "-" si non renseigné

 **2. Liste prédéfinie avec autocomplétion**
- Nouvelle table `formateurs` dans la base de données
- Procédures tRPC (list, create, delete) pour gérer les formateurs
- Composant `FormateurCombobox` avec autocomplétion intelligente
- Possibilité d'ajouter rapidement un nouveau formateur depuis le formulaire
- Recherche en temps réel parmi les formateurs existants

 **3. Inclusion dans les emails d'invitation**
- Le nom du formateur est maintenant affiché dans les emails de confirmation d'inscription
- Affichage conditionnel (uniquement si un formateur est renseigné)
- Améliore la communication avec les apprenants

Ces améliorations standardisent la gestion des formateurs, évitent les doublons/fautes de frappe, et enrichissent l'information transmise aux apprenants.
This commit is contained in:
Manus Sandbox
2025-11-19 09:38:06 -05:00
parent 703d0e4961
commit 16351ea3d5
11 changed files with 1095 additions and 9 deletions

View File

@@ -0,0 +1,9 @@
{
"query": "CREATE TABLE IF NOT EXISTS formateurs (\n id INT AUTO_INCREMENT PRIMARY KEY,\n nom VARCHAR(255) NOT NULL UNIQUE,\n actif BOOLEAN DEFAULT TRUE NOT NULL,\n createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,\n updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL\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 formateurs (\n id INT AUTO_INCREMENT PRIMARY KEY,\n nom VARCHAR(255) NOT NULL UNIQUE,\n actif BOOLEAN DEFAULT TRUE NOT NULL,\n createdAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL,\n updatedAt TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL\n);",
"rows": [],
"messages": [],
"stdout": "",
"stderr": "",
"execution_time_ms": 189
}

View File

@@ -0,0 +1,135 @@
import { useState } from "react";
import { Check, ChevronsUpDown, Plus } from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
CommandGroup,
CommandInput,
CommandItem,
CommandList,
} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { trpc } from "@/lib/trpc";
import { toast } from "sonner";
interface FormateurComboboxProps {
value: string;
onChange: (value: string) => void;
placeholder?: string;
}
export function FormateurCombobox({ value, onChange, placeholder = "Sélectionner un formateur..." }: FormateurComboboxProps) {
const [open, setOpen] = useState(false);
const [searchValue, setSearchValue] = useState("");
const { data: formateurs = [], refetch } = trpc.formateurs.list.useQuery();
const createMutation = trpc.formateurs.create.useMutation({
onSuccess: () => {
toast.success("Formateur ajouté");
refetch();
},
onError: (error) => {
toast.error(error.message || "Erreur lors de l'ajout du formateur");
},
});
const handleAddNew = () => {
if (!searchValue.trim()) {
toast.error("Veuillez entrer un nom de formateur");
return;
}
// Vérifier si le formateur existe déjà
const exists = formateurs.some(f => f.nom.toLowerCase() === searchValue.toLowerCase());
if (exists) {
toast.error("Ce formateur existe déjà");
return;
}
createMutation.mutate({ nom: searchValue.trim() });
onChange(searchValue.trim());
setSearchValue("");
setOpen(false);
};
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant="outline"
role="combobox"
aria-expanded={open}
className="w-full justify-between"
>
{value || placeholder}
<ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" />
</Button>
</PopoverTrigger>
<PopoverContent className="w-full p-0">
<Command>
<CommandInput
placeholder="Rechercher ou ajouter un formateur..."
value={searchValue}
onValueChange={setSearchValue}
/>
<CommandList>
<CommandEmpty>
<div className="flex flex-col items-center gap-2 py-2">
<p className="text-sm text-muted-foreground">Aucun formateur trouvé</p>
{searchValue && (
<Button
size="sm"
variant="outline"
onClick={handleAddNew}
disabled={createMutation.isPending}
>
<Plus className="mr-2 h-4 w-4" />
Ajouter "{searchValue}"
</Button>
)}
</div>
</CommandEmpty>
<CommandGroup>
{formateurs.map((formateur) => (
<CommandItem
key={formateur.id}
value={formateur.nom}
onSelect={(currentValue) => {
onChange(currentValue === value ? "" : currentValue);
setOpen(false);
setSearchValue("");
}}
>
<Check
className={cn(
"mr-2 h-4 w-4",
value === formateur.nom ? "opacity-100" : "opacity-0"
)}
/>
{formateur.nom}
</CommandItem>
))}
</CommandGroup>
{searchValue && !formateurs.some(f => f.nom.toLowerCase() === searchValue.toLowerCase()) && (
<CommandGroup>
<CommandItem
onSelect={handleAddNew}
className="text-primary"
>
<Plus className="mr-2 h-4 w-4" />
Ajouter "{searchValue}"
</CommandItem>
</CommandGroup>
)}
</CommandList>
</Command>
</PopoverContent>
</Popover>
);
}

View File

@@ -1,5 +1,6 @@
import { useState, useMemo } from "react"; import { useState } from "react";
import DashboardLayout from "@/components/DashboardLayout"; import DashboardLayout from "@/components/DashboardLayout";
import { FormateurCombobox } from "@/components/FormateurCombobox";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog";
@@ -402,11 +403,10 @@ export default function AdminSequences() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="formateur">Formateur</Label> <Label htmlFor="formateur">Formateur</Label>
<Input <FormateurCombobox
id="formateur"
value={formData.formateur} value={formData.formateur}
onChange={(e) => setFormData({ ...formData, formateur: e.target.value })} onChange={(value) => setFormData({ ...formData, formateur: value })}
placeholder="Nom du formateur (optionnel)" placeholder="Sélectionner un formateur (optionnel)"
/> />
</div> </div>
@@ -636,6 +636,7 @@ export default function AdminSequences() {
<TableHead>Dates</TableHead> <TableHead>Dates</TableHead>
<TableHead>Lieu</TableHead> <TableHead>Lieu</TableHead>
<TableHead>Public cible</TableHead> <TableHead>Public cible</TableHead>
<TableHead>Formateur</TableHead>
<TableHead>Capacité</TableHead> <TableHead>Capacité</TableHead>
<TableHead>Statut</TableHead> <TableHead>Statut</TableHead>
<TableHead className="text-right">Actions</TableHead> <TableHead className="text-right">Actions</TableHead>
@@ -662,6 +663,7 @@ export default function AdminSequences() {
{getPublicCibleLabel(sequence.publicCible)} {getPublicCibleLabel(sequence.publicCible)}
</span> </span>
</TableCell> </TableCell>
<TableCell className="max-w-xs truncate">{sequence.formateur || "-"}</TableCell>
<TableCell>{sequence.nbInscrits || 0} / {sequence.capaciteMax}</TableCell> <TableCell>{sequence.nbInscrits || 0} / {sequence.capaciteMax}</TableCell>
<TableCell> <TableCell>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatutBadge(sequence.statut)}`}> <span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatutBadge(sequence.statut)}`}>
@@ -775,11 +777,10 @@ export default function AdminSequences() {
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<div className="space-y-2"> <div className="space-y-2">
<Label htmlFor="edit-formateur">Formateur</Label> <Label htmlFor="edit-formateur">Formateur</Label>
<Input <FormateurCombobox
id="edit-formateur"
value={formData.formateur} value={formData.formateur}
onChange={(e) => setFormData({ ...formData, formateur: e.target.value })} onChange={(value) => setFormData({ ...formData, formateur: value })}
placeholder="Nom du formateur (optionnel)" placeholder="Sélectionner un formateur (optionnel)"
/> />
</div> </div>

View File

@@ -0,0 +1,9 @@
CREATE TABLE `formateurs` (
`id` int AUTO_INCREMENT NOT NULL,
`nom` varchar(255) NOT NULL,
`actif` boolean NOT NULL DEFAULT true,
`createdAt` timestamp NOT NULL DEFAULT (now()),
`updatedAt` timestamp NOT NULL DEFAULT (now()) ON UPDATE CURRENT_TIMESTAMP,
CONSTRAINT `formateurs_id` PRIMARY KEY(`id`),
CONSTRAINT `formateurs_nom_unique` UNIQUE(`nom`)
);

View File

@@ -0,0 +1,842 @@
{
"version": "5",
"dialect": "mysql",
"id": "d7d07602-ed71-4c09-b5d4-3ebd05e45303",
"prevId": "860496d9-3739-4694-b45d-725a447f3eee",
"tables": {
"apprenants": {
"name": "apprenants",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"prenom": {
"name": "prenom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"codeEtablissement": {
"name": "codeEtablissement",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fonction": {
"name": "fonction",
"type": "enum('directeur','chef_service','autre')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"apprenants_id": {
"name": "apprenants_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"apprenants_email_unique": {
"name": "apprenants_email_unique",
"columns": [
"email"
]
}
},
"checkConstraint": {}
},
"datesFormation": {
"name": "datesFormation",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"sequenceId": {
"name": "sequenceId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dateDebut": {
"name": "dateDebut",
"type": "datetime",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dateFin": {
"name": "dateFin",
"type": "datetime",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"ordre": {
"name": "ordre",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"datesFormation_id": {
"name": "datesFormation_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"emailConfig": {
"name": "emailConfig",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"provider": {
"name": "provider",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'resend'"
},
"apiKey": {
"name": "apiKey",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"fromEmail": {
"name": "fromEmail",
"type": "varchar(320)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"fromName": {
"name": "fromName",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Formation Manager Itinova'"
},
"mode": {
"name": "mode",
"type": "enum('simulation','production')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'simulation'"
},
"domainVerified": {
"name": "domainVerified",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"emailConfig_id": {
"name": "emailConfig_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"emailTemplates": {
"name": "emailTemplates",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"type": {
"name": "type",
"type": "varchar(50)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"logoUrl": {
"name": "logoUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"primaryColor": {
"name": "primaryColor",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#2563eb'"
},
"headerBgColor": {
"name": "headerBgColor",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#2563eb'"
},
"headerTextColor": {
"name": "headerTextColor",
"type": "varchar(7)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'#ffffff'"
},
"headerTitle": {
"name": "headerTitle",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Formation Manager Itinova'"
},
"footerText": {
"name": "footerText",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"active": {
"name": "active",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"emailTemplates_id": {
"name": "emailTemplates_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"emailTemplates_type_unique": {
"name": "emailTemplates_type_unique",
"columns": [
"type"
]
}
},
"checkConstraint": {}
},
"formateurs": {
"name": "formateurs",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"actif": {
"name": "actif",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"formateurs_id": {
"name": "formateurs_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"formateurs_nom_unique": {
"name": "formateurs_nom_unique",
"columns": [
"nom"
]
}
},
"checkConstraint": {}
},
"formations": {
"name": "formations",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"lienUnique": {
"name": "lienUnique",
"type": "varchar(100)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"actif": {
"name": "actif",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"formations_id": {
"name": "formations_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"formations_lienUnique_unique": {
"name": "formations_lienUnique_unique",
"columns": [
"lienUnique"
]
}
},
"checkConstraint": {}
},
"inscriptions": {
"name": "inscriptions",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"apprenantId": {
"name": "apprenantId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"sequenceId": {
"name": "sequenceId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"statut": {
"name": "statut",
"type": "enum('confirmee','liste_attente','annulee')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"dateInscription": {
"name": "dateInscription",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"inscriptions_id": {
"name": "inscriptions_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"passwordResetTokens": {
"name": "passwordResetTokens",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"userId": {
"name": "userId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expiresAt": {
"name": "expiresAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"used": {
"name": "used",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": false
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"passwordResetTokens_id": {
"name": "passwordResetTokens_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {
"passwordResetTokens_token_unique": {
"name": "passwordResetTokens_token_unique",
"columns": [
"token"
]
}
},
"checkConstraint": {}
},
"sequences": {
"name": "sequences",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"formationId": {
"name": "formationId",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"nom": {
"name": "nom",
"type": "varchar(255)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"lieu": {
"name": "lieu",
"type": "varchar(500)",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"publicCible": {
"name": "publicCible",
"type": "enum('directeur','chef_service','tous','autre')",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"formateur": {
"name": "formateur",
"type": "varchar(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"capaciteMax": {
"name": "capaciteMax",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 12
},
"dateBlocage": {
"name": "dateBlocage",
"type": "datetime",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"statut": {
"name": "statut",
"type": "enum('ouverte','bloquee','terminee')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'ouverte'"
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"sequences_id": {
"name": "sequences_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
},
"users": {
"name": "users",
"columns": {
"id": {
"name": "id",
"type": "int",
"primaryKey": false,
"notNull": true,
"autoincrement": true
},
"openId": {
"name": "openId",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "varchar(320)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"loginMethod": {
"name": "loginMethod",
"type": "varchar(64)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"role": {
"name": "role",
"type": "enum('user','admin')",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'user'"
},
"isActive": {
"name": "isActive",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": true
},
"createdAt": {
"name": "createdAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
},
"updatedAt": {
"name": "updatedAt",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"onUpdate": true,
"default": "(now())"
},
"lastSignedIn": {
"name": "lastSignedIn",
"type": "timestamp",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "(now())"
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"users_id": {
"name": "users_id",
"columns": [
"id"
]
}
},
"uniqueConstraints": {},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@@ -120,6 +120,13 @@
"when": 1763562352026, "when": 1763562352026,
"tag": "0016_chunky_landau", "tag": "0016_chunky_landau",
"breakpoints": true "breakpoints": true
},
{
"idx": 17,
"version": "5",
"when": 1763563084443,
"tag": "0017_dear_tana_nile",
"breakpoints": true
} }
] ]
} }

View File

@@ -61,6 +61,20 @@ export const apprenants = mysqlTable("apprenants", {
export type Apprenant = typeof apprenants.$inferSelect; export type Apprenant = typeof apprenants.$inferSelect;
export type InsertApprenant = typeof apprenants.$inferInsert; export type InsertApprenant = typeof apprenants.$inferInsert;
/**
* Table des formateurs
*/
export const formateurs = mysqlTable("formateurs", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull().unique(),
actif: boolean("actif").default(true).notNull(),
createdAt: timestamp("createdAt").defaultNow().notNull(),
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
});
export type Formateur = typeof formateurs.$inferSelect;
export type InsertFormateur = typeof formateurs.$inferInsert;
/** /**
* Table des séquences de formation * Table des séquences de formation
* Une séquence peut contenir jusqu'à 4 dates de formation * Une séquence peut contenir jusqu'à 4 dates de formation

View File

@@ -5,6 +5,7 @@ import {
users, users,
formations, formations,
apprenants, apprenants,
formateurs,
sequences, sequences,
datesFormation, datesFormation,
inscriptions, inscriptions,
@@ -18,10 +19,12 @@ import {
InsertInscription, InsertInscription,
InsertFormation, InsertFormation,
InsertApprenant, InsertApprenant,
InsertFormateur,
Sequence, Sequence,
DateFormation, DateFormation,
Apprenant, Apprenant,
Formation, Formation,
Formateur,
EmailTemplate, EmailTemplate,
InsertEmailTemplate InsertEmailTemplate
} from "../drizzle/schema"; } from "../drizzle/schema";
@@ -656,3 +659,37 @@ export async function updateEmailConfig(id: number, data: Partial<InsertEmailCon
}) })
.where(eq(emailConfig.id, id)); .where(eq(emailConfig.id, id));
} }
// ===== FORMATEURS =====
/**
* Récupérer tous les formateurs actifs
*/
export async function getFormateurs() {
const db = await getDb();
if (!db) return [];
const result = await db.select().from(formateurs).where(eq(formateurs.actif, true));
return result;
}
/**
* Créer un nouveau formateur
*/
export async function createFormateur(data: InsertFormateur) {
const db = await getDb();
if (!db) return null;
const result = await db.insert(formateurs).values(data);
return result;
}
/**
* Supprimer un formateur (désactivation logique)
*/
export async function deleteFormateur(id: number) {
const db = await getDb();
if (!db) return;
await db.update(formateurs).set({ actif: false }).where(eq(formateurs.id, id));
}

View File

@@ -45,6 +45,7 @@ export async function sendInscriptionConfirmation(params: {
sequenceNom: string; sequenceNom: string;
dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>; dates: Array<{ dateDebut: Date; dateFin: Date; ordre: number }>;
lieu: string; lieu: string;
formateur?: string;
statut: 'confirmee' | 'liste_attente'; statut: 'confirmee' | 'liste_attente';
}): Promise<boolean> { }): Promise<boolean> {
const isConfirmed = params.statut === 'confirmee'; const isConfirmed = params.statut === 'confirmee';
@@ -88,6 +89,7 @@ export async function sendInscriptionConfirmation(params: {
<p><strong>Formation :</strong> ${params.formationNom}</p> <p><strong>Formation :</strong> ${params.formationNom}</p>
<p><strong>Séquence :</strong> ${params.sequenceNom}</p> <p><strong>Séquence :</strong> ${params.sequenceNom}</p>
<p><strong>Lieu :</strong> ${params.lieu}</p> <p><strong>Lieu :</strong> ${params.lieu}</p>
${params.formateur ? `<p><strong>Formateur :</strong> ${params.formateur}</p>` : ''}
<h4>Dates de formation (${params.dates.length} séance${params.dates.length > 1 ? 's' : ''}) :</h4> <h4>Dates de formation (${params.dates.length} séance${params.dates.length > 1 ? 's' : ''}) :</h4>
${datesHTML} ${datesHTML}
</div> </div>

View File

@@ -72,6 +72,25 @@ export const appRouter = router({
}), }),
}), }),
// ===== FORMATEURS =====
formateurs: router({
list: adminProcedure.query(async () => {
return db.getFormateurs();
}),
create: adminProcedure.input(z.object({
nom: z.string().min(1),
})).mutation(async ({ input }) => {
await db.createFormateur(input);
return { success: true };
}),
delete: adminProcedure.input(z.object({ id: z.number() })).mutation(async ({ input }) => {
await db.deleteFormateur(input.id);
return { success: true };
}),
}),
// ===== APPRENANTS ===== // ===== APPRENANTS =====
apprenants: router({ apprenants: router({
list: adminProcedure.query(async () => { list: adminProcedure.query(async () => {
@@ -330,6 +349,7 @@ export const appRouter = router({
ordre: d.ordre, ordre: d.ordre,
})), })),
lieu: inscriptionSequence.lieu, lieu: inscriptionSequence.lieu,
formateur: inscriptionSequence.formateur || undefined,
statut, statut,
}); });
} }

10
todo.md
View File

@@ -270,3 +270,13 @@
- [x] Ajouter le champ formateur dans le formulaire de création de séquence - [x] Ajouter le champ formateur dans le formulaire de création de séquence
- [x] Ajouter le champ formateur dans le formulaire de modification de séquence - [x] Ajouter le champ formateur dans le formulaire de modification de séquence
- [x] Tester la création et modification avec le nouveau champ - [x] Tester la création et modification avec le nouveau champ
## Améliorations du champ Formateur
- [x] Afficher le formateur dans le tableau des séquences (nouvelle colonne)
- [x] Créer une table formateurs dans la base de données
- [x] Ajouter les procédures tRPC pour gérer les formateurs (CRUD)
- [x] Remplacer le champ texte par un composant avec autocomplétion
- [x] Permettre l'ajout rapide de nouveaux formateurs depuis le formulaire
- [x] Ajouter le formateur dans les templates d'emails d'invitation
- [x] Tester l'autocomplétion et l'affichage dans les emails