init aegis
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
```
|
||||
@@ -0,0 +1,22 @@
|
||||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
+13
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>AEGIS by gise</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
Generated
+3296
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"name": "aegis",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"@tailwindcss/vite": "^4.3.3",
|
||||
"otpauth": "^9.5.1",
|
||||
"pocketbase": "^0.27.0",
|
||||
"qrcode.react": "^4.2.0",
|
||||
"react": "^19.2.7",
|
||||
"react-dom": "^19.2.7",
|
||||
"tailwindcss": "^4.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^10.0.1",
|
||||
"@types/node": "^24.13.2",
|
||||
"@types/react": "^19.2.17",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.3",
|
||||
"eslint": "^10.6.0",
|
||||
"eslint-plugin-react-hooks": "^7.1.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.3",
|
||||
"globals": "^17.7.0",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.62.0",
|
||||
"vite": "^8.1.1"
|
||||
}
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
@@ -0,0 +1,24 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
+184
@@ -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
@@ -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 |
@@ -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 |
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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);
|
||||
@@ -0,0 +1,5 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>,
|
||||
)
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
export default {
|
||||
content: [
|
||||
"./index.html",
|
||||
"./src/**/*.{js,ts,jsx,tsx}",
|
||||
],
|
||||
theme: {
|
||||
extend: {},
|
||||
},
|
||||
plugins: [],
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"allowArbitraryExtensions": true,
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"module": "nodenext",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [
|
||||
react(),
|
||||
tailwindcss()
|
||||
],
|
||||
})
|
||||
Reference in New Issue
Block a user