Modification site web
This commit is contained in:
@@ -0,0 +1 @@
|
||||
node_modules
|
||||
+456
-727
File diff suppressed because it is too large
Load Diff
+19
-14
@@ -1,17 +1,22 @@
|
||||
{
|
||||
"name": "SafeAccess — Gestion de badges",
|
||||
"short_name": "SafeAccess",
|
||||
"description": "Gestion de badges d'accès NFC, RFID, QR Code et Bluetooth avec 2FA",
|
||||
"start_url": "/index.html",
|
||||
"name": "CyberusGate Access",
|
||||
"short_name": "CG Access",
|
||||
"start_url": "index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#0D2B5E",
|
||||
"theme_color": "#1565C0",
|
||||
"orientation": "portrait",
|
||||
"background_color": "#070c16",
|
||||
"theme_color": "#070c16",
|
||||
"icons": [
|
||||
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
|
||||
],
|
||||
"categories": ["security", "utilities", "business"],
|
||||
"lang": "fr",
|
||||
"scope": "/"
|
||||
}
|
||||
{
|
||||
"src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDBkMmZmIiBzdHJva2Utd2lkdGg9IjIiPjxwYXRoIGQ9Ik0xMiAyMnM4LTQgOC0xMFY1bC04LTMtOCAzdjdjMCA2IDggMTAgOCAxMHoiPjwvcGF0aD48L3N2Zz4=",
|
||||
"sizes": "192x192",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCIgZmlsbD0ibm9uZSIgc3Ryb2tlPSIjMDBkMmZmIiBzdHJva2Utd2lkdGg9IjIiPjxwYXRoIGQ9Ik0xMiAyMnM4LTQgOC0xMFY1bC04LTMtOCAzdjdjMCA2IDggMTAgOCAxMHoiPjwvcGF0aD48L3N2Zz4=",
|
||||
"sizes": "512x512",
|
||||
"type": "image/svg+xml",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,12 +1,10 @@
|
||||
const CACHE = 'safeaccess-v1';
|
||||
const ASSETS = ['/', '/index.html', '/manifest.json'];
|
||||
const CACHE_NAME = 'cyberus-cache-v3';
|
||||
const urlsToCache = ['./', './index.html', './manifest.json'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
|
||||
self.addEventListener('install', event => {
|
||||
event.waitUntil(caches.open(CACHE_NAME).then(cache => cache.addAll(urlsToCache)));
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(r => r || fetch(e.request)).catch(() => caches.match('/index.html'))
|
||||
);
|
||||
});
|
||||
self.addEventListener('fetch', event => {
|
||||
event.respondWith(caches.match(event.request).then(response => response || fetch(event.request)));
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 53 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 902 B |
Binary file not shown.
|
After Width: | Height: | Size: 2.7 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 989 B |
Binary file not shown.
|
After Width: | Height: | Size: 3.6 KiB |
+844
@@ -0,0 +1,844 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="fr">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
||||
<meta name="theme-color" content="#1565C0">
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
<meta name="apple-mobile-web-app-title" content="SafeAccess">
|
||||
<title>SafeAccess</title>
|
||||
<link rel="manifest" href="manifest.json">
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png">
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png">
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png">
|
||||
<link rel="manifest" href="/site.webmanifest">
|
||||
<style>
|
||||
:root {
|
||||
--blue-dark: #0D2B5E;
|
||||
--blue-main: #1565C0;
|
||||
--blue-light: #1E88E5;
|
||||
--blue-pale: #E3F2FD;
|
||||
--green: #2E7D32;
|
||||
--green-light: #E8F5E9;
|
||||
--red: #C62828;
|
||||
--red-light: #FFEBEE;
|
||||
--orange: #E65100;
|
||||
--gray-1: #F5F6FA;
|
||||
--gray-2: #E8ECF1;
|
||||
--gray-3: #B0BEC5;
|
||||
--gray-4: #607D8B;
|
||||
--gray-5: #37474F;
|
||||
--white: #FFFFFF;
|
||||
--text-main: #1A2332;
|
||||
--text-sub: #546E7A;
|
||||
--radius: 14px;
|
||||
--radius-sm: 8px;
|
||||
--shadow: 0 2px 16px rgba(0,0,0,0.10);
|
||||
--shadow-lg: 0 8px 32px rgba(0,0,0,0.16);
|
||||
}
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; -webkit-tap-highlight-color: transparent; }
|
||||
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--gray-1); color: var(--text-main); min-height: 100vh; overflow-x: hidden; }
|
||||
|
||||
/* SCREENS */
|
||||
.screen { display: none; min-height: 100vh; flex-direction: column; }
|
||||
.screen.active { display: flex; }
|
||||
|
||||
/* ======= SPLASH / LOGIN ======= */
|
||||
#screen-login { background: linear-gradient(145deg, #0D2B5E 0%, #1565C0 60%, #1E88E5 100%); align-items: center; justify-content: center; padding: 40px 24px; }
|
||||
.login-logo { width: 72px; height: 72px; background: rgba(255,255,255,0.15); border-radius: 20px; display: flex; align-items: center; justify-content: center; margin-bottom: 16px; border: 2px solid rgba(255,255,255,0.3); }
|
||||
.login-logo svg { width: 40px; height: 40px; fill: white; }
|
||||
.login-title { font-size: 28px; font-weight: 700; color: white; letter-spacing: -0.5px; margin-bottom: 4px; }
|
||||
.login-sub { font-size: 14px; color: rgba(255,255,255,0.7); margin-bottom: 40px; }
|
||||
.login-card { background: white; border-radius: 20px; padding: 28px 24px; width: 100%; max-width: 380px; box-shadow: var(--shadow-lg); }
|
||||
.login-card h2 { font-size: 18px; font-weight: 600; color: var(--text-main); margin-bottom: 4px; }
|
||||
.login-card p { font-size: 13px; color: var(--text-sub); margin-bottom: 24px; }
|
||||
.form-group { margin-bottom: 16px; }
|
||||
.form-group label { display: block; font-size: 12px; font-weight: 600; color: var(--gray-4); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 6px; }
|
||||
.form-group input { width: 100%; padding: 12px 14px; border: 1.5px solid var(--gray-2); border-radius: var(--radius-sm); font-size: 15px; color: var(--text-main); background: var(--gray-1); outline: none; transition: border-color 0.2s; }
|
||||
.form-group input:focus { border-color: var(--blue-main); background: white; }
|
||||
.btn-primary { width: 100%; padding: 14px; background: var(--blue-main); color: white; border: none; border-radius: var(--radius-sm); font-size: 16px; font-weight: 600; cursor: pointer; transition: background 0.2s, transform 0.1s; }
|
||||
.btn-primary:active { background: var(--blue-dark); transform: scale(0.98); }
|
||||
.btn-outline { width: 100%; padding: 12px; background: transparent; color: var(--blue-main); border: 1.5px solid var(--blue-main); border-radius: var(--radius-sm); font-size: 15px; font-weight: 600; cursor: pointer; margin-top: 10px; transition: all 0.2s; }
|
||||
.btn-outline:active { background: var(--blue-pale); }
|
||||
|
||||
/* ======= 2FA ======= */
|
||||
#screen-2fa { background: var(--gray-1); align-items: center; justify-content: center; padding: 40px 24px; }
|
||||
.mfa-card { background: white; border-radius: 20px; padding: 32px 24px; width: 100%; max-width: 380px; box-shadow: var(--shadow-lg); text-align: center; }
|
||||
.mfa-icon { width: 64px; height: 64px; border-radius: 50%; background: var(--blue-pale); display: flex; align-items: center; justify-content: center; margin: 0 auto 16px; }
|
||||
.mfa-icon svg { width: 32px; height: 32px; }
|
||||
.mfa-card h2 { font-size: 20px; font-weight: 700; margin-bottom: 6px; }
|
||||
.mfa-card p { font-size: 14px; color: var(--text-sub); margin-bottom: 8px; }
|
||||
.mfa-code-display { font-size: 36px; font-weight: 800; letter-spacing: 8px; color: var(--blue-main); margin: 20px 0; font-variant-numeric: tabular-nums; }
|
||||
.mfa-progress { height: 4px; background: var(--gray-2); border-radius: 2px; overflow: hidden; margin-bottom: 20px; }
|
||||
.mfa-progress-bar { height: 100%; background: var(--blue-main); border-radius: 2px; transition: width 1s linear; }
|
||||
.mfa-input-row { display: flex; gap: 8px; justify-content: center; margin-bottom: 20px; }
|
||||
.mfa-digit { width: 44px; height: 52px; border: 2px solid var(--gray-2); border-radius: 10px; font-size: 22px; font-weight: 700; text-align: center; color: var(--text-main); outline: none; transition: border-color 0.2s; }
|
||||
.mfa-digit:focus { border-color: var(--blue-main); }
|
||||
.mfa-digit.filled { border-color: var(--blue-light); background: var(--blue-pale); }
|
||||
.mfa-method-row { display: flex; gap: 8px; margin-top: 12px; }
|
||||
.mfa-method-btn { flex: 1; padding: 10px 8px; border: 1.5px solid var(--gray-2); border-radius: var(--radius-sm); font-size: 12px; font-weight: 600; color: var(--text-sub); background: white; cursor: pointer; transition: all 0.2s; }
|
||||
.mfa-method-btn.active { border-color: var(--blue-main); color: var(--blue-main); background: var(--blue-pale); }
|
||||
|
||||
/* ======= MAIN APP ======= */
|
||||
#screen-app { background: var(--gray-1); }
|
||||
.app-header { background: var(--blue-dark); padding: 48px 20px 20px; display: flex; align-items: center; justify-content: space-between; }
|
||||
.app-header-left { display: flex; align-items: center; gap: 12px; }
|
||||
.app-header-avatar { width: 40px; height: 40px; border-radius: 50%; border: 2px solid rgba(255,255,255,0.3); overflow: hidden; background: var(--blue-light); display: flex; align-items: center; justify-content: center; font-size: 16px; font-weight: 700; color: white; }
|
||||
.app-header-info h1 { font-size: 16px; font-weight: 700; color: white; }
|
||||
.app-header-info p { font-size: 12px; color: rgba(255,255,255,0.65); }
|
||||
.app-header-right { display: flex; gap: 10px; }
|
||||
.icon-btn { width: 38px; height: 38px; border-radius: 50%; background: rgba(255,255,255,0.12); border: none; display: flex; align-items: center; justify-content: center; cursor: pointer; }
|
||||
.icon-btn svg { width: 20px; height: 20px; stroke: white; fill: none; stroke-width: 2; }
|
||||
|
||||
/* NAV TABS */
|
||||
.nav-tabs { display: flex; background: white; border-bottom: 1px solid var(--gray-2); position: sticky; top: 0; z-index: 100; }
|
||||
.nav-tab { flex: 1; padding: 12px 4px; display: flex; flex-direction: column; align-items: center; gap: 4px; font-size: 10px; font-weight: 600; color: var(--gray-3); text-transform: uppercase; letter-spacing: 0.3px; border: none; background: transparent; cursor: pointer; position: relative; transition: color 0.2s; }
|
||||
.nav-tab.active { color: var(--blue-main); }
|
||||
.nav-tab.active::after { content: ''; position: absolute; bottom: 0; left: 20%; right: 20%; height: 2px; background: var(--blue-main); border-radius: 2px 2px 0 0; }
|
||||
.nav-tab svg { width: 22px; height: 22px; stroke: currentColor; fill: none; stroke-width: 2; }
|
||||
|
||||
/* TAB CONTENT */
|
||||
.tab-pane { display: none; padding: 16px; flex-direction: column; gap: 14px; }
|
||||
.tab-pane.active { display: flex; }
|
||||
|
||||
/* BADGE CARD */
|
||||
.badge-card { background: white; border-radius: var(--radius); box-shadow: var(--shadow); overflow: hidden; }
|
||||
.badge-card-header { background: linear-gradient(135deg, var(--blue-dark), var(--blue-main)); padding: 20px; display: flex; align-items: center; gap: 14px; }
|
||||
.badge-avatar { width: 56px; height: 56px; border-radius: 50%; background: rgba(255,255,255,0.2); border: 2px solid rgba(255,255,255,0.4); display: flex; align-items: center; justify-content: center; font-size: 22px; font-weight: 700; color: white; flex-shrink: 0; }
|
||||
.badge-info h2 { font-size: 18px; font-weight: 700; color: white; }
|
||||
.badge-info p { font-size: 13px; color: rgba(255,255,255,0.7); }
|
||||
.badge-id { font-size: 11px; color: rgba(255,255,255,0.5); margin-top: 2px; }
|
||||
.badge-status { display: inline-flex; align-items: center; gap: 6px; padding: 5px 12px; border-radius: 20px; font-size: 12px; font-weight: 700; margin-top: 14px; }
|
||||
.badge-status.ready { background: rgba(46,125,50,0.2); color: #A5D6A7; border: 1px solid rgba(165,214,167,0.3); }
|
||||
.badge-status.offline { background: rgba(198,40,40,0.2); color: #EF9A9A; border: 1px solid rgba(239,154,154,0.3); }
|
||||
.badge-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #69F0AE; animation: pulse 2s infinite; }
|
||||
@keyframes pulse { 0%,100%{opacity:1;} 50%{opacity:0.4;} }
|
||||
|
||||
/* QR ZONE */
|
||||
.qr-zone { padding: 20px; display: flex; flex-direction: column; align-items: center; gap: 12px; }
|
||||
.qr-box { width: 170px; height: 170px; border: 3px solid var(--blue-main); border-radius: 12px; padding: 8px; position: relative; }
|
||||
.qr-corner { position: absolute; width: 18px; height: 18px; border-color: var(--blue-dark); border-style: solid; }
|
||||
.qr-corner.tl { top: -2px; left: -2px; border-width: 3px 0 0 3px; border-radius: 4px 0 0 0; }
|
||||
.qr-corner.tr { top: -2px; right: -2px; border-width: 3px 3px 0 0; border-radius: 0 4px 0 0; }
|
||||
.qr-corner.bl { bottom: -2px; left: -2px; border-width: 0 0 3px 3px; border-radius: 0 0 0 4px; }
|
||||
.qr-corner.br { bottom: -2px; right: -2px; border-width: 0 3px 3px 0; border-radius: 0 0 4px 0; }
|
||||
.qr-canvas { width: 100%; height: 100%; }
|
||||
.badge-system-tag { font-size: 11px; color: var(--gray-4); font-weight: 500; }
|
||||
.badge-actions { padding: 0 16px 16px; display: flex; flex-direction: column; gap: 10px; }
|
||||
.btn-action { padding: 13px; border-radius: var(--radius-sm); font-size: 15px; font-weight: 600; cursor: pointer; border: none; transition: all 0.15s; text-align: center; }
|
||||
.btn-blue { background: var(--blue-main); color: white; }
|
||||
.btn-blue:active { background: var(--blue-dark); transform: scale(0.97); }
|
||||
.btn-ghost { background: transparent; color: var(--blue-main); border: 1.5px solid var(--blue-main); }
|
||||
.btn-ghost:active { background: var(--blue-pale); }
|
||||
|
||||
/* NFC ZONE */
|
||||
.nfc-area { padding: 20px; display: flex; flex-direction: column; align-items: center; }
|
||||
.nfc-ring { width: 120px; height: 120px; border-radius: 50%; border: 3px solid var(--blue-light); display: flex; align-items: center; justify-content: center; position: relative; margin-bottom: 14px; animation: nfc-pulse 2.5s ease-in-out infinite; }
|
||||
@keyframes nfc-pulse { 0%,100%{box-shadow:0 0 0 0 rgba(21,101,192,0.3);} 50%{box-shadow:0 0 0 18px rgba(21,101,192,0);} }
|
||||
.nfc-ring svg { width: 52px; height: 52px; stroke: var(--blue-main); fill: none; stroke-width: 1.5; }
|
||||
.nfc-status { font-size: 14px; font-weight: 600; color: var(--blue-main); }
|
||||
.nfc-sub { font-size: 12px; color: var(--text-sub); text-align: center; margin-top: 4px; }
|
||||
|
||||
/* BADGES LIST */
|
||||
.section-title { font-size: 13px; font-weight: 700; color: var(--gray-4); text-transform: uppercase; letter-spacing: 0.6px; margin-bottom: 2px; }
|
||||
.badge-list-item { display: flex; align-items: center; gap: 12px; padding: 14px; background: white; border-radius: var(--radius); box-shadow: var(--shadow); cursor: pointer; transition: transform 0.15s; }
|
||||
.badge-list-item:active { transform: scale(0.98); }
|
||||
.badge-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.badge-icon svg { width: 24px; height: 24px; }
|
||||
.badge-icon.nfc { background: #E3F2FD; }
|
||||
.badge-icon.nfc svg { stroke: var(--blue-main); fill: none; stroke-width: 2; }
|
||||
.badge-icon.rfid { background: #F3E5F5; }
|
||||
.badge-icon.rfid svg { stroke: #7B1FA2; fill: none; stroke-width: 2; }
|
||||
.badge-icon.qr { background: #E8F5E9; }
|
||||
.badge-icon.qr svg { stroke: var(--green); fill: none; stroke-width: 2; }
|
||||
.badge-icon.bt { background: #FFF3E0; }
|
||||
.badge-icon.bt svg { stroke: #E65100; fill: none; stroke-width: 2; }
|
||||
.badge-list-info { flex: 1; min-width: 0; }
|
||||
.badge-list-name { font-size: 15px; font-weight: 600; color: var(--text-main); }
|
||||
.badge-list-sub { font-size: 12px; color: var(--text-sub); white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
|
||||
.badge-list-right { display: flex; flex-direction: column; align-items: flex-end; gap: 4px; }
|
||||
.tech-chip { font-size: 10px; font-weight: 700; padding: 3px 8px; border-radius: 10px; text-transform: uppercase; }
|
||||
.tech-chip.nfc { background: var(--blue-pale); color: var(--blue-main); }
|
||||
.tech-chip.rfid { background: #F3E5F5; color: #7B1FA2; }
|
||||
.tech-chip.qr { background: var(--green-light); color: var(--green); }
|
||||
.tech-chip.bt { background: #FFF3E0; color: var(--orange); }
|
||||
.active-dot { width: 8px; height: 8px; border-radius: 50%; }
|
||||
.active-dot.on { background: #4CAF50; }
|
||||
.active-dot.off { background: var(--gray-3); }
|
||||
|
||||
/* HISTORY */
|
||||
.history-search { display: flex; align-items: center; gap: 10px; background: white; border-radius: var(--radius-sm); padding: 10px 14px; box-shadow: var(--shadow); }
|
||||
.history-search svg { width: 18px; height: 18px; stroke: var(--gray-3); fill: none; stroke-width: 2; flex-shrink: 0; }
|
||||
.history-search input { flex: 1; border: none; outline: none; font-size: 14px; color: var(--text-main); background: transparent; }
|
||||
.history-group-label { font-size: 12px; font-weight: 700; color: var(--gray-4); text-transform: uppercase; letter-spacing: 0.5px; padding: 4px 0; }
|
||||
.history-item { display: flex; align-items: center; gap: 12px; padding: 12px 14px; background: white; border-radius: var(--radius-sm); box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
|
||||
.history-icon { width: 36px; height: 36px; border-radius: 50%; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.history-icon.in { background: var(--green-light); }
|
||||
.history-icon.in svg { stroke: var(--green); }
|
||||
.history-icon.out { background: #E3F2FD; }
|
||||
.history-icon.out svg { stroke: var(--blue-main); }
|
||||
.history-icon.refused { background: var(--red-light); }
|
||||
.history-icon.refused svg { stroke: var(--red); }
|
||||
.history-icon svg { width: 18px; height: 18px; fill: none; stroke-width: 2.5; }
|
||||
.history-info { flex: 1; }
|
||||
.history-info .loc { font-size: 14px; font-weight: 600; color: var(--text-main); }
|
||||
.history-info .type { font-size: 12px; color: var(--text-sub); }
|
||||
.history-right { text-align: right; }
|
||||
.history-right .status { font-size: 11px; font-weight: 700; padding: 3px 8px; border-radius: 10px; }
|
||||
.status.ok { background: var(--green-light); color: var(--green); }
|
||||
.status.refused { background: var(--red-light); color: var(--red); }
|
||||
.history-right .time { font-size: 11px; color: var(--gray-4); margin-top: 3px; }
|
||||
|
||||
/* DASHBOARD */
|
||||
.stat-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.stat-card { background: white; border-radius: var(--radius); padding: 16px; box-shadow: var(--shadow); }
|
||||
.stat-card-label { font-size: 11px; font-weight: 600; color: var(--text-sub); text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; }
|
||||
.stat-card-value { font-size: 28px; font-weight: 800; color: var(--text-main); line-height: 1; }
|
||||
.stat-card-sub { font-size: 12px; color: var(--text-sub); margin-top: 4px; }
|
||||
.stat-card.blue .stat-card-value { color: var(--blue-main); }
|
||||
.stat-card.green .stat-card-value { color: var(--green); }
|
||||
.stat-card.red .stat-card-value { color: var(--red); }
|
||||
.stat-card.orange .stat-card-value { color: var(--orange); }
|
||||
.zone-list-item { display: flex; align-items: center; justify-content: space-between; padding: 12px 14px; background: white; border-radius: var(--radius-sm); box-shadow: 0 1px 6px rgba(0,0,0,0.06); }
|
||||
.zone-info .zone-name { font-size: 14px; font-weight: 600; }
|
||||
.zone-info .zone-level { font-size: 12px; color: var(--text-sub); }
|
||||
.zone-badge { font-size: 11px; font-weight: 700; padding: 4px 10px; border-radius: 10px; }
|
||||
.zone-badge.high { background: var(--red-light); color: var(--red); }
|
||||
.zone-badge.medium { background: #FFF3E0; color: var(--orange); }
|
||||
.zone-badge.low { background: var(--green-light); color: var(--green); }
|
||||
|
||||
/* SETTINGS */
|
||||
.settings-section { background: white; border-radius: var(--radius); overflow: hidden; box-shadow: var(--shadow); }
|
||||
.settings-item { display: flex; align-items: center; gap: 14px; padding: 14px 16px; border-bottom: 1px solid var(--gray-2); cursor: pointer; transition: background 0.15s; }
|
||||
.settings-item:last-child { border-bottom: none; }
|
||||
.settings-item:active { background: var(--gray-1); }
|
||||
.settings-icon { width: 36px; height: 36px; border-radius: 8px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.settings-icon svg { width: 20px; height: 20px; }
|
||||
.settings-icon.blue { background: var(--blue-pale); }
|
||||
.settings-icon.blue svg { stroke: var(--blue-main); fill: none; stroke-width: 2; }
|
||||
.settings-icon.green { background: var(--green-light); }
|
||||
.settings-icon.green svg { stroke: var(--green); fill: none; stroke-width: 2; }
|
||||
.settings-icon.red { background: var(--red-light); }
|
||||
.settings-icon.red svg { stroke: var(--red); fill: none; stroke-width: 2; }
|
||||
.settings-icon.orange { background: #FFF3E0; }
|
||||
.settings-icon.orange svg { stroke: var(--orange); fill: none; stroke-width: 2; }
|
||||
.settings-text { flex: 1; }
|
||||
.settings-text .s-title { font-size: 15px; font-weight: 600; }
|
||||
.settings-text .s-sub { font-size: 12px; color: var(--text-sub); }
|
||||
.settings-arrow { color: var(--gray-3); }
|
||||
.toggle { width: 44px; height: 24px; background: var(--gray-3); border-radius: 12px; position: relative; cursor: pointer; transition: background 0.2s; }
|
||||
.toggle.on { background: var(--blue-main); }
|
||||
.toggle::after { content: ''; position: absolute; top: 2px; left: 2px; width: 20px; height: 20px; border-radius: 50%; background: white; transition: left 0.2s; box-shadow: 0 1px 4px rgba(0,0,0,0.2); }
|
||||
.toggle.on::after { left: 22px; }
|
||||
|
||||
/* NOTIFICATIONS */
|
||||
.notif-banner { background: var(--blue-pale); border-left: 3px solid var(--blue-main); border-radius: 0 var(--radius-sm) var(--radius-sm) 0; padding: 12px 14px; display: flex; gap: 10px; align-items: flex-start; }
|
||||
.notif-banner.warning { background: #FFF8E1; border-color: #FBC02D; }
|
||||
.notif-banner.success { background: var(--green-light); border-color: var(--green); }
|
||||
.notif-icon { width: 20px; height: 20px; flex-shrink: 0; margin-top: 1px; }
|
||||
.notif-icon svg { width: 20px; height: 20px; fill: none; stroke-width: 2; }
|
||||
.notif-banner .n-title { font-size: 13px; font-weight: 700; color: var(--text-main); }
|
||||
.notif-banner .n-sub { font-size: 12px; color: var(--text-sub); }
|
||||
|
||||
/* MODAL */
|
||||
.modal-overlay { display: none; position: fixed; inset: 0; background: rgba(0,0,0,0.5); z-index: 1000; align-items: flex-end; }
|
||||
.modal-overlay.open { display: flex; }
|
||||
.modal-sheet { background: white; border-radius: 20px 20px 0 0; padding: 8px 20px 32px; width: 100%; max-height: 80vh; overflow-y: auto; }
|
||||
.modal-handle { width: 36px; height: 4px; background: var(--gray-2); border-radius: 2px; margin: 10px auto 20px; }
|
||||
.modal-title { font-size: 18px; font-weight: 700; margin-bottom: 16px; }
|
||||
.add-badge-option { display: flex; align-items: center; gap: 14px; padding: 14px; border-radius: var(--radius-sm); border: 1.5px solid var(--gray-2); margin-bottom: 10px; cursor: pointer; transition: all 0.15s; }
|
||||
.add-badge-option:active { background: var(--gray-1); border-color: var(--blue-main); }
|
||||
.add-badge-opt-icon { width: 44px; height: 44px; border-radius: 10px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.add-badge-opt-icon svg { width: 24px; height: 24px; fill: none; stroke-width: 2; }
|
||||
.add-badge-opt-text .t { font-size: 15px; font-weight: 600; }
|
||||
.add-badge-opt-text .s { font-size: 12px; color: var(--text-sub); }
|
||||
|
||||
/* TOAST */
|
||||
.toast { position: fixed; bottom: 80px; left: 50%; transform: translateX(-50%) translateY(20px); background: #263238; color: white; padding: 10px 20px; border-radius: 24px; font-size: 14px; font-weight: 500; opacity: 0; transition: all 0.3s; z-index: 9999; white-space: nowrap; pointer-events: none; }
|
||||
.toast.show { opacity: 1; transform: translateX(-50%) translateY(0); }
|
||||
|
||||
/* FAB */
|
||||
.fab { position: fixed; bottom: 80px; right: 20px; width: 52px; height: 52px; border-radius: 50%; background: var(--blue-main); border: none; box-shadow: 0 4px 16px rgba(21,101,192,0.4); cursor: pointer; display: flex; align-items: center; justify-content: center; z-index: 200; transition: transform 0.15s; }
|
||||
.fab:active { transform: scale(0.9); }
|
||||
.fab svg { width: 26px; height: 26px; stroke: white; fill: none; stroke-width: 2; }
|
||||
|
||||
/* CHIP ROW */
|
||||
.chip-row { display: flex; gap: 8px; overflow-x: auto; padding-bottom: 2px; scrollbar-width: none; }
|
||||
.chip-row::-webkit-scrollbar { display: none; }
|
||||
.chip { padding: 6px 14px; border-radius: 20px; font-size: 13px; font-weight: 600; cursor: pointer; border: 1.5px solid var(--gray-2); color: var(--text-sub); background: white; white-space: nowrap; transition: all 0.15s; }
|
||||
.chip.active { background: var(--blue-main); color: white; border-color: var(--blue-main); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<!-- ===== LOGIN ===== -->
|
||||
<div id="screen-login" class="screen active">
|
||||
<div style="text-align:center;">
|
||||
<div class="login-logo" style="margin:0 auto 16px;">
|
||||
<svg viewBox="0 0 40 40"><path d="M20 4L6 12v16l14 8 14-8V12L20 4z" fill="white" opacity="0.9"/><circle cx="20" cy="20" r="6" fill="#1E88E5"/></svg>
|
||||
</div>
|
||||
<div class="login-title">SafeAccess</div>
|
||||
<div class="login-sub">Gestion de badges d'accès sécurisé</div>
|
||||
</div>
|
||||
<div class="login-card">
|
||||
<h2>Connexion</h2>
|
||||
<p>Entrez vos identifiants pour continuer</p>
|
||||
<div class="form-group">
|
||||
<label>Identifiant</label>
|
||||
<input type="email" id="login-email" placeholder="alice.dupont@société.fr" value="alice.dupont@societe.fr">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>Mot de passe</label>
|
||||
<input type="password" id="login-pass" placeholder="••••••••" value="password123">
|
||||
</div>
|
||||
<button class="btn-primary" onclick="goTo2FA()">Se connecter →</button>
|
||||
<button class="btn-outline" onclick="showToast('Contactez votre administrateur')">Mot de passe oublié</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== 2FA ===== -->
|
||||
<div id="screen-2fa" class="screen">
|
||||
<div class="mfa-card">
|
||||
<div class="mfa-icon">
|
||||
<svg viewBox="0 0 32 32" fill="none" stroke="#1565C0" stroke-width="2"><rect x="8" y="14" width="16" height="12" rx="3"/><path d="M11 14v-4a5 5 0 0110 0v4"/><circle cx="16" cy="21" r="2" fill="#1565C0" stroke="none"/></svg>
|
||||
</div>
|
||||
<h2>Vérification 2FA</h2>
|
||||
<p>Code TOTP (renouvelle toutes les 30s)</p>
|
||||
<div class="mfa-code-display" id="totp-display">------</div>
|
||||
<div class="mfa-progress"><div class="mfa-progress-bar" id="totp-bar" style="width:100%"></div></div>
|
||||
<p style="font-size:13px;color:var(--text-sub);margin-bottom:12px;">Entrez le code de votre application d'authentification :</p>
|
||||
<div class="mfa-input-row" id="mfa-inputs"></div>
|
||||
<div class="mfa-method-row">
|
||||
<button class="mfa-method-btn active" onclick="selectMFA(this,'totp')">📱 TOTP</button>
|
||||
<button class="mfa-method-btn" onclick="selectMFA(this,'sms')">💬 SMS</button>
|
||||
<button class="mfa-method-btn" onclick="selectMFA(this,'email')">📧 Email</button>
|
||||
</div>
|
||||
<button class="btn-primary" style="margin-top:20px;" onclick="verifyMFA()">Vérifier →</button>
|
||||
<button class="btn-outline" style="margin-top:8px;" onclick="goTo('screen-login')">← Retour</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ===== MAIN APP ===== -->
|
||||
<div id="screen-app" class="screen">
|
||||
<div class="app-header">
|
||||
<div class="app-header-left">
|
||||
<div class="app-header-avatar">AD</div>
|
||||
<div class="app-header-info">
|
||||
<h1>Alice Dupont</h1>
|
||||
<p>AC-0012 · Niveau 3</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="app-header-right">
|
||||
<button class="icon-btn" onclick="showToast('Notifications')">
|
||||
<svg viewBox="0 0 24 24"><path d="M18 8A6 6 0 006 8c0 7-3 9-3 9h18s-3-2-3-9"/><path d="M13.73 21a2 2 0 01-3.46 0"/></svg>
|
||||
</button>
|
||||
<button class="icon-btn" onclick="showToast('Paramètres rapides')">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<nav class="nav-tabs">
|
||||
<button class="nav-tab active" onclick="switchTab('badge',this)">
|
||||
<svg viewBox="0 0 24 24"><rect x="2" y="5" width="20" height="14" rx="3"/><path d="M8 12h.01M12 12h.01M16 12h.01"/></svg>
|
||||
Badge
|
||||
</button>
|
||||
<button class="nav-tab" onclick="switchTab('mes-badges',this)">
|
||||
<svg viewBox="0 0 24 24"><path d="M20 7H4a2 2 0 00-2 2v10a2 2 0 002 2h16a2 2 0 002-2V9a2 2 0 00-2-2z"/><path d="M16 21V5a2 2 0 00-2-2h-4a2 2 0 00-2 2v16"/></svg>
|
||||
Mes badges
|
||||
</button>
|
||||
<button class="nav-tab" onclick="switchTab('historique',this)">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>
|
||||
Historique
|
||||
</button>
|
||||
<button class="nav-tab" onclick="switchTab('dashboard',this)">
|
||||
<svg viewBox="0 0 24 24"><rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/></svg>
|
||||
Tableau
|
||||
</button>
|
||||
<button class="nav-tab" onclick="switchTab('settings',this)">
|
||||
<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 00.33 1.82l.06.06a2 2 0 010 2.83 2 2 0 01-2.83 0l-.06-.06a1.65 1.65 0 00-1.82-.33 1.65 1.65 0 00-1 1.51V21a2 2 0 01-4 0v-.09A1.65 1.65 0 009 19.4a1.65 1.65 0 00-1.82.33l-.06.06a2 2 0 01-2.83-2.83l.06-.06A1.65 1.65 0 004.68 15a1.65 1.65 0 00-1.51-1H3a2 2 0 010-4h.09A1.65 1.65 0 004.6 9a1.65 1.65 0 00-.33-1.82l-.06-.06a2 2 0 012.83-2.83l.06.06A1.65 1.65 0 009 4.68a1.65 1.65 0 001-1.51V3a2 2 0 014 0v.09a1.65 1.65 0 001 1.51 1.65 1.65 0 001.82-.33l.06-.06a2 2 0 012.83 2.83l-.06.06A1.65 1.65 0 0019.4 9a1.65 1.65 0 001.51 1H21a2 2 0 010 4h-.09a1.65 1.65 0 00-1.51 1z"/></svg>
|
||||
Réglages
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<!-- TAB: MON E-BADGE -->
|
||||
<div id="tab-badge" class="tab-pane active">
|
||||
<div class="notif-banner success">
|
||||
<div class="notif-icon"><svg viewBox="0 0 20 20" fill="none" stroke="#2E7D32" stroke-width="2"><path d="M9 12l2 2 4-4"/><path d="M4 6a8 8 0 1016 0A8 8 0 004 6"/></svg></div>
|
||||
<div><div class="n-title">NFC disponible</div><div class="n-sub">Votre téléphone est détecté compatible NFC</div></div>
|
||||
</div>
|
||||
<div class="badge-card">
|
||||
<div class="badge-card-header">
|
||||
<div class="badge-avatar">AD</div>
|
||||
<div class="badge-info">
|
||||
<h2>Alice Dupont</h2>
|
||||
<p>Technicien Réseau Senior</p>
|
||||
<div class="badge-id">ID: AC-0012 · Émis le 01/01/2025</div>
|
||||
<div class="badge-status ready">
|
||||
<div class="badge-status-dot"></div>
|
||||
PRÊT À SCANNER
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="qr-zone">
|
||||
<div class="qr-box">
|
||||
<canvas id="qr-canvas" class="qr-canvas" width="154" height="154"></canvas>
|
||||
<div class="qr-corner tl"></div><div class="qr-corner tr"></div>
|
||||
<div class="qr-corner bl"></div><div class="qr-corner br"></div>
|
||||
</div>
|
||||
<div class="badge-system-tag">SafeAccess system · NFC/QR</div>
|
||||
</div>
|
||||
<div class="nfc-area">
|
||||
<div class="nfc-ring" id="nfc-ring">
|
||||
<svg viewBox="0 0 52 52"><path d="M26 8C16 8 8 16 8 26s8 18 18 18 18-8 18-18"/><path d="M26 14c-7 0-12 5-12 12s5 12 12 12 12-5 12-12"/><path d="M26 20c-3 0-6 3-6 6s3 6 6 6 6-3 6-6"/><circle cx="26" cy="26" r="3" fill="#1565C0"/></svg>
|
||||
</div>
|
||||
<div class="nfc-status" id="nfc-status-text">En attente d'un lecteur NFC</div>
|
||||
<div class="nfc-sub">Approchez votre téléphone du lecteur</div>
|
||||
</div>
|
||||
<div class="badge-actions">
|
||||
<button class="btn-action btn-blue" onclick="simulateNFC()">📡 SIMULER LECTURE NFC</button>
|
||||
<button class="btn-action btn-ghost" onclick="switchTab('historique', document.querySelectorAll('.nav-tab')[2])">HISTORIQUE D'ACCÈS</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: MES BADGES -->
|
||||
<div id="tab-mes-badges" class="tab-pane">
|
||||
<div class="chip-row">
|
||||
<div class="chip active">Tous</div>
|
||||
<div class="chip">NFC</div>
|
||||
<div class="chip">RFID</div>
|
||||
<div class="chip">QR Code</div>
|
||||
<div class="chip">Bluetooth</div>
|
||||
</div>
|
||||
<div class="section-title">Mes badges (4)</div>
|
||||
<div id="badge-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: HISTORIQUE -->
|
||||
<div id="tab-historique" class="tab-pane">
|
||||
<div class="history-search">
|
||||
<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="M21 21l-4.35-4.35"/></svg>
|
||||
<input type="text" placeholder="Rechercher un accès..." id="history-search">
|
||||
</div>
|
||||
<div id="history-list"></div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: DASHBOARD -->
|
||||
<div id="tab-dashboard" class="tab-pane">
|
||||
<div class="section-title">Tableau de bord</div>
|
||||
<div class="stat-grid">
|
||||
<div class="stat-card blue"><div class="stat-card-label">Utilisateurs</div><div class="stat-card-value">17</div><div class="stat-card-sub">actifs ce mois</div></div>
|
||||
<div class="stat-card"><div class="stat-card-label">Zones</div><div class="stat-card-value">16</div><div class="stat-card-sub">zones actives</div></div>
|
||||
<div class="stat-card green"><div class="stat-card-label">Accès aujourd'hui</div><div class="stat-card-value">143</div><div class="stat-card-sub">autorisés</div></div>
|
||||
<div class="stat-card red"><div class="stat-card-label">Alertes</div><div class="stat-card-value">0</div><div class="stat-card-sub">incidents actifs</div></div>
|
||||
</div>
|
||||
<div class="section-title" style="margin-top:4px;">Gestion des zones</div>
|
||||
<div id="zone-list"></div>
|
||||
<div class="section-title" style="margin-top:4px;">Logs récents</div>
|
||||
<div id="dash-logs"></div>
|
||||
</div>
|
||||
|
||||
<!-- TAB: RÉGLAGES -->
|
||||
<div id="tab-settings" class="tab-pane">
|
||||
<div class="section-title">Sécurité</div>
|
||||
<div class="settings-section">
|
||||
<div class="settings-item">
|
||||
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><rect x="5" y="11" width="14" height="10" rx="2"/><path d="M8 11V7a4 4 0 018 0v4"/><circle cx="12" cy="16" r="1" fill="#1565C0"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">Double authentification (2FA)</div><div class="s-sub">TOTP activé · App Authenticator</div></div>
|
||||
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">Biométrie</div><div class="s-sub">Empreinte / Face ID</div></div>
|
||||
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="settings-item" onclick="showToast('PIN modifié')">
|
||||
<div class="settings-icon orange"><svg viewBox="0 0 24 24"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0110 0v4"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">Code PIN</div><div class="s-sub">Modifier le code PIN de secours</div></div>
|
||||
<div class="settings-arrow">›</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title" style="margin-top:4px;">NFC & Technologies</div>
|
||||
<div class="settings-section">
|
||||
<div class="settings-item">
|
||||
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><path d="M12 4C8 4 4 8 4 12s4 8 8 8 8-4 8-8"/><path d="M12 7c-2.5 0-5 2.5-5 5s2.5 5 5 5 5-2.5 5-5"/><circle cx="12" cy="12" r="2" fill="#1565C0"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">NFC actif</div><div class="s-sub">Lecture/écriture NFC activée</div></div>
|
||||
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-icon orange"><svg viewBox="0 0 24 24"><polyline points="6.5 6.5 17.5 12 6.5 17.5 6.5 6.5"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">Bluetooth BLE</div><div class="s-sub">Badges longue portée</div></div>
|
||||
<div class="toggle" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
<div class="settings-item">
|
||||
<div class="settings-icon green"><svg viewBox="0 0 24 24"><path d="M3 3h18v18H3z"/><path d="M9 9h2v2H9zM13 9h2v2h-2zM9 13h2v2H9z"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">QR Code dynamique</div><div class="s-sub">Régénération toutes les 60s</div></div>
|
||||
<div class="toggle on" onclick="this.classList.toggle('on')"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section-title" style="margin-top:4px;">Compte</div>
|
||||
<div class="settings-section">
|
||||
<div class="settings-item" onclick="showToast('Profil ouvert')">
|
||||
<div class="settings-icon blue"><svg viewBox="0 0 24 24"><path d="M20 21v-2a4 4 0 00-4-4H8a4 4 0 00-4 4v2"/><circle cx="12" cy="7" r="4"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">Mon profil</div><div class="s-sub">Alice Dupont · AC-0012</div></div>
|
||||
<div class="settings-arrow">›</div>
|
||||
</div>
|
||||
<div class="settings-item" onclick="goTo('screen-login')">
|
||||
<div class="settings-icon red"><svg viewBox="0 0 24 24"><path d="M9 21H5a2 2 0 01-2-2V5a2 2 0 012-2h4"/><polyline points="16 17 21 12 16 7"/><line x1="21" y1="12" x2="9" y2="12"/></svg></div>
|
||||
<div class="settings-text"><div class="s-title">Déconnexion</div><div class="s-sub">alice.dupont@societe.fr</div></div>
|
||||
<div class="settings-arrow">›</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="text-align:center;padding:20px 0;font-size:11px;color:var(--gray-3);">SafeAccess v2.4.1 · Build 2025.06</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- FAB -->
|
||||
<button class="fab" id="fab-btn" style="display:none;" onclick="showAddBadgeModal()">
|
||||
<svg viewBox="0 0 24 24"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
</button>
|
||||
|
||||
<!-- MODAL -->
|
||||
<div class="modal-overlay" id="modal-add">
|
||||
<div class="modal-sheet">
|
||||
<div class="modal-handle"></div>
|
||||
<div class="modal-title">Ajouter un badge</div>
|
||||
<div class="add-badge-option" onclick="addBadge('NFC')">
|
||||
<div class="add-badge-opt-icon" style="background:#E3F2FD;"><svg viewBox="0 0 24 24" fill="none" stroke="#1565C0" stroke-width="2"><path d="M12 4C8 4 4 8 4 12s4 8 8 8 8-4 8-8"/><circle cx="12" cy="12" r="2" fill="#1565C0"/></svg></div>
|
||||
<div class="add-badge-opt-text"><div class="t">Badge NFC</div><div class="s">ISO 14443 · Mifare · NFC Type A/B</div></div>
|
||||
</div>
|
||||
<div class="add-badge-option" onclick="addBadge('RFID')">
|
||||
<div class="add-badge-opt-icon" style="background:#F3E5F5;"><svg viewBox="0 0 24 24" fill="none" stroke="#7B1FA2" stroke-width="2"><path d="M5 12.5c0-3.9 3.1-7 7-7s7 3.1 7 7"/><path d="M8 12.5c0-2.2 1.8-4 4-4s4 1.8 4 4"/><circle cx="12" cy="12.5" r="2"/></svg></div>
|
||||
<div class="add-badge-opt-text"><div class="t">Badge RFID</div><div class="s">125kHz · HID · Em-Marine · Wiegand</div></div>
|
||||
</div>
|
||||
<div class="add-badge-option" onclick="addBadge('QR Code')">
|
||||
<div class="add-badge-opt-icon" style="background:#E8F5E9;"><svg viewBox="0 0 24 24" fill="none" stroke="#2E7D32" stroke-width="2"><path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3zM17 14h.01M14 14h.01M14 17h.01M17 17h4M17 20v1M21 14v3"/></svg></div>
|
||||
<div class="add-badge-opt-text"><div class="t">QR Code dynamique</div><div class="s">Rotation toutes les 60s · HMAC signé</div></div>
|
||||
</div>
|
||||
<div class="add-badge-option" onclick="addBadge('Bluetooth')">
|
||||
<div class="add-badge-opt-icon" style="background:#FFF3E0;"><svg viewBox="0 0 24 24" fill="none" stroke="#E65100" stroke-width="2"><polyline points="6.5 6.5 17.5 12 6.5 17.5 6.5 6.5"/><polyline points="17.5 12 6.5 17.5"/></svg></div>
|
||||
<div class="add-badge-opt-text"><div class="t">Badge Bluetooth BLE</div><div class="s">Longue portée · iBeacon · Eddystone</div></div>
|
||||
</div>
|
||||
<button class="btn-outline" onclick="closeModal()" style="margin-top:8px;">Annuler</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- TOAST -->
|
||||
<div class="toast" id="toast"></div>
|
||||
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/qrcodejs/1.0.0/qrcode.min.js"></script>
|
||||
<script>
|
||||
// ===== STATE =====
|
||||
let currentTab = 'badge';
|
||||
let mfaEntered = '';
|
||||
let totpTimer = 30;
|
||||
let totpInterval = null;
|
||||
const correctCode = '472851';
|
||||
|
||||
const badges = [
|
||||
{ id: 'B001', name: 'Badge Principal', tech: 'nfc', uid: 'A3:F2:1B:CC', zones: ['Bureau Principal', 'Parking', 'Cafétéria'], active: true },
|
||||
{ id: 'B002', name: 'Badge Labo', tech: 'rfid', uid: '125K-88F2A1', zones: ['Labo A', 'Labo B'], active: true },
|
||||
{ id: 'B003', name: 'QR Code', tech: 'qr', uid: 'QR-DYN-2025', zones: ['Bureau Principal'], active: true },
|
||||
{ id: 'B004', name: 'Badge BLE', tech: 'bt', uid: 'BT:A4:CC:12:FF', zones: ['Parking'], active: false },
|
||||
];
|
||||
|
||||
const history = [
|
||||
{ date: "Aujourd'hui, 09:30", type: 'in', loc: 'Bureau Principal', sub: '· NFC · B001', status: 'ok' },
|
||||
{ date: "Hier, 17:15", type: 'out', loc: 'Parking', sub: '· RFID · B002', status: 'ok' },
|
||||
{ date: "Mercredi, 14:05", type: 'in', loc: 'Labo A', sub: '· RFID · B002', status: 'refused' },
|
||||
{ date: "Mercredi, 08:50", type: 'in', loc: 'Bureau Principal', sub: '· NFC · B001', status: 'ok' },
|
||||
{ date: "Mardi, 18:20", type: 'out', loc: 'Bureau Principal', sub: '· QR · B003', status: 'ok' },
|
||||
{ date: "Mardi, 09:00", type: 'in', loc: 'Cafétéria', sub: '· NFC · B001', status: 'ok' },
|
||||
];
|
||||
|
||||
const zones = [
|
||||
{ name: 'Bureau Principal', level: 'Niveau 1', risk: 'low' },
|
||||
{ name: 'Labo A', level: 'Niveau 3 — Restreint', risk: 'high' },
|
||||
{ name: 'Parking', level: 'Niveau 1', risk: 'low' },
|
||||
{ name: 'Salle Serveurs', level: 'Niveau 4 — Critique', risk: 'high' },
|
||||
{ name: 'Cafétéria', level: 'Niveau 0 — Public', risk: 'low' },
|
||||
{ name: 'Direction', level: 'Niveau 2', risk: 'medium' },
|
||||
];
|
||||
|
||||
// ===== NAVIGATION =====
|
||||
function goTo(screenId) {
|
||||
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
|
||||
document.getElementById(screenId).classList.add('active');
|
||||
document.getElementById('fab-btn').style.display = screenId === 'screen-app' ? 'flex' : 'none';
|
||||
}
|
||||
|
||||
function switchTab(tab, btn) {
|
||||
currentTab = tab;
|
||||
document.querySelectorAll('.tab-pane').forEach(p => p.classList.remove('active'));
|
||||
document.querySelectorAll('.nav-tab').forEach(b => b.classList.remove('active'));
|
||||
document.getElementById('tab-' + tab).classList.add('active');
|
||||
if(btn) btn.classList.add('active');
|
||||
if(tab === 'mes-badges') renderBadgeList();
|
||||
if(tab === 'historique') renderHistory();
|
||||
if(tab === 'dashboard') renderDashboard();
|
||||
}
|
||||
|
||||
// ===== LOGIN / 2FA =====
|
||||
function goTo2FA() {
|
||||
const e = document.getElementById('login-email').value;
|
||||
const p = document.getElementById('login-pass').value;
|
||||
if(!e || !p) { showToast('Renseignez vos identifiants'); return; }
|
||||
goTo('screen-2fa');
|
||||
initTOTP();
|
||||
buildMFAInputs();
|
||||
}
|
||||
|
||||
function initTOTP() {
|
||||
if(totpInterval) clearInterval(totpInterval);
|
||||
totpTimer = 30;
|
||||
updateTOTP();
|
||||
totpInterval = setInterval(() => {
|
||||
totpTimer--;
|
||||
if(totpTimer <= 0) { totpTimer = 30; generateNewCode(); }
|
||||
updateTOTP();
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
let currentCode = correctCode;
|
||||
function generateNewCode() {
|
||||
currentCode = String(Math.floor(100000 + Math.random() * 900000));
|
||||
document.getElementById('totp-display').textContent = currentCode;
|
||||
}
|
||||
|
||||
function updateTOTP() {
|
||||
document.getElementById('totp-display').textContent = currentCode;
|
||||
document.getElementById('totp-bar').style.width = (totpTimer / 30 * 100) + '%';
|
||||
}
|
||||
|
||||
function buildMFAInputs() {
|
||||
const row = document.getElementById('mfa-inputs');
|
||||
row.innerHTML = '';
|
||||
mfaEntered = '';
|
||||
for(let i = 0; i < 6; i++) {
|
||||
const inp = document.createElement('input');
|
||||
inp.className = 'mfa-digit';
|
||||
inp.maxLength = 1;
|
||||
inp.inputMode = 'numeric';
|
||||
inp.pattern = '[0-9]';
|
||||
inp.dataset.idx = i;
|
||||
inp.addEventListener('input', e => {
|
||||
if(/[0-9]/.test(e.target.value)) {
|
||||
e.target.classList.add('filled');
|
||||
if(i < 5) row.children[i+1].focus();
|
||||
} else { e.target.value = ''; e.target.classList.remove('filled'); }
|
||||
});
|
||||
inp.addEventListener('keydown', e => { if(e.key === 'Backspace' && !inp.value && i > 0) row.children[i-1].focus(); });
|
||||
row.appendChild(inp);
|
||||
}
|
||||
setTimeout(() => row.children[0].focus(), 100);
|
||||
}
|
||||
|
||||
function selectMFA(btn, method) {
|
||||
document.querySelectorAll('.mfa-method-btn').forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
if(method === 'sms') showToast('SMS envoyé au +33 6 ** ** ** 42');
|
||||
if(method === 'email') showToast('Email envoyé à a.dupont@****');
|
||||
}
|
||||
|
||||
function verifyMFA() {
|
||||
const inputs = document.querySelectorAll('.mfa-digit');
|
||||
let code = '';
|
||||
inputs.forEach(i => code += i.value);
|
||||
if(code.length < 6) { showToast('Entrez les 6 chiffres'); return; }
|
||||
if(code === currentCode || code === correctCode) {
|
||||
if(totpInterval) clearInterval(totpInterval);
|
||||
showToast('✓ Authentification réussie !');
|
||||
setTimeout(() => {
|
||||
goTo('screen-app');
|
||||
renderQR();
|
||||
renderBadgeList();
|
||||
renderHistory();
|
||||
renderDashboard();
|
||||
}, 800);
|
||||
} else {
|
||||
showToast('❌ Code incorrect, réessayez');
|
||||
buildMFAInputs();
|
||||
}
|
||||
}
|
||||
|
||||
// ===== QR CODE =====
|
||||
function renderQR() {
|
||||
const canvas = document.getElementById('qr-canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const size = 154;
|
||||
const data = 'SAFEACCESS:AC-0012:' + Date.now();
|
||||
drawQR(ctx, data, size);
|
||||
setInterval(() => {
|
||||
const newData = 'SAFEACCESS:AC-0012:' + Date.now();
|
||||
drawQR(ctx, newData, size);
|
||||
}, 60000);
|
||||
}
|
||||
|
||||
function drawQR(ctx, data, size) {
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.fillStyle = '#fff';
|
||||
ctx.fillRect(0, 0, size, size);
|
||||
const cells = 25;
|
||||
const cellSize = Math.floor(size / cells);
|
||||
const hash = Array.from(data).reduce((a, c) => ((a << 5) - a + c.charCodeAt(0)) | 0, 0);
|
||||
for(let r = 0; r < cells; r++) {
|
||||
for(let c = 0; c < cells; c++) {
|
||||
const seed = (r * 31 + c * 17 + hash + r*c) % 100;
|
||||
const shouldFill = seed < 45 || (r < 7 && c < 7) || (r < 7 && c > cells-8) || (r > cells-8 && c < 7);
|
||||
if(shouldFill) {
|
||||
ctx.fillStyle = '#0D2B5E';
|
||||
ctx.fillRect(c * cellSize + 1, r * cellSize + 1, cellSize - 1, cellSize - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Finder patterns
|
||||
[[0,0],[0,cells-7],[cells-7,0]].forEach(([row,col]) => {
|
||||
ctx.strokeStyle = '#0D2B5E'; ctx.lineWidth = 2;
|
||||
ctx.strokeRect(col*cellSize+1, row*cellSize+1, 6*cellSize, 6*cellSize);
|
||||
ctx.fillStyle = '#0D2B5E';
|
||||
ctx.fillRect((col+2)*cellSize, (row+2)*cellSize, 3*cellSize, 3*cellSize);
|
||||
});
|
||||
}
|
||||
|
||||
// ===== NFC SIMULATION =====
|
||||
function simulateNFC() {
|
||||
const ring = document.getElementById('nfc-ring');
|
||||
const status = document.getElementById('nfc-status-text');
|
||||
ring.style.borderColor = '#FBC02D';
|
||||
ring.style.animation = 'nfc-pulse 0.8s ease-in-out infinite';
|
||||
status.textContent = 'Lecture NFC en cours...';
|
||||
status.style.color = '#E65100';
|
||||
setTimeout(() => {
|
||||
ring.style.borderColor = '#4CAF50';
|
||||
ring.style.animation = 'none';
|
||||
status.textContent = '✓ Accès autorisé — Bureau Principal';
|
||||
status.style.color = '#2E7D32';
|
||||
showToast('✓ Accès autorisé — Bureau Principal');
|
||||
setTimeout(() => {
|
||||
ring.style.borderColor = 'var(--blue-light)';
|
||||
ring.style.animation = 'nfc-pulse 2.5s ease-in-out infinite';
|
||||
status.textContent = 'En attente d\'un lecteur NFC';
|
||||
status.style.color = 'var(--blue-main)';
|
||||
}, 3000);
|
||||
}, 1800);
|
||||
}
|
||||
|
||||
// ===== BADGE LIST =====
|
||||
const techIcons = {
|
||||
nfc: `<svg viewBox="0 0 24 24" fill="none" stroke="#1565C0" stroke-width="2"><path d="M12 4C8 4 4 8 4 12s4 8 8 8 8-4 8-8"/><circle cx="12" cy="12" r="2" fill="#1565C0"/></svg>`,
|
||||
rfid: `<svg viewBox="0 0 24 24" fill="none" stroke="#7B1FA2" stroke-width="2"><path d="M5 12.5c0-3.9 3.1-7 7-7s7 3.1 7 7"/><path d="M8 12.5c0-2.2 1.8-4 4-4s4 1.8 4 4"/><circle cx="12" cy="12.5" r="2"/></svg>`,
|
||||
qr: `<svg viewBox="0 0 24 24" fill="none" stroke="#2E7D32" stroke-width="2"><path d="M3 3h7v7H3zM14 3h7v7h-7zM3 14h7v7H3z"/></svg>`,
|
||||
bt: `<svg viewBox="0 0 24 24" fill="none" stroke="#E65100" stroke-width="2"><polyline points="6.5 6.5 17.5 12 6.5 17.5 6.5 6.5"/></svg>`,
|
||||
};
|
||||
|
||||
function renderBadgeList() {
|
||||
const list = document.getElementById('badge-list');
|
||||
list.innerHTML = badges.map(b => `
|
||||
<div class="badge-list-item" onclick="showToast('Badge ${b.name} sélectionné')">
|
||||
<div class="badge-icon ${b.tech}">${techIcons[b.tech]}</div>
|
||||
<div class="badge-list-info">
|
||||
<div class="badge-list-name">${b.name}</div>
|
||||
<div class="badge-list-sub">${b.uid} · ${b.zones.join(', ')}</div>
|
||||
</div>
|
||||
<div class="badge-list-right">
|
||||
<span class="tech-chip ${b.tech}">${b.tech.toUpperCase()}</span>
|
||||
<div class="active-dot ${b.active ? 'on' : 'off'}"></div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
// ===== HISTORY =====
|
||||
const inIcon = `<svg viewBox="0 0 24 24"><polyline points="20 6 9 17 4 12"/></svg>`;
|
||||
const outIcon = `<svg viewBox="0 0 24 24"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>`;
|
||||
const refIcon = `<svg viewBox="0 0 24 24"><line x1="18" y1="6" x2="6" y2="18"/><line x1="6" y1="6" x2="18" y2="18"/></svg>`;
|
||||
|
||||
function renderHistory(filter = '') {
|
||||
const list = document.getElementById('history-list');
|
||||
const filtered = history.filter(h => !filter || h.loc.toLowerCase().includes(filter.toLowerCase()));
|
||||
let html = '';
|
||||
let lastDate = '';
|
||||
filtered.forEach(h => {
|
||||
if(h.date !== lastDate) {
|
||||
html += `<div class="history-group-label">${h.date}</div>`;
|
||||
lastDate = h.date;
|
||||
}
|
||||
const iconType = h.status === 'refused' ? 'refused' : h.type === 'in' ? 'in' : 'out';
|
||||
const icon = h.status === 'refused' ? refIcon : h.type === 'in' ? inIcon : outIcon;
|
||||
const typeLabel = h.type === 'in' ? 'Entrée' : 'Sortie';
|
||||
html += `
|
||||
<div class="history-item">
|
||||
<div class="history-icon ${iconType}">${icon}</div>
|
||||
<div class="history-info">
|
||||
<div class="loc">${typeLabel} · ${h.loc}</div>
|
||||
<div class="type">${h.sub}</div>
|
||||
</div>
|
||||
<div class="history-right">
|
||||
<span class="status ${h.status === 'ok' ? 'ok' : 'refused'}">${h.status === 'ok' ? 'AUTORISÉ' : 'REFUSÉ'}</span>
|
||||
<div class="time">${h.date.split(',')[1] || ''}</div>
|
||||
</div>
|
||||
</div>`;
|
||||
});
|
||||
list.innerHTML = html || '<div style="text-align:center;padding:20px;color:var(--gray-4);">Aucun résultat</div>';
|
||||
}
|
||||
|
||||
document.getElementById('history-search').addEventListener('input', e => renderHistory(e.target.value));
|
||||
|
||||
// ===== DASHBOARD =====
|
||||
function renderDashboard() {
|
||||
const zl = document.getElementById('zone-list');
|
||||
zl.innerHTML = zones.map(z => `
|
||||
<div class="zone-list-item">
|
||||
<div class="zone-info"><div class="zone-name">${z.name}</div><div class="zone-level">${z.level}</div></div>
|
||||
<span class="zone-badge ${z.risk}">${z.risk === 'high' ? 'Restreint' : z.risk === 'medium' ? 'Modéré' : 'Public'}</span>
|
||||
</div>`).join('');
|
||||
|
||||
const dl = document.getElementById('dash-logs');
|
||||
dl.innerHTML = history.slice(0,4).map(h => `
|
||||
<div class="history-item" style="margin-bottom:8px;">
|
||||
<div class="history-icon ${h.status === 'refused' ? 'refused' : h.type === 'in' ? 'in' : 'out'}">${h.status === 'refused' ? refIcon : h.type === 'in' ? inIcon : outIcon}</div>
|
||||
<div class="history-info"><div class="loc">${h.loc}</div><div class="type">${h.sub}</div></div>
|
||||
<div class="history-right"><span class="status ${h.status === 'ok' ? 'ok' : 'refused'}">${h.status === 'ok' ? 'OK' : 'REFUSÉ'}</span></div>
|
||||
</div>`).join('');
|
||||
}
|
||||
|
||||
// ===== ADD BADGE MODAL =====
|
||||
function showAddBadgeModal() {
|
||||
document.getElementById('modal-add').classList.add('open');
|
||||
}
|
||||
function closeModal() {
|
||||
document.getElementById('modal-add').classList.remove('open');
|
||||
}
|
||||
function addBadge(tech) {
|
||||
closeModal();
|
||||
const newBadge = {
|
||||
id: 'B00' + (badges.length + 1),
|
||||
name: 'Nouveau badge ' + tech,
|
||||
tech: tech.toLowerCase() === 'nfc' ? 'nfc' : tech.toLowerCase() === 'rfid' ? 'rfid' : tech.toLowerCase() === 'qr code' ? 'qr' : 'bt',
|
||||
uid: 'NEW-' + Math.random().toString(36).substr(2,8).toUpperCase(),
|
||||
zones: ['Bureau Principal'],
|
||||
active: true,
|
||||
};
|
||||
badges.push(newBadge);
|
||||
if(currentTab === 'mes-badges') renderBadgeList();
|
||||
showToast('✓ Badge ' + tech + ' ajouté');
|
||||
}
|
||||
|
||||
// ===== TOAST =====
|
||||
function showToast(msg) {
|
||||
const t = document.getElementById('toast');
|
||||
t.textContent = msg;
|
||||
t.classList.add('show');
|
||||
setTimeout(() => t.classList.remove('show'), 2500);
|
||||
}
|
||||
|
||||
// ===== INIT =====
|
||||
document.getElementById('fab-btn').style.display = 'none';
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "SafeAccess — Gestion de badges",
|
||||
"short_name": "SafeAccess",
|
||||
"description": "Gestion de badges d'accès NFC, RFID, QR Code et Bluetooth avec 2FA",
|
||||
"start_url": "/index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "#0D2B5E",
|
||||
"theme_color": "#1565C0",
|
||||
"orientation": "portrait",
|
||||
"icons": [
|
||||
{ "src": "icons/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any maskable" },
|
||||
{ "src": "icons/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any maskable" }
|
||||
],
|
||||
"categories": ["security", "utilities", "business"],
|
||||
"lang": "fr",
|
||||
"scope": "/"
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
const CACHE = 'safeaccess-v1';
|
||||
const ASSETS = ['/', '/index.html', '/manifest.json'];
|
||||
|
||||
self.addEventListener('install', e => {
|
||||
e.waitUntil(caches.open(CACHE).then(c => c.addAll(ASSETS)));
|
||||
});
|
||||
|
||||
self.addEventListener('fetch', e => {
|
||||
e.respondWith(
|
||||
caches.match(e.request).then(r => r || fetch(e.request)).catch(() => caches.match('/index.html'))
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user