init aegis

This commit is contained in:
maximus
2026-07-22 14:41:21 +02:00
commit 82075f3e6c
33 changed files with 5486 additions and 0 deletions
+184
View File
@@ -0,0 +1,184 @@
.counter {
font-size: 16px;
padding: 5px 10px;
border-radius: 5px;
color: var(--accent);
background: var(--accent-bg);
border: 2px solid transparent;
transition: border-color 0.3s;
margin-bottom: 24px;
&:hover {
border-color: var(--accent-border);
}
&:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
}
.hero {
position: relative;
.base,
.framework,
.vite {
inset-inline: 0;
margin: 0 auto;
}
.base {
width: 170px;
position: relative;
z-index: 0;
}
.framework,
.vite {
position: absolute;
}
.framework {
z-index: 1;
top: 34px;
height: 28px;
transform: perspective(2000px) rotateZ(300deg) rotateX(44deg) rotateY(39deg)
scale(1.4);
}
.vite {
z-index: 0;
top: 107px;
height: 26px;
width: auto;
transform: perspective(2000px) rotateZ(300deg) rotateX(40deg) rotateY(39deg)
scale(0.8);
}
}
#center {
display: flex;
flex-direction: column;
gap: 25px;
place-content: center;
place-items: center;
flex-grow: 1;
@media (max-width: 1024px) {
padding: 32px 20px 24px;
gap: 18px;
}
}
#next-steps {
display: flex;
border-top: 1px solid var(--border);
text-align: left;
& > div {
flex: 1 1 0;
padding: 32px;
@media (max-width: 1024px) {
padding: 24px 20px;
}
}
.icon {
margin-bottom: 16px;
width: 22px;
height: 22px;
}
@media (max-width: 1024px) {
flex-direction: column;
text-align: center;
}
}
#docs {
border-right: 1px solid var(--border);
@media (max-width: 1024px) {
border-right: none;
border-bottom: 1px solid var(--border);
}
}
#next-steps ul {
list-style: none;
padding: 0;
display: flex;
gap: 8px;
margin: 32px 0 0;
.logo {
height: 18px;
}
a {
color: var(--text-h);
font-size: 16px;
border-radius: 6px;
background: var(--social-bg);
display: flex;
padding: 6px 12px;
align-items: center;
gap: 8px;
text-decoration: none;
transition: box-shadow 0.3s;
&:hover {
box-shadow: var(--shadow);
}
.button-icon {
height: 18px;
width: 18px;
}
}
@media (max-width: 1024px) {
margin-top: 20px;
flex-wrap: wrap;
justify-content: center;
li {
flex: 1 1 calc(50% - 8px);
}
a {
width: 100%;
justify-content: center;
box-sizing: border-box;
}
}
}
#spacer {
height: 88px;
border-top: 1px solid var(--border);
@media (max-width: 1024px) {
height: 48px;
}
}
.ticks {
position: relative;
width: 100%;
&::before,
&::after {
content: '';
position: absolute;
top: -4.5px;
border: 5px solid transparent;
}
&::before {
left: 0;
border-left-color: var(--border);
}
&::after {
right: 0;
border-right-color: var(--border);
}
}
+48
View File
@@ -0,0 +1,48 @@
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';
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 handleLogout = () => {
pb.authStore.clear(); // Détruit le jeton de sécurité
setIsAuthenticated(false);
};
// Tant que l'utilisateur n'est pas totalement authentifié (MFA inclus),
// on le maintient dans le sas de sécurité.
if (!isAuthenticated) {
return <Login onLoginSuccess={() => setIsAuthenticated(true)} />;
}
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} />;
}
};
return (
<DashboardLayout
activeView={activeView}
onNavigate={setActiveView}
onLogout={handleLogout}
>
{renderContent()}
</DashboardLayout>
);
}
export default App;
Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4.0 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.5 KiB

