init aegis
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user