Checkpoint: Maquette complète de l'interface d'administration de l'intranet CHK — 100 % en français, thème Centre de commande numérique (sidebar marine sombre + contenu clair), 10 pages fonctionnelles : Tableau de bord, Pages, Médias, Widgets, Applications, Utilisateurs, Statistiques, Notifications, Rôles et droits, Paramètres.

This commit is contained in:
Manus 2026-05-21 19:42:05 +00:00
parent 82d4eb5ee3
commit 3fcddede76
16 changed files with 2073 additions and 106 deletions

0
.gitkeep Normal file
View file

View file

@ -6,12 +6,10 @@
<meta
name="viewport"
content="width=device-width, initial-scale=1.0, maximum-scale=1" />
<title>{{project_title}}</title>
<!-- THIS IS THE START OF A COMMENT BLOCK, BLOCK TO BE DELETED: Google Fonts here, example:
<title>CHK Intranet — Administration</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />
THIS IS THE END OF A COMMENT BLOCK, BLOCK TO BE DELETED -->
<link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=Inter:wght@400;500;600&display=swap" rel="stylesheet" />
</head>
<body>

View file

@ -1,37 +1,49 @@
import { Toaster } from "@/components/ui/sonner";
import { TooltipProvider } from "@/components/ui/tooltip";
import NotFound from "@/pages/NotFound";
import { Route, Switch } from "wouter";
import ErrorBoundary from "./components/ErrorBoundary";
import { ThemeProvider } from "./contexts/ThemeContext";
import Home from "./pages/Home";
import AdminLayout from "./components/AdminLayout";
// Pages
import Dashboard from "./pages/Dashboard";
import Pages from "./pages/Pages";
import Medias from "./pages/Medias";
import Widgets from "./pages/Widgets";
import Applications from "./pages/Applications";
import Utilisateurs from "./pages/Utilisateurs";
import Statistiques from "./pages/Statistiques";
import Notifications from "./pages/Notifications";
import Roles from "./pages/Roles";
import Parametres from "./pages/Parametres";
import NotFound from "./pages/NotFound";
function Router() {
return (
<Switch>
<Route path={"/"} component={Home} />
<Route path={"/404"} component={NotFound} />
{/* Final fallback route */}
<Route component={NotFound} />
</Switch>
<AdminLayout>
<Switch>
<Route path="/" component={Dashboard} />
<Route path="/pages" component={Pages} />
<Route path="/medias" component={Medias} />
<Route path="/widgets" component={Widgets} />
<Route path="/applications" component={Applications} />
<Route path="/utilisateurs" component={Utilisateurs} />
<Route path="/statistiques" component={Statistiques} />
<Route path="/notifications" component={Notifications} />
<Route path="/roles" component={Roles} />
<Route path="/parametres" component={Parametres} />
<Route component={NotFound} />
</Switch>
</AdminLayout>
);
}
// NOTE: About Theme
// - First choose a default theme according to your design style (dark or light bg), than change color palette in index.css
// to keep consistent foreground/background color across components
// - If you want to make theme switchable, pass `switchable` ThemeProvider and use `useTheme` hook
function App() {
return (
<ErrorBoundary>
<ThemeProvider
defaultTheme="light"
// switchable
>
<ThemeProvider defaultTheme="light">
<TooltipProvider>
<Toaster />
<Toaster richColors position="top-right" />
<Router />
</TooltipProvider>
</ThemeProvider>

View file

@ -0,0 +1,229 @@
/**
* AdminLayout Mise en page principale de l'administration
* Thème : Centre de commande numérique sidebar marine sombre + zone de contenu claire
*/
import { useState } from "react";
import { Link, useLocation } from "wouter";
import {
LayoutDashboard,
FileText,
Image,
Users,
Settings,
Bell,
Search,
Menu,
ChevronDown,
Eye,
Globe,
Puzzle,
BarChart3,
Shield,
LogOut,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { toast } from "sonner";
const NAV_ITEMS = [
{ icon: LayoutDashboard, label: "Tableau de bord", path: "/" },
{ icon: FileText, label: "Pages", path: "/pages" },
{ icon: Image, label: "Médias", path: "/medias" },
{ icon: Puzzle, label: "Widgets", path: "/widgets" },
{ icon: Globe, label: "Applications", path: "/applications" },
{ icon: Users, label: "Utilisateurs", path: "/utilisateurs" },
{ icon: BarChart3, label: "Statistiques", path: "/statistiques" },
{ icon: Bell, label: "Notifications", path: "/notifications" },
{ icon: Shield, label: "Rôles et droits", path: "/roles" },
{ icon: Settings, label: "Paramètres", path: "/parametres" },
];
export default function AdminLayout({ children }: { children: React.ReactNode }) {
const [location] = useLocation();
const [sidebarOpen, setSidebarOpen] = useState(true);
const [mobileMenuOpen, setMobileMenuOpen] = useState(false);
return (
<div className="flex h-screen overflow-hidden">
{/* Fond semi-transparent sur mobile */}
{mobileMenuOpen && (
<div
className="fixed inset-0 bg-black/50 z-40 lg:hidden"
onClick={() => setMobileMenuOpen(false)}
/>
)}
{/* Barre latérale */}
<aside
className={`
fixed lg:static inset-y-0 left-0 z-50
flex flex-col
bg-sidebar text-sidebar-foreground
transition-all duration-250 ease-[cubic-bezier(0.23,1,0.32,1)]
${sidebarOpen ? "w-64" : "w-[72px]"}
${mobileMenuOpen ? "translate-x-0" : "-translate-x-full lg:translate-x-0"}
sidebar-scroll overflow-y-auto
`}
style={{
backgroundImage: `url(https://d2xsxph8kpxj0f.cloudfront.net/92503813/BjLykXfjQtn3q53KiKhQT8/chk-sidebar-pattern-J2BvTqzuGLrQGWQU7i7suC.webp)`,
backgroundSize: "cover",
backgroundPosition: "center",
}}
>
{/* Zone logo */}
<div className="flex items-center gap-3 px-4 py-5 border-b border-sidebar-border">
<div className="w-10 h-10 rounded-lg bg-sidebar-primary/20 flex items-center justify-center shrink-0">
<span className="text-sidebar-primary font-bold text-lg font-[Outfit]">+</span>
</div>
{sidebarOpen && (
<div className="overflow-hidden">
<h1 className="font-[Outfit] font-semibold text-sm text-sidebar-foreground leading-tight">
CHK Intranet
</h1>
<p className="text-[11px] text-sidebar-foreground/60 leading-tight">
Administration
</p>
</div>
)}
</div>
{/* Navigation */}
<nav className="flex-1 py-4 px-2 space-y-1">
{NAV_ITEMS.map((item) => {
const isActive =
location === item.path ||
(item.path !== "/" && location.startsWith(item.path));
return (
<Link key={item.path} href={item.path}>
<div
className={`
flex items-center gap-3 px-3 py-2.5 rounded-md
transition-all duration-150 ease-out
group relative
${
isActive
? "bg-sidebar-accent text-sidebar-primary"
: "text-sidebar-foreground/70 hover:bg-sidebar-accent/50 hover:text-sidebar-foreground"
}
`}
>
{isActive && (
<div className="absolute left-0 top-1/2 -translate-y-1/2 w-[3px] h-5 bg-sidebar-primary rounded-r-full" />
)}
<item.icon className="w-5 h-5 shrink-0" />
{sidebarOpen && (
<span className="text-sm font-medium truncate">{item.label}</span>
)}
</div>
</Link>
);
})}
</nav>
{/* Pied de la barre latérale */}
<div className="p-3 border-t border-sidebar-border">
<button
onClick={() => setSidebarOpen(!sidebarOpen)}
className="hidden lg:flex items-center justify-center w-full py-2 rounded-md text-sidebar-foreground/50 hover:text-sidebar-foreground hover:bg-sidebar-accent/50 transition-colors"
title={sidebarOpen ? "Réduire le menu" : "Agrandir le menu"}
>
<Menu className="w-4 h-4" />
</button>
</div>
</aside>
{/* Zone de contenu principale */}
<div className="flex-1 flex flex-col overflow-hidden">
{/* En-tête supérieur */}
<header className="h-16 border-b border-border bg-card flex items-center justify-between px-4 lg:px-6 shrink-0">
<div className="flex items-center gap-4">
<button
onClick={() => setMobileMenuOpen(true)}
className="lg:hidden p-2 rounded-md hover:bg-muted transition-colors"
aria-label="Ouvrir le menu"
>
<Menu className="w-5 h-5" />
</button>
{/* Recherche globale */}
<div className="relative hidden sm:block">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Rechercher dans l'intranet..."
className="pl-9 w-64 h-9 bg-muted/50 border-0 focus-visible:ring-1"
/>
</div>
</div>
<div className="flex items-center gap-3">
{/* Bouton mode simulation */}
<Button
variant="outline"
size="sm"
className="hidden md:flex items-center gap-2 text-xs border-dashed border-accent text-accent hover:bg-accent/10"
onClick={() =>
toast.info(
"Mode simulation activé — Vous visualisez l'intranet en tant qu'utilisateur standard."
)
}
>
<Eye className="w-3.5 h-3.5" />
Mode simulation
</Button>
{/* Cloche de notifications */}
<button
className="relative p-2 rounded-md hover:bg-muted transition-colors"
aria-label="Notifications"
onClick={() => toast.info("Aucune nouvelle notification")}
>
<Bell className="w-5 h-5 text-muted-foreground" />
<span className="absolute top-1.5 right-1.5 w-2 h-2 bg-destructive rounded-full" />
</button>
{/* Menu utilisateur */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<button className="flex items-center gap-2 pl-3 pr-2 py-1.5 rounded-md hover:bg-muted transition-colors">
<div className="w-8 h-8 rounded-full bg-primary/10 flex items-center justify-center">
<span className="text-xs font-semibold text-primary">PB</span>
</div>
<div className="hidden md:block text-left">
<p className="text-sm font-medium leading-tight">P. Boursiquot</p>
<p className="text-[11px] text-muted-foreground leading-tight">Administrateur</p>
</div>
<ChevronDown className="w-4 h-4 text-muted-foreground" />
</button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-48">
<DropdownMenuItem onClick={() => toast.info("Profil — Fonctionnalité à venir")}>
<Settings className="w-4 h-4 mr-2" />
Mon profil
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem
className="text-destructive"
onClick={() => toast.info("Déconnexion — Fonctionnalité à venir")}
>
<LogOut className="w-4 h-4 mr-2" />
Déconnexion
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</div>
</header>
{/* Contenu de la page */}
<main className="flex-1 overflow-y-auto p-4 lg:p-6">
{children}
</main>
</div>
</div>
);
}

View file

@ -40,77 +40,46 @@
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
--font-sans: 'Inter', system-ui, sans-serif;
--font-heading: 'Outfit', system-ui, sans-serif;
}
:root {
--primary: var(--color-blue-700);
--primary-foreground: var(--color-blue-50);
--sidebar-primary: var(--color-blue-600);
--sidebar-primary-foreground: var(--color-blue-50);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--radius: 0.65rem;
--background: oklch(1 0 0);
--foreground: oklch(0.235 0.015 65);
--radius: 0.5rem;
/* CHK Medical Command Center Theme - Light */
--background: oklch(0.98 0.002 240);
--foreground: oklch(0.15 0.02 250);
--card: oklch(1 0 0);
--card-foreground: oklch(0.235 0.015 65);
--card-foreground: oklch(0.15 0.02 250);
--popover: oklch(1 0 0);
--popover-foreground: oklch(0.235 0.015 65);
--secondary: oklch(0.98 0.001 286.375);
--secondary-foreground: oklch(0.4 0.015 65);
--muted: oklch(0.967 0.001 286.375);
--muted-foreground: oklch(0.552 0.016 285.938);
--accent: oklch(0.967 0.001 286.375);
--accent-foreground: oklch(0.141 0.005 285.823);
--destructive: oklch(0.577 0.245 27.325);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(0.92 0.004 286.32);
--input: oklch(0.92 0.004 286.32);
--ring: oklch(0.623 0.214 259.815);
--sidebar: oklch(0.985 0 0);
--sidebar-foreground: oklch(0.235 0.015 65);
--sidebar-accent: oklch(0.967 0.001 286.375);
--sidebar-accent-foreground: oklch(0.141 0.005 285.823);
--sidebar-border: oklch(0.92 0.004 286.32);
--sidebar-ring: oklch(0.623 0.214 259.815);
}
.dark {
--primary: var(--color-blue-700);
--primary-foreground: var(--color-blue-50);
--sidebar-primary: var(--color-blue-500);
--sidebar-primary-foreground: var(--color-blue-50);
--background: oklch(0.141 0.005 285.823);
--foreground: oklch(0.85 0.005 65);
--card: oklch(0.21 0.006 285.885);
--card-foreground: oklch(0.85 0.005 65);
--popover: oklch(0.21 0.006 285.885);
--popover-foreground: oklch(0.85 0.005 65);
--secondary: oklch(0.24 0.006 286.033);
--secondary-foreground: oklch(0.7 0.005 65);
--muted: oklch(0.274 0.006 286.033);
--muted-foreground: oklch(0.705 0.015 286.067);
--accent: oklch(0.274 0.006 286.033);
--accent-foreground: oklch(0.92 0.005 65);
--destructive: oklch(0.704 0.191 22.216);
--destructive-foreground: oklch(0.985 0 0);
--border: oklch(1 0 0 / 10%);
--input: oklch(1 0 0 / 15%);
--ring: oklch(0.488 0.243 264.376);
--chart-1: var(--color-blue-300);
--chart-2: var(--color-blue-500);
--chart-3: var(--color-blue-600);
--chart-4: var(--color-blue-700);
--chart-5: var(--color-blue-800);
--sidebar: oklch(0.21 0.006 285.885);
--sidebar-foreground: oklch(0.85 0.005 65);
--sidebar-accent: oklch(0.274 0.006 286.033);
--sidebar-accent-foreground: oklch(0.985 0 0);
--sidebar-border: oklch(1 0 0 / 10%);
--sidebar-ring: oklch(0.488 0.243 264.376);
--popover-foreground: oklch(0.15 0.02 250);
--primary: oklch(0.45 0.15 240);
--primary-foreground: oklch(0.98 0.005 240);
--secondary: oklch(0.96 0.01 240);
--secondary-foreground: oklch(0.35 0.05 240);
--muted: oklch(0.96 0.005 240);
--muted-foreground: oklch(0.55 0.02 250);
--accent: oklch(0.65 0.18 195);
--accent-foreground: oklch(0.98 0.005 195);
--destructive: oklch(0.55 0.22 25);
--destructive-foreground: oklch(0.98 0 0);
--border: oklch(0.92 0.005 240);
--input: oklch(0.92 0.005 240);
--ring: oklch(0.55 0.18 220);
--chart-1: oklch(0.65 0.18 195);
--chart-2: oklch(0.55 0.15 240);
--chart-3: oklch(0.6 0.15 155);
--chart-4: oklch(0.7 0.15 60);
--chart-5: oklch(0.5 0.12 280);
/* Sidebar - Dark Navy */
--sidebar: oklch(0.18 0.03 250);
--sidebar-foreground: oklch(0.9 0.01 240);
--sidebar-primary: oklch(0.65 0.18 195);
--sidebar-primary-foreground: oklch(0.98 0.005 195);
--sidebar-accent: oklch(0.25 0.04 250);
--sidebar-accent-foreground: oklch(0.95 0.01 240);
--sidebar-border: oklch(0.28 0.03 250);
--sidebar-ring: oklch(0.65 0.18 195);
}
@layer base {
@ -119,6 +88,10 @@
}
body {
@apply bg-background text-foreground;
font-family: 'Inter', system-ui, sans-serif;
}
h1, h2, h3, h4, h5, h6 {
font-family: 'Outfit', system-ui, sans-serif;
}
button:not(:disabled),
[role="button"]:not([aria-disabled="true"]),
@ -134,24 +107,11 @@
}
@layer components {
/**
* Custom container utility that centers content and adds responsive padding.
*
* This overrides Tailwind's default container behavior to:
* - Auto-center content (mx-auto)
* - Add responsive horizontal padding
* - Set max-width for large screens
*
* Usage: <div className="container">...</div>
*
* For custom widths, use max-w-* utilities directly:
* <div className="max-w-6xl mx-auto px-4">...</div>
*/
.container {
width: 100%;
margin-left: auto;
margin-right: auto;
padding-left: 1rem; /* 16px - mobile padding */
padding-left: 1rem;
padding-right: 1rem;
}
@ -162,16 +122,61 @@
@media (min-width: 640px) {
.container {
padding-left: 1.5rem; /* 24px - tablet padding */
padding-left: 1.5rem;
padding-right: 1.5rem;
}
}
@media (min-width: 1024px) {
.container {
padding-left: 2rem; /* 32px - desktop padding */
padding-left: 2rem;
padding-right: 2rem;
max-width: 1280px; /* Standard content width */
max-width: 1280px;
}
}
}
}
/* Custom scrollbar for sidebar */
.sidebar-scroll::-webkit-scrollbar {
width: 4px;
}
.sidebar-scroll::-webkit-scrollbar-track {
background: transparent;
}
.sidebar-scroll::-webkit-scrollbar-thumb {
background: oklch(0.35 0.03 250);
border-radius: 2px;
}
/* Animations */
@keyframes fade-in-up {
from {
opacity: 0;
transform: translateY(8px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes counter {
from {
opacity: 0;
transform: translateY(4px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.animate-fade-in-up {
animation: fade-in-up 0.3s cubic-bezier(0.23, 1, 0.32, 1) forwards;
}
.stagger-1 { animation-delay: 30ms; }
.stagger-2 { animation-delay: 60ms; }
.stagger-3 { animation-delay: 90ms; }
.stagger-4 { animation-delay: 120ms; }
.stagger-5 { animation-delay: 150ms; }

View file

@ -0,0 +1,76 @@
/**
* Applications Gestion des raccourcis vers les outils métier
*/
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Plus, ExternalLink, Settings, Trash2, Edit } from "lucide-react";
import { toast } from "sonner";
const APPLICATIONS = [
{ id: 1, nom: "Outlook Web", url: "https://outlook.office365.com", categorie: "Communication", actif: true, couleur: "#0078D4", initiale: "O" },
{ id: 2, nom: "PMSIpilot", url: "#", categorie: "Médical", actif: true, couleur: "#1a5276", initiale: "P" },
{ id: 3, nom: "Vidal Hoptimal", url: "#", categorie: "Médical", actif: true, couleur: "#c0392b", initiale: "V" },
{ id: 4, nom: "Dedalus", url: "#", categorie: "Médical", actif: true, couleur: "#117a65", initiale: "D" },
{ id: 5, nom: "GLPI", url: "#", categorie: "Support", actif: true, couleur: "#6c3483", initiale: "G" },
{ id: 6, nom: "Antibiogarde", url: "#", categorie: "Médical", actif: false, couleur: "#935116", initiale: "A" },
{ id: 7, nom: "Intranet RH", url: "#", categorie: "RH", actif: true, couleur: "#1f618d", initiale: "R" },
{ id: 8, nom: "Portail Formation", url: "#", categorie: "Formation", actif: false, couleur: "#196f3d", initiale: "F" },
];
export default function Applications() {
const [apps, setApps] = useState(APPLICATIONS);
const toggleApp = (id: number) => {
setApps((prev) => prev.map((a) => a.id === id ? { ...a, actif: !a.actif } : a));
const app = apps.find((a) => a.id === id);
toast.success(`Application "${app?.nom}" ${app?.actif ? "masquée" : "affichée"} sur l'intranet`);
};
return (
<div className="space-y-6 animate-fade-in-up">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Applications</h1>
<p className="text-sm text-muted-foreground mt-1">Gérez les raccourcis vers les outils métier de l'intranet</p>
</div>
<Button className="bg-accent text-accent-foreground hover:bg-accent/90 shadow-sm" onClick={() => toast.info("Ajout d'application — Fonctionnalité à venir")}>
<Plus className="w-4 h-4 mr-2" /> Ajouter une application
</Button>
</div>
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
{apps.map((app) => (
<Card key={app.id} className={`shadow-sm hover:shadow-md transition-all duration-200 ${!app.actif ? "opacity-60" : ""}`}>
<CardContent className="p-4">
<div className="flex items-start justify-between mb-3">
<div
className="w-12 h-12 rounded-xl flex items-center justify-center text-white font-bold text-lg font-[Outfit]"
style={{ backgroundColor: app.couleur }}
>
{app.initiale}
</div>
<Switch checked={app.actif} onCheckedChange={() => toggleApp(app.id)} />
</div>
<p className="font-semibold text-sm">{app.nom}</p>
<Badge variant="secondary" className="text-[10px] mt-1">{app.categorie}</Badge>
<div className="flex items-center gap-1 mt-3">
<Button variant="ghost" size="sm" className="h-7 px-2 text-xs flex-1" onClick={() => toast.info("Modification — Fonctionnalité à venir")}>
<Edit className="w-3 h-3 mr-1" /> Modifier
</Button>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0" onClick={() => window.open(app.url, "_blank")}>
<ExternalLink className="w-3.5 h-3.5" />
</Button>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 text-destructive" onClick={() => toast.info("Suppression — Fonctionnalité à venir")}>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}

View file

@ -0,0 +1,340 @@
/**
* Tableau de bord Vue d'ensemble principale
* Thème : Centre de commande numérique widgets modulaires, indicateurs, graphiques
*/
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Users,
FileText,
Eye,
TrendingUp,
TrendingDown,
Clock,
AlertTriangle,
CheckCircle,
ArrowRight,
Activity,
} from "lucide-react";
import {
BarChart,
Bar,
XAxis,
YAxis,
CartesianGrid,
Tooltip,
ResponsiveContainer,
LineChart,
Line,
PieChart,
Pie,
Cell,
} from "recharts";
const visitData = [
{ name: "Lun", visites: 245 },
{ name: "Mar", visites: 312 },
{ name: "Mer", visites: 289 },
{ name: "Jeu", visites: 378 },
{ name: "Ven", visites: 420 },
{ name: "Sam", visites: 156 },
{ name: "Dim", visites: 98 },
];
const trafficData = [
{ name: "Sem. 1", utilisateurs: 1200 },
{ name: "Sem. 2", utilisateurs: 1350 },
{ name: "Sem. 3", utilisateurs: 1180 },
{ name: "Sem. 4", utilisateurs: 1520 },
{ name: "Sem. 5", utilisateurs: 1680 },
{ name: "Sem. 6", utilisateurs: 1420 },
];
const appUsageData = [
{ name: "Outlook", value: 35, color: "#0891b2" },
{ name: "PMSIpilot", value: 20, color: "#0f172a" },
{ name: "Vidal", value: 18, color: "#10b981" },
{ name: "GLPI", value: 15, color: "#f59e0b" },
{ name: "Autres", value: 12, color: "#8b5cf6" },
];
const recentActivity = [
{ action: "Page modifiée", target: "Protocoles Urgences", user: "Dr. Martin", time: "Il y a 5 min", status: "success" },
{ action: "Utilisateur ajouté", target: "Sophie Leclerc (IDE)", user: "Admin", time: "Il y a 12 min", status: "success" },
{ action: "Alerte système", target: "Serveur PMSIpilot indisponible", user: "Système", time: "Il y a 25 min", status: "warning" },
{ action: "Widget mis à jour", target: "Annuaire téléphonique", user: "P. Boursiquot", time: "Il y a 1h", status: "success" },
{ action: "Page publiée", target: "Informations COVID-19", user: "Service Communication", time: "Il y a 2h", status: "success" },
];
export default function Dashboard() {
return (
<div className="space-y-6 animate-fade-in-up">
{/* En-tête de page */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">
Tableau de bord
</h1>
<p className="text-sm text-muted-foreground mt-1">
Vue d'ensemble de l'activité de l'intranet CHK
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="text-xs py-1 px-2.5">
<Activity className="w-3 h-3 mr-1" />
En ligne
</Badge>
<span className="text-xs text-muted-foreground">
Dernière mise à jour : il y a 2 min
</span>
</div>
</div>
{/* Indicateurs clés */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
<KPICard
title="Utilisateurs actifs"
value="347"
change="+12 %"
trend="up"
icon={Users}
description="ce mois"
/>
<KPICard
title="Pages publiées"
value="24"
change="+3"
trend="up"
icon={FileText}
description="cette semaine"
/>
<KPICard
title="Visites aujourd'hui"
value="1 248"
change="+8,2 %"
trend="up"
icon={Eye}
description="vs hier"
/>
<KPICard
title="Durée moyenne"
value="4 min 32"
change="-15 %"
trend="down"
icon={Clock}
description="par session"
/>
</div>
{/* Rangée de graphiques */}
<div className="grid grid-cols-1 lg:grid-cols-3 gap-4">
{/* Histogramme — Visites */}
<Card className="lg:col-span-2 shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-[Outfit] font-semibold">
Visites cette semaine
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={220}>
<BarChart data={visitData}>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.92 0.005 240)" />
<XAxis dataKey="name" tick={{ fontSize: 12 }} stroke="oklch(0.55 0.02 250)" />
<YAxis tick={{ fontSize: 12 }} stroke="oklch(0.55 0.02 250)" />
<Tooltip
formatter={(value) => [`${value} visites`, "Visites"]}
contentStyle={{
borderRadius: "8px",
border: "1px solid oklch(0.92 0.005 240)",
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
fontFamily: "Inter, sans-serif",
fontSize: "12px",
}}
/>
<Bar dataKey="visites" fill="oklch(0.65 0.18 195)" radius={[4, 4, 0, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
{/* Camembert — Applications les plus utilisées */}
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-[Outfit] font-semibold">
Applications les plus utilisées
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={160}>
<PieChart>
<Pie
data={appUsageData}
cx="50%"
cy="50%"
innerRadius={45}
outerRadius={70}
paddingAngle={2}
dataKey="value"
>
{appUsageData.map((entry, index) => (
<Cell key={index} fill={entry.color} />
))}
</Pie>
<Tooltip
formatter={(value) => [`${value} %`, "Utilisation"]}
contentStyle={{
borderRadius: "8px",
border: "1px solid oklch(0.92 0.005 240)",
fontSize: "12px",
}}
/>
</PieChart>
</ResponsiveContainer>
<div className="space-y-1.5 mt-2">
{appUsageData.map((item) => (
<div key={item.name} className="flex items-center justify-between text-xs">
<div className="flex items-center gap-2">
<div className="w-2.5 h-2.5 rounded-full" style={{ backgroundColor: item.color }} />
<span className="text-muted-foreground">{item.name}</span>
</div>
<span className="font-medium">{item.value} %</span>
</div>
))}
</div>
</CardContent>
</Card>
</div>
{/* Rangée inférieure */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
{/* Activité récente */}
<Card className="shadow-sm">
<CardHeader className="pb-3 flex flex-row items-center justify-between">
<CardTitle className="text-base font-[Outfit] font-semibold">
Activité récente
</CardTitle>
<Button variant="ghost" size="sm" className="text-xs text-accent">
Voir tout <ArrowRight className="w-3 h-3 ml-1" />
</Button>
</CardHeader>
<CardContent className="space-y-3">
{recentActivity.map((item, i) => (
<div key={i} className="flex items-start gap-3 py-2 border-b border-border/50 last:border-0">
<div
className={`mt-0.5 w-6 h-6 rounded-full flex items-center justify-center shrink-0 ${
item.status === "warning"
? "bg-amber-100 text-amber-600"
: "bg-emerald-100 text-emerald-600"
}`}
>
{item.status === "warning" ? (
<AlertTriangle className="w-3.5 h-3.5" />
) : (
<CheckCircle className="w-3.5 h-3.5" />
)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-foreground truncate">
{item.action} {" "}
<span className="text-accent">{item.target}</span>
</p>
<p className="text-xs text-muted-foreground mt-0.5">
{item.user} · {item.time}
</p>
</div>
</div>
))}
</CardContent>
</Card>
{/* Tendance utilisateurs */}
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-[Outfit] font-semibold">
Tendance des connexions (6 semaines)
</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={240}>
<LineChart data={trafficData}>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.92 0.005 240)" />
<XAxis dataKey="name" tick={{ fontSize: 12 }} stroke="oklch(0.55 0.02 250)" />
<YAxis tick={{ fontSize: 12 }} stroke="oklch(0.55 0.02 250)" />
<Tooltip
formatter={(value) => [`${value} utilisateurs`, "Connexions"]}
contentStyle={{
borderRadius: "8px",
border: "1px solid oklch(0.92 0.005 240)",
boxShadow: "0 4px 12px rgba(0,0,0,0.08)",
fontFamily: "Inter, sans-serif",
fontSize: "12px",
}}
/>
<Line
type="monotone"
dataKey="utilisateurs"
stroke="oklch(0.45 0.15 240)"
strokeWidth={2.5}
dot={{ r: 4, fill: "oklch(0.45 0.15 240)" }}
activeDot={{ r: 6 }}
/>
</LineChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
</div>
);
}
function KPICard({
title,
value,
change,
trend,
icon: Icon,
description,
}: {
title: string;
value: string;
change: string;
trend: "up" | "down";
icon: React.ComponentType<{ className?: string }>;
description: string;
}) {
return (
<Card className="shadow-sm hover:shadow-md transition-shadow duration-200">
<CardContent className="p-5">
<div className="flex items-start justify-between">
<div>
<p className="text-xs font-medium text-muted-foreground uppercase tracking-wide">
{title}
</p>
<p className="text-2xl font-[Outfit] font-bold mt-1.5 text-foreground tabular-nums">
{value}
</p>
<div className="flex items-center gap-1.5 mt-1.5">
{trend === "up" ? (
<TrendingUp className="w-3.5 h-3.5 text-emerald-500" />
) : (
<TrendingDown className="w-3.5 h-3.5 text-amber-500" />
)}
<span
className={`text-xs font-medium ${
trend === "up" ? "text-emerald-600" : "text-amber-600"
}`}
>
{change}
</span>
<span className="text-xs text-muted-foreground">{description}</span>
</div>
</div>
<div className="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center">
<Icon className="w-5 h-5 text-accent" />
</div>
</div>
</CardContent>
</Card>
);
}

248
client/src/pages/Medias.tsx Normal file
View file

@ -0,0 +1,248 @@
/**
* Médias Bibliothèque de fichiers et images
* Gestion des images, documents et vidéos de l'intranet
*/
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Upload,
Search,
Grid3X3,
List,
Image,
FileText,
Film,
Download,
Trash2,
Eye,
Filter,
} from "lucide-react";
import { toast } from "sonner";
const FICHIERS = [
{ id: 1, nom: "Logo-CHU-Kourou.png", type: "image", taille: "245 Ko", date: "21/05/2026", miniature: "https://images.unsplash.com/photo-1505751172876-fa1923c5c528?w=200&h=200&fit=crop" },
{ id: 2, nom: "Protocole-urgences.pdf", type: "document", taille: "1,2 Mo", date: "20/05/2026", miniature: null },
{ id: 3, nom: "Photo-equipe-2026.jpg", type: "image", taille: "3,4 Mo", date: "19/05/2026", miniature: "https://images.unsplash.com/photo-1559839734-2b71ea197ec2?w=200&h=200&fit=crop" },
{ id: 4, nom: "Banniere-accueil.png", type: "image", taille: "890 Ko", date: "18/05/2026", miniature: "https://images.unsplash.com/photo-1538108149393-fbbd81895907?w=200&h=200&fit=crop" },
{ id: 5, nom: "Organigramme-2026.pdf", type: "document", taille: "560 Ko", date: "17/05/2026", miniature: null },
{ id: 6, nom: "Video-presentation.mp4", type: "video", taille: "45 Mo", date: "16/05/2026", miniature: "https://images.unsplash.com/photo-1576091160399-112ba8d25d1d?w=200&h=200&fit=crop" },
{ id: 7, nom: "Plan-hopital.png", type: "image", taille: "1,8 Mo", date: "15/05/2026", miniature: "https://images.unsplash.com/photo-1587351021759-3e566b6af7cc?w=200&h=200&fit=crop" },
{ id: 8, nom: "Charte-graphique.pdf", type: "document", taille: "2,1 Mo", date: "14/05/2026", miniature: null },
{ id: 9, nom: "Photo-batiment.jpg", type: "image", taille: "4,2 Mo", date: "13/05/2026", miniature: "https://images.unsplash.com/photo-1519494026892-80bbd2d6fd0d?w=200&h=200&fit=crop" },
];
const TYPE_LABELS: Record<string, string> = {
image: "Image",
document: "Document",
video: "Vidéo",
};
export default function Medias() {
const [modeAffichage, setModeAffichage] = useState<"grille" | "liste">("grille");
const [recherche, setRecherche] = useState("");
const fichiersFiltres = FICHIERS.filter((f) =>
f.nom.toLowerCase().includes(recherche.toLowerCase())
);
const getIconeFichier = (type: string) => {
switch (type) {
case "image": return <Image className="w-5 h-5 text-cyan-600" />;
case "document": return <FileText className="w-5 h-5 text-amber-600" />;
case "video": return <Film className="w-5 h-5 text-purple-600" />;
default: return <FileText className="w-5 h-5 text-muted-foreground" />;
}
};
return (
<div className="space-y-6 animate-fade-in-up">
{/* En-tête */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Médias</h1>
<p className="text-sm text-muted-foreground mt-1">
Bibliothèque de fichiers et images de l'intranet
</p>
</div>
<Button
className="bg-accent text-accent-foreground hover:bg-accent/90 shadow-sm"
onClick={() => toast.info("Téléversement — Fonctionnalité à venir")}
>
<Upload className="w-4 h-4 mr-2" />
Téléverser un fichier
</Button>
</div>
{/* Barre d'outils */}
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row items-center gap-3">
<div className="relative flex-1 w-full">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Rechercher un fichier..."
className="pl-9"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
/>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => toast.info("Filtres — Fonctionnalité à venir")}
>
<Filter className="w-4 h-4 mr-1" /> Filtrer
</Button>
<div className="flex border rounded-md overflow-hidden">
<button
className={`p-2 ${modeAffichage === "grille" ? "bg-muted" : "hover:bg-muted/50"} transition-colors`}
onClick={() => setModeAffichage("grille")}
title="Affichage en grille"
>
<Grid3X3 className="w-4 h-4" />
</button>
<button
className={`p-2 ${modeAffichage === "liste" ? "bg-muted" : "hover:bg-muted/50"} transition-colors`}
onClick={() => setModeAffichage("liste")}
title="Affichage en liste"
>
<List className="w-4 h-4" />
</button>
</div>
</div>
</div>
</CardContent>
</Card>
{/* Résumé statistique */}
<div className="grid grid-cols-3 gap-4">
<Card className="shadow-sm">
<CardContent className="p-4 text-center">
<p className="text-2xl font-[Outfit] font-bold text-foreground">9</p>
<p className="text-xs text-muted-foreground mt-1">Fichiers au total</p>
</CardContent>
</Card>
<Card className="shadow-sm">
<CardContent className="p-4 text-center">
<p className="text-2xl font-[Outfit] font-bold text-foreground">59,4 Mo</p>
<p className="text-xs text-muted-foreground mt-1">Espace utilisé</p>
</CardContent>
</Card>
<Card className="shadow-sm">
<CardContent className="p-4 text-center">
<p className="text-2xl font-[Outfit] font-bold text-foreground">5</p>
<p className="text-xs text-muted-foreground mt-1">Images</p>
</CardContent>
</Card>
</div>
{/* Grille ou liste de fichiers */}
{modeAffichage === "grille" ? (
<div className="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4 xl:grid-cols-5 gap-4">
{fichiersFiltres.map((fichier) => (
<Card
key={fichier.id}
className="shadow-sm group hover:shadow-md transition-all duration-200 overflow-hidden"
>
<div className="aspect-square relative bg-muted/30 flex items-center justify-center">
{fichier.miniature ? (
<img
src={fichier.miniature}
alt={fichier.nom}
className="w-full h-full object-cover"
/>
) : (
<div className="flex flex-col items-center gap-2">
{getIconeFichier(fichier.type)}
<span className="text-[10px] text-muted-foreground uppercase font-medium">
{fichier.nom.split(".").pop()}
</span>
</div>
)}
{/* Actions au survol */}
<div className="absolute inset-0 bg-black/50 opacity-0 group-hover:opacity-100 transition-opacity duration-200 flex items-center justify-center gap-2">
<button
className="p-2 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
onClick={() => toast.info("Prévisualisation — Fonctionnalité à venir")}
title="Prévisualiser"
>
<Eye className="w-4 h-4" />
</button>
<button
className="p-2 rounded-full bg-white/20 hover:bg-white/30 text-white transition-colors"
onClick={() => toast.info("Téléchargement — Fonctionnalité à venir")}
title="Télécharger"
>
<Download className="w-4 h-4" />
</button>
<button
className="p-2 rounded-full bg-white/20 hover:bg-red-500/50 text-white transition-colors"
onClick={() => toast.info("Suppression — Fonctionnalité à venir")}
title="Supprimer"
>
<Trash2 className="w-4 h-4" />
</button>
</div>
</div>
<CardContent className="p-3">
<p className="text-xs font-medium truncate">{fichier.nom}</p>
<div className="flex items-center justify-between mt-1">
<span className="text-[10px] text-muted-foreground">{fichier.taille}</span>
<span className="text-[10px] text-muted-foreground">{fichier.date}</span>
</div>
</CardContent>
</Card>
))}
</div>
) : (
<Card className="shadow-sm">
<CardContent className="p-0">
<div className="divide-y">
{fichiersFiltres.map((fichier) => (
<div
key={fichier.id}
className="flex items-center gap-4 p-4 hover:bg-muted/30 transition-colors group"
>
<div className="w-10 h-10 rounded-lg bg-muted/50 flex items-center justify-center shrink-0">
{getIconeFichier(fichier.type)}
</div>
<div className="flex-1 min-w-0">
<p className="text-sm font-medium truncate">{fichier.nom}</p>
<p className="text-xs text-muted-foreground">
{fichier.taille} · {fichier.date}
</p>
</div>
<Badge variant="secondary" className="text-[10px]">
{TYPE_LABELS[fichier.type] ?? fichier.type}
</Badge>
<div className="flex gap-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => toast.info("Téléchargement — Fonctionnalité à venir")}
>
<Download className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 text-destructive"
onClick={() => toast.info("Suppression — Fonctionnalité à venir")}
>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
))}
</div>
</CardContent>
</Card>
)}
</div>
);
}

View file

@ -0,0 +1,84 @@
/**
* Notifications Centre de messages et alertes
*/
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Bell, AlertTriangle, Info, CheckCircle, Trash2, Check } from "lucide-react";
import { toast } from "sonner";
const NOTIFICATIONS = [
{ id: 1, type: "alerte", titre: "Serveur PMSIpilot indisponible", message: "Le serveur PMSIpilot est inaccessible depuis 14h30. L'équipe DSI est informée.", temps: "Il y a 25 min", lue: false },
{ id: 2, type: "info", titre: "Mise à jour de l'intranet planifiée", message: "Une maintenance est prévue le samedi 24 mai de 22h à 2h du matin.", temps: "Il y a 2h", lue: false },
{ id: 3, type: "succes", titre: "Sauvegarde automatique réussie", message: "La sauvegarde quotidienne des données s'est terminée avec succès.", temps: "Il y a 4h", lue: true },
{ id: 4, type: "info", titre: "Nouvel utilisateur inscrit", message: "Sophie Leclerc (IDE - Urgences) a été ajoutée au système.", temps: "Hier à 16h45", lue: true },
{ id: 5, type: "alerte", titre: "Tentative de connexion suspecte", message: "3 tentatives de connexion échouées depuis une adresse IP inconnue.", temps: "Hier à 09h12", lue: true },
];
const ICONES: Record<string, React.ReactNode> = {
alerte: <AlertTriangle className="w-4 h-4 text-amber-600" />,
info: <Info className="w-4 h-4 text-blue-600" />,
succes: <CheckCircle className="w-4 h-4 text-emerald-600" />,
};
const COULEURS: Record<string, string> = {
alerte: "bg-amber-100",
info: "bg-blue-100",
succes: "bg-emerald-100",
};
export default function Notifications() {
const [notifs, setNotifs] = useState(NOTIFICATIONS);
const marquerToutesLues = () => {
setNotifs((prev) => prev.map((n) => ({ ...n, lue: true })));
toast.success("Toutes les notifications ont été marquées comme lues");
};
const nonLues = notifs.filter((n) => !n.lue).length;
return (
<div className="space-y-6 animate-fade-in-up">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground flex items-center gap-2">
Notifications
{nonLues > 0 && <Badge className="bg-destructive text-white text-xs">{nonLues}</Badge>}
</h1>
<p className="text-sm text-muted-foreground mt-1">Alertes et messages du système</p>
</div>
{nonLues > 0 && (
<Button variant="outline" size="sm" onClick={marquerToutesLues}>
<Check className="w-4 h-4 mr-2" /> Tout marquer comme lu
</Button>
)}
</div>
<div className="space-y-3">
{notifs.map((notif) => (
<Card key={notif.id} className={`shadow-sm transition-all duration-200 ${!notif.lue ? "border-l-4 border-l-accent" : ""}`}>
<CardContent className="p-4">
<div className="flex items-start gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center shrink-0 ${COULEURS[notif.type]}`}>
{ICONES[notif.type]}
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className={`text-sm font-semibold ${!notif.lue ? "text-foreground" : "text-muted-foreground"}`}>{notif.titre}</p>
{!notif.lue && <Badge className="bg-accent/20 text-accent text-[10px] px-1.5">Nouveau</Badge>}
</div>
<p className="text-xs text-muted-foreground mt-1">{notif.message}</p>
<p className="text-[11px] text-muted-foreground/70 mt-1.5">{notif.temps}</p>
</div>
<Button variant="ghost" size="sm" className="h-7 w-7 p-0 text-muted-foreground hover:text-destructive" onClick={() => { setNotifs((prev) => prev.filter((n) => n.id !== notif.id)); toast.success("Notification supprimée"); }}>
<Trash2 className="w-3.5 h-3.5" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}

266
client/src/pages/Pages.tsx Normal file
View file

@ -0,0 +1,266 @@
/**
* Pages Gestion du contenu de l'intranet (CMS)
* Fonctionnalités : liste, création, édition, publication, catégorisation
*/
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import {
Plus,
Search,
MoreHorizontal,
Edit,
Trash2,
Eye,
Copy,
FileText,
Globe,
Lock,
} from "lucide-react";
import { toast } from "sonner";
const PAGES_DATA = [
{ id: 1, titre: "Accueil", slug: "/", statut: "publie", categorie: "Général", auteur: "Admin", modifie: "21/05/2026", vues: 12540 },
{ id: 2, titre: "Protocoles Urgences", slug: "/protocoles-urgences", statut: "publie", categorie: "Médical", auteur: "Dr. Martin", modifie: "20/05/2026", vues: 3420 },
{ id: 3, titre: "Annuaire du personnel", slug: "/annuaire", statut: "publie", categorie: "RH", auteur: "Admin", modifie: "19/05/2026", vues: 8900 },
{ id: 4, titre: "Informations COVID-19", slug: "/covid-19", statut: "publie", categorie: "Médical", auteur: "Service Communication", modifie: "18/05/2026", vues: 2100 },
{ id: 5, titre: "Planning des gardes", slug: "/planning-gardes", statut: "brouillon", categorie: "RH", auteur: "Cadre de santé", modifie: "17/05/2026", vues: 0 },
{ id: 6, titre: "Procédures qualité", slug: "/qualite", statut: "publie", categorie: "Qualité", auteur: "Responsable Qualité", modifie: "16/05/2026", vues: 1560 },
{ id: 7, titre: "Documentation utilisateurs", slug: "/doc-utilisateurs", statut: "publie", categorie: "Support", auteur: "P. Boursiquot", modifie: "15/05/2026", vues: 4200 },
{ id: 8, titre: "Nouvelle page pharmacie", slug: "/pharmacie-v2", statut: "brouillon", categorie: "Médical", auteur: "Pharmacien", modifie: "14/05/2026", vues: 0 },
];
export default function Pages() {
const [recherche, setRecherche] = useState("");
const [dialogCreation, setDialogCreation] = useState(false);
const [filtreCategorie, setFiltreCategorie] = useState("toutes");
const pagesFiltrees = PAGES_DATA.filter((page) => {
const correspondRecherche = page.titre.toLowerCase().includes(recherche.toLowerCase());
const correspondCategorie =
filtreCategorie === "toutes" || page.categorie === filtreCategorie;
return correspondRecherche && correspondCategorie;
});
return (
<div className="space-y-6 animate-fade-in-up">
{/* En-tête */}
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Pages</h1>
<p className="text-sm text-muted-foreground mt-1">
Gérez les pages de contenu de l'intranet
</p>
</div>
<Button
className="bg-accent text-accent-foreground hover:bg-accent/90 shadow-sm"
onClick={() => setDialogCreation(true)}
>
<Plus className="w-4 h-4 mr-2" />
Nouvelle page
</Button>
</div>
{/* Filtres */}
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row gap-3">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="Rechercher une page..."
className="pl-9"
value={recherche}
onChange={(e) => setRecherche(e.target.value)}
/>
</div>
<Select value={filtreCategorie} onValueChange={setFiltreCategorie}>
<SelectTrigger className="w-full sm:w-52">
<SelectValue placeholder="Catégorie" />
</SelectTrigger>
<SelectContent>
<SelectItem value="toutes">Toutes les catégories</SelectItem>
<SelectItem value="Général">Général</SelectItem>
<SelectItem value="Médical">Médical</SelectItem>
<SelectItem value="RH">Ressources humaines</SelectItem>
<SelectItem value="Qualité">Qualité</SelectItem>
<SelectItem value="Support">Support</SelectItem>
</SelectContent>
</Select>
</div>
</CardContent>
</Card>
{/* Tableau des pages */}
<Card className="shadow-sm">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead className="w-[300px]">Page</TableHead>
<TableHead>Catégorie</TableHead>
<TableHead>Statut</TableHead>
<TableHead>Auteur</TableHead>
<TableHead className="text-right">Vues</TableHead>
<TableHead>Modifié</TableHead>
<TableHead className="w-10"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pagesFiltrees.map((page) => (
<TableRow key={page.id} className="group hover:bg-muted/30">
<TableCell>
<div className="flex items-center gap-3">
<div className="w-8 h-8 rounded bg-primary/5 flex items-center justify-center">
<FileText className="w-4 h-4 text-primary" />
</div>
<div>
<p className="font-medium text-sm">{page.titre}</p>
<p className="text-xs text-muted-foreground">{page.slug}</p>
</div>
</div>
</TableCell>
<TableCell>
<Badge variant="secondary" className="text-xs font-normal">
{page.categorie}
</Badge>
</TableCell>
<TableCell>
{page.statut === "publie" ? (
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 text-xs">
<Globe className="w-3 h-3 mr-1" />
Publié
</Badge>
) : (
<Badge variant="outline" className="text-xs text-muted-foreground">
<Lock className="w-3 h-3 mr-1" />
Brouillon
</Badge>
)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">{page.auteur}</TableCell>
<TableCell className="text-right text-sm tabular-nums">
{page.vues.toLocaleString("fr-FR")}
</TableCell>
<TableCell className="text-sm text-muted-foreground">{page.modifie}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity"
>
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => toast.info("Éditeur de page — Fonctionnalité à venir")}>
<Edit className="w-4 h-4 mr-2" /> Modifier
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toast.info("Prévisualisation — Fonctionnalité à venir")}>
<Eye className="w-4 h-4 mr-2" /> Prévisualiser
</DropdownMenuItem>
<DropdownMenuItem onClick={() => toast.success("Page dupliquée avec succès")}>
<Copy className="w-4 h-4 mr-2" /> Dupliquer
</DropdownMenuItem>
<DropdownMenuItem
className="text-destructive"
onClick={() => toast.info("Suppression — Fonctionnalité à venir")}
>
<Trash2 className="w-4 h-4 mr-2" /> Supprimer
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Dialogue de création */}
<Dialog open={dialogCreation} onOpenChange={setDialogCreation}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle className="font-[Outfit]">Créer une nouvelle page</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="space-y-2">
<label className="text-sm font-medium">Titre de la page</label>
<Input placeholder="Ex : Protocole d'accueil des urgences" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Catégorie</label>
<Select>
<SelectTrigger>
<SelectValue placeholder="Sélectionner une catégorie" />
</SelectTrigger>
<SelectContent>
<SelectItem value="general">Général</SelectItem>
<SelectItem value="medical">Médical</SelectItem>
<SelectItem value="rh">Ressources humaines</SelectItem>
<SelectItem value="qualite">Qualité</SelectItem>
<SelectItem value="support">Support</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Description (optionnel)</label>
<Textarea
placeholder="Brève description du contenu de la page..."
rows={3}
/>
</div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogCreation(false)}>
Annuler
</Button>
<Button
className="bg-accent text-accent-foreground hover:bg-accent/90"
onClick={() => {
setDialogCreation(false);
toast.success("Page créée avec succès !");
}}
>
Créer la page
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View file

@ -0,0 +1,115 @@
/**
* Paramètres Configuration générale de l'intranet
*/
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Textarea } from "@/components/ui/textarea";
import { Switch } from "@/components/ui/switch";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Settings, Globe, Bell, Shield, Palette, Save } from "lucide-react";
import { toast } from "sonner";
export default function Parametres() {
return (
<div className="space-y-6 animate-fade-in-up">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Paramètres</h1>
<p className="text-sm text-muted-foreground mt-1">Configuration générale de l'intranet CHK</p>
</div>
<Tabs defaultValue="general">
<TabsList className="mb-4">
<TabsTrigger value="general"><Settings className="w-4 h-4 mr-2" />Général</TabsTrigger>
<TabsTrigger value="apparence"><Palette className="w-4 h-4 mr-2" />Apparence</TabsTrigger>
<TabsTrigger value="notifications"><Bell className="w-4 h-4 mr-2" />Notifications</TabsTrigger>
<TabsTrigger value="securite"><Shield className="w-4 h-4 mr-2" />Sécurité</TabsTrigger>
</TabsList>
<TabsContent value="general">
<Card className="shadow-sm">
<CardHeader><CardTitle className="text-base font-[Outfit]">Informations générales</CardTitle></CardHeader>
<CardContent className="space-y-5">
<div className="space-y-2"><label className="text-sm font-medium">Nom de l'intranet</label><Input defaultValue="Intranet CHK — Centre Hospitalier de Kourou" /></div>
<div className="space-y-2"><label className="text-sm font-medium">Description</label><Textarea defaultValue="Portail intranet du Centre Hospitalier de Kourou — Guyane française" rows={3} /></div>
<div className="space-y-2"><label className="text-sm font-medium">URL de l'intranet</label><Input defaultValue="https://chk-intranet.pboursiquot.cmck.org" /></div>
<div className="space-y-2"><label className="text-sm font-medium">Adresse e-mail de contact</label><Input type="email" defaultValue="dsi@chk.gf" /></div>
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/40">
<div><p className="text-sm font-medium">Mode maintenance</p><p className="text-xs text-muted-foreground">Affiche une page de maintenance aux utilisateurs</p></div>
<Switch onCheckedChange={(v) => toast.info(v ? "Mode maintenance activé" : "Mode maintenance désactivé")} />
</div>
<Button className="bg-accent text-accent-foreground hover:bg-accent/90" onClick={() => toast.success("Paramètres enregistrés avec succès")}>
<Save className="w-4 h-4 mr-2" /> Enregistrer
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="apparence">
<Card className="shadow-sm">
<CardHeader><CardTitle className="text-base font-[Outfit]">Apparence et thème</CardTitle></CardHeader>
<CardContent className="space-y-5">
<div className="space-y-2"><label className="text-sm font-medium">Couleur principale</label>
<div className="flex gap-3">
{["#0891b2", "#1d4ed8", "#059669", "#7c3aed", "#dc2626"].map((c) => (
<button key={c} className="w-8 h-8 rounded-full border-2 border-transparent hover:border-foreground transition-colors" style={{ backgroundColor: c }} onClick={() => toast.info("Changement de couleur — Fonctionnalité à venir")} />
))}
</div>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/40">
<div><p className="text-sm font-medium">Logo personnalisé</p><p className="text-xs text-muted-foreground">Remplacer le logo par défaut par celui du CHK</p></div>
<Button variant="outline" size="sm" onClick={() => toast.info("Téléversement du logo — Fonctionnalité à venir")}>Choisir un fichier</Button>
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/40">
<div><p className="text-sm font-medium">Favicon</p><p className="text-xs text-muted-foreground">Icône affichée dans l'onglet du navigateur</p></div>
<Button variant="outline" size="sm" onClick={() => toast.info("Téléversement du favicon — Fonctionnalité à venir")}>Choisir un fichier</Button>
</div>
<Button className="bg-accent text-accent-foreground hover:bg-accent/90" onClick={() => toast.success("Apparence enregistrée")}>
<Save className="w-4 h-4 mr-2" /> Enregistrer
</Button>
</CardContent>
</Card>
</TabsContent>
<TabsContent value="notifications">
<Card className="shadow-sm">
<CardHeader><CardTitle className="text-base font-[Outfit]">Préférences de notifications</CardTitle></CardHeader>
<CardContent className="space-y-4">
{[
{ label: "Alertes système", desc: "Pannes, erreurs et indisponibilités" },
{ label: "Nouvelles publications", desc: "Lorsqu'une page est publiée ou modifiée" },
{ label: "Nouveaux utilisateurs", desc: "Lors de la création d'un compte" },
{ label: "Rapports hebdomadaires", desc: "Résumé des statistiques chaque lundi" },
].map((item) => (
<div key={item.label} className="flex items-center justify-between p-4 rounded-lg bg-muted/40">
<div><p className="text-sm font-medium">{item.label}</p><p className="text-xs text-muted-foreground">{item.desc}</p></div>
<Switch defaultChecked onCheckedChange={(v) => toast.info(`Notification "${item.label}" ${v ? "activée" : "désactivée"}`)} />
</div>
))}
</CardContent>
</Card>
</TabsContent>
<TabsContent value="securite">
<Card className="shadow-sm">
<CardHeader><CardTitle className="text-base font-[Outfit]">Sécurité et accès</CardTitle></CardHeader>
<CardContent className="space-y-5">
<div className="space-y-2"><label className="text-sm font-medium">Durée de session (minutes)</label><Input type="number" defaultValue="60" /></div>
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/40">
<div><p className="text-sm font-medium">Double authentification (2FA)</p><p className="text-xs text-muted-foreground">Obligatoire pour les administrateurs</p></div>
<Switch defaultChecked onCheckedChange={(v) => toast.info(`2FA ${v ? "activée" : "désactivée"}`)} />
</div>
<div className="flex items-center justify-between p-4 rounded-lg bg-muted/40">
<div><p className="text-sm font-medium">Journalisation des connexions</p><p className="text-xs text-muted-foreground">Enregistrer toutes les tentatives de connexion</p></div>
<Switch defaultChecked onCheckedChange={(v) => toast.info(`Journalisation ${v ? "activée" : "désactivée"}`)} />
</div>
<Button className="bg-accent text-accent-foreground hover:bg-accent/90" onClick={() => toast.success("Paramètres de sécurité enregistrés")}>
<Save className="w-4 h-4 mr-2" /> Enregistrer
</Button>
</CardContent>
</Card>
</TabsContent>
</Tabs>
</div>
);
}

109
client/src/pages/Roles.tsx Normal file
View file

@ -0,0 +1,109 @@
/**
* Rôles et droits Gestion des permissions par profil
*/
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Shield, Users, Edit, Eye } from "lucide-react";
import { toast } from "sonner";
const ROLES = [
{
nom: "Administrateur",
couleur: "bg-purple-100 text-purple-700",
icone: Shield,
description: "Accès complet à toutes les fonctionnalités de l'administration",
utilisateurs: 1,
permissions: {
"Gérer les pages": true,
"Publier du contenu": true,
"Gérer les médias": true,
"Gérer les utilisateurs": true,
"Voir les statistiques": true,
"Configurer les paramètres": true,
"Gérer les widgets": true,
"Gérer les applications": true,
},
},
{
nom: "Éditeur",
couleur: "bg-blue-100 text-blue-700",
icone: Edit,
description: "Peut créer, modifier et publier des pages et des médias",
utilisateurs: 3,
permissions: {
"Gérer les pages": true,
"Publier du contenu": true,
"Gérer les médias": true,
"Gérer les utilisateurs": false,
"Voir les statistiques": true,
"Configurer les paramètres": false,
"Gérer les widgets": false,
"Gérer les applications": false,
},
},
{
nom: "Lecteur",
couleur: "bg-gray-100 text-gray-600",
icone: Eye,
description: "Accès en lecture seule à l'ensemble du contenu de l'intranet",
utilisateurs: 3,
permissions: {
"Gérer les pages": false,
"Publier du contenu": false,
"Gérer les médias": false,
"Gérer les utilisateurs": false,
"Voir les statistiques": false,
"Configurer les paramètres": false,
"Gérer les widgets": false,
"Gérer les applications": false,
},
},
];
export default function Roles() {
return (
<div className="space-y-6 animate-fade-in-up">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Rôles et droits</h1>
<p className="text-sm text-muted-foreground mt-1">Configurez les permissions pour chaque profil d'utilisateur</p>
</div>
<div className="grid gap-6">
{ROLES.map((role) => (
<Card key={role.nom} className="shadow-sm">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-muted flex items-center justify-center">
<role.icone className="w-5 h-5 text-muted-foreground" />
</div>
<div>
<div className="flex items-center gap-2">
<CardTitle className="text-base font-[Outfit]">{role.nom}</CardTitle>
<Badge className={`text-xs ${role.couleur}`}>{role.utilisateurs} utilisateur{role.utilisateurs > 1 ? "s" : ""}</Badge>
</div>
<p className="text-xs text-muted-foreground mt-0.5">{role.description}</p>
</div>
</div>
</div>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 sm:grid-cols-4 gap-3">
{Object.entries(role.permissions).map(([permission, actif]) => (
<div key={permission} className="flex items-center justify-between p-3 rounded-lg bg-muted/40">
<span className="text-xs font-medium">{permission}</span>
<Switch
checked={actif}
onCheckedChange={() => toast.info(`Modification des droits — Fonctionnalité à venir`)}
/>
</div>
))}
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}

View file

@ -0,0 +1,114 @@
/**
* Statistiques Analyse de l'audience et de l'utilisation
*/
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, LineChart, Line, AreaChart, Area } from "recharts";
const donneesJournalieres = [
{ jour: "Lun", visites: 245, utilisateurs: 180 },
{ jour: "Mar", visites: 312, utilisateurs: 230 },
{ jour: "Mer", visites: 289, utilisateurs: 210 },
{ jour: "Jeu", visites: 378, utilisateurs: 290 },
{ jour: "Ven", visites: 420, utilisateurs: 340 },
{ jour: "Sam", visites: 156, utilisateurs: 90 },
{ jour: "Dim", visites: 98, utilisateurs: 55 },
];
const donneesMensuelles = [
{ mois: "Déc", visites: 8200 },
{ mois: "Jan", visites: 9100 },
{ mois: "Fév", visites: 8700 },
{ mois: "Mar", visites: 10200 },
{ mois: "Avr", visites: 11500 },
{ mois: "Mai", visites: 12800 },
];
const pagesPopulaires = [
{ page: "Accueil", vues: 12540 },
{ page: "Annuaire", vues: 8900 },
{ page: "Protocoles", vues: 3420 },
{ page: "Documentation", vues: 4200 },
{ page: "Procédures qualité", vues: 1560 },
];
export default function Statistiques() {
return (
<div className="space-y-6 animate-fade-in-up">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Statistiques</h1>
<p className="text-sm text-muted-foreground mt-1">Analyse de l'audience et de l'utilisation de l'intranet</p>
</div>
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4">
{[
{ label: "Visites ce mois", valeur: "12 800", variation: "+11 %" },
{ label: "Utilisateurs uniques", valeur: "347", variation: "+12 %" },
{ label: "Pages vues", valeur: "48 200", variation: "+8 %" },
{ label: "Durée moyenne", valeur: "4 min 32", variation: "-15 %" },
].map((kpi) => (
<Card key={kpi.label} className="shadow-sm">
<CardContent className="p-4">
<p className="text-xs text-muted-foreground uppercase tracking-wide">{kpi.label}</p>
<p className="text-xl font-[Outfit] font-bold mt-1">{kpi.valeur}</p>
<p className="text-xs text-emerald-600 mt-1">{kpi.variation} vs mois précédent</p>
</CardContent>
</Card>
))}
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-[Outfit] font-semibold">Visites et utilisateurs cette semaine</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={240}>
<BarChart data={donneesJournalieres}>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.92 0.005 240)" />
<XAxis dataKey="jour" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip formatter={(v, n) => [v, n === "visites" ? "Visites" : "Utilisateurs"]} contentStyle={{ borderRadius: "8px", fontSize: "12px" }} />
<Bar dataKey="visites" fill="oklch(0.65 0.18 195)" radius={[3, 3, 0, 0]} name="visites" />
<Bar dataKey="utilisateurs" fill="oklch(0.45 0.15 240)" radius={[3, 3, 0, 0]} name="utilisateurs" />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-[Outfit] font-semibold">Évolution mensuelle des visites</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={240}>
<AreaChart data={donneesMensuelles}>
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.92 0.005 240)" />
<XAxis dataKey="mois" tick={{ fontSize: 12 }} />
<YAxis tick={{ fontSize: 12 }} />
<Tooltip formatter={(v) => [v, "Visites"]} contentStyle={{ borderRadius: "8px", fontSize: "12px" }} />
<Area type="monotone" dataKey="visites" stroke="oklch(0.45 0.15 240)" fill="oklch(0.45 0.15 240 / 0.1)" strokeWidth={2} />
</AreaChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
<Card className="shadow-sm">
<CardHeader className="pb-2">
<CardTitle className="text-base font-[Outfit] font-semibold">Pages les plus consultées</CardTitle>
</CardHeader>
<CardContent>
<ResponsiveContainer width="100%" height={200}>
<BarChart data={pagesPopulaires} layout="vertical">
<CartesianGrid strokeDasharray="3 3" stroke="oklch(0.92 0.005 240)" />
<XAxis type="number" tick={{ fontSize: 12 }} />
<YAxis dataKey="page" type="category" tick={{ fontSize: 12 }} width={130} />
<Tooltip formatter={(v) => [v, "Vues"]} contentStyle={{ borderRadius: "8px", fontSize: "12px" }} />
<Bar dataKey="vues" fill="oklch(0.65 0.18 195)" radius={[0, 4, 4, 0]} />
</BarChart>
</ResponsiveContainer>
</CardContent>
</Card>
</div>
);
}

View file

@ -0,0 +1,186 @@
/**
* Utilisateurs Gestion des comptes et des accès
*/
import { useState } from "react";
import { Card, CardContent } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { Avatar, AvatarFallback } from "@/components/ui/avatar";
import {
Table, TableBody, TableCell, TableHead, TableHeader, TableRow,
} from "@/components/ui/table";
import {
DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import {
Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter,
} from "@/components/ui/dialog";
import {
Select, SelectContent, SelectItem, SelectTrigger, SelectValue,
} from "@/components/ui/select";
import { Plus, Search, MoreHorizontal, Edit, Trash2, Shield, UserCheck, UserX } from "lucide-react";
import { toast } from "sonner";
const UTILISATEURS = [
{ id: 1, nom: "Pierre Boursiquot", email: "p.boursiquot@chk.gf", role: "Administrateur", service: "DSI", statut: "actif", connexion: "Il y a 5 min" },
{ id: 2, nom: "Dr. Sophie Martin", email: "s.martin@chk.gf", role: "Éditeur", service: "Urgences", statut: "actif", connexion: "Il y a 1h" },
{ id: 3, nom: "Marie Leclerc", email: "m.leclerc@chk.gf", role: "Éditeur", service: "RH", statut: "actif", connexion: "Hier" },
{ id: 4, nom: "Jean-Paul Dumont", email: "jp.dumont@chk.gf", role: "Lecteur", service: "Pharmacie", statut: "actif", connexion: "Il y a 3 jours" },
{ id: 5, nom: "Isabelle Noel", email: "i.noel@chk.gf", role: "Éditeur", service: "Qualité", statut: "inactif", connexion: "Il y a 2 semaines" },
{ id: 6, nom: "Thomas Bernard", email: "t.bernard@chk.gf", role: "Lecteur", service: "Chirurgie", statut: "actif", connexion: "Il y a 2h" },
{ id: 7, nom: "Lucie Fontaine", email: "l.fontaine@chk.gf", role: "Lecteur", service: "Maternité", statut: "actif", connexion: "Aujourd'hui" },
];
const ROLE_COLORS: Record<string, string> = {
"Administrateur": "bg-purple-100 text-purple-700",
"Éditeur": "bg-blue-100 text-blue-700",
"Lecteur": "bg-gray-100 text-gray-600",
};
function initiales(nom: string) {
return nom.split(" ").map((n) => n[0]).join("").toUpperCase().slice(0, 2);
}
export default function Utilisateurs() {
const [recherche, setRecherche] = useState("");
const [dialogCreation, setDialogCreation] = useState(false);
const utilisateursFiltres = UTILISATEURS.filter((u) =>
u.nom.toLowerCase().includes(recherche.toLowerCase()) ||
u.email.toLowerCase().includes(recherche.toLowerCase())
);
return (
<div className="space-y-6 animate-fade-in-up">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Utilisateurs</h1>
<p className="text-sm text-muted-foreground mt-1">Gestion des comptes et des droits d'accès</p>
</div>
<Button className="bg-accent text-accent-foreground hover:bg-accent/90 shadow-sm" onClick={() => setDialogCreation(true)}>
<Plus className="w-4 h-4 mr-2" /> Nouvel utilisateur
</Button>
</div>
{/* Résumé */}
<div className="grid grid-cols-3 gap-4">
{[
{ label: "Utilisateurs actifs", valeur: "6" },
{ label: "Administrateurs", valeur: "1" },
{ label: "Comptes inactifs", valeur: "1" },
].map((stat) => (
<Card key={stat.label} className="shadow-sm">
<CardContent className="p-4 text-center">
<p className="text-2xl font-[Outfit] font-bold">{stat.valeur}</p>
<p className="text-xs text-muted-foreground mt-1">{stat.label}</p>
</CardContent>
</Card>
))}
</div>
{/* Barre de recherche */}
<Card className="shadow-sm">
<CardContent className="p-4">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input placeholder="Rechercher un utilisateur..." className="pl-9" value={recherche} onChange={(e) => setRecherche(e.target.value)} />
</div>
</CardContent>
</Card>
{/* Tableau */}
<Card className="shadow-sm">
<CardContent className="p-0">
<Table>
<TableHeader>
<TableRow>
<TableHead>Utilisateur</TableHead>
<TableHead>Rôle</TableHead>
<TableHead>Service</TableHead>
<TableHead>Statut</TableHead>
<TableHead>Dernière connexion</TableHead>
<TableHead className="w-10"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{utilisateursFiltres.map((u) => (
<TableRow key={u.id} className="group hover:bg-muted/30">
<TableCell>
<div className="flex items-center gap-3">
<Avatar className="w-8 h-8">
<AvatarFallback className="text-xs bg-primary/10 text-primary">{initiales(u.nom)}</AvatarFallback>
</Avatar>
<div>
<p className="text-sm font-medium">{u.nom}</p>
<p className="text-xs text-muted-foreground">{u.email}</p>
</div>
</div>
</TableCell>
<TableCell>
<Badge className={`text-xs font-normal ${ROLE_COLORS[u.role] ?? ""}`}>{u.role}</Badge>
</TableCell>
<TableCell className="text-sm text-muted-foreground">{u.service}</TableCell>
<TableCell>
{u.statut === "actif" ? (
<Badge className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100 text-xs"><UserCheck className="w-3 h-3 mr-1" />Actif</Badge>
) : (
<Badge variant="outline" className="text-xs text-muted-foreground"><UserX className="w-3 h-3 mr-1" />Inactif</Badge>
)}
</TableCell>
<TableCell className="text-sm text-muted-foreground">{u.connexion}</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 opacity-0 group-hover:opacity-100 transition-opacity">
<MoreHorizontal className="w-4 h-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={() => toast.info("Modification — Fonctionnalité à venir")}><Edit className="w-4 h-4 mr-2" />Modifier</DropdownMenuItem>
<DropdownMenuItem onClick={() => toast.info("Gestion des droits — Fonctionnalité à venir")}><Shield className="w-4 h-4 mr-2" />Gérer les droits</DropdownMenuItem>
<DropdownMenuItem className="text-destructive" onClick={() => toast.info("Suppression — Fonctionnalité à venir")}><Trash2 className="w-4 h-4 mr-2" />Supprimer</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
{/* Dialogue de création */}
<Dialog open={dialogCreation} onOpenChange={setDialogCreation}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="font-[Outfit]">Créer un utilisateur</DialogTitle>
</DialogHeader>
<div className="space-y-4 py-4">
<div className="grid grid-cols-2 gap-3">
<div className="space-y-2"><label className="text-sm font-medium">Prénom</label><Input placeholder="Sophie" /></div>
<div className="space-y-2"><label className="text-sm font-medium">Nom</label><Input placeholder="Martin" /></div>
</div>
<div className="space-y-2"><label className="text-sm font-medium">Adresse e-mail</label><Input type="email" placeholder="s.martin@chk.gf" /></div>
<div className="space-y-2">
<label className="text-sm font-medium">Rôle</label>
<Select>
<SelectTrigger><SelectValue placeholder="Sélectionner un rôle" /></SelectTrigger>
<SelectContent>
<SelectItem value="admin">Administrateur</SelectItem>
<SelectItem value="editeur">Éditeur</SelectItem>
<SelectItem value="lecteur">Lecteur</SelectItem>
</SelectContent>
</Select>
</div>
<div className="space-y-2"><label className="text-sm font-medium">Service</label><Input placeholder="Ex : Urgences" /></div>
</div>
<DialogFooter>
<Button variant="outline" onClick={() => setDialogCreation(false)}>Annuler</Button>
<Button className="bg-accent text-accent-foreground hover:bg-accent/90" onClick={() => { setDialogCreation(false); toast.success("Utilisateur créé avec succès !"); }}>Créer</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}

View file

@ -0,0 +1,77 @@
/**
* Widgets Gestion des blocs de contenu modulaires
*/
import { useState } from "react";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Badge } from "@/components/ui/badge";
import { Switch } from "@/components/ui/switch";
import { Plus, GripVertical, Settings, Trash2, Clock, Link, Image, FileText, Bell, Calendar, Users } from "lucide-react";
import { toast } from "sonner";
const WIDGETS = [
{ id: 1, nom: "Liens rapides", description: "Raccourcis vers les applications métier", icone: Link, actif: true, zone: "Accueil" },
{ id: 2, nom: "Actualités du CHK", description: "Dernières nouvelles et annonces internes", icone: Bell, actif: true, zone: "Accueil" },
{ id: 3, nom: "Agenda partagé", description: "Calendrier des événements institutionnels", icone: Calendar, actif: true, zone: "Accueil" },
{ id: 4, nom: "Annuaire rapide", description: "Recherche rapide dans l'annuaire du personnel", icone: Users, actif: false, zone: "Barre latérale" },
{ id: 5, nom: "Bannière image", description: "Image promotionnelle ou message important", icone: Image, actif: true, zone: "Accueil" },
{ id: 6, nom: "Documents récents", description: "Derniers documents publiés sur l'intranet", icone: FileText, actif: false, zone: "Barre latérale" },
{ id: 7, nom: "Horloge & Météo", description: "Heure locale et météo de Kourou", icone: Clock, actif: true, zone: "En-tête" },
];
export default function Widgets() {
const [widgets, setWidgets] = useState(WIDGETS);
const toggleWidget = (id: number) => {
setWidgets((prev) => prev.map((w) => w.id === id ? { ...w, actif: !w.actif } : w));
const widget = widgets.find((w) => w.id === id);
toast.success(`Widget "${widget?.nom}" ${widget?.actif ? "désactivé" : "activé"}`);
};
return (
<div className="space-y-6 animate-fade-in-up">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-[Outfit] font-bold text-foreground">Widgets</h1>
<p className="text-sm text-muted-foreground mt-1">Gérez les blocs de contenu modulaires de l'intranet</p>
</div>
<Button className="bg-accent text-accent-foreground hover:bg-accent/90 shadow-sm" onClick={() => toast.info("Création de widget — Fonctionnalité à venir")}>
<Plus className="w-4 h-4 mr-2" /> Nouveau widget
</Button>
</div>
<div className="grid gap-3">
{widgets.map((widget) => (
<Card key={widget.id} className="shadow-sm hover:shadow-md transition-shadow duration-200">
<CardContent className="p-4">
<div className="flex items-center gap-4">
<button className="text-muted-foreground/40 hover:text-muted-foreground cursor-grab active:cursor-grabbing" title="Déplacer">
<GripVertical className="w-5 h-5" />
</button>
<div className="w-10 h-10 rounded-lg bg-accent/10 flex items-center justify-center shrink-0">
<widget.icone className="w-5 h-5 text-accent" />
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2">
<p className="text-sm font-semibold">{widget.nom}</p>
<Badge variant="secondary" className="text-[10px]">{widget.zone}</Badge>
</div>
<p className="text-xs text-muted-foreground mt-0.5">{widget.description}</p>
</div>
<div className="flex items-center gap-3">
<Switch checked={widget.actif} onCheckedChange={() => toggleWidget(widget.id)} />
<Button variant="ghost" size="sm" className="h-8 w-8 p-0" onClick={() => toast.info("Configuration — Fonctionnalité à venir")}>
<Settings className="w-4 h-4" />
</Button>
<Button variant="ghost" size="sm" className="h-8 w-8 p-0 text-destructive" onClick={() => toast.info("Suppression — Fonctionnalité à venir")}>
<Trash2 className="w-4 h-4" />
</Button>
</div>
</div>
</CardContent>
</Card>
))}
</div>
</div>
);
}

108
ideas.md Normal file
View file

@ -0,0 +1,108 @@
# Brainstorm — Interface d'Administration Intranet CHK
## Contexte
Interface d'administration pour l'intranet du Centre Hospitalier de Kourou (CHU Guyane site Kourou). Thème hospitalier clair, sobre et dynamique. Doit permettre la gestion de pages, médias, utilisateurs, widgets, avec un mode simulation utilisateur.
---
<response>
<text>
## Idée 1 : "Clinical Precision" — Design Néo-Brutaliste Médical
**Design Movement :** Néo-Brutalisme appliqué au domaine médical — lignes franches, contrastes nets, typographie affirmée, avec une touche de rigueur clinique.
**Core Principles :**
1. Contraste radical entre zones d'action et zones de repos visuel
2. Typographie hiérarchique forte (titres massifs, corps lisible)
3. Couleurs franches et limitées (blanc pur, noir profond, cyan médical en accent)
4. Grilles structurées mais asymétriques
**Color Philosophy :** Blanc chirurgical (#FAFBFC) comme base, noir charbon (#1A1A2E) pour les textes, cyan médical (#0891B2) comme couleur d'action évoquant la stérilité et la technologie médicale. Accents corail (#F97316) pour les alertes.
**Layout Paradigm :** Sidebar compacte à gauche (icônes + labels), contenu principal en grille asymétrique avec des cartes à bordures franches (2px solid), pas de border-radius excessif.
**Signature Elements :** Bordures épaisses sur les cartes actives, indicateurs de statut en pastilles colorées vives, micro-animations de "pulse" sur les notifications.
**Interaction Philosophy :** Feedback immédiat et franc — les éléments cliqués se décalent légèrement (translateY), les hover révèlent des bordures colorées.
**Animation :** Transitions courtes (120-180ms), scale(0.98) au clic, apparition des panneaux par slide-in latéral rapide.
**Typography System :** Space Grotesk (titres, bold 700) + Inter (corps, regular 400/medium 500). Hiérarchie marquée par la taille et le poids.
</text>
<probability>0.07</probability>
</response>
---
<response>
<text>
## Idée 2 : "Airy Healthcare" — Design Scandinave Médical
**Design Movement :** Minimalisme scandinave adapté au secteur santé — espaces généreux, lumière naturelle simulée, formes organiques douces, sensation de calme et de confiance.
**Core Principles :**
1. Espaces blancs abondants créant une sensation d'air et de sérénité
2. Couleurs désaturées et naturelles inspirées de l'environnement guyanais
3. Formes légèrement arrondies évoquant le soin et l'humain
4. Hiérarchie visuelle par l'espacement plutôt que par la couleur
**Color Philosophy :** Fond ivoire chaud (#F8F9FA), bleu-gris apaisant (#64748B) pour la navigation, teal médical (#0D9488) comme accent principal évoquant la nature guyanaise et la santé. Touches de doré doux (#D4A574) pour les éléments premium/admin.
**Layout Paradigm :** Sidebar étendue avec navigation verticale espacée, contenu en colonnes fluides avec beaucoup de padding. Les sections respirent avec des gaps généreux (24-32px).
**Signature Elements :** Ombres très douces (0 1px 3px rgba), icônes en trait fin (stroke-width: 1.5), séparateurs subtils en dégradé transparent.
**Interaction Philosophy :** Interactions douces et progressives — les éléments s'illuminent au hover (légère augmentation de luminosité), les transitions sont fluides et apaisantes.
**Animation :** Durées moyennes (200-300ms), easing doux (cubic-bezier(0.4, 0, 0.2, 1)), fade-in progressif des contenus au scroll, micro-animations de respiration sur les indicateurs de statut.
**Typography System :** Plus Jakarta Sans (titres, semibold 600) + DM Sans (corps, regular 400/medium 500). Tailles généreuses avec un line-height aéré (1.6-1.7).
</text>
<probability>0.05</probability>
</response>
---
<response>
<text>
## Idée 3 : "Digital Command Center" — Design Dashboard Hospitalier
**Design Movement :** Interface de commande inspirée des tableaux de bord de contrôle aérospatial (clin d'œil à Kourou/CSG) — données en temps réel, organisation modulaire, efficacité maximale.
**Core Principles :**
1. Densité d'information maîtrisée avec une lisibilité parfaite
2. Modules indépendants et repositionnables (widget-based)
3. Indicateurs visuels instantanés (badges, jauges, compteurs)
4. Navigation contextuelle qui s'adapte à la section active
**Color Philosophy :** Fond blanc cassé (#F1F5F9) avec sidebar en bleu marine profond (#0F172A) créant un ancrage visuel fort. Accent cyan vif (#06B6D4) pour les actions et les données en temps réel. Vert émeraude (#10B981) pour les statuts positifs, ambre (#F59E0B) pour les alertes.
**Layout Paradigm :** Sidebar sombre fixe à gauche (navigation principale), header léger avec breadcrumb et actions rapides, zone de contenu en grille modulaire (CSS Grid) avec des cartes-widgets redimensionnables.
**Signature Elements :** Mini-graphiques sparkline dans les cartes de statistiques, badges numériques animés sur la navigation, barre de progression subtile en haut de page lors des chargements.
**Interaction Philosophy :** Efficacité et feedback — hover avec élévation (shadow increase), clic avec ripple effect subtil, drag & drop pour réorganiser les widgets du dashboard.
**Animation :** Rapides et fonctionnelles (150-250ms), counter animations pour les chiffres, stagger reveal (30ms) pour les listes, slide-down pour les menus déroulants.
**Typography System :** Outfit (titres et navigation, medium 500/semibold 600) + Inter (données et corps, regular 400/medium 500). Tailles compactes mais lisibles, utilisation de tabular-nums pour les chiffres.
</text>
<probability>0.08</probability>
</response>
---
## Choix retenu : Idée 3 — "Digital Command Center"
Cette approche est la plus adaptée pour une interface d'administration hospitalière car elle combine :
- L'efficacité d'un tableau de bord de contrôle (essentiel pour les administrateurs)
- Un ancrage visuel fort avec la sidebar sombre (navigation claire)
- La modularité des widgets (gestion flexible du contenu)
- Un clin d'œil à l'identité spatiale de Kourou
- Une palette professionnelle et sobre qui reflète le milieu hospitalier