+43
View File
@@ -0,0 +1,43 @@
export default function BackupModale(){
return (
<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>
);
}
+27
View File
@@ -0,0 +1,27 @@
import React from 'react';
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>
<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>
);
}
+25
View File
@@ -0,0 +1,25 @@
import React from 'react';
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>
<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>
);
}
+193
View File
@@ -0,0 +1,193 @@
import { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
export default function ServicesWidget() {
const [contracts, setContracts] = useState([]);
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);
}
};
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>
< 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) => (
<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>
< 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>
</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
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>
)}
</>
);
}
+99
View File
@@ -0,0 +1,99 @@
import { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
interface InfrastructureNode {
id: string;
label: string;
type: string;
status: string;
uptime_sla: string;
}
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'
};
}
};
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>
);
}
+9
View File
@@ -0,0 +1,9 @@
import PocketBase from 'pocketbase';
// Remplacez cette URL par l'IP de votre serveur si PocketBase n'est pas en local
const PB_URL = 'https://pocket.gise.be';
export const pb = new PocketBase(PB_URL);
// Optionnel : Désactive l'annulation auto pour éviter les soucis avec le StrictMode de React
pb.autoCancellation(false);
+5
View File
@@ -0,0 +1,5 @@
@import "tailwindcss";
@tailwind base;
@tailwind components;
@tailwind utilities;
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
// 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>
)}
];
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>
{/* 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 */}
<main className="flex-1 overflow-y-auto">
{children}
</main>
</div>
);
}
+10
View File
@@ -0,0 +1,10 @@
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import './index.css'
import App from './App.tsx'
createRoot(document.getElementById('root')!).render(
<StrictMode>
<App />
</StrictMode>,
)
+94
View File
@@ -0,0 +1,94 @@
import React from 'react';
export default function AccountSettings() {
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900 selection:bg-blue-900 selection:text-white">
{/* Header */}
<header className="bg-white border-b border-slate-200 px-8 py-6">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-start sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Paramètres du compte</h1>
<p className="text-sm text-slate-500 mt-1">Gestion de la sécurité et informations contractuelles</p>
</div>
</div>
</header>
{/* Contenu Principal */}
<main className="max-w-7xl mx-auto px-8 py-8 space-y-8">
<div className="grid grid-cols-1 lg:grid-cols-2 gap-8">
{/* Colonne 1 : Sécurité */}
<div className="space-y-8">
<section className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4 border-b border-slate-100 pb-2 flex items-center">
<svg className="w-5 h-5 mr-2 text-slate-400" 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>
Sécurité de l'accès
</h2>
<div className="space-y-6">
{/* MFA Status */}
<div className="flex items-center justify-between">
<div>
<p className="text-sm font-medium text-slate-900">Authentification à double facteur (MFA)</p>
<p className="text-xs text-slate-500 mt-1">Exigée par les politiques de sécurité GISE</p>
</div>
<span className="inline-flex items-center px-2.5 py-1 rounded-full text-xs font-semibold bg-emerald-50 text-emerald-700 border border-emerald-200">
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1.5"></span>
Actif
</span>
</div>
<div className="pt-4 border-t border-slate-100">
<button className="text-sm font-medium text-blue-900 hover:text-blue-800 bg-blue-50 hover:bg-blue-100 px-4 py-2 rounded-lg transition-colors duration-200 border border-blue-100">
Générer de nouveaux codes de secours
</button>
</div>
{/* Mot de passe */}
<div className="pt-4 border-t border-slate-100">
<p className="text-sm font-medium text-slate-900 mb-2">Mot de passe institutionnel</p>
<p className="text-xs text-slate-500 mb-4">Dernière modification : Il y a 43 jours</p>
<button className="text-sm font-medium text-slate-700 hover:text-slate-900 bg-white hover:bg-slate-50 border border-slate-300 px-4 py-2 rounded-lg transition-colors duration-200 shadow-sm">
Modifier le mot de passe
</button>
</div>
</div>
</section>
</div>
{/* Colonne 2 : Organisation & Contrat */}
<div className="space-y-8">
<section className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4 border-b border-slate-100 pb-2 flex items-center">
<svg className="w-5 h-5 mr-2 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" /></svg>
Profil de l'Organisation
</h2>
<dl className="space-y-4 text-sm">
<div>
<dt className="text-slate-500 font-medium">Entité Légale</dt>
<dd className="mt-1 font-semibold text-slate-900">Cabinet Juridique Example & Associés</dd>
</div>
<div>
<dt className="text-slate-500 font-medium">Administrateur du compte</dt>
<dd className="mt-1 text-slate-900">Direction Générale (direction@example.com)</dd>
</div>
<div>
<dt className="text-slate-500 font-medium">Niveau d'Infogérance (SLA)</dt>
<dd className="mt-1 flex items-center">
<span className="px-2 py-1 bg-blue-900 text-white text-xs font-bold rounded shadow-sm mr-2 tracking-wider">VIP PLATINUM</span>
<span className="text-slate-700 font-medium">Couverture 24/7 (99.99%)</span>
</dd>
</div>
</dl>
</section>
</div>
</div>
</main>
</div>
);
}
+221
View File
@@ -0,0 +1,221 @@
import React, { useState } from 'react';
import { pb } from '../../config/pocketbase';
import * as OTPAuth from 'otpauth';
import { QRCodeSVG } from 'qrcode.react';
interface LoginProps {
onLoginSuccess: () => void;
}
export default function Login({ onLoginSuccess }: LoginProps) {
const [email, setEmail] = useState('');
const [password, setPassword] = useState('');
const [mfaCode, setMfaCode] = useState('');
const [error, setError] = useState('');
const [isLoading, setIsLoading] = useState(false);
const [step, setStep] = useState<1 | 2>(1);
// Nouveaux états pour la cryptographie TOTP
const [userId, setUserId] = useState('');
const [totpSecret, setTotpSecret] = useState('');
const [qrUrl, setQrUrl] = useState('');
const [isFirstSetup, setIsFirstSetup] = useState(false);
const handleLogin = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// 1. Authentification PocketBase
const authData = await pb.collection('aegis_users').authWithPassword(email, password);
// 2. Vérification du MFA
if (authData.record.mfa_enabled) {
setUserId(authData.record.id);
// Si le client n'a pas encore configuré son MFA
if (!authData.record.totp_secret) {
// Génération cryptographique native pour le navigateur
const totp = new OTPAuth.TOTP({
issuer: 'AEGIS by GISE',
label: email,
algorithm: 'SHA1',
digits: 6,
period: 30,
secret: new OTPAuth.Secret({ size: 20 })
});
const newSecret = totp.secret.base32;
const otpauth = totp.toString(); // Génère l'URL pour le QR Code
setTotpSecret(newSecret);
setQrUrl(otpauth);
setIsFirstSetup(true);
} else {
// Le client l'a déjà configuré dans le passé
setTotpSecret(authData.record.totp_secret);
setIsFirstSetup(false);
}
setStep(2);
} else {
onLoginSuccess();
}
} catch (err: any) {
console.error("Erreur d'authentification", err);
setError("Identifiants institutionnels incorrects ou accès révoqué.");
pb.authStore.clear();
} finally {
setIsLoading(false);
}
};
const handleFinalStep = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsLoading(true);
try {
// On recrée l'instance TOTP avec le secret pour vérifier le code saisi
const totp = new OTPAuth.TOTP({
issuer: 'AEGIS by GISE',
label: email,
algorithm: 'SHA1',
digits: 6,
period: 30,
secret: OTPAuth.Secret.fromBase32(totpSecret)
});
// Validation : Retourne un nombre si valide, null sinon (tolérance de 1 fenêtre de 30s)
const isValid = totp.validate({ token: mfaCode, window: 1 }) !== null;
if (isValid) {
// Si c'était la première configuration, on sauvegarde le secret en base de données
if (isFirstSetup) {
await pb.collection('aegis_users').update(userId, {
totp_secret: totpSecret
});
}
onLoginSuccess(); // La porte du coffre s'ouvre !
} else {
setError("Code de sécurité invalide ou expiré.");
setMfaCode('');
}
} catch (err) {
console.error(err);
setError("Une erreur critique est survenue lors de la vérification.");
} finally {
setIsLoading(false);
}
};
const handleCancelMFA = () => {
pb.authStore.clear();
setStep(1);
setPassword('');
setMfaCode('');
setError('');
setIsFirstSetup(false);
};
return (
<div className="min-h-screen bg-slate-50 flex flex-col justify-center py-12 sm:px-6 lg:px-8 font-sans selection:bg-blue-900 selection:text-white">
<div className="sm:mx-auto sm:w-full sm:max-w-md">
<div className="flex justify-center">
<div className="h-12 w-12 bg-blue-900 rounded-lg flex items-center justify-center shadow-sm border border-blue-800">
<svg className="w-8 h-8 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
</div>
<h2 className="mt-6 text-center text-3xl font-bold tracking-tight text-slate-900">
AEGIS <span className="font-light text-slate-500">by GISE</span>
</h2>
<p className="mt-2 text-center text-sm text-slate-500 uppercase tracking-widest font-semibold">
Accès restreint
</p>
</div>
<div className="mt-8 sm:mx-auto sm:w-full sm:max-w-md">
<div className="bg-white py-8 px-4 shadow-sm border border-slate-200 rounded-xl sm:px-10">
{step === 1 ? (
<form className="space-y-6" onSubmit={handleLogin}>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm font-medium animate-in fade-in slide-in-from-top-1">
{error}
</div>
)}
<div>
<label htmlFor="email" className="block text-sm font-medium text-slate-700">Identifiant institutionnel</label>
<div className="mt-1">
<input id="email" type="email" value={email} onChange={(e) => setEmail(e.target.value)} required className="block w-full appearance-none rounded-md border border-slate-300 px-3 py-2 placeholder-slate-400 shadow-sm focus:border-blue-900 focus:outline-none focus:ring-blue-900 sm:text-sm" placeholder="direction@client.com" />
</div>
</div>
<div>
<label htmlFor="password" className="block text-sm font-medium text-slate-700">Mot de passe</label>
<div className="mt-1">
<input id="password" type="password" value={password} onChange={(e) => setPassword(e.target.value)} required className="block w-full appearance-none rounded-md border border-slate-300 px-3 py-2 placeholder-slate-400 shadow-sm focus:border-blue-900 focus:outline-none focus:ring-blue-900 sm:text-sm" placeholder="••••••••••••" />
</div>
</div>
<div>
<button type="submit" disabled={isLoading} className="flex w-full justify-center rounded-md border border-transparent bg-blue-900 py-2.5 px-4 text-sm font-medium text-white shadow-sm hover:bg-blue-800 focus:outline-none transition-colors disabled:opacity-70">
{isLoading ? 'Chiffrement en cours...' : 'Authentification'}
</button>
</div>
</form>
) : (
<form className="space-y-6 animate-in fade-in slide-in-from-right-4 duration-300" onSubmit={handleFinalStep}>
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm font-medium animate-in fade-in slide-in-from-top-1">
{error}
</div>
)}
{isFirstSetup ? (
<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">
<QRCodeSVG value={qrUrl} size={150} />
</div>
</div>
) : (
<div className="text-center mb-6">
<p className="text-sm font-medium text-slate-900">Validation à double facteur</p>
<p className="text-xs text-slate-500 mt-1">Saisissez le code généré par votre application d'authentification.</p>
</div>
)}
<div>
<input
type="text"
value={mfaCode}
onChange={(e) => setMfaCode(e.target.value)}
required
maxLength={6}
className="block w-full appearance-none rounded-md border border-slate-300 px-3 py-3 text-center text-2xl tracking-[0.5em] text-slate-900 placeholder-slate-300 shadow-sm focus:border-blue-900 focus:outline-none font-mono"
placeholder="000000"
/>
</div>
<div className="flex space-x-3">
<button type="button" onClick={handleCancelMFA} disabled={isLoading} className="flex w-1/3 justify-center rounded-md border border-slate-300 bg-white py-2.5 px-4 text-sm font-medium text-slate-700 shadow-sm hover:bg-slate-50 transition-colors disabled:opacity-70">
Annuler
</button>
<button type="submit" disabled={isLoading || mfaCode.length !== 6} className="flex w-2/3 justify-center rounded-md border border-transparent bg-emerald-600 py-2.5 px-4 text-sm font-medium text-white shadow-sm hover:bg-emerald-700 transition-colors disabled:opacity-70">
{isLoading ? 'Vérification...' : 'Déverrouiller'}
</button>
</div>
</form>
)}
</div>
</div>
</div>
);
}
+216
View File
@@ -0,0 +1,216 @@
import React, { useState } from 'react';
import { pb } from '../../config/pocketbase';
interface NewTicketProps {
onCancel: () => void;
onTicketCreated: () => void; // Rappel pour recharger la liste et revenir en arrière
}
export default function NewTicket({ onCancel, onTicketCreated }: NewTicketProps) {
const [category, setCategory] = useState<'Incident Critique' | 'Évolution' | 'Administratif'>('Incident Critique');
const [ci, Ci] = useState('general');
const [subject, setSubject] = useState('');
const [affectedAsset, setAffectedAsset] = useState('');
const [incidentTime, setIncidentTime] = useState('');
const [description, setDescription] = useState('');
const [isSubmitting, setIsSubmitting] = useState(false);
const [error, setError] = useState('');
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setError('');
setIsSubmitting(true);
try {
const currentUser = pb.authStore.record;
if (!currentUser) throw new Error("Utilisateur non authentifié");
// Construction de la description enrichie avec les métadonnées techniques
let fullDescription = description;
if (affectedAsset.trim()) {
fullDescription = `[Équipement concerné: ${affectedAsset}]\n` + fullDescription;
}
if (category === 'Incident Critique' && incidentTime) {
fullDescription = `[Heure de l'incident: ${new Date(incidentTime).toLocaleString('fr-FR')}]\n` + fullDescription;
}
// Enregistrement dans PocketBase
await pb.collection('aegis_tickets').create({
author: currentUser.id,
company: currentUser.company,
category,
subject,
description: fullDescription,
status: 'Ouvert',
});
onTicketCreated(); // Retourne à la liste et rafraîchit
} catch (err) {
console.error("Erreur lors de la création du ticket :", err);
setError("Impossible d'enregistrer le ticket sur le serveur sécurisé.");
} finally {
setIsSubmitting(false);
}
};
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900 pb-12">
{/* Header */}
<header className="bg-white border-b border-slate-200 px-8 py-4 sticky top-0 z-10 shadow-sm">
<div className="max-w-7xl mx-auto flex items-center">
<button
type="button"
onClick={onCancel}
className="mr-4 p-2 text-slate-400 hover:text-slate-900 hover:bg-slate-50 rounded-full transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
</svg>
</button>
<div>
<h1 className="text-xl font-bold text-slate-900 tracking-tight">Ouvrir un nouveau ticket</h1>
<p className="text-sm text-slate-500 mt-0.5">Qualification de votre requête ITSM</p>
</div>
</div>
</header>
{/* Formulaire Principal */}
<main className="max-w-4xl mx-auto px-8 pt-8">
<form onSubmit={handleSubmit} className="space-y-8 bg-white rounded-xl shadow-sm border border-slate-200 p-8">
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 px-4 py-3 rounded-md text-sm font-medium">
{error}
</div>
)}
{/* Section 1 : Qualification */}
<div>
<h2 className="text-base font-semibold text-slate-900 mb-4 border-b border-slate-100 pb-2">1. Qualification de la demande</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="category" className="block text-sm font-medium text-slate-700 mb-1">Type de demande</label>
<select
id="category"
value={category}
onChange={(e) => setCategory(e.target.value as any)}
className="block w-full rounded-md border border-slate-300 py-2.5 px-3 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900 bg-white"
>
<option value="Incident Critique">Incident Critique (Panne, Dégradation)</option>
<option value="Évolution">Évolution (Ajout de ressources, Modification)</option>
<option value="Administratif">Administratif (Facturation, Contrat)</option>
</select>
</div>
<div>
<label htmlFor="ci" className="block text-sm font-medium text-slate-700 mb-1">Infrastructure concernée (CI)</label>
<select
id="ci"
value={ci}
onChange={(e) => Ci(e.target.value)}
className="block w-full rounded-md border border-slate-300 py-2.5 px-3 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900 bg-white"
>
<option value="general">Général / Non spécifique</option>
<option value="px1">Serveur Proxmox Principal (Hyperviseur)</option>
<option value="fw1">Pare-feu Périphérique (WAN)</option>
<option value="db1">Base de données MariaDB</option>
</select>
</div>
</div>
</div>
{/* Section 2 : Détails techniques & Actifs */}
<div>
<h2 className="text-base font-semibold text-slate-900 mb-4 border-b border-slate-100 pb-2">2. Détails techniques</h2>
<div className="space-y-6">
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label htmlFor="subject" className="block text-sm font-medium text-slate-700 mb-1">Sujet de l'intervention <span className="text-blue-900 font-bold">*</span></label>
<input
type="text"
id="subject"
required
value={subject}
onChange={(e) => setSubject(e.target.value)}
placeholder="Ex: Perte de paquets sur le lien WAN principal"
className="block w-full rounded-md border border-slate-300 py-2.5 px-3 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900"
/>
</div>
<div>
<label htmlFor="affectedAsset" className="block text-sm font-medium text-slate-700 mb-1">
Équipement ou Produit concerné <span className="text-slate-400 font-normal">(Optionnel)</span>
</label>
<input
type="text"
id="affectedAsset"
value={affectedAsset}
onChange={(e) => setAffectedAsset(e.target.value)}
placeholder="Ex: Ordinateur Direction, Imprimante X, etc."
className="block w-full rounded-md border border-slate-300 py-2.5 px-3 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900"
/>
</div>
</div>
{category === 'Incident Critique' && (
<div>
<label htmlFor="incidentTime" className="block text-sm font-medium text-slate-700 mb-1">
Heure exacte du début de l'incident <span className="text-red-500 font-bold">*</span>
</label>
<input
type="datetime-local"
id="incidentTime"
required
value={incidentTime}
onChange={(e) => setIncidentTime(e.target.value)}
className="block w-full max-w-md rounded-md border border-slate-300 py-2.5 px-3 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900 bg-white"
/>
</div>
)}
<div>
<label htmlFor="description" className="block text-sm font-medium text-slate-700 mb-1">
Description détaillée <span className="text-blue-900 font-bold">*</span>
</label>
<textarea
id="description"
required
rows={6}
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Décrivez précisément votre besoin ou les symptômes observés..."
className="block w-full rounded-md border border-slate-300 py-2.5 px-3 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900 resize-y"
/>
</div>
</div>
</div>
{/* Actions */}
<div className="pt-4 flex justify-end space-x-3 border-t border-slate-100">
<button
type="button"
onClick={onCancel}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50 transition-colors"
>
Annuler
</button>
<button
type="submit"
disabled={isSubmitting}
className={`px-6 py-2 text-sm font-medium text-white rounded-lg transition-colors shadow-sm ${
category === 'Incident Critique' ? 'bg-red-600 hover:bg-red-700 focus:ring-red-600' : 'bg-blue-900 hover:bg-blue-800 focus:ring-blue-900'
} focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50`}
>
{isSubmitting ? 'Transmission en cours...' : 'Soumettre le ticket'}
</button>
</div>
</form>
</main>
</div>
);
}
+154
View File
@@ -0,0 +1,154 @@
import React, { 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
interface Ticket {
id: string;
subject: string;
category: string;
status: 'Ouvert' | 'En analyse' | 'Résolu';
created: string;
}
export default function ServiceDesk() {
const [tickets, setTickets] = useState<Ticket[]>([]);
const [isLoading, setIsLoading] = useState(true);
// États de navigation interne au module Service Desk
const [currentView, setCurrentView] = useState<'list' | 'new' | 'detail'>('list');
const [selectedTicketId, setSelectedTicketId] = useState<string | null>(null);
// Récupération des tickets de l'entreprise connectée
const fetchTickets = async () => {
try {
const records = await pb.collection('aegis_tickets').getFullList<Ticket>({
sort: '-created', // Du plus récent au plus ancien
});
setTickets(records);
} catch (error) {
console.error("Erreur lors de la récupération des tickets :", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchTickets();
}, []);
// Gestion du clic sur un ticket pour ouvrir la discussion
const handleSelectTicket = (id: string) => {
setSelectedTicketId(id);
setCurrentView('detail');
};
// Styles visuels pour les statuts
const getStatusStyle = (status: Ticket['status']) => {
switch (status) {
case 'Ouvert': return 'bg-blue-50 text-blue-700 border-blue-100';
case 'En analyse': return 'bg-amber-50 text-amber-700 border-amber-100';
case 'Résolu': return 'bg-emerald-50 text-emerald-700 border-emerald-100';
default: return 'bg-slate-50 text-slate-700 border-slate-100';
}
};
// Rendu conditionnel selon la vue active
if (currentView === 'new') {
return (
<NewTicket
onCancel={() => setCurrentView('list')}
onTicketCreated={() => {
setCurrentView('list');
fetchTickets(); // On recharge la liste pour afficher le nouveau ticket
}}
/>
);
}
if (currentView === 'detail' && selectedTicketId) {
return (
<TicketDetail
ticketId={selectedTicketId}
onBack={() => {
setCurrentView('list');
setSelectedTicketId(null);
fetchTickets();
}}
/>
);
}
// Vue par défaut : La liste des tickets
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">Centre de Support (Service Desk)</h1>
<p className="text-sm text-slate-500 mt-1">Canal de communication sécurisé avec vos experts GISE.</p>
</div>
<div className="flex items-center space-x-3">
<button
onClick={() => setCurrentView('new')}
className="bg-blue-900 hover:bg-blue-800 text-white px-4 py-2.5 rounded-lg text-sm font-medium transition-colors shadow-sm"
>
Ouvrir un ticket sécurisé
</button>
</div>
</div>
</header>
<main className="max-w-7xl mx-auto px-8 py-8">
{/* Liste des tickets */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
<div className="p-6 border-b border-slate-100">
<h2 className="text-lg font-semibold text-slate-900">Requêtes en cours et historiques</h2>
</div>
{isLoading ? (
<div className="flex justify-center items-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-900"></div>
</div>
) : tickets.length === 0 ? (
<div className="text-center py-12 text-sm text-slate-500 italic">
Aucun ticket enregistré pour le moment.
</div>
) : (
<div className="divide-y divide-slate-100">
{tickets.map((ticket) => (
<div
key={ticket.id}
onClick={() => handleSelectTicket(ticket.id)}
className="p-6 flex items-center justify-between hover:bg-slate-50 transition-colors cursor-pointer group"
>
<div className="space-y-1">
<div className="flex items-center space-x-3">
<span className="text-xs font-semibold px-2 py-0.5 bg-slate-100 text-slate-700 rounded border border-slate-200">
{ticket.category}
</span>
<span className="text-xs text-slate-400">
Ouvert le {new Date(ticket.created).toLocaleDateString('fr-FR')}
</span>
</div>
<h3 className="text-sm font-semibold text-slate-900 group-hover:text-blue-900 transition-colors">
{ticket.subject}
</h3>
</div>
<div>
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${getStatusStyle(ticket.status)}`}>
{ticket.status}
</span>
</div>
</div>
))}
</div>
)}
</div>
</main>
</div>
);
}
+200
View File
@@ -0,0 +1,200 @@
import React, { useState, useEffect } from 'react';
import { pb } from '../../config/pocketbase';
interface Ticket {
id: string;
subject: string;
description: string;
category: string;
status: 'Ouvert' | 'En analyse' | 'Résolu';
created: string;
}
interface Message {
id: string;
content: string;
created: string;
expand?: {
author?: {
name?: string;
email?: string;
avatar?: string;
};
};
}
interface TicketDetailProps {
ticketId: string;
onBack: () => void;
}
export default function TicketDetail({ ticketId, onBack }: TicketDetailProps) {
const [ticket, setTicket] = useState<Ticket | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [newMessage, setNewMessage] = useState('');
const [isLoading, setIsLoading] = useState(true);
const [isSending, setIsSending] = useState(false);
const currentUser = pb.authStore.record;
// Récupération du ticket et de ses messages associés
const fetchTicketData = async () => {
try {
const ticketData = await pb.collection('aegis_tickets').getOne<Ticket>(ticketId);
setTicket(ticketData);
const messageRecords = await pb.collection('ticket_messages').getFullList<Message>({
filter: `ticket = "${ticketId}"`,
expand: 'author',
sort: 'created',
});
setMessages(messageRecords);
} catch (error) {
console.error("Erreur lors du chargement de la discussion :", error);
} finally {
setIsLoading(false);
}
};
useEffect(() => {
fetchTicketData();
}, [ticketId]);
// Envoi d'un nouveau message dans la discussion
const handleSendMessage = async (e: React.FormEvent) => {
e.preventDefault();
if (!newMessage.trim() || !currentUser) return;
setIsSending(true);
try {
await pb.collection('ticket_messages').create({
ticket: ticketId,
author: currentUser.id,
content: newMessage.trim(),
});
setNewMessage('');
await fetchTicketData(); // Recharger les messages
} catch (error) {
console.error("Erreur lors de l'envoi du message :", error);
alert("Impossible d'envoyer le message.");
} finally {
setIsSending(false);
}
};
if (isLoading) {
return (
<div className="flex justify-center items-center py-20">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-900"></div>
</div>
);
}
if (!ticket) {
return (
<div className="bg-white rounded-xl p-6 text-center border border-slate-200">
<p className="text-sm text-slate-500 mb-4">Ticket introuvable ou accès non autorisé.</p>
<button onClick={onBack} className="px-4 py-2 bg-slate-900 text-white rounded-lg text-sm font-medium">
Retour aux tickets
</button>
</div>
);
}
return (
<div className="space-y-6 max-w-4xl mx-auto">
{/* Barre de navigation / Retour */}
<div className="flex items-center justify-between bg-white p-4 rounded-xl border border-slate-200 shadow-sm">
<button
onClick={onBack}
className="flex items-center space-x-2 text-sm font-medium text-slate-600 hover:text-slate-900 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M15 19l-7-7 7-7" />
</svg>
<span>Retour au Centre de Support</span>
</button>
<div className="flex items-center space-x-3">
<span className="text-xs font-semibold px-2.5 py-1 bg-slate-100 text-slate-700 rounded border border-slate-200 uppercase">
{ticket.category}
</span>
<span className={`px-3 py-1 rounded-full text-xs font-medium border ${
ticket.status === 'Résolu' ? 'bg-emerald-50 text-emerald-700 border-emerald-100' : 'bg-blue-50 text-blue-700 border-blue-100'
}`}>
{ticket.status}
</span>
</div>
</div>
{/* En-tête du ticket & Description initiale */}
<div className="bg-white p-6 rounded-xl border border-slate-200 shadow-sm space-y-4">
<h1 className="text-xl font-bold text-slate-900">{ticket.subject}</h1>
<div className="bg-slate-50 p-4 rounded-lg border border-slate-100 text-sm text-slate-700 whitespace-pre-wrap">
<p className="text-xs font-semibold text-slate-400 mb-1">Description initiale :</p>
{ticket.description}
</div>
<p className="text-xs text-slate-400">
Créé le {new Date(ticket.created).toLocaleString('fr-FR')}
</p>
</div>
{/* Fil de discussion / Messages */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden flex flex-col h-[500px]">
<div className="p-4 border-b border-slate-100 bg-slate-50/50">
<h2 className="text-sm font-semibold text-slate-800">Échanges avec l'équipe GISE</h2>
</div>
{/* Liste des messages */}
<div className="flex-1 overflow-y-auto p-6 space-y-4">
{messages.length === 0 ? (
<div className="text-center py-12 text-sm text-slate-400 italic">
Aucun message pour l'instant. Démarrez la conversation ci-dessous.
</div>
) : (
messages.map((msg) => {
const isMyMessage = msg.author === currentUser?.id;
const authorName = msg.expand?.author?.name || msg.expand?.author?.email || 'Expert GISE';
return (
<div key={msg.id} className={`flex flex-col ${isMyMessage ? 'items-end' : 'items-start'}`}>
<div className="flex items-center space-x-2 mb-1">
<span className="text-xs font-medium text-slate-600">{authorName}</span>
<span className="text-[10px] text-slate-400">
{new Date(msg.created).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
<div className={`max-w-[80%] rounded-2xl px-4 py-3 text-sm shadow-sm ${
isMyMessage
? 'bg-blue-900 text-white rounded-br-none'
: 'bg-slate-100 text-slate-800 rounded-bl-none border border-slate-200/60'
}`}>
{msg.content}
</div>
</div>
);
})
)}
</div>
{/* Formulaire de réponse */}
<form onSubmit={handleSendMessage} className="p-4 border-t border-slate-100 bg-white flex items-center space-x-3">
<input
type="text"
value={newMessage}
onChange={(e) => setNewMessage(e.target.value)}
placeholder="Écrivez votre message ou complément d'information..."
className="flex-1 rounded-lg border border-slate-300 px-4 py-2.5 text-sm focus:border-blue-900 focus:outline-none focus:ring-1 focus:ring-blue-900"
/>
<button
type="submit"
disabled={isSending || !newMessage.trim()}
className="bg-blue-900 hover:bg-blue-800 text-white px-5 py-2.5 rounded-lg text-sm font-medium transition-colors disabled:opacity-50 shadow-sm"
>
{isSending ? 'Envoi...' : 'Envoyer'}
</button>
</form>
</div>
</div>
);
}
+200
View File
@@ -0,0 +1,200 @@
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
onEvolutionClick={() => setActiveModal('evolution')}
onServiceClick={(service) => {
setSelectedService(service);
setActiveModal('analytics');
}}
/>
{/* 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>
);
}
+119
View File
@@ -0,0 +1,119 @@
import { useState } from 'react';
// Données fictives pour le maquettage visuel
const mockDocuments = [
{ id: 'DOC-102', title: 'Rapport d\'Audit de Vulnérabilité Q2', category: 'Audit', date: '12 Juil. 2026', size: '2.4 MB', encrypted: true },
{ id: 'DOC-101', title: 'Facture Infogérance - Juin 2026', category: 'Facture', date: '01 Juil. 2026', size: '1.1 MB', encrypted: true },
{ id: 'DOC-095', title: 'Certificat de Conformité ISO 27001', category: 'Rapport de conformité', date: '15 Jan. 2026', size: '4.8 MB', encrypted: true }
];
export default function DocumentVault() {
const [searchTerm, setSearchTerm] = useState('');
const getCategoryBadge = (category: string) => {
switch (category) {
case 'Audit': return 'bg-purple-50 text-purple-700 border-purple-200';
case 'Facture': return 'bg-slate-100 text-slate-700 border-slate-200';
case 'Rapport de conformité': return 'bg-emerald-50 text-emerald-700 border-emerald-200';
default: return 'bg-blue-50 text-blue-700 border-blue-200';
}
};
return (
<div className="min-h-screen bg-slate-50 font-sans text-slate-900 selection:bg-blue-900 selection:text-white">
{/* Header du Coffre-Fort */}
<header className="bg-white border-b border-slate-200 px-8 py-6">
<div className="max-w-7xl mx-auto flex flex-col sm:flex-row justify-between items-start sm:items-center space-y-4 sm:space-y-0">
<div>
<h1 className="text-2xl font-bold text-slate-900 tracking-tight">Coffre-Fort Documentaire</h1>
<p className="text-sm text-slate-500 mt-1 flex items-center">
<svg className="w-4 h-4 mr-1.5 text-emerald-500" 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>
Espace de stockage chiffré de bout en bout
</p>
</div>
</div>
</header>
{/* Contenu Principal */}
<main className="max-w-7xl mx-auto px-8 py-8">
{/* Barre de recherche (Visuelle) */}
<div className="mb-6 flex">
<div className="relative flex-1 max-w-lg">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<svg className="h-5 w-5 text-slate-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<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>
<input
type="text"
placeholder="Rechercher un document, une facture..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="block w-full pl-10 pr-3 py-2 border border-slate-300 rounded-lg leading-5 bg-white placeholder-slate-400 focus:outline-none focus:ring-1 focus:ring-blue-900 focus:border-blue-900 sm:text-sm shadow-sm"
/>
</div>
</div>
{/* Liste des Documents (La Carte) */}
<div className="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th scope="col" className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
Nom du fichier
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
Catégorie
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-semibold text-slate-500 uppercase tracking-wider">
Ajouté le
</th>
<th scope="col" className="px-6 py-3 text-right text-xs font-semibold text-slate-500 uppercase tracking-wider">
Action
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-slate-200">
{mockDocuments.map((doc) => (
<tr key={doc.id} className="hover:bg-slate-50 transition-colors">
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<svg className="flex-shrink-0 h-6 w-6 text-slate-400 mr-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="1.5" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
<div>
<div className="text-sm font-medium text-slate-900">{doc.title}</div>
<div className="text-xs text-slate-500">{doc.size} PDF</div>
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium border ${getCategoryBadge(doc.category)}`}>
{doc.category}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap text-sm text-slate-500">
{doc.date}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<button className="text-blue-900 hover:text-blue-700 bg-blue-50 hover:bg-blue-100 px-3 py-1.5 rounded flex items-center justify-end ml-auto transition-colors">
<svg className="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth="2" d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Télécharger
</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</main>
</div>
);
}