Implemented the following: - `DashboardPage`: displays an overview of stats, recent projects, and tabs for future analytics/reports. - `ProjectsPage` and `PersonsPage`: include searchable tables, actions, and mobile-friendly card views. - Integrated reusable components like `AuthLoading`, `DropdownMenu`, `Table`, and `Card`.
581 lines
24 KiB
TypeScript
581 lines
24 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Label } from "@/components/ui/label";
|
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { toast } from "sonner";
|
|
import {
|
|
Save,
|
|
RefreshCw,
|
|
Shield,
|
|
Bell,
|
|
Mail,
|
|
Database,
|
|
Server,
|
|
FileJson,
|
|
Loader2
|
|
} from "lucide-react";
|
|
|
|
export default function AdminSettingsPage() {
|
|
const [activeTab, setActiveTab] = useState("general");
|
|
const [isLoading, setIsLoading] = useState(false);
|
|
|
|
// Mock system settings
|
|
const systemSettings = {
|
|
general: {
|
|
siteName: "Application de Création de Groupes",
|
|
siteDescription: "Une application web moderne dédiée à la création et à la gestion de groupes",
|
|
contactEmail: "admin@example.com",
|
|
maxProjectsPerUser: "10",
|
|
maxPersonsPerProject: "100",
|
|
},
|
|
authentication: {
|
|
enableGithubAuth: true,
|
|
requireEmailVerification: false,
|
|
sessionTimeout: "7",
|
|
maxLoginAttempts: "5",
|
|
passwordMinLength: "8",
|
|
},
|
|
notifications: {
|
|
enableEmailNotifications: true,
|
|
enableSystemNotifications: true,
|
|
notifyOnNewUser: true,
|
|
notifyOnNewProject: false,
|
|
adminEmailRecipients: "admin@example.com",
|
|
},
|
|
maintenance: {
|
|
maintenanceMode: false,
|
|
maintenanceMessage: "Le site est actuellement en maintenance. Veuillez réessayer plus tard.",
|
|
debugMode: false,
|
|
logLevel: "error",
|
|
},
|
|
};
|
|
|
|
const { register: registerGeneral, handleSubmit: handleSubmitGeneral, formState: { errors: errorsGeneral } } = useForm({
|
|
defaultValues: systemSettings.general,
|
|
});
|
|
|
|
const { register: registerAuth, handleSubmit: handleSubmitAuth, formState: { errors: errorsAuth } } = useForm({
|
|
defaultValues: systemSettings.authentication,
|
|
});
|
|
|
|
const { register: registerNotif, handleSubmit: handleSubmitNotif, formState: { errors: errorsNotif } } = useForm({
|
|
defaultValues: systemSettings.notifications,
|
|
});
|
|
|
|
const { register: registerMaint, handleSubmit: handleSubmitMaint, formState: { errors: errorsMaint } } = useForm({
|
|
defaultValues: systemSettings.maintenance,
|
|
});
|
|
|
|
const onSubmitGeneral = async (data: any) => {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
setIsLoading(false);
|
|
toast.success("Paramètres généraux mis à jour avec succès");
|
|
};
|
|
|
|
const onSubmitAuth = async (data: any) => {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
setIsLoading(false);
|
|
toast.success("Paramètres d'authentification mis à jour avec succès");
|
|
};
|
|
|
|
const onSubmitNotif = async (data: any) => {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
setIsLoading(false);
|
|
toast.success("Paramètres de notification mis à jour avec succès");
|
|
};
|
|
|
|
const onSubmitMaint = async (data: any) => {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
setIsLoading(false);
|
|
toast.success("Paramètres de maintenance mis à jour avec succès");
|
|
};
|
|
|
|
const handleExportConfig = async () => {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
setIsLoading(false);
|
|
toast.success("Configuration exportée avec succès");
|
|
};
|
|
|
|
const handleClearCache = async () => {
|
|
setIsLoading(true);
|
|
// Simulate API call
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
setIsLoading(false);
|
|
toast.success("Cache vidé avec succès");
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
|
|
<h1 className="text-2xl sm:text-3xl font-bold">Paramètres système</h1>
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5 text-primary" />
|
|
<span className="text-sm text-muted-foreground">Configuration globale</span>
|
|
</div>
|
|
</div>
|
|
|
|
<Tabs defaultValue="general" className="space-y-4" onValueChange={setActiveTab}>
|
|
<TabsList className="w-full flex justify-start overflow-auto">
|
|
<TabsTrigger value="general" className="flex-1 sm:flex-none">Général</TabsTrigger>
|
|
<TabsTrigger value="authentication" className="flex-1 sm:flex-none">Authentification</TabsTrigger>
|
|
<TabsTrigger value="notifications" className="flex-1 sm:flex-none">Notifications</TabsTrigger>
|
|
<TabsTrigger value="maintenance" className="flex-1 sm:flex-none">Maintenance</TabsTrigger>
|
|
</TabsList>
|
|
|
|
<TabsContent value="general" className="space-y-4">
|
|
<Card>
|
|
<form onSubmit={handleSubmitGeneral(onSubmitGeneral)}>
|
|
<CardHeader>
|
|
<CardTitle>Paramètres généraux</CardTitle>
|
|
<CardDescription>
|
|
Configurez les paramètres généraux de l'application
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="siteName">Nom du site</Label>
|
|
<Input
|
|
id="siteName"
|
|
{...registerGeneral("siteName", { required: "Le nom du site est requis" })}
|
|
/>
|
|
{errorsGeneral.siteName && (
|
|
<p className="text-sm text-destructive">{errorsGeneral.siteName.message as string}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="contactEmail">Email de contact</Label>
|
|
<Input
|
|
id="contactEmail"
|
|
type="email"
|
|
{...registerGeneral("contactEmail", {
|
|
required: "L'email de contact est requis",
|
|
pattern: {
|
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
message: "Adresse email invalide"
|
|
}
|
|
})}
|
|
/>
|
|
{errorsGeneral.contactEmail && (
|
|
<p className="text-sm text-destructive">{errorsGeneral.contactEmail.message as string}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="siteDescription">Description du site</Label>
|
|
<Textarea
|
|
id="siteDescription"
|
|
rows={3}
|
|
{...registerGeneral("siteDescription")}
|
|
/>
|
|
</div>
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maxProjectsPerUser">Nombre max. de projets par utilisateur</Label>
|
|
<Input
|
|
id="maxProjectsPerUser"
|
|
type="number"
|
|
{...registerGeneral("maxProjectsPerUser", {
|
|
required: "Ce champ est requis",
|
|
min: { value: 1, message: "La valeur minimale est 1" }
|
|
})}
|
|
/>
|
|
{errorsGeneral.maxProjectsPerUser && (
|
|
<p className="text-sm text-destructive">{errorsGeneral.maxProjectsPerUser.message as string}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maxPersonsPerProject">Nombre max. de personnes par projet</Label>
|
|
<Input
|
|
id="maxPersonsPerProject"
|
|
type="number"
|
|
{...registerGeneral("maxPersonsPerProject", {
|
|
required: "Ce champ est requis",
|
|
min: { value: 1, message: "La valeur minimale est 1" }
|
|
})}
|
|
/>
|
|
{errorsGeneral.maxPersonsPerProject && (
|
|
<p className="text-sm text-destructive">{errorsGeneral.maxPersonsPerProject.message as string}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button type="submit" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Enregistrement...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
Enregistrer les modifications
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="authentication" className="space-y-4">
|
|
<Card>
|
|
<form onSubmit={handleSubmitAuth(onSubmitAuth)}>
|
|
<CardHeader>
|
|
<CardTitle>Paramètres d'authentification</CardTitle>
|
|
<CardDescription>
|
|
Configurez les options d'authentification et de sécurité
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="enableGithubAuth">Authentification GitHub</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Activer l'authentification via GitHub OAuth
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="enableGithubAuth"
|
|
{...registerAuth("enableGithubAuth")}
|
|
defaultChecked={systemSettings.authentication.enableGithubAuth}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="requireEmailVerification">Vérification d'email</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Exiger la vérification de l'email lors de l'inscription
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="requireEmailVerification"
|
|
{...registerAuth("requireEmailVerification")}
|
|
defaultChecked={systemSettings.authentication.requireEmailVerification}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="grid gap-4 sm:grid-cols-3">
|
|
<div className="space-y-2">
|
|
<Label htmlFor="sessionTimeout">Durée de session (jours)</Label>
|
|
<Input
|
|
id="sessionTimeout"
|
|
type="number"
|
|
{...registerAuth("sessionTimeout", {
|
|
required: "Ce champ est requis",
|
|
min: { value: 1, message: "La valeur minimale est 1" }
|
|
})}
|
|
/>
|
|
{errorsAuth.sessionTimeout && (
|
|
<p className="text-sm text-destructive">{errorsAuth.sessionTimeout.message as string}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maxLoginAttempts">Tentatives de connexion max.</Label>
|
|
<Input
|
|
id="maxLoginAttempts"
|
|
type="number"
|
|
{...registerAuth("maxLoginAttempts", {
|
|
required: "Ce champ est requis",
|
|
min: { value: 1, message: "La valeur minimale est 1" }
|
|
})}
|
|
/>
|
|
{errorsAuth.maxLoginAttempts && (
|
|
<p className="text-sm text-destructive">{errorsAuth.maxLoginAttempts.message as string}</p>
|
|
)}
|
|
</div>
|
|
<div className="space-y-2">
|
|
<Label htmlFor="passwordMinLength">Longueur min. du mot de passe</Label>
|
|
<Input
|
|
id="passwordMinLength"
|
|
type="number"
|
|
{...registerAuth("passwordMinLength", {
|
|
required: "Ce champ est requis",
|
|
min: { value: 6, message: "La valeur minimale est 6" }
|
|
})}
|
|
/>
|
|
{errorsAuth.passwordMinLength && (
|
|
<p className="text-sm text-destructive">{errorsAuth.passwordMinLength.message as string}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button type="submit" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Enregistrement...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
Enregistrer les modifications
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="notifications" className="space-y-4">
|
|
<Card>
|
|
<form onSubmit={handleSubmitNotif(onSubmitNotif)}>
|
|
<CardHeader>
|
|
<CardTitle>Paramètres de notification</CardTitle>
|
|
<CardDescription>
|
|
Configurez les options de notification système et email
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="enableEmailNotifications">Notifications par email</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Activer l'envoi de notifications par email
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="enableEmailNotifications"
|
|
{...registerNotif("enableEmailNotifications")}
|
|
defaultChecked={systemSettings.notifications.enableEmailNotifications}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="enableSystemNotifications">Notifications système</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Activer les notifications dans l'application
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="enableSystemNotifications"
|
|
{...registerNotif("enableSystemNotifications")}
|
|
defaultChecked={systemSettings.notifications.enableSystemNotifications}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="notifyOnNewUser">Notification nouvel utilisateur</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Notifier les administrateurs lors de l'inscription d'un nouvel utilisateur
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="notifyOnNewUser"
|
|
{...registerNotif("notifyOnNewUser")}
|
|
defaultChecked={systemSettings.notifications.notifyOnNewUser}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="notifyOnNewProject">Notification nouveau projet</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Notifier les administrateurs lors de la création d'un nouveau projet
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="notifyOnNewProject"
|
|
{...registerNotif("notifyOnNewProject")}
|
|
defaultChecked={systemSettings.notifications.notifyOnNewProject}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="adminEmailRecipients">Destinataires des emails administratifs</Label>
|
|
<Input
|
|
id="adminEmailRecipients"
|
|
{...registerNotif("adminEmailRecipients", {
|
|
required: "Ce champ est requis",
|
|
pattern: {
|
|
value: /^[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}$/i,
|
|
message: "Adresse email invalide"
|
|
}
|
|
})}
|
|
/>
|
|
<p className="text-xs text-muted-foreground">
|
|
Séparez les adresses email par des virgules pour plusieurs destinataires
|
|
</p>
|
|
{errorsNotif.adminEmailRecipients && (
|
|
<p className="text-sm text-destructive">{errorsNotif.adminEmailRecipients.message as string}</p>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button type="submit" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Enregistrement...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
Enregistrer les modifications
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
</TabsContent>
|
|
|
|
<TabsContent value="maintenance" className="space-y-4">
|
|
<Card>
|
|
<form onSubmit={handleSubmitMaint(onSubmitMaint)}>
|
|
<CardHeader>
|
|
<CardTitle>Maintenance et débogage</CardTitle>
|
|
<CardDescription>
|
|
Configurez les options de maintenance et de débogage
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="maintenanceMode" className="font-semibold text-destructive">Mode maintenance</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Activer le mode maintenance (le site sera inaccessible aux utilisateurs)
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="maintenanceMode"
|
|
{...registerMaint("maintenanceMode")}
|
|
defaultChecked={systemSettings.maintenance.maintenanceMode}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="maintenanceMessage">Message de maintenance</Label>
|
|
<Textarea
|
|
id="maintenanceMessage"
|
|
rows={3}
|
|
{...registerMaint("maintenanceMessage")}
|
|
/>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="flex items-center justify-between">
|
|
<div className="space-y-0.5">
|
|
<Label htmlFor="debugMode">Mode débogage</Label>
|
|
<p className="text-sm text-muted-foreground">
|
|
Activer le mode débogage (affiche des informations supplémentaires)
|
|
</p>
|
|
</div>
|
|
<Switch
|
|
id="debugMode"
|
|
{...registerMaint("debugMode")}
|
|
defaultChecked={systemSettings.maintenance.debugMode}
|
|
/>
|
|
</div>
|
|
|
|
<div className="space-y-2">
|
|
<Label htmlFor="logLevel">Niveau de journalisation</Label>
|
|
<select
|
|
id="logLevel"
|
|
className="flex h-10 w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
|
|
{...registerMaint("logLevel")}
|
|
>
|
|
<option value="error">Error</option>
|
|
<option value="warn">Warning</option>
|
|
<option value="info">Info</option>
|
|
<option value="debug">Debug</option>
|
|
<option value="trace">Trace</option>
|
|
</select>
|
|
</div>
|
|
|
|
<Separator />
|
|
|
|
<div className="grid gap-4 sm:grid-cols-2">
|
|
<div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="w-full"
|
|
onClick={handleExportConfig}
|
|
disabled={isLoading}
|
|
>
|
|
<FileJson className="mr-2 h-4 w-4" />
|
|
Exporter la configuration
|
|
</Button>
|
|
</div>
|
|
<div>
|
|
<Button
|
|
type="button"
|
|
variant="outline"
|
|
className="w-full"
|
|
onClick={handleClearCache}
|
|
disabled={isLoading}
|
|
>
|
|
<RefreshCw className="mr-2 h-4 w-4" />
|
|
Vider le cache
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
<CardFooter>
|
|
<Button type="submit" disabled={isLoading}>
|
|
{isLoading ? (
|
|
<>
|
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
Enregistrement...
|
|
</>
|
|
) : (
|
|
<>
|
|
<Save className="mr-2 h-4 w-4" />
|
|
Enregistrer les modifications
|
|
</>
|
|
)}
|
|
</Button>
|
|
</CardFooter>
|
|
</form>
|
|
</Card>
|
|
</TabsContent>
|
|
</Tabs>
|
|
</div>
|
|
);
|
|
} |