diff --git a/.gitkeep b/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/client/index.html b/client/index.html index 2574d20..4e41351 100644 --- a/client/index.html +++ b/client/index.html @@ -6,12 +6,10 @@ - {{project_title}} - + diff --git a/client/src/App.tsx b/client/src/App.tsx index 0828668..6b23984 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -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 ( - - - - {/* Final fallback route */} - - + + + + + + + + + + + + + + + ); } -// 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 ( - + - + diff --git a/client/src/components/AdminLayout.tsx b/client/src/components/AdminLayout.tsx new file mode 100644 index 0000000..0d6bbab --- /dev/null +++ b/client/src/components/AdminLayout.tsx @@ -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 ( +
+ {/* Fond semi-transparent sur mobile */} + {mobileMenuOpen && ( +
setMobileMenuOpen(false)} + /> + )} + + {/* Barre latérale */} + + + {/* Zone de contenu principale */} +
+ {/* En-tête supérieur */} +
+
+ + {/* Recherche globale */} +
+ + +
+
+ +
+ {/* Bouton mode simulation */} + + + {/* Cloche de notifications */} + + + {/* Menu utilisateur */} + + + + + + toast.info("Profil — Fonctionnalité à venir")}> + + Mon profil + + + toast.info("Déconnexion — Fonctionnalité à venir")} + > + + Déconnexion + + + +
+
+ + {/* Contenu de la page */} +
+ {children} +
+
+
+ ); +} diff --git a/client/src/index.css b/client/src/index.css index 72b423d..4acfc6e 100644 --- a/client/src/index.css +++ b/client/src/index.css @@ -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:
...
- * - * For custom widths, use max-w-* utilities directly: - *
...
- */ .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; } } -} \ No newline at end of file +} + +/* 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; } diff --git a/client/src/pages/Applications.tsx b/client/src/pages/Applications.tsx new file mode 100644 index 0000000..3a95d83 --- /dev/null +++ b/client/src/pages/Applications.tsx @@ -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 ( +
+
+
+

Applications

+

Gérez les raccourcis vers les outils métier de l'intranet

+
+ +
+ +
+ {apps.map((app) => ( + + +
+
+ {app.initiale} +
+ toggleApp(app.id)} /> +
+

{app.nom}

+ {app.categorie} +
+ + + +
+
+
+ ))} +
+
+ ); +} diff --git a/client/src/pages/Dashboard.tsx b/client/src/pages/Dashboard.tsx new file mode 100644 index 0000000..56d5b6b --- /dev/null +++ b/client/src/pages/Dashboard.tsx @@ -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 ( +
+ {/* En-tête de page */} +
+
+

+ Tableau de bord +

+

+ Vue d'ensemble de l'activité de l'intranet CHK +

+
+
+ + + En ligne + + + Dernière mise à jour : il y a 2 min + +
+
+ + {/* Indicateurs clés */} +
+ + + + +
+ + {/* Rangée de graphiques */} +
+ {/* Histogramme — Visites */} + + + + Visites cette semaine + + + + + + + + + [`${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", + }} + /> + + + + + + + {/* Camembert — Applications les plus utilisées */} + + + + Applications les plus utilisées + + + + + + + {appUsageData.map((entry, index) => ( + + ))} + + [`${value} %`, "Utilisation"]} + contentStyle={{ + borderRadius: "8px", + border: "1px solid oklch(0.92 0.005 240)", + fontSize: "12px", + }} + /> + + +
+ {appUsageData.map((item) => ( +
+
+
+ {item.name} +
+ {item.value} % +
+ ))} +
+ + +
+ + {/* Rangée inférieure */} +
+ {/* Activité récente */} + + + + Activité récente + + + + + {recentActivity.map((item, i) => ( +
+
+ {item.status === "warning" ? ( + + ) : ( + + )} +
+
+

+ {item.action} —{" "} + {item.target} +

+

+ {item.user} · {item.time} +

+
+
+ ))} +
+
+ + {/* Tendance utilisateurs */} + + + + Tendance des connexions (6 semaines) + + + + + + + + + [`${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", + }} + /> + + + + + +
+
+ ); +} + +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 ( + + +
+
+

+ {title} +

+

+ {value} +

+
+ {trend === "up" ? ( + + ) : ( + + )} + + {change} + + {description} +
+
+
+ +
+
+
+
+ ); +} diff --git a/client/src/pages/Medias.tsx b/client/src/pages/Medias.tsx new file mode 100644 index 0000000..c4a51a8 --- /dev/null +++ b/client/src/pages/Medias.tsx @@ -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 = { + 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 ; + case "document": return ; + case "video": return ; + default: return ; + } + }; + + return ( +
+ {/* En-tête */} +
+
+

Médias

+

+ Bibliothèque de fichiers et images de l'intranet +

+
+ +
+ + {/* Barre d'outils */} + + +
+
+ + setRecherche(e.target.value)} + /> +
+
+ +
+ + +
+
+
+
+
+ + {/* Résumé statistique */} +
+ + +

9

+

Fichiers au total

+
+
+ + +

59,4 Mo

+

Espace utilisé

+
+
+ + +

5

+

Images

+
+
+
+ + {/* Grille ou liste de fichiers */} + {modeAffichage === "grille" ? ( +
+ {fichiersFiltres.map((fichier) => ( + +
+ {fichier.miniature ? ( + {fichier.nom} + ) : ( +
+ {getIconeFichier(fichier.type)} + + {fichier.nom.split(".").pop()} + +
+ )} + {/* Actions au survol */} +
+ + + +
+
+ +

{fichier.nom}

+
+ {fichier.taille} + {fichier.date} +
+
+
+ ))} +
+ ) : ( + + +
+ {fichiersFiltres.map((fichier) => ( +
+
+ {getIconeFichier(fichier.type)} +
+
+

{fichier.nom}

+

+ {fichier.taille} · {fichier.date} +

+
+ + {TYPE_LABELS[fichier.type] ?? fichier.type} + +
+ + +
+
+ ))} +
+
+
+ )} +
+ ); +} diff --git a/client/src/pages/Notifications.tsx b/client/src/pages/Notifications.tsx new file mode 100644 index 0000000..d902ac6 --- /dev/null +++ b/client/src/pages/Notifications.tsx @@ -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 = { + alerte: , + info: , + succes: , +}; + +const COULEURS: Record = { + 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 ( +
+
+
+

+ Notifications + {nonLues > 0 && {nonLues}} +

+

Alertes et messages du système

+
+ {nonLues > 0 && ( + + )} +
+ +
+ {notifs.map((notif) => ( + + +
+
+ {ICONES[notif.type]} +
+
+
+

{notif.titre}

+ {!notif.lue && Nouveau} +
+

{notif.message}

+

{notif.temps}

+
+ +
+
+
+ ))} +
+
+ ); +} diff --git a/client/src/pages/Pages.tsx b/client/src/pages/Pages.tsx new file mode 100644 index 0000000..76c2ee7 --- /dev/null +++ b/client/src/pages/Pages.tsx @@ -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 ( +
+ {/* En-tête */} +
+
+

Pages

+

+ Gérez les pages de contenu de l'intranet +

+
+ +
+ + {/* Filtres */} + + +
+
+ + setRecherche(e.target.value)} + /> +
+ +
+
+
+ + {/* Tableau des pages */} + + + + + + Page + Catégorie + Statut + Auteur + Vues + Modifié + + + + + {pagesFiltrees.map((page) => ( + + +
+
+ +
+
+

{page.titre}

+

{page.slug}

+
+
+
+ + + {page.categorie} + + + + {page.statut === "publie" ? ( + + + Publié + + ) : ( + + + Brouillon + + )} + + {page.auteur} + + {page.vues.toLocaleString("fr-FR")} + + {page.modifie} + + + + + + + toast.info("Éditeur de page — Fonctionnalité à venir")}> + Modifier + + toast.info("Prévisualisation — Fonctionnalité à venir")}> + Prévisualiser + + toast.success("Page dupliquée avec succès")}> + Dupliquer + + toast.info("Suppression — Fonctionnalité à venir")} + > + Supprimer + + + + +
+ ))} +
+
+
+
+ + {/* Dialogue de création */} + + + + Créer une nouvelle page + +
+
+ + +
+
+ + +
+
+ +