refact & cleanup : Sidebar, Button, card, EmptyState, Loader, ServiceObject, StatusBadge, UptameObject, BackupWidget, SecurityWidget, ServicesWidget, SupportCtaWidget, UptimeWidget, DashboardLayout

This commit is contained in:
LathanDevers
2026-07-29 22:50:04 +02:00
parent 438d739b1a
commit ec900efe3a
42 changed files with 3357 additions and 3966 deletions
View File
-3339
View File
File diff suppressed because it is too large Load Diff
+3
View File
@@ -12,12 +12,15 @@
"dependencies": {
"@tailwindcss/vite": "^4.3.3",
"@zitadel/react-auth": "^1.2.1",
"clsx": "^2.1.1",
"oidc-client-ts": "^3.5.0",
"otpauth": "^9.5.1",
"pocketbase": "^0.27.0",
"qrcode.react": "^4.2.0",
"react": "^19.2.7",
"react-dom": "^19.2.7",
"react-router-dom": "^7.18.1",
"tailwind-merge": "^3.6.0",
"tailwindcss": "^4.3.3"
},
"devDependencies": {
+2189
View File
File diff suppressed because it is too large Load Diff
+52 -30
View File
@@ -1,46 +1,68 @@
import { useState } from 'react';
import { pb } from './config/pocketbase';
import Login from './pages/Auth/Login';
import DashboardLayout from './layouts/DashboardLayout';
import TrustCenter from './pages/TrustCenter/TrustCenter';
import ServiceDesk from './pages/Support/ServiceDesk';
import DocumentVault from './pages/Vault/DocumentVault';
import AccountSettings from './pages/Account/AccountSettings';
import { Routes, Route, Navigate, useNavigate } from 'react-router-dom';
import { pb } from '@/services/pocketbase';
// Layout & Pages
import Login from '@/pages/Login';
import DashboardLayout from '@/layouts/DashboardLayout';
import TrustCenter from '@/pages/TrustCenter';
import ServiceDesk from '@/pages/ServiceDesk';
import DocumentVault from '@/pages/DocumentVault';
import AccountSettings from '@/pages/AccountSettings';
import { InfrastructureEvolution } from '@/pages/InfrastructureEvolution';
import Analytics from '@/pages/Analytics';
import Backups from '@/pages/Backups';
function App() {
// On initialise l'état avec la présence du token, mais on ne met plus
// d'écouteur automatique. C'est le composant Login qui gérera le flux.
const [isAuthenticated, setIsAuthenticated] = useState(pb.authStore.isValid);
const [activeView, setActiveView] = useState<'dashboard' | 'support' | 'vault' | 'account'>('dashboard');
const navigate = useNavigate();
const handleLogout = () => {
pb.authStore.clear(); // Détruit le jeton de sécurité
pb.authStore.clear();
setIsAuthenticated(false);
navigate('/', { replace: true });
};
// Tant que l'utilisateur n'est pas totalement authentifié (MFA inclus),
// on le maintient dans le sas de sécurité.
// --- SAS DE SÉCURITÉ (Auth Guard) ---
// Si non authentifié, on force l'affichage du composant Login peu importe l'URL demandée.
if (!isAuthenticated) {
return <Login onLoginSuccess={() => setIsAuthenticated(true)} />;
return (
<Routes>
<Route
path="*"
element={
<Login onLoginSuccess={() => {
setIsAuthenticated(true);
navigate('/dashboard', { replace: true });
}} />
}
/>
</Routes>
);
}
const renderContent = () => {
switch (activeView) {
case 'dashboard': return <TrustCenter onNavigate={setActiveView} />;
case 'support': return <ServiceDesk />;
case 'vault': return <DocumentVault />;
case 'account': return <AccountSettings />;
default: return <TrustCenter onNavigate={setActiveView} />;
}
};
// --- APPLICATION SÉCURISÉE ---
// Une fois connecté, le DashboardLayout enveloppe nos véritables routes (URLs)
return (
<DashboardLayout
activeView={activeView}
onNavigate={setActiveView}
onLogout={handleLogout}
>
{renderContent()}
<DashboardLayout onLogout={handleLogout}>
<Routes>
{/* Redirection par défaut */}
<Route path="/" element={<Navigate to="/dashboard" replace />} />
{/* Pages du menu principal */}
<Route path="/dashboard" element={<TrustCenter />} />
<Route path="/support" element={<ServiceDesk />} />
<Route path="/vault" element={<DocumentVault />} />
<Route path="/account" element={<AccountSettings />} />
{/* Pages stratégiques (anciennes modales) */}
<Route path="/evolution-infrastructure" element={<InfrastructureEvolution />} />
<Route path="/analytics" element={<Analytics />} />
<Route path="/backups" element={<Backups />} />
{/* Sécurité : Si l'URL n'existe pas, on redirige vers le dashboard */}
<Route path="*" element={<Navigate to="/dashboard" replace />} />
</Routes>
</DashboardLayout>
);
}
+57
View File
@@ -0,0 +1,57 @@
import React from 'react';
import { NavLink } from 'react-router-dom';
import { cn } from '@/utils/utils';
interface SidebarProps {
onLogout: () => void;
}
const NAVIGATION_ITEMS = [
{ name: 'Trust Center', path: '/dashboard' },
{ name: 'Service Desk', path: '/support' },
{ name: 'Coffre-fort', path: '/vault' },
{ name: 'Paramètres', path: '/account' },
];
export const Sidebar: React.FC<SidebarProps> = ({ onLogout }) => {
return (
<aside className="w-64 bg-slate-900 flex flex-col shrink-0">
{/* En-tête / Logo */}
<div className="h-16 flex items-center px-6 border-b border-slate-800">
<span className="text-xl font-bold text-white tracking-widest">Aegis<span className="text-blue-500">.</span></span>
</div>
{/* Menu de navigation URL-based */}
<nav className="flex-1 px-4 py-6 space-y-2">
{NAVIGATION_ITEMS.map((item) => (
<NavLink
key={item.path}
to={item.path}
className={({ isActive }) =>
cn(
"flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors duration-200",
isActive
? "bg-blue-900/50 text-white border border-blue-800/50"
: "text-slate-400 hover:bg-slate-800 hover:text-slate-200 border border-transparent"
)
}
>
{item.name}
</NavLink>
))}
</nav>
{/* Zone de déconnexion */}
<div className="p-4 border-t border-slate-800">
<button
onClick={onLogout}
className="w-full flex items-center justify-center px-4 py-2 text-sm font-medium text-slate-400 hover:text-red-400 hover:bg-slate-800 rounded-lg transition-all cursor-pointer"
>
Déconnexion
</button>
</div>
</aside>
);
};
export default Sidebar;
+37
View File
@@ -0,0 +1,37 @@
import React from 'react';
import { cn } from '@/utils/utils';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'primary' | 'outline' | 'ghost';
size?: 'sm' | 'md' | 'lg';
isLoading?: boolean;
}
export const Button: React.FC<ButtonProps> = ({
children, variant = 'primary', size = 'md', isLoading, className, disabled, ...props
}) => {
const baseStyles = "inline-flex items-center justify-center font-medium rounded-lg transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2";
const variants = {
primary: "bg-blue-900 text-white hover:bg-blue-800 focus:ring-blue-900 shadow-sm",
outline: "border border-slate-200 text-slate-900 hover:border-blue-900 hover:bg-blue-50 focus:ring-blue-900",
ghost: "text-slate-600 hover:text-slate-900 hover:bg-slate-100 focus:ring-slate-500",
};
const sizes = {
sm: "px-3 py-1.5 text-sm",
md: "px-4 py-2 text-base",
lg: "w-full py-3 text-base",
};
return (
<button
disabled={disabled || isLoading}
className={cn(baseStyles, variants[variant], sizes[size], (disabled || isLoading) && "opacity-50 cursor-not-allowed", className)}
{...props}
>
{isLoading && <span className="mr-2 w-4 h-4 rounded-full border-2 border-current border-b-transparent animate-spin" />}
{children}
</button>
);
};
+31
View File
@@ -0,0 +1,31 @@
import React from 'react';
import { cn } from '@/utils/utils';
export const Card = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("bg-white rounded-xl shadow-sm border border-slate-200 flex flex-col overflow-hidden", className)}
{...props}
/>
)
);
Card.displayName = "Card";
export const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div
ref={ref}
className={cn("px-6 py-5 border-b border-slate-100 flex flex-col space-y-1.5", className)}
{...props}
/>
)
);
CardHeader.displayName = "CardHeader";
export const CardContent = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 flex-1", className)} {...props} />
)
);
CardContent.displayName = "CardContent";
+18
View File
@@ -0,0 +1,18 @@
import React from 'react';
import { cn } from '@utils/utils';
interface EmptyStateProps {
message: string;
className?: string;
}
export const EmptyState: React.FC<EmptyStateProps> = ({ message, className }) => {
return (
<div className={cn(
"flex-1 flex justify-center items-center py-8 text-sm text-slate-500 italic",
className
)}>
{message}
</div>
);
};
+14
View File
@@ -0,0 +1,14 @@
import React from 'react';
import { cn } from '@utils/utils';
interface LoaderProps {
className?: string;
}
export const Loader: React.FC<LoaderProps> = ({ className }) => {
return (
<div className={cn("flex-1 flex justify-center items-center py-8", className)}>
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-slate-900"></div>
</div>
);
};
+23
View File
@@ -0,0 +1,23 @@
import React from 'react';
import type { ManagedContract } from '@/types/ManagedContract';
import { StatusBadge } from '@/components/ui/StatusBadge';
interface ServiceObjectProps {
contract: ManagedContract;
}
export const ServiceObject: React.FC<ServiceObjectProps> = ({ contract }) => {
return (
<div className="p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center">
<div>
<h3 className="text-sm font-medium text-slate-900">{contract.service_name}</h3>
{contract.description && (
<p className="text-xs text-slate-500 mt-1">{contract.description}</p>
)}
</div>
{/* Fonctionne instantanément avec 'Actif', 'En déploiement', 'Suspendu' */}
<StatusBadge status={contract.status} />
</div>
);
};
+66
View File
@@ -0,0 +1,66 @@
// Fichier : src/components/ui/StatusBadge.tsx
import React from 'react';
import { cn } from '@/utils/utils';
import type { AegisStatus } from '@/types/Status';
interface StatusBadgeProps {
status: AegisStatus;
className?: string;
}
// Configuration visuelle unique pour chaque statut
const STATUS_CONFIG: Record<AegisStatus, { bg: string; dot: string }> = {
// Statuts Infrastructure & Contrats Positifs
'Opérationnel': {
bg: 'bg-emerald-50 text-emerald-700 border-emerald-200/60',
dot: 'bg-emerald-500',
},
'Actif': {
bg: 'bg-emerald-50 text-emerald-700 border-emerald-200/60',
dot: 'bg-emerald-500',
},
// Statuts Intermédiaires / En cours
'Dégradé': {
bg: 'bg-amber-50 text-amber-700 border-amber-200/60',
dot: 'bg-amber-500',
},
'En déploiement': {
bg: 'bg-blue-50 text-blue-700 border-blue-200/60',
dot: 'bg-blue-500',
},
// Statuts Alertes / Inactifs
'Hors Ligne': {
bg: 'bg-red-50 text-red-700 border-red-200/60',
dot: 'bg-red-500',
},
'Suspendu': {
bg: 'bg-slate-100 text-slate-700 border-slate-200',
dot: 'bg-slate-400',
},
};
export const StatusBadge: React.FC<StatusBadgeProps> = ({ status, className }) => {
// Récupération de la configuration ou fallback de sécurité
const config = STATUS_CONFIG[status] || {
bg: 'bg-slate-50 text-slate-600 border-slate-200',
dot: 'bg-slate-400',
};
return (
<div
className={cn(
"px-2.5 py-1 rounded-full text-xs font-semibold inline-flex items-center border transition-colors",
config.bg,
className
)}
>
<span className={cn("w-1.5 h-1.5 rounded-full mr-2 shrink-0", config.dot)} />
{status}
</div>
);
};
export default StatusBadge;
+32
View File
@@ -0,0 +1,32 @@
import React from 'react';
import type { InfrastructureNode } from '@/types/InfrastructureNode';
import { StatusBadge } from '@ui/StatusBadge';
interface UptimeObjectProps {
node: InfrastructureNode;
}
export const UptimeObject: React.FC<UptimeObjectProps> = ({ node }) => {
return (
// La "key" a été retirée d'ici, elle sera gérée par le parent
<div className="py-4 flex items-center justify-between first:pt-0 last:pb-0">
{/* Infos Serveur */}
<div>
<p className="font-medium text-slate-900">{node.label}</p>
<p className="text-xs text-slate-500 mt-0.5">{node.type}</p>
</div>
{/* Métriques & Badge */}
<div className="flex items-center space-x-6">
<div className="text-right">
<p className="text-xs text-slate-500">SLA</p>
<p className="text-sm font-semibold text-slate-900">{node.uptime_sla}</p>
</div>
<StatusBadge status={node.status} />
</div>
</div>
);
};
+32 -22
View File
@@ -1,25 +1,35 @@
export default function BackupWidget({ onClick }: { onClick?: () => void }) {
return (
<div
onClick={onClick}
className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 cursor-pointer hover:border-blue-300 hover:shadow-md transition-all group"
>
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900 group-hover:text-blue-900 transition-colors">Sauvegardes</h2>
<span className="bg-slate-100 text-slate-600 text-xs px-2 py-1 rounded font-medium border border-slate-200">
PRA Immuable
</span>
</div>
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100 mb-4 group-hover:bg-blue-50/50 transition-colors">
<p className="text-sm text-slate-500 mb-1">Dernier snapshot système</p>
<p className="text-slate-900 font-medium">Aujourd'hui à 03:00 AM</p>
</div>
import { Card, CardContent } from '@/components/ui/Card';
<div className="flex items-center text-emerald-600 text-sm font-medium">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" /></svg>
Intégrité validée avec succès
</div>
</div>
interface BackupWidgetProps {
onClick?: () => void;
}
export default function BackupWidget({ onClick }: BackupWidgetProps) {
return (
<Card
onClick={onClick}
className="cursor-pointer hover:border-blue-300 hover:shadow-md transition-all group"
>
<CardContent className="p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-slate-900 group-hover:text-blue-900 transition-colors">Sauvegardes</h2>
<span className="bg-slate-100 text-slate-600 text-xs px-2 py-1 rounded font-medium border border-slate-200">
PRA Immuable
</span>
</div>
<div className="bg-slate-50 rounded-lg p-4 border border-slate-100 mb-4 group-hover:bg-blue-50/50 transition-colors">
<p className="text-sm text-slate-500 mb-1">Dernier snapshot système</p>
<p className="text-slate-900 font-medium">Aujourd'hui à 03:00 AM</p>
</div>
<div className="flex items-center text-emerald-600 text-sm font-medium">
<svg className="w-5 h-5 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M5 13l4 4L19 7" />
</svg>
Intégrité validée avec succès
</div>
</CardContent>
</Card>
);
}
+29 -18
View File
@@ -1,23 +1,34 @@
export default function SecurityWidget() {
return (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Posture de Sécurité</h2>
<div className="flex items-baseline space-x-2 mb-4">
<span className="text-3xl font-bold text-slate-900">1,432</span>
<span className="text-sm text-slate-500">menaces bloquées (30j)</span>
</div>
import { Card, CardContent } from '@/components/ui/Card';
<div className="space-y-3">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Bouclier Cloudflare WAF</span>
<span className="text-emerald-500 font-medium">Actif</span>
interface SecurityWidgetProps {
onClick?: () => void;
}
export default function SecurityWidget({ onClick }: SecurityWidgetProps) {
return (
<Card
onClick={onClick}
className="cursor-pointer hover:border-blue-300 hover:shadow-md transition-all group"
>
<CardContent className="p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4 group-hover:text-blue-900 transition-colors">Posture de Sécurité</h2>
<div className="flex items-baseline space-x-2 mb-4">
<span className="text-3xl font-bold text-slate-900">1,432</span>
<span className="text-sm text-slate-500">menaces bloquées (30j)</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Dernier audit de vulnérabilité</span>
<span className="text-slate-900 font-medium">Il y a 12 jours</span>
<div className="space-y-3">
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Bouclier Cloudflare WAF</span>
<span className="text-emerald-500 font-medium">Actif</span>
</div>
<div className="flex justify-between items-center text-sm">
<span className="text-slate-600">Dernier audit de vulnérabilité</span>
<span className="text-slate-900 font-medium">Il y a 12 jours</span>
</div>
</div>
</div>
</div>
</CardContent>
</Card>
);
}
+29 -182
View File
@@ -1,193 +1,40 @@
import { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
import { useNavigate } from 'react-router-dom';
import { useContracts } from '@/hooks/useContracts';
import { ServiceObject } from '@/components/ui/ServiceObject';
import { Card, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
import { Loader } from '@/components/ui/Loader';
import { EmptyState } from '@/components/ui/EmptyState';
export default function ServicesWidget() {
const [contracts, setContracts] = useState<any[]>([]);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
useEffect(() => {
const fetchContracts = async () => {
try {
const records = await pb.collection('aegis_managed_contracts').getFullList({
sort: '-created',
});
setContracts(records);
} catch (error) {
console.error("Erreur lors de la récupération des contrats :", error);
} finally {
setIsLoading(false);
}
};
fetchContracts();
}, []);
// Fonction pour envoyer la demande d'évolution vers PocketBase
const handleEvolutionRequest = async (subject: string, description: string) => {
setIsSubmitting(true);
try {
await pb.collection('aegis_tickets').create({
author: pb.authStore.model?.id,
company: pb.authStore.model?.company,
category: "Évolution",
subject: subject,
description: description,
status: "Ouvert"
});
setSubmitSuccess(true);
// Ferme la modale après 2.5 secondes de message de succès
setTimeout(() => {
setSubmitSuccess(false);
setIsModalOpen(false);
}, 2500);
} catch (error) {
console.error("Erreur lors de la création du ticket d'évolution :", error);
alert("Une erreur est survenue. Veuillez contacter le support par téléphone.");
} finally {
setIsSubmitting(false);
}
};
const { contracts, isLoading, error } = useContracts();
const navigate = useNavigate();
return (
<>
<div className= "bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col" >
<div className="mb-4" >
<h2 className="text-lg font-semibold text-slate-900" > Vos services gérés </h2>
< p className = "text-sm text-slate-500" > Catalogue des contrats d'infogérance actifs</p>
</div>
<Card>
<CardContent className="p-6 flex flex-col h-full">
<div className="mb-4">
<h2 className="text-lg font-semibold text-slate-900">Vos services gérés</h2>
<p className="text-sm text-slate-500">Catalogue des contrats d'infogérance actifs</p>
</div>
< div className = "flex-1 overflow-y-auto mb-6 space-y-3" >
{
isLoading?(
<p className = "text-sm text-slate-500 animate-pulse" > Chargement de vos contrats sécurisés...</ p >
) : contracts.length === 0 ? (
<p className= "text-sm text-slate-500" > Aucun contrat actif détecté.</p>
) : (
contracts.map((contract?: any) => (
<div key= { contract.id } className = "p-4 bg-slate-50 rounded-lg border border-slate-100 flex justify-between items-center" >
<div>
<h3 className="text-sm font-medium text-slate-900" > { contract.service_name } </h3>
{
contract.description && (
<p className="text-xs text-slate-500 mt-1"> { contract.description } </p>
)}
</div>
< span className = {`text-xs px-2 py-1 rounded-full font-medium ${contract.status === 'Actif' ? 'bg-emerald-50 text-emerald-600' :
contract.status === 'En déploiement' ? 'bg-blue-50 text-blue-600' :
'bg-slate-100 text-slate-600'
}`}>
{ contract.status }
</span>
</div>
))
<div className="flex-1 overflow-y-auto mb-6 space-y-3">
{error && <EmptyState message={error} className="text-red-500" />}
{isLoading && !error && <Loader />}
{!isLoading && !error && contracts.length === 0 && (
<EmptyState message="Aucun contrat actif détecté." />
)}
</div>
< button
onClick = {() => setIsModalOpen(true)}
className = "w-full bg-blue-900 text-white font-medium py-3 rounded-lg hover:bg-blue-800 transition-colors shadow-sm"
>
Demander une évolution du parc
</button>
</div>
{/* MODALE STRATÉGIQUE */ }
{
isModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-slate-900/50 backdrop-blur-sm p-4" >
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg overflow-hidden animate-in fade-in zoom-in-95 duration-200" >
<div className="p-6" >
<div className="flex justify-between items-center mb-5" >
<h3 className="text-xl font-bold text-slate-900" > Évolution de l'infrastructure</h3>
{
!isSubmitting && !submitSuccess && (
<button onClick={ () => setIsModalOpen(false) } className = "text-slate-400 hover:text-slate-600" >
<svg className="w-6 h-6" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
<path strokeLinecap="round" strokeLinejoin = "round" strokeWidth = { 2} d = "M6 18L18 6M6 6l12 12" />
</svg>
</button>
)
}
</div>
{
submitSuccess ? (
<div className= "text-center py-8" >
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center mx-auto mb-4" >
<svg className="w-8 h-8" fill = "none" viewBox = "0 0 24 24" stroke = "currentColor" >
<path strokeLinecap="round" strokeLinejoin = "round" strokeWidth = { 2} d = "M5 13l4 4L19 7" />
</svg>
</div>
< h4 className = "text-lg font-semibold text-slate-900 mb-2" > Demande transmise avec succès </h4>
< p className = "text-sm text-slate-500" > Un expert GISE analyse votre besoin et reviendra vers vous sous 24h.</p>
</div>
) : isSubmitting ? (
<div className= "text-center py-12" >
<p className="text-sm font-medium text-slate-500 animate-pulse" > Création de votre ticket projet sécurisé...</p>
{!isLoading && !error && contracts.map((contract) => (
<ServiceObject key={contract.id} contract={contract} />
))}
</div>
) : (
<>
<p className= "text-sm text-slate-600 mb-6" >
Quel est votre prochain objectif d'infrastructure ? Sélectionnez une initiative stratégique pour ouvrir un ticket projet avec votre expert.
</p>
< div className = "space-y-3" >
<button
onClick={ () => handleEvolutionRequest("Projet d'Évolution : Cybersécurité", "Le client souhaite renforcer sa sécurité (Audits, Tests d'intrusion, Plan de Reprise d'Activité).") }
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Renforcer la sécurité face aux cybermenaces </h4>
< p className = "text-xs text-slate-500 mt-1" > Audits de sécurité, tests d'intrusion et Plan de Reprise d'Activité(PRA).</p>
</button>
<Button size="lg" onClick={() => navigate('/evolution-infrastructure')}>
Demander une évolution du parc
</Button>
< button
onClick = {() => handleEvolutionRequest("Projet d'Évolution : Architecture Réseau", "Le client souhaite étendre ses capacités (Migration Cloud Privé, nouvelles succursales).")
}
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Étendre les capacités du réseau actuel </h4>
< p className = "text-xs text-slate-500 mt-1" > Migration vers Cloud Privé Sécurisé, ajout de succursales sécurisées.</p>
</button>
< button
onClick = {() => handleEvolutionRequest("Projet d'Évolution : Conformité Légal", "Le client demande un accompagnement vCISO pour la mise en conformité (RGPD, NIS2).")
}
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Mise en conformité légale(RGPD, NIS2) </h4>
< p className = "text-xs text-slate-500 mt-1" > Accompagnement vCISO et mise aux normes de votre système d'information.</p>
</button>
< button
onClick = {() => handleEvolutionRequest("Projet d'Évolution : Outils Souverains", "Le client souhaite déployer de nouveaux outils collaboratifs souverains.")}
className = "w-full text-left p-4 border border-slate-200 rounded-lg hover:border-blue-900 hover:bg-blue-50 transition-colors group"
>
<h4 className="font-medium text-slate-900 group-hover:text-blue-900" > Autre demande stratégique </h4>
< p className = "text-xs text-slate-500 mt-1" > Déploiement d'outils collaboratifs souverains ou besoins spécifiques.</p>
</button>
</div>
</>
)}
</div>
{
!isSubmitting && !submitSuccess && (
<div className="bg-slate-50 px-6 py-4 border-t border-slate-200 flex justify-end" >
<button onClick={ () => setIsModalOpen(false) } className = "text-sm font-medium text-slate-600 hover:text-slate-900" >
Annuler
</button>
</div>
)
}
</div>
</div>
)}
</>
</CardContent>
</Card>
);
}
@@ -0,0 +1,27 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardContent } from '@/components/ui/Card';
import { Button } from '@/components/ui/Button';
export const SupportCtaWidget: React.FC = () => {
const navigate = useNavigate();
return (
<Card className="bg-blue-900 border-blue-800 text-white shadow-sm">
<CardContent className="p-6">
<h3 className="text-lg font-semibold mb-2">Centre de Support</h3>
<p className="text-blue-200 text-sm mb-4 leading-relaxed">
Déclarez un incident critique ou demandez une évolution de votre infrastructure.
</p>
<Button
onClick={() => navigate('/support')}
className="w-full bg-white text-blue-900 hover:bg-slate-50 font-medium py-2.5 px-4 rounded-lg transition-colors shadow-sm"
>
Ouvrir un ticket sécurisé
</Button>
</CardContent>
</Card>
);
};
export default SupportCtaWidget;
+33 -93
View File
@@ -1,99 +1,39 @@
import { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
interface InfrastructureNode {
id: string;
label: string;
type: string;
status: string;
uptime_sla: string;
}
import { useInfrastructureNodes } from '@/hooks/useInfrastructureNodes';
import { UptimeObject } from '@/components/ui/UptimeObject';
import { Card, CardContent } from '@/components/ui/Card';
import { Loader } from '@/components/ui/Loader';
import { EmptyState } from '@/components/ui/EmptyState';
export default function UptimeWidget() {
const [nodes, setNodes] = useState<InfrastructureNode[]>([]);
const [isLoading, setIsLoading] = useState(true);
useEffect(() => {
const fetchNodes = async () => {
try {
// Récupération sécurisée filtrée automatiquement par le Token JWT de la compagnie
const records = await pb.collection('aegis_infrastructure_nodes').getFullList<InfrastructureNode>({
sort: 'created',
});
setNodes(records);
} catch (error) {
console.error("Erreur lors de la récupération des nœuds :", error);
} finally {
setIsLoading(false);
}
};
fetchNodes();
}, []);
// Fonction pour adapter dynamiquement la couleur du badge selon le statut renvoyé par la base
const getStatusStyle = (status: string) => {
switch (status) {
case 'Opérationnel':
return {
bg: 'bg-emerald-50 text-emerald-600 border-emerald-100',
dot: 'bg-emerald-500'
};
case 'Dégradé':
return {
bg: 'bg-amber-50 text-amber-600 border-amber-100',
dot: 'bg-amber-500'
};
case 'Hors Ligne':
return {
bg: 'bg-red-50 text-red-600 border-red-100',
dot: 'bg-red-500'
};
default:
return {
bg: 'bg-slate-50 text-slate-600 border-slate-100',
dot: 'bg-slate-400'
};
}
};
const { nodes, isLoading, error } = useInfrastructureNodes();
return (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6 flex flex-col">
<h2 className="text-lg font-semibold text-slate-900 mb-4">État des Services & SLA</h2>
{isLoading ? (
<div className="flex-1 flex justify-center items-center py-8">
<div className="animate-spin rounded-full h-6 w-6 border-b-2 border-slate-900"></div>
</div>
) : nodes.length === 0 ? (
<div className="flex-1 flex justify-center items-center py-8 text-sm text-slate-500 italic">
Aucun équipement enregistré pour ce contrat.
</div>
) : (
<div className="divide-y divide-slate-100 flex-1">
{nodes.map((node) => {
const style = getStatusStyle(node.status);
return (
<div key={node.id} className="py-4 flex items-center justify-between first:pt-0 last:pb-0">
<div>
<p className="font-medium text-slate-900">{node.label}</p>
<p className="text-xs text-slate-500 mt-0.5">{node.type}</p>
</div>
<div className="flex items-center space-x-6">
<div className="text-right">
<p className="text-xs text-slate-500">SLA</p>
<p className="text-sm font-semibold text-slate-900">{node.uptime_sla}%</p>
</div>
<div className={`${style.bg} px-3 py-1 rounded-full text-xs font-medium flex items-center border`}>
<div className={`w-1.5 h-1.5 rounded-full ${style.dot} mr-2`}></div>
{node.status}
</div>
</div>
</div>
);
})}
</div>
)}
</div>
<Card>
<CardContent className="p-6 flex flex-col h-full">
<h2 className="text-lg font-semibold text-slate-900 mb-4">État des Services & SLA</h2>
{/* 1. Gestion de l'erreur */}
{error && (
<EmptyState message={error} className="text-red-500 not-italic font-medium" />
)}
{/* 2. Gestion du chargement */}
{isLoading && !error && <Loader />}
{/* 3. Gestion de l'état vide */}
{!isLoading && !error && nodes.length === 0 && (
<EmptyState message="Aucun équipement enregistré pour ce contrat." />
)}
{/* 4. Affichage des données */}
{!isLoading && !error && nodes.length > 0 && (
<div className="divide-y divide-slate-100 flex-1">
{nodes.map((node) => (
<UptimeObject key={node.id} node={node} />
))}
</div>
)}
</CardContent>
</Card>
);
}
+122
View File
@@ -0,0 +1,122 @@
// Fichier : src/data/evolutionOptions.ts
export interface EvolutionOption {
id: string;
category: 'security' | 'cloud' | 'sovereignty' | 'governance' | 'custom';
categoryLabel: string;
title: string;
description: string;
subject: string;
payloadDescription: string;
badgeText?: string;
}
export interface EvolutionCategory {
id: string;
label: string;
}
export const EVOLUTION_CATEGORIES = [
{ id: 'all', label: 'Toutes les initiatives' },
{ id: 'security', label: 'Cybersécurité & Zero Trust' },
{ id: 'cloud', label: 'Cloud Privé & Réseau' },
{ id: 'sovereignty', label: 'Outils Souverains & Data' },
{ id: 'governance', label: 'Conformité & vCISO' },
];
export const EVOLUTION_OPTIONS: EvolutionOption[] = [
// --- CYBERSÉCURITÉ & ZERO TRUST ---
{
id: 'sec-zero-trust',
category: 'security',
categoryLabel: 'Cybersécurité',
title: 'Segmentation Réseau Zero Trust & Firewalls Dedicated',
description: 'Mise en place de micro-segmentation par VLANs et déploiement de pare-feu virtuels/physiques (OPNsense) hautement sécurisés.',
subject: "Projet d'Évolution : Zero Trust & Pare-feu",
payloadDescription: "Le client souhaite déployer une politique Zero Trust avec segmentation par VLANs et pare-feu OPNsense dédiés.",
badgeText: 'Socle Souverain'
},
{
id: 'sec-pentest-pra',
category: 'security',
categoryLabel: 'Cybersécurité',
title: 'Audit de Vulnérabilités & Plan de Reprise d\'Activité (PRA)',
description: 'Tests d\'intrusion (Pen-test) de surface, audit de configuration et rédaction du Plan de Reprise d\'Activité en cas de sinistre majeur.',
subject: "Projet d'Évolution : Audits & PRA",
payloadDescription: "Demande d'audit de vulnérabilités, tests d'intrusion et élaboration/revue du Plan de Reprise d'Activité (PRA).",
},
// --- CLOUD PRIVÉ & INFRASTRUCTURE ---
{
id: 'cloud-proxmox',
category: 'cloud',
categoryLabel: 'Cloud Privé',
title: 'Migration vers Hyperviseur Bare-Metal Proxmox',
description: 'Migration des serveurs physiques vétustes vers nos hyperviseurs bare-metal chiffrés et interconnectés via tunnels VPN dédiés.',
subject: "Projet d'Évolution : Migration Cloud Privé Proxmox",
payloadDescription: "Le client souhaite migrer ses serveurs locaux/vétustes vers des hyperviseurs bare-metal Proxmox managés par GISE.",
badgeText: 'Haute Disponibilité'
},
{
id: 'cloud-sdwan-vpn',
category: 'cloud',
categoryLabel: 'Réseau',
title: 'Interconnexion Multi-site & Tunnels VPN WireGuard',
description: 'Liaison chiffrée permanente entre vos différentes succursales et le datacenter avec basculement automatique en cas de coupure.',
subject: "Projet d'Évolution : Architecture Multi-site & VPN",
payloadDescription: "Demande d'extension du réseau avec interconnexion VPN WireGuard/OPNsense entre plusieurs sites.",
},
// --- OU TILS SOUVERAINS & DONNÉES ---
{
id: 'sov-suite',
category: 'sovereignty',
categoryLabel: 'Souveraineté',
title: 'Suite Collaborative Souveraine (GED & Coffre-Fort)',
description: 'Déploiement d\'espaces de stockage chiffrés (Nextcloud souverain), coffre-fort de mots de passe d\'entreprise (Vaultwarden) et visioconférence sécurisée.',
subject: "Projet d'Évolution : Suite Collaborative Souveraine",
payloadDescription: "Le client demande le déploiement de la suite souveraine (GED Nextcloud, Vaultwarden d'entreprise, outils chiffrés).",
badgeText: '100% Souverain'
},
{
id: 'sov-backups',
category: 'sovereignty',
categoryLabel: 'Sauvegardes',
title: 'Sauvegardes Immuables & Anti-Ransomware (Règle 3-2-1-1-0)',
description: 'Stockage des sauvegardes dans un coffre-fort numérique immuable (non modifiable et non supprimable même en cas de piratage).',
subject: "Projet d'Évolution : Sauvegardes Immuables",
payloadDescription: "Mise en place ou renforcement de la politique de sauvegardes immuables anti-ransomware (Règle 3-2-1-1-0).",
},
// --- CONFORMITÉ & GOUVERNANCE ---
{
id: 'gov-vciso',
category: 'governance',
categoryLabel: 'Gouvernance',
title: 'Accompagnement vCISO (Directeur Sécurité Partagé)',
description: 'Mise à disposition d\'un expert RSSI/vCISO dédié pour piloter la stratégie SI, former vos collaborateurs et auditer les prestataires.',
subject: "Projet d'Évolution : Accompagnement vCISO",
payloadDescription: "Le client souhaite un accompagnement vCISO (RSSI dédié à temps partagé) pour piloter sa gouvernance IT.",
badgeText: 'Conseil VIP'
},
{
id: 'gov-compliance',
category: 'governance',
categoryLabel: 'Conformité',
title: 'Mise en Conformité Légale (RGPD, NIS2, DORA)',
description: 'Analyse d\'écart et mise aux normes réglementaires européennes imposées aux infrastructures critiques et professions réglementées.',
subject: "Projet d'Évolution : Conformité NIS2 / RGPD / DORA",
payloadDescription: "Demande de mise en conformité réglementaire (RGPD, directive NIS2, DORA ou normes sectorielles).",
},
// --- DEMANDE SUR MESURE ---
{
id: 'custom-project',
category: 'custom',
categoryLabel: 'Sur-Mesure',
title: 'Projet d\'Ingénierie & Besoins Spécifiques',
description: 'Vous avez une contrainte technique spécifique ou un projet d\'extension sur-mesure ? Discutez-en directement avec votre directeur de compte.',
subject: "Projet d'Évolution : Demande spécifique",
payloadDescription: "Le client formule une demande d'évolution stratégique spécifique à cadrer lors d'un entretien.",
}
];
+29
View File
@@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
import { pb } from '@/services/pocketbase';
import type { ManagedContract } from '@/types/ManagedContract';
export const useContracts = () => {
const [contracts, setContracts] = useState<ManagedContract[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchContracts = async () => {
try {
setIsLoading(true);
const records = await pb.collection('aegis_managed_contracts').getFullList<ManagedContract>({
sort: '-created',
});
setContracts(records);
} catch (err) {
console.error("Erreur contrats :", err);
setError("Impossible de charger le catalogue des services.");
} finally {
setIsLoading(false);
}
};
fetchContracts();
}, []);
return { contracts, isLoading, error };
};
+31
View File
@@ -0,0 +1,31 @@
import { useState } from 'react';
import { pb } from '@/services/pocketbase';
export const useEvolutionRequest = () => {
const [isSubmitting, setIsSubmitting] = useState(false);
const [submitSuccess, setSubmitSuccess] = useState(false);
const [error, setError] = useState<string | null>(null);
const submitRequest = async (subject: string, description: string) => {
setIsSubmitting(true);
setError(null);
try {
await pb.collection('aegis_tickets').create({
author: pb.authStore.model?.id,
company: pb.authStore.model?.company,
category: "Évolution",
subject,
description,
status: "Ouvert"
});
setSubmitSuccess(true);
} catch (err) {
console.error("Erreur ticket d'évolution :", err);
setError("Une erreur de communication est survenue. Nos systèmes sont alertés.");
} finally {
setIsSubmitting(false);
}
};
return { submitRequest, isSubmitting, submitSuccess, error };
};
+29
View File
@@ -0,0 +1,29 @@
import { useState, useEffect } from 'react';
import { pb } from '@services/pocketbase';
import type { InfrastructureNode } from '@/types/InfrastructureNode';
export const useInfrastructureNodes = () => {
const [nodes, setNodes] = useState<InfrastructureNode[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const fetchNodes = async () => {
try {
setIsLoading(true);
const records = await pb.collection('aegis_infrastructure_nodes').getFullList<InfrastructureNode>({
sort: 'created',
});
setNodes(records);
} catch (err) {
setError("Impossible de charger l'infrastructure.");
} finally {
setIsLoading(false);
}
};
fetchNodes();
}, []);
return { nodes, isLoading, error };
};
+9 -79
View File
@@ -1,93 +1,23 @@
import React from 'react';
import Sidebar from '@/components/layout/Sidebar';
// Définition stricte des propriétés attendues par le Layout
interface DashboardLayoutProps {
children: React.ReactNode;
activeView: 'dashboard' | 'support' | 'vault' | 'account';
onNavigate: (view: 'dashboard' | 'support' | 'vault' | 'account') => void;
onLogout: () => void;
}
export default function DashboardLayout({ children, activeView, onNavigate, onLogout }: DashboardLayoutProps) {
const navItems = [
{ id: 'dashboard', label: 'Tableau de bord', icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" /></svg>
)},
{ id: 'support', label: 'Centre de Support', icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M18.364 5.636l-3.536 3.536m0 5.656l3.536 3.536M9.172 9.172L5.636 5.636m3.536 9.192l-3.536 3.536M21 12a9 9 0 11-18 0 9 9 0 0118 0zm-5 0a4 4 0 11-8 0 4 4 0 018 0z" /></svg>
)},
{ id: 'vault', label: 'Coffre-Fort', icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" /></svg>
)}
];
export default function DashboardLayout({ children, onLogout }: DashboardLayoutProps) {
return (
<div className="flex h-screen bg-slate-50 overflow-hidden font-sans selection:bg-blue-900 selection:text-white">
{/* Sidebar Latérale (Institutionnelle) */}
<aside className="w-64 bg-white border-r border-slate-200 flex flex-col justify-between shadow-sm z-10">
{/* En-tête Sidebar */}
<div>
<div className="h-20 flex items-center px-8 border-b border-slate-100">
<h2 className="text-2xl font-bold tracking-tight text-slate-900">
AEGIS <span className="text-xs font-semibold text-blue-900 ml-1 bg-blue-50 px-2 py-1 rounded">VIP</span>
</h2>
</div>
<div className="flex h-screen bg-slate-50 overflow-hidden">
{/* Intégration de la navbar extraite */}
<Sidebar onLogout={onLogout} />
{/* Navigation */}
<nav className="p-4 space-y-1">
{navItems.map((item) => (
<button
key={item.id}
onClick={() => onNavigate(item.id as any)}
className={`w-full flex items-center px-4 py-3 text-sm font-medium rounded-lg transition-colors duration-200 ${
activeView === item.id
? 'bg-blue-50 text-blue-900'
: 'text-slate-600 hover:bg-slate-50 hover:text-slate-900'
}`}
>
<span className={`mr-3 ${activeView === item.id ? 'text-blue-900' : 'text-slate-400'}`}>
{item.icon}
</span>
{item.label}
</button>
))}
</nav>
</div>
{/* Pied de la Sidebar (Profil & Déconnexion) */}
<div className="p-4 border-t border-slate-100">
<button
onClick={() => onNavigate('account')}
className={`w-full flex items-center px-4 py-3 rounded-lg transition-colors duration-200 text-left ${
activeView === 'account' ? 'bg-slate-100 ring-1 ring-slate-200' : 'hover:bg-slate-50'
}`}
>
<div className="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center text-slate-600 font-bold text-sm shrink-0">
DC
</div>
<div className="ml-3 overflow-hidden">
<p className="text-sm font-medium text-slate-900 truncate">Direction Client</p>
<p className="text-xs text-slate-500 truncate">Paramètres du compte</p>
</div>
</button>
<button
onClick={onLogout}
className="w-full flex items-center px-4 py-2 text-sm font-medium text-red-600 rounded-lg hover:bg-red-50 transition-colors duration-200"
>
<svg className="w-4 h-4 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1" /></svg>
Verrouiller la session
</button>
</div>
</aside>
{/* Zone de contenu principal */}
{/* CONTENU PRINCIPAL */}
<main className="flex-1 overflow-y-auto">
{children}
<div className="p-8">
{children}
</div>
</main>
</div>
);
}
+4 -1
View File
@@ -2,9 +2,12 @@ import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
import { BrowserRouter } from 'react-router-dom'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</StrictMode>,
)
+56
View File
@@ -0,0 +1,56 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
export const Analytics: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-5xl mx-auto py-8 px-8">
<button
onClick={() => navigate('/dashboard')}
className="text-sm font-semibold text-slate-500 hover:text-slate-900 transition-colors flex items-center gap-1 mb-6"
>
Retour au tableau de bord
</button>
<Card>
<CardHeader className="bg-slate-50">
<h3 className="text-xl font-bold text-slate-900">Télémétrie & Analytics</h3>
<p className="text-sm text-slate-500">Données réseau en temps réel</p>
</CardHeader>
<CardContent>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Disponibilité (30j)</p>
<p className="text-2xl font-bold text-emerald-600">100%</p>
</div>
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Charge CPU moy.</p>
<p className="text-2xl font-bold text-slate-900">14%</p>
</div>
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Trafic chiffré</p>
<p className="text-2xl font-bold text-slate-900">1.2 TB</p>
</div>
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Requêtes bloquées</p>
<p className="text-2xl font-bold text-blue-900">3,492</p>
</div>
</div>
{/* Représentation visuelle du graphe */}
<h4 className="text-sm font-semibold text-slate-900 mb-3">Trafic Réseau WAN (Dernières 24h)</h4>
<div className="h-48 w-full bg-slate-50 border border-slate-100 rounded-lg flex items-end p-2 space-x-1">
{[40, 20, 60, 80, 50, 30, 70, 90, 60, 40, 30, 20, 10, 50, 80, 60, 40, 70, 90, 100, 80, 60, 40, 50].map((val, i) => (
<div key={i} className="bg-blue-200 hover:bg-blue-400 w-full rounded-t-sm transition-colors" style={{ height: `${val}%` }}></div>
))}
</div>
</CardContent>
</Card>
</div>
);
};
export default Analytics;
+65
View File
@@ -0,0 +1,65 @@
import React from 'react';
import { useNavigate } from 'react-router-dom';
import { Card, CardHeader, CardContent } from '@/components/ui/Card';
// On pourrait extraire ces données dans un Custom Hook plus tard
const BACKUP_HISTORY = [
{ date: 'Aujourd\'hui, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '12 GB' },
{ date: 'Hier, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '8.4 GB' },
{ date: 'Dimanche, 01:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Totale (Full)', size: '240 GB' },
{ date: 'Samedi, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '4.1 GB' },
];
export const Backups: React.FC = () => {
const navigate = useNavigate();
return (
<div className="max-w-5xl mx-auto py-8 px-8">
<button
onClick={() => navigate('/dashboard')}
className="text-sm font-semibold text-slate-500 hover:text-slate-900 transition-colors flex items-center gap-1 mb-6"
>
Retour au tableau de bord
</button>
<Card>
<CardHeader className="bg-slate-50">
<h3 className="text-xl font-bold text-slate-900">Historique des Sauvegardes PRA</h3>
<p className="text-sm text-slate-500">Journal d'exécution de la règle 3-2-1-1-0</p>
</CardHeader>
{/* On retire le padding sur le content pour que le tableau touche les bords */}
<CardContent className="p-0 overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-white">
<tr>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Date d'exécution</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Cible (Nœud)</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Type</th>
<th className="px-6 py-4 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">Volume</th>
<th className="px-6 py-4 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">Statut</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-slate-100">
{BACKUP_HISTORY.map((bkp, i) => (
<tr key={i} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">{bkp.date}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.node}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.type}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.size}</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<span className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200/60">
Succès (Chiffré)
</span>
</td>
</tr>
))}
</tbody>
</table>
</CardContent>
</Card>
</div>
);
};
export default Backups;
+215
View File
@@ -0,0 +1,215 @@
import React, { useState, useEffect } from 'react';
import { useNavigate } from 'react-router-dom';
import { useEvolutionRequest } from '@/hooks/useEvolutionRequest';
import { Button } from '@/components/ui/Button';
import { EmptyState } from '@/components/ui/EmptyState';
import { cn } from '@/utils/utils';
import {
EVOLUTION_OPTIONS,
EVOLUTION_CATEGORIES,
type EvolutionOption
} from '@/data/evolutionOptions';
export const InfrastructureEvolution: React.FC = () => {
const navigate = useNavigate();
const { submitRequest, isSubmitting, submitSuccess, error } = useEvolutionRequest();
const [selectedCategory, setSelectedCategory] = useState<string>('all');
const [searchQuery, setSearchQuery] = useState<string>('');
const [selectedOptionId, setSelectedOptionId] = useState<string | null>(null);
// Redirection automatique vers le Dashboard après confirmation de la demande
useEffect(() => {
if (submitSuccess) {
const timer = setTimeout(() => {
navigate('/dashboard');
}, 3000);
return () => clearTimeout(timer);
}
}, [submitSuccess, navigate]);
// Filtrage combiné par onglet et recherche textuelle
const filteredOptions = EVOLUTION_OPTIONS.filter((opt) => {
const matchesCategory = selectedCategory === 'all' || opt.category === selectedCategory;
const matchesSearch = opt.title.toLowerCase().includes(searchQuery.toLowerCase()) ||
opt.description.toLowerCase().includes(searchQuery.toLowerCase());
return matchesCategory && matchesSearch;
});
const handleSelectOption = (option: EvolutionOption) => {
setSelectedOptionId(option.id);
submitRequest(option.subject, option.payloadDescription);
};
return (
<div className="max-w-5xl mx-auto py-8 px-4 sm:px-6">
{/* EN-TÊTE DE LA PAGE */}
<div className="mb-8">
<div className="flex items-center justify-between mb-2">
<button
onClick={() => navigate('/dashboard')}
className="text-sm font-semibold text-slate-500 hover:text-slate-900 transition-colors flex items-center gap-1"
>
Retour au tableau de bord
</button>
<span className="text-xs font-mono font-bold tracking-widest text-slate-400 uppercase">
AEGIS Catalogue Projets
</span>
</div>
<h1 className="text-3xl font-extrabold text-slate-900 tracking-tight">
Évolution & Extension de l'Infrastructure
</h1>
<p className="text-slate-600 mt-2 text-base">
Sélectionnez une initiative stratégique. Votre demande ouvrira immédiatement un ticket projet sécurisé auprès de votre référent technique GISE.
</p>
</div>
{/* ÉTAT DE CONFIRMATION AVEC SUCCÈS */}
{submitSuccess ? (
<div className="bg-white rounded-xl border border-emerald-200 shadow-sm p-12 text-center max-w-xl mx-auto my-12 animate-in fade-in zoom-in-95 duration-200">
<div className="w-16 h-16 bg-emerald-100 text-emerald-600 rounded-full flex items-center justify-center mx-auto mb-6">
<svg className="w-8 h-8" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2.5} d="M5 13l4 4L19 7" />
</svg>
</div>
<h2 className="text-2xl font-bold text-slate-900 mb-2">
Demande d'évolution enregistrée
</h2>
<p className="text-slate-600 text-sm mb-6 leading-relaxed">
Votre ticket projet a é chiffré et transmis à notre équipe d'ingénierie. Un expert GISE analyse vos prérequis et reprendra contact avec vous sous 24h ouvrées.
</p>
<p className="text-xs text-slate-400 font-mono">
Redirection automatique vers le Trust Center...
</p>
</div>
) : (
<>
{/* BARRE DE RECHERCHE ET ONGLETS DE FILTRAGE */}
<div className="space-y-4 mb-8">
<div className="flex flex-col sm:flex-row gap-4 justify-between items-stretch sm:items-center">
{/* Onglets de catégories */}
<div className="flex items-center gap-1 overflow-x-auto pb-2 sm:pb-0 scrollbar-none">
{EVOLUTION_CATEGORIES.map((cat) => (
<button
key={cat.id}
onClick={() => setSelectedCategory(cat.id)}
className={cn(
"px-3.5 py-2 text-xs font-semibold rounded-lg whitespace-nowrap transition-all duration-150",
selectedCategory === cat.id
? "bg-slate-900 text-white shadow-sm"
: "bg-white text-slate-600 hover:bg-slate-100 border border-slate-200"
)}
>
{cat.label}
</button>
))}
</div>
{/* Champ de recherche */}
<div className="relative min-w-60">
<input
type="text"
placeholder="Rechercher une initiative..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-9 pr-4 py-2 text-xs bg-white border border-slate-200 rounded-lg text-slate-900 placeholder-slate-400 focus:outline-none focus:ring-2 focus:ring-blue-900 focus:border-transparent"
/>
<svg className="w-4 h-4 text-slate-400 absolute left-3 top-2.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
</div>
</div>
{/* AFFICHAGE DES ERREURS D'ENVOI */}
{error && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 text-red-700 rounded-xl text-sm flex items-center justify-between">
<span>{error}</span>
</div>
)}
{/* GRILLE DES OPTIONS */}
{filteredOptions.length === 0 ? (
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
<EmptyState message="Aucune initiative ne correspond à votre recherche." />
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 gap-5">
{filteredOptions.map((option) => {
const isThisSubmitting = isSubmitting && selectedOptionId === option.id;
return (
<div
key={option.id}
className={cn(
"group bg-white rounded-xl border p-6 flex flex-col justify-between transition-all duration-200 hover:border-blue-900 hover:shadow-md relative overflow-hidden",
isThisSubmitting ? "border-blue-900 bg-blue-50/30" : "border-slate-200"
)}
>
<div>
{/* En-tête de la carte */}
<div className="flex items-center justify-between gap-2 mb-3">
<span className="text-[11px] font-mono font-bold tracking-wider uppercase px-2 py-0.5 rounded bg-slate-100 text-slate-600">
{option.categoryLabel}
</span>
{option.badgeText && (
<span className="text-[10px] font-semibold tracking-wide uppercase px-2 py-0.5 rounded-full bg-blue-50 text-blue-800 border border-blue-100">
{option.badgeText}
</span>
)}
</div>
{/* Titre & Description */}
<h3 className="text-base font-bold text-slate-900 group-hover:text-blue-900 transition-colors">
{option.title}
</h3>
<p className="text-xs text-slate-600 mt-2 leading-relaxed">
{option.description}
</p>
</div>
{/* Bouton d'action */}
<div className="mt-6 pt-4 border-t border-slate-100 flex justify-end">
<Button
size="sm"
isLoading={isThisSubmitting}
disabled={isSubmitting}
onClick={() => handleSelectOption(option)}
className="w-full sm:w-auto text-xs"
>
Engager cette initiative
</Button>
</div>
</div>
);
})}
</div>
)}
{/* PIED DE PAGE D'ASSISTANCE */}
<div className="mt-12 bg-slate-900 text-slate-300 rounded-xl p-6 flex flex-col sm:flex-row items-center justify-between gap-4">
<div>
<h4 className="text-sm font-bold text-white">Besoin d'un cadrage sur-mesure ?</h4>
<p className="text-xs text-slate-400 mt-0.5">
Nos ingénieurs restent joignables par téléphone pour les demandes urgentes ou complexes.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={() => navigate('/support')}
className="border-slate-700 text-slate-200 hover:bg-slate-800 hover:border-slate-600 text-xs whitespace-nowrap"
>
Consulter le Service Desk
</Button>
</div>
</>
)}
</div>
);
};
export default InfrastructureEvolution;
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { pb } from '../../config/pocketbase';
import { pb } from '@services/pocketbase';
import * as OTPAuth from 'otpauth';
import { QRCodeSVG } from 'qrcode.react';
@@ -221,7 +221,7 @@ export default function Login({ onLoginSuccess }: LoginProps) {
<div className="text-center mb-6">
<p className="text-sm font-medium text-slate-900">Configuration de la sécurité</p>
<p className="text-xs text-slate-500 mt-2 mb-4">Scannez ce QR Code avec Google Authenticator ou Authy pour lier votre appareil.</p>
<div className="flex justify-center p-4 bg-white border border-slate-200 rounded-lg inline-block shadow-sm">
<div className="flex justify-center p-4 bg-white border border-slate-200 rounded-lg shadow-sm">
<QRCodeSVG value={qrUrl} size={150} />
</div>
</div>
@@ -1,5 +1,5 @@
import React, { useState } from 'react';
import { pb } from '../../config/pocketbase';
import { pb } from '@services/pocketbase';
interface NewTicketProps {
onCancel: () => void;
@@ -1,7 +1,7 @@
import { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
import NewTicket from './NewTicket'; // Ajustez le chemin d'import selon votre arborescence
import TicketDetail from './TicketDetail'; // Ajustez le chemin d'import selon votre arborescence
import { pb } from '@services/pocketbase';
import NewTicket from './NewTicket';
import TicketDetail from './TicketDetail';
interface Ticket {
id: string;
@@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
import { pb } from '@services/pocketbase';
interface Ticket {
id: string;
+51
View File
@@ -0,0 +1,51 @@
import { useNavigate } from 'react-router-dom';
import UptimeWidget from '@/components/widgets/UptimeWidget';
import BackupWidget from '@/components/widgets/BackupWidget';
import SecurityWidget from '@/components/widgets/SecurityWidget';
import ServicesWidget from '@/components/widgets/ServicesWidget';
import SupportCtaWidget from '@/components/widgets/SupportCtaWidget';
export default function TrustCenter() {
const navigate = useNavigate();
return (
<div className="bg-slate-50 font-sans text-slate-900 relative min-h-[calc(100vh-4rem)]">
{/* Header */}
<header className="bg-white border-b border-slate-200 px-8 py-6">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Tableau de bord de l'infrastructure</h1>
<p className="text-sm text-slate-500 mt-1">Espace sécurisé AEGIS • Synchronisation en temps réel</p>
</div>
<div className="flex items-center space-x-3">
<span className="flex h-3 w-3 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</span>
<span className="text-sm font-medium text-slate-700">Connexion chiffrée</span>
</div>
</div>
</header>
{/* Main Grid */}
<main className="max-w-7xl mx-auto px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<UptimeWidget />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<BackupWidget onClick={() => navigate('/backups')} />
<SecurityWidget onClick={() => navigate('/analytics')} />
</div>
</div>
<div className="space-y-6">
<ServicesWidget />
<SupportCtaWidget />
</div>
</div>
</main>
</div>
);
}
-194
View File
@@ -1,194 +0,0 @@
import { useState } from 'react';
import UptimeWidget from '../../components/widgets/UptimeWidget';
import BackupWidget from '../../components/widgets/BackupWidget';
import SecurityWidget from '../../components/widgets/SecurityWidget';
import ServicesWidget from '../../components/widgets/ServicesWidget';
interface TrustCenterProps {
onNavigate?: (view: 'dashboard' | 'support' | 'vault' | 'account') => void;
}
export default function TrustCenter({ onNavigate }: TrustCenterProps) {
const [activeModal, setActiveModal] = useState<'none' | 'evolution' | 'analytics' | 'backups'>('none');
//const [selectedService, setSelectedService] = useState<string>('');
const [evolutionChoice, setEvolutionChoice] = useState<string>('');
const closeModal = () => setActiveModal('none');
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900 relative">
{/* Header */}
<header className="bg-white border-b border-slate-200 px-8 py-6">
<div className="max-w-7xl mx-auto flex justify-between items-center">
<div>
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Tableau de bord de l'infrastructure</h1>
<p className="text-sm text-slate-500 mt-1">Espace sécurisé AEGIS • Synchronisation en temps réel</p>
</div>
<div className="flex items-center space-x-3">
<span className="flex h-3 w-3 relative">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-emerald-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-3 w-3 bg-emerald-500"></span>
</span>
<span className="text-sm font-medium text-slate-700">Connexion chiffrée</span>
</div>
</div>
</header>
{/* Main Grid */}
<main className="max-w-7xl mx-auto px-8 py-8">
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
<div className="lg:col-span-2 space-y-6">
<UptimeWidget />
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<BackupWidget onClick={() => setActiveModal('backups')} />
<SecurityWidget />
</div>
</div>
<div className="space-y-6">
<ServicesWidget />
{/* CTA Support ITSM Actif */}
<div className="bg-blue-900 rounded-xl shadow-sm border border-blue-800 p-6 text-white">
<h3 className="text-lg font-semibold mb-2">Centre de Support</h3>
<p className="text-blue-200 text-sm mb-4 leading-relaxed">
Déclarez un incident critique ou demandez une évolution de votre infrastructure.
</p>
<button
onClick={() => onNavigate && onNavigate('support')}
className="w-full bg-white text-blue-900 hover:bg-slate-50 font-medium py-2.5 px-4 rounded-lg transition-colors shadow-sm"
>
Ouvrir un ticket sécurisé
</button>
</div>
</div>
</div>
</main>
{/* --- OVERLAY MODALES --- */}
{activeModal !== 'none' && (
<div className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-slate-900/60 backdrop-blur-sm">
{/* MODALE 1 : ÉVOLUTION STRATÉGIQUE */}
{activeModal === 'evolution' && (
<div className="bg-white rounded-2xl shadow-xl w-full max-w-2xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
<h3 className="text-lg font-bold text-slate-900">Demande d'évolution d'infrastructure</h3>
<button onClick={closeModal} className="text-slate-400 hover:text-slate-900"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
</div>
<div className="p-6">
<h4 className="text-base font-semibold text-slate-900 mb-4">Quel est votre prochain objectif d'infrastructure ?</h4>
<div className="space-y-3">
{[
{ id: 'sec', title: 'Renforcer la sécurité face aux cybermenaces', desc: 'Audits de vulnérabilité, Plan de Reprise d\'Activité (PRA)' },
{ id: 'net', title: 'Étendre les capacités du réseau actuel', desc: 'Migration Cloud Privé, ouverture de nouveaux sites' },
{ id: 'gov', title: 'Mise en conformité légale & Gouvernance', desc: 'Accompagnement RGPD, Directive NIS2, vCISO' },
{ id: 'oth', title: 'Autre demande stratégique', desc: 'Outils souverains, audit spécifique' }
].map((option) => (
<div
key={option.id}
onClick={() => setEvolutionChoice(option.id)}
className={`p-4 rounded-xl border-2 cursor-pointer transition-all ${evolutionChoice === option.id ? 'border-blue-900 bg-blue-50' : 'border-slate-200 hover:border-blue-300'}`}
>
<p className="font-semibold text-slate-900">{option.title}</p>
<p className="text-sm text-slate-500 mt-1">{option.desc}</p>
</div>
))}
</div>
<div className="mt-8 flex justify-end space-x-3">
<button onClick={closeModal} className="px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50 rounded-lg">Annuler</button>
<button onClick={() => { alert('Redirection vers un ticket projet avec le choix stratégique.'); closeModal(); }} className="px-6 py-2 text-sm font-medium text-white bg-blue-900 hover:bg-blue-800 rounded-lg shadow-sm">Valider la demande stratégique</button>
</div>
</div>
</div>
)}
{/* MODALE 2 : ANALYTICS SERVICES */}
{activeModal === 'analytics' && (
<div className="bg-white rounded-2xl shadow-xl w-full max-w-3xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
<div>
<h3 className="text-lg font-bold text-slate-900">Télémétrie & Analytics</h3>
<p className="text-sm text-slate-500">{'selectedService'}</p>
</div>
<button onClick={closeModal} className="text-slate-400 hover:text-slate-900"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
</div>
<div className="p-6">
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Disponibilité (30j)</p>
<p className="text-2xl font-bold text-emerald-600">100%</p>
</div>
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Charge CPU moy.</p>
<p className="text-2xl font-bold text-slate-900">14%</p>
</div>
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Trafic chiffré</p>
<p className="text-2xl font-bold text-slate-900">1.2 TB</p>
</div>
<div className="bg-slate-50 border border-slate-100 p-4 rounded-xl text-center">
<p className="text-xs text-slate-500 uppercase font-semibold mb-1">Requêtes bloquées</p>
<p className="text-2xl font-bold text-blue-900">3,492</p>
</div>
</div>
{/* Représentation visuelle d'un graphe */}
<h4 className="text-sm font-semibold text-slate-900 mb-3">Trafic Réseau WAN (Dernières 24h)</h4>
<div className="h-32 w-full bg-slate-50 border border-slate-100 rounded-lg flex items-end p-2 space-x-1">
{[40, 20, 60, 80, 50, 30, 70, 90, 60, 40, 30, 20, 10, 50, 80, 60, 40, 70, 90, 100, 80, 60, 40, 50].map((val, i) => (
<div key={i} className="bg-blue-200 hover:bg-blue-400 w-full rounded-t-sm transition-colors" style={{ height: `${val}%` }}></div>
))}
</div>
</div>
</div>
)}
{/* MODALE 3 : HISTORIQUE DES SAUVEGARDES */}
{activeModal === 'backups' && (
<div className="bg-white rounded-2xl shadow-xl w-full max-w-3xl overflow-hidden animate-in fade-in zoom-in-95 duration-200">
<div className="px-6 py-4 border-b border-slate-100 flex justify-between items-center bg-slate-50">
<h3 className="text-lg font-bold text-slate-900">Historique des Sauvegardes PRA</h3>
<button onClick={closeModal} className="text-slate-400 hover:text-slate-900"><svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M6 18L18 6M6 6l12 12" /></svg></button>
</div>
<div className="p-0 overflow-x-auto">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Date d'exécution</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Cible (Nœud)</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Type</th>
<th className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase">Volume</th>
<th className="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase">Statut</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-slate-100">
{[
{ date: 'Aujourd\'hui, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '12 GB' },
{ date: 'Hier, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '8.4 GB' },
{ date: 'Dimanche, 01:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Totale (Full)', size: '240 GB' },
{ date: 'Samedi, 03:00 AM', node: 'Cluster Proxmox (Compta)', type: 'Incrémentielle', size: '4.1 GB' },
].map((bkp, i) => (
<tr key={i} className="hover:bg-slate-50">
<td className="px-6 py-4 whitespace-nowrap text-sm font-medium text-slate-900">{bkp.date}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.node}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.type}</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">{bkp.size}</td>
<td className="px-6 py-4 whitespace-nowrap text-right">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-100">
Succès (Chiffré)
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)}
</div>
);
}
@@ -1,6 +1,6 @@
import PocketBase from 'pocketbase';
const PB_URL = 'https://pocket.bunker.lan';
const PB_URL = import.meta.env.VITE_PB_URL;
export const pb = new PocketBase(PB_URL);
+9
View File
@@ -0,0 +1,9 @@
import type { NodeStatus } from '@/types/Status';
export interface InfrastructureNode {
id: string;
label: string;
type: string;
status: NodeStatus;
uptime_sla: string;
}
+8
View File
@@ -0,0 +1,8 @@
import type { ContractStatus } from '@/types/Status';
export interface ManagedContract {
id: string;
service_name: string;
description?: string;
status: ContractStatus;
}
+8
View File
@@ -0,0 +1,8 @@
// Statuts relatifs aux équipements d'infrastructure
export type NodeStatus = 'Opérationnel' | 'Dégradé' | 'Hors Ligne';
// Statuts relatifs aux contrats d'infogérance
export type ContractStatus = 'Actif' | 'En déploiement' | 'Suspendu';
// Type unifié regroupant tous les statuts possibles sur la plateforme AEGIS
export type AegisStatus = NodeStatus | ContractStatus;
+7
View File
@@ -0,0 +1,7 @@
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}
+16
View File
@@ -8,6 +8,22 @@
"allowArbitraryExtensions": true,
"skipLibCheck": true,
"paths": {
"@/*": ["./src/*"],
"@components/*": ["./src/components/*"],
"@ui/*": ["./src/components/ui/*"],
"@common/*": ["./src/components/common/*"],
"@pages/*": ["./src/pages/*"],
"@hooks/*": ["./src/hooks/*"],
"@layouts/*": ["./src/layouts/*"],
"@features/*": ["./src/features/*"],
"@services/*": ["./src/services/*"],
"@utils/*": ["./src/utils/*"],
"@assets/*": ["./src/assets/*"],
"@types/*": ["./src/types/*"],
"@widgets/*": ["./src/components/widgets/*"],
},
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
+18
View File
@@ -1,6 +1,7 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import tailwindcss from '@tailwindcss/vite'
import path from 'path'
// https://vite.dev/config/
export default defineConfig({
@@ -8,4 +9,21 @@ export default defineConfig({
react(),
tailwindcss()
],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@ui': path.resolve(__dirname, './src/components/ui'),
'@common': path.resolve(__dirname, './src/components/common'),
'@pages': path.resolve(__dirname, './src/pages'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@layouts': path.resolve(__dirname, './src/layouts'),
'@features': path.resolve(__dirname, './src/features'),
'@services': path.resolve(__dirname, './src/services'),
'@utils': path.resolve(__dirname, './src/utils'),
'@assets': path.resolve(__dirname, './src/assets'),
'@data': path.resolve(__dirname, './src/data'),
'@widgets': path.resolve(__dirname, './src/components/widgets'),
}
}
})