Checkpoint: Ajout d'une page de paramètres pour gérer l'URL des QR codes
Modifications apportées : - Création de la table parametres dans la base de données - Création du fichier server/parametresDb.ts avec les fonctions getParametres et updateUrlPublique - Ajout du router parametres dans server/routers.ts avec les procédures get et updateUrlPublique - Création de la page client/src/pages/AdminParametres.tsx avec formulaire de modification - Ajout de la route /admin/parametres dans App.tsx - Ajout du lien "Paramètres" dans le menu après "Configuration SMTP" dans DashboardLayout - Modification de server/qrCodeGenerator.ts pour utiliser l'URL de la BDD au lieu de getAppBaseUrl - URL par défaut définie : https://formations.itinova.org Fonctionnalités : - Interface graphique pour modifier l'URL publique de l'application - Les QR codes d'émargement utilisent maintenant l'URL configurée dans la base de données - Validation de l'URL avant enregistrement - Message d'information sur l'impact des modifications
This commit is contained in:
9
.manus/db/db-query-1768202390917.json
Normal file
9
.manus/db/db-query-1768202390917.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"query": "CREATE TABLE IF NOT EXISTS `parametres` (\n `id` int AUTO_INCREMENT NOT NULL,\n `urlPublique` varchar(500) NOT NULL DEFAULT 'https://formations.itinova.org',\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `parametres_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 `parametres` (\n `id` int AUTO_INCREMENT NOT NULL,\n `urlPublique` varchar(500) NOT NULL DEFAULT 'https://formations.itinova.org',\n `updatedAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n CONSTRAINT `parametres_id` PRIMARY KEY(`id`)\n);",
|
||||||
|
"rows": [],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 824
|
||||||
|
}
|
||||||
9
.manus/db/db-query-1768202397049.json
Normal file
9
.manus/db/db-query-1768202397049.json
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"query": "INSERT INTO `parametres` (`urlPublique`) VALUES ('https://formations.itinova.org');",
|
||||||
|
"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 INSERT INTO `parametres` (`urlPublique`) VALUES ('https://formations.itinova.org');",
|
||||||
|
"rows": [],
|
||||||
|
"messages": [],
|
||||||
|
"stdout": "",
|
||||||
|
"stderr": "",
|
||||||
|
"execution_time_ms": 65
|
||||||
|
}
|
||||||
@@ -30,6 +30,7 @@ import AdminRappelsStats from "./pages/admin/AdminRappelsStats";
|
|||||||
import AdminFormateurs from "./pages/admin/AdminFormateurs";
|
import AdminFormateurs from "./pages/admin/AdminFormateurs";
|
||||||
import AdminNotifications from "./pages/admin/AdminNotifications";
|
import AdminNotifications from "./pages/admin/AdminNotifications";
|
||||||
import AdminAttestations from "./pages/admin/AdminAttestations";
|
import AdminAttestations from "./pages/admin/AdminAttestations";
|
||||||
|
import AdminParametres from "./pages/AdminParametres";
|
||||||
import QuestionnaireReponse from "./pages/QuestionnaireReponse";
|
import QuestionnaireReponse from "./pages/QuestionnaireReponse";
|
||||||
import Inscription from "./pages/Inscription";
|
import Inscription from "./pages/Inscription";
|
||||||
import Login from "./pages/Login";
|
import Login from "./pages/Login";
|
||||||
@@ -57,6 +58,7 @@ function Router() {
|
|||||||
<Route path={"/admin/attestations"} component={AdminAttestations} />
|
<Route path={"/admin/attestations"} component={AdminAttestations} />
|
||||||
<Route path={"/admin/email-templates"} component={AdminEmailTemplates} />
|
<Route path={"/admin/email-templates"} component={AdminEmailTemplates} />
|
||||||
<Route path={"/admin/email-config"} component={AdminEmailConfig} />
|
<Route path={"/admin/email-config"} component={AdminEmailConfig} />
|
||||||
|
<Route path={"/admin/parametres"} component={AdminParametres} />
|
||||||
<Route path={"/admin/calendrier"} component={AdminCalendrier} />
|
<Route path={"/admin/calendrier"} component={AdminCalendrier} />
|
||||||
<Route path={"/admin/rappels"} component={AdminRappels} />
|
<Route path={"/admin/rappels"} component={AdminRappels} />
|
||||||
<Route path={"/admin/rappels/historique"} component={AdminRappelsHistorique} />
|
<Route path={"/admin/rappels/historique"} component={AdminRappelsHistorique} />
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ const menuSections = [
|
|||||||
{ icon: ClipboardList, label: "Questionnaires", path: "/admin/questionnaires" },
|
{ icon: ClipboardList, label: "Questionnaires", path: "/admin/questionnaires" },
|
||||||
{ icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" },
|
{ icon: Mail, label: "Templates d'emails", path: "/admin/email-templates" },
|
||||||
{ icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" },
|
{ icon: Settings, label: "Configuration SMTP", path: "/admin/email-config" },
|
||||||
|
{ icon: Settings, label: "Paramètres", path: "/admin/parametres" },
|
||||||
{ icon: FileText, label: "Attestations de formation", path: "/admin/attestations" },
|
{ icon: FileText, label: "Attestations de formation", path: "/admin/attestations" },
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
123
client/src/pages/AdminParametres.tsx
Normal file
123
client/src/pages/AdminParametres.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { useState, useEffect } from "react";
|
||||||
|
import { trpc } from "@/lib/trpc";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { Loader2, Save, Settings } from "lucide-react";
|
||||||
|
|
||||||
|
export default function AdminParametres() {
|
||||||
|
const { data: parametres, isLoading, refetch } = trpc.parametres.get.useQuery();
|
||||||
|
const [urlPublique, setUrlPublique] = useState("");
|
||||||
|
|
||||||
|
// Initialiser l'URL quand les données sont chargées
|
||||||
|
useEffect(() => {
|
||||||
|
if (parametres) {
|
||||||
|
setUrlPublique(parametres.urlPublique);
|
||||||
|
}
|
||||||
|
}, [parametres]);
|
||||||
|
|
||||||
|
const updateMutation = trpc.parametres.updateUrlPublique.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
toast.success("Paramètres mis à jour avec succès");
|
||||||
|
refetch();
|
||||||
|
},
|
||||||
|
onError: (error) => {
|
||||||
|
toast.error(`Erreur : ${error.message}`);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = (e: React.FormEvent) => {
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// Validation de l'URL
|
||||||
|
try {
|
||||||
|
new URL(urlPublique);
|
||||||
|
} catch {
|
||||||
|
toast.error("L'URL saisie n'est pas valide");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMutation.mutate({ urlPublique });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return (
|
||||||
|
<div className="flex items-center justify-center min-h-[400px]">
|
||||||
|
<Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="container max-w-4xl py-8">
|
||||||
|
<div className="flex items-center gap-3 mb-6">
|
||||||
|
<Settings className="h-8 w-8 text-primary" />
|
||||||
|
<div>
|
||||||
|
<h1 className="text-3xl font-bold">Paramètres de l'application</h1>
|
||||||
|
<p className="text-muted-foreground">
|
||||||
|
Configuration globale de l'application
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Card>
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle>URL publique de l'application</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Cette URL est utilisée pour générer les QR codes d'émargement.
|
||||||
|
Elle doit correspondre à l'URL publique de votre serveur.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent>
|
||||||
|
<form onSubmit={handleSubmit} className="space-y-4">
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="urlPublique">URL publique</Label>
|
||||||
|
<Input
|
||||||
|
id="urlPublique"
|
||||||
|
type="url"
|
||||||
|
placeholder="https://formations.itinova.org"
|
||||||
|
value={urlPublique}
|
||||||
|
onChange={(e) => setUrlPublique(e.target.value)}
|
||||||
|
required
|
||||||
|
/>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Exemple : https://formations.itinova.org (sans slash à la fin)
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 pt-4">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
disabled={updateMutation.isPending}
|
||||||
|
className="gap-2"
|
||||||
|
>
|
||||||
|
{updateMutation.isPending ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
|
Enregistrement...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Save className="h-4 w-4" />
|
||||||
|
Enregistrer
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<div className="mt-6 p-4 bg-muted rounded-lg">
|
||||||
|
<h3 className="font-semibold mb-2">💡 Information</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
Après avoir modifié l'URL publique, les nouveaux QR codes générés
|
||||||
|
utiliseront automatiquement cette URL. Les QR codes déjà générés
|
||||||
|
resteront inchangés.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
2253
drizzle/meta/0032_snapshot.json
Normal file
2253
drizzle/meta/0032_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -225,6 +225,13 @@
|
|||||||
"when": 1768157247820,
|
"when": 1768157247820,
|
||||||
"tag": "0031_simple_anthem",
|
"tag": "0031_simple_anthem",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 32,
|
||||||
|
"version": "5",
|
||||||
|
"when": 1768202368241,
|
||||||
|
"tag": "0032_flowery_puppet_master",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -632,3 +632,18 @@ export const presences = mysqlTable("presences", {
|
|||||||
|
|
||||||
export type Presence = typeof presences.$inferSelect;
|
export type Presence = typeof presences.$inferSelect;
|
||||||
export type InsertPresence = typeof presences.$inferInsert;
|
export type InsertPresence = typeof presences.$inferInsert;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table des paramètres de l'application
|
||||||
|
* Stocke les paramètres globaux de configuration
|
||||||
|
*/
|
||||||
|
export const parametres = mysqlTable("parametres", {
|
||||||
|
id: int("id").autoincrement().primaryKey(),
|
||||||
|
/** URL publique de l'application pour les QR codes d'émargement */
|
||||||
|
urlPublique: varchar("urlPublique", { length: 500 }).notNull().default("https://formations.itinova.org"),
|
||||||
|
updatedAt: timestamp("updatedAt").defaultNow().onUpdateNow().notNull(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export type Parametre = typeof parametres.$inferSelect;
|
||||||
|
export type InsertParametre = typeof parametres.$inferInsert;
|
||||||
|
|||||||
42
server/parametresDb.ts
Normal file
42
server/parametresDb.ts
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { getDb } from "./db";
|
||||||
|
import { parametres } from "../drizzle/schema";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Récupérer les paramètres de l'application
|
||||||
|
* S'il n'y a pas de paramètres, en créer un avec les valeurs par défaut
|
||||||
|
*/
|
||||||
|
export async function getParametres() {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
const result = await db.select().from(parametres).limit(1);
|
||||||
|
|
||||||
|
if (result.length === 0) {
|
||||||
|
// Créer les paramètres par défaut
|
||||||
|
await db.insert(parametres).values({
|
||||||
|
urlPublique: "https://formations.itinova.org",
|
||||||
|
});
|
||||||
|
|
||||||
|
const newResult = await db.select().from(parametres).limit(1);
|
||||||
|
return newResult[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Mettre à jour l'URL publique
|
||||||
|
*/
|
||||||
|
export async function updateUrlPublique(urlPublique: string) {
|
||||||
|
const db = await getDb();
|
||||||
|
if (!db) throw new Error("Database not available");
|
||||||
|
|
||||||
|
const params = await getParametres();
|
||||||
|
|
||||||
|
await db.update(parametres)
|
||||||
|
.set({ urlPublique })
|
||||||
|
.where(eq(parametres.id, params.id));
|
||||||
|
|
||||||
|
return getParametres();
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
import QRCode from "qrcode";
|
import QRCode from "qrcode";
|
||||||
import { randomBytes } from "crypto";
|
import { randomBytes } from "crypto";
|
||||||
import { getAppBaseUrl } from "./_core/appUrl";
|
import { getParametres } from "./parametresDb";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Générer un token unique pour le QR code
|
* Générer un token unique pour le QR code
|
||||||
@@ -16,8 +16,8 @@ export function generateQRToken(): string {
|
|||||||
*/
|
*/
|
||||||
export async function generateQRCodeDataURL(token: string): Promise<string> {
|
export async function generateQRCodeDataURL(token: string): Promise<string> {
|
||||||
// URL complète pour scanner le QR code
|
// URL complète pour scanner le QR code
|
||||||
const baseUrl = getAppBaseUrl();
|
const parametres = await getParametres();
|
||||||
const url = `${baseUrl}/emargement/${token}`;
|
const url = `${parametres.urlPublique}/emargement/${token}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const qrCodeDataURL = await QRCode.toDataURL(url, {
|
const qrCodeDataURL = await QRCode.toDataURL(url, {
|
||||||
@@ -44,8 +44,8 @@ export async function generateQRCodeDataURL(token: string): Promise<string> {
|
|||||||
* @returns Un buffer PNG du QR code
|
* @returns Un buffer PNG du QR code
|
||||||
*/
|
*/
|
||||||
export async function generateQRCodeBuffer(token: string): Promise<Buffer> {
|
export async function generateQRCodeBuffer(token: string): Promise<Buffer> {
|
||||||
const baseUrl = getAppBaseUrl();
|
const parametres = await getParametres();
|
||||||
const url = `${baseUrl}/emargement/${token}`;
|
const url = `${parametres.urlPublique}/emargement/${token}`;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const buffer = await QRCode.toBuffer(url, {
|
const buffer = await QRCode.toBuffer(url, {
|
||||||
|
|||||||
@@ -1465,6 +1465,24 @@ export const appRouter = router({
|
|||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
// ===== PARAMÈTRES DE L'APPLICATION =====
|
||||||
|
parametres: router({
|
||||||
|
get: adminProcedure.query(async () => {
|
||||||
|
const { getParametres } = await import("./parametresDb");
|
||||||
|
return getParametres();
|
||||||
|
}),
|
||||||
|
|
||||||
|
updateUrlPublique: adminProcedure
|
||||||
|
.input(z.object({
|
||||||
|
urlPublique: z.string().url(),
|
||||||
|
}))
|
||||||
|
.mutation(async ({ input }) => {
|
||||||
|
const { updateUrlPublique } = await import("./parametresDb");
|
||||||
|
await updateUrlPublique(input.urlPublique);
|
||||||
|
return { success: true };
|
||||||
|
}),
|
||||||
|
}),
|
||||||
|
|
||||||
// ===== ANALYTICS =====
|
// ===== ANALYTICS =====
|
||||||
analytics: router({
|
analytics: router({
|
||||||
globalStats: adminProcedure.query(async () => {
|
globalStats: adminProcedure.query(async () => {
|
||||||
|
|||||||
9
todo.md
9
todo.md
@@ -681,3 +681,12 @@
|
|||||||
- [x] Corriger l'appel à createAttestation (utiliser enregistrerAttestation)
|
- [x] Corriger l'appel à createAttestation (utiliser enregistrerAttestation)
|
||||||
|
|
||||||
- [x] Corriger le lien QR code pour utiliser l'URL publique au lieu de localhost
|
- [x] Corriger le lien QR code pour utiliser l'URL publique au lieu de localhost
|
||||||
|
|
||||||
|
## Page de paramètres de l'application
|
||||||
|
|
||||||
|
- [x] Créer la table parametres dans la base de données
|
||||||
|
- [x] Créer les procédures tRPC pour gérer les paramètres
|
||||||
|
- [x] Créer la page AdminParametres
|
||||||
|
- [x] Ajouter le lien dans le menu après "Configuration SMTP"
|
||||||
|
- [x] Modifier qrCodeGenerator pour utiliser l'URL de la BDD
|
||||||
|
- [x] Définir l'URL par défaut : https://formations.itinova.org
|
||||||
|
|||||||
Reference in New Issue
Block a user