Checkpoint: Correction : déplacement du champ "Public cible" des formations vers les séquences. Le public cible est maintenant défini au niveau de chaque séquence, permettant d'avoir des séquences différentes pour des publics différents au sein d'une même formation. Mise à jour complète du schéma de base de données, des procédures tRPC, du formulaire de création/édition de séquences avec affichage dans le tableau, et affichage du public cible pour chaque séquence sur la page d'inscription publique.

This commit is contained in:
Manus Sandbox
2025-11-13 11:18:02 -05:00
parent 5f4a049e05
commit 732b84fee8
9 changed files with 543 additions and 34 deletions

View File

@@ -22,7 +22,6 @@ export default function AdminFormations() {
const [formData, setFormData] = useState({
nom: "",
description: "",
publicCible: "autre" as "directeur" | "chef_service" | "autre",
lienUnique: "",
actif: true,
});
@@ -64,7 +63,7 @@ export default function AdminFormations() {
});
const resetForm = () => {
setFormData({ nom: "", description: "", publicCible: "autre" as "directeur" | "chef_service" | "autre", lienUnique: "", actif: true });
setFormData({ nom: "", description: "", lienUnique: "", actif: true });
setEditingId(null);
};
@@ -72,7 +71,6 @@ export default function AdminFormations() {
setFormData({
nom: formation.nom,
description: formation.description || "",
publicCible: formation.publicCible || "autre",
lienUnique: formation.lienUnique,
actif: formation.actif,
});
@@ -169,22 +167,6 @@ export default function AdminFormations() {
rows={3}
/>
</div>
<div className="space-y-2">
<Label htmlFor="publicCible">Public cible *</Label>
<Select
value={formData.publicCible}
onValueChange={(value: "directeur" | "chef_service" | "autre") => setFormData({ ...formData, publicCible: value })}
>
<SelectTrigger id="publicCible">
<SelectValue placeholder="Sélectionner le public cible" />
</SelectTrigger>
<SelectContent>
<SelectItem value="directeur">Directeur</SelectItem>
<SelectItem value="chef_service">Chef de service</SelectItem>
<SelectItem value="autre">Autre</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="lienUnique">Lien unique *</Label>
<Input
@@ -288,20 +270,16 @@ export default function AdminFormations() {
<TableRow>
<TableHead>Nom</TableHead>
<TableHead>Description</TableHead>
<TableHead>Public cible</TableHead>
<TableHead>Lien d'inscription</TableHead>
<TableHead>Statut</TableHead>
<TableHead className="text-right">Actions</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredFormations.map((formation) => {
const publicCibleLabel = formation.publicCible === "directeur" ? "Directeur" : formation.publicCible === "chef_service" ? "Chef de service" : "Autre";
return (
{filteredFormations.map((formation) => (
<TableRow key={formation.id}>
<TableCell className="font-medium">{formation.nom}</TableCell>
<TableCell className="max-w-xs truncate">{formation.description || "-"}</TableCell>
<TableCell>{publicCibleLabel}</TableCell>
<TableCell>
<a
href={getInscriptionUrl(formation.lienUnique)}
@@ -341,8 +319,7 @@ export default function AdminFormations() {
</div>
</TableCell>
</TableRow>
);
})}
))}
</TableBody>
</Table>
) : (

View File

@@ -33,6 +33,7 @@ export default function AdminSequences() {
formationId: "",
nom: "",
lieu: "",
publicCible: "autre" as "directeur" | "chef_service" | "autre",
capaciteMax: "12",
dateBlocage: "",
statut: "ouverte" as "ouverte" | "bloquee" | "terminee",
@@ -85,6 +86,7 @@ export default function AdminSequences() {
formationId: "",
nom: "",
lieu: "",
publicCible: "autre",
capaciteMax: "12",
dateBlocage: "",
statut: "ouverte",
@@ -135,6 +137,7 @@ export default function AdminSequences() {
formationId: sequence.formationId.toString(),
nom: sequence.nom || "",
lieu: sequence.lieu || "",
publicCible: sequence.publicCible || "autre",
capaciteMax: sequence.capaciteMax?.toString() || "12",
dateBlocage: safeFormatDate(sequence.dateBlocage),
statut: sequence.statut || "ouverte",
@@ -159,6 +162,7 @@ export default function AdminSequences() {
formationId: parseInt(formData.formationId),
nom: formData.nom,
lieu: formData.lieu,
publicCible: formData.publicCible,
capaciteMax: parseInt(formData.capaciteMax),
dateBlocage: formData.dateBlocage,
statut: formData.statut,
@@ -350,6 +354,23 @@ export default function AdminSequences() {
/>
</div>
<div className="space-y-2">
<Label htmlFor="publicCible">Public cible *</Label>
<Select
value={formData.publicCible}
onValueChange={(value: "directeur" | "chef_service" | "autre") => setFormData({ ...formData, publicCible: value })}
>
<SelectTrigger id="publicCible">
<SelectValue placeholder="Sélectionner le public cible" />
</SelectTrigger>
<SelectContent>
<SelectItem value="directeur">Directeur</SelectItem>
<SelectItem value="chef_service">Chef de service</SelectItem>
<SelectItem value="autre">Autre</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<Label htmlFor="capaciteMax">Capacité maximale *</Label>
<Input
@@ -560,6 +581,7 @@ export default function AdminSequences() {
<TableHead>Nom</TableHead>
<TableHead>Dates</TableHead>
<TableHead>Lieu</TableHead>
<TableHead>Public cible</TableHead>
<TableHead>Capacité</TableHead>
<TableHead>Statut</TableHead>
<TableHead className="text-right">Actions</TableHead>
@@ -581,6 +603,7 @@ export default function AdminSequences() {
</div>
</TableCell>
<TableCell className="max-w-xs truncate">{sequence.lieu}</TableCell>
<TableCell>{sequence.publicCible === "directeur" ? "Directeur" : sequence.publicCible === "chef_service" ? "Chef de service" : "Autre"}</TableCell>
<TableCell>{sequence.capaciteMax}</TableCell>
<TableCell>
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium ${getStatutBadge(sequence.statut)}`}>

View File

@@ -230,10 +230,6 @@ export default function Inscription() {
{formation.description}
</p>
)}
<div className="inline-flex items-center gap-2 bg-blue-100 text-blue-800 px-4 py-2 rounded-full text-sm font-medium">
<Users className="w-4 h-4" />
Public cible : {formation.publicCible === "directeur" ? "Directeurs" : formation.publicCible === "chef_service" ? "Chefs de service" : "Autre"}
</div>
</div>
{/* Sessions disponibles */}
@@ -277,6 +273,10 @@ export default function Inscription() {
<MapPin className="w-4 h-4" />
<span>{sequence.lieu}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>Public cible : {sequence.publicCible === "directeur" ? "Directeurs" : sequence.publicCible === "chef_service" ? "Chefs de service" : "Autre"}</span>
</div>
<div className="flex items-center gap-2">
<Users className="w-4 h-4" />
<span>{sequence.placesRestantes} places disponibles</span>

View File

@@ -0,0 +1,2 @@
ALTER TABLE `sequences` ADD `publicCible` enum('directeur','chef_service','autre') NOT NULL;--> statement-breakpoint
ALTER TABLE `formations` DROP COLUMN `publicCible`;

View File

@@ -0,0 +1,492 @@
{
"version": "5",
"dialect": "mysql",
"id": "b9b19655-1104-4112-ac8d-d568013faea2",
"prevId": "9dc05bf2-d92e-4abf-be1e-7357b388c9a5",
"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": {}
},
"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": {}
},
"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','autre')",
"primaryKey": false,
"notNull": true,
"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": true,
"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'"
},
"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": {
"users_openId_unique": {
"name": "users_openId_unique",
"columns": [
"openId"
]
}
},
"checkConstraint": {}
}
},
"views": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"tables": {},
"indexes": {}
}
}

View File

@@ -36,6 +36,13 @@
"when": 1763047968048,
"tag": "0004_futuristic_malice",
"breakpoints": true
},
{
"idx": 5,
"version": "5",
"when": 1763050409917,
"tag": "0005_cuddly_bushwacker",
"breakpoints": true
}
]
}

View File

@@ -32,8 +32,6 @@ export const formations = mysqlTable("formations", {
id: int("id").autoincrement().primaryKey(),
nom: varchar("nom", { length: 255 }).notNull(),
description: text("description"),
/** Public cible de la formation (Directeur, Chef de service, Autre) */
publicCible: mysqlEnum("publicCible", ["directeur", "chef_service", "autre"]).notNull(),
/** Lien unique pour l'inscription à cette formation */
lienUnique: varchar("lienUnique", { length: 100 }).notNull().unique(),
actif: boolean("actif").default(true).notNull(),
@@ -71,6 +69,8 @@ export const sequences = mysqlTable("sequences", {
nom: varchar("nom", { length: 255 }).notNull(),
/** Lieu de la formation */
lieu: varchar("lieu", { length: 500 }).notNull(),
/** Public cible de la séquence (Directeur, Chef de service, Autre) */
publicCible: mysqlEnum("publicCible", ["directeur", "chef_service", "autre"]).notNull(),
/** Capacité maximale (12 par défaut) */
capaciteMax: int("capaciteMax").default(12).notNull(),
/** Date de blocage des inscriptions (J-15 avant la première date) */

View File

@@ -47,7 +47,6 @@ export const appRouter = router({
create: adminProcedure.input(z.object({
nom: z.string().min(1),
description: z.string().optional(),
publicCible: z.enum(["directeur", "chef_service", "autre"]),
lienUnique: z.string().min(1),
actif: z.boolean().optional(),
})).mutation(async ({ input }) => {
@@ -59,7 +58,6 @@ export const appRouter = router({
id: z.number(),
nom: z.string().min(1).optional(),
description: z.string().optional(),
publicCible: z.enum(["directeur", "chef_service", "autre"]).optional(),
lienUnique: z.string().min(1).optional(),
actif: z.boolean().optional(),
})).mutation(async ({ input }) => {
@@ -156,6 +154,7 @@ export const appRouter = router({
formationId: z.number(),
nom: z.string().min(1),
lieu: z.string().min(1),
publicCible: z.enum(["directeur", "chef_service", "autre"]),
capaciteMax: z.number().default(12),
dateBlocage: z.string(),
statut: z.enum(["ouverte", "bloquee", "terminee"]).optional(),
@@ -194,6 +193,7 @@ export const appRouter = router({
formationId: z.number(),
nom: z.string().min(1),
lieu: z.string().min(1),
publicCible: z.enum(["directeur", "chef_service", "autre"]),
capaciteMax: z.number(),
dateBlocage: z.string(),
statut: z.enum(["ouverte", "bloquee", "terminee"]),

View File

@@ -122,3 +122,11 @@
- [x] Mettre à jour le schéma de base de données pour les formations
- [x] Mettre à jour les formulaires de gestion des formations
- [x] Afficher le public cible dans les listes et exports
- [x] Déplacer le champ "Public cible" des formations vers les séquences
- [x] Retirer publicCible du schéma formations
- [x] Ajouter publicCible au schéma sequences
- [x] Mettre à jour les procédures tRPC pour sequences
- [x] Mettre à jour le formulaire de création/édition de séquences
- [x] Afficher le public cible dans la liste des séquences
- [x] Afficher le public cible par séquence sur la page d'inscription publique