Download static/admin-dashboard.html from vsmdvic/idea: direct link, hf CLI and curl.
- Browser
- Download file 37.5 kB
-
https://huggingface.co/spaces/vsmdvic/idea/resolve/main/static/admin-dashboard.html
- Command line
-
hf download hf://spaces/vsmdvic/idea/static/admin-dashboard.html
-
curl -L -o admin-dashboard.html https://huggingface.co/spaces/vsmdvic/idea/resolve/main/static/admin-dashboard.html
37.5 kB
| <html lang="ro"> | |
| <head> | |
| <link rel="icon" type="image/svg+xml" href="favicon.svg"> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover"> | |
| <title>IDEA | Admin</title> | |
| <link rel="stylesheet" href="style.css"> | |
| <script src="errors.js"></script> | |
| <style> | |
| /* Tabel elevi cu status */ | |
| .elev-row-admin { grid-template-columns:90px 1fr 100px 80px 90px; } | |
| .prof-row { grid-template-columns:1fr 140px 70px 70px; } | |
| .mat-row { grid-template-columns:1fr 70px; } | |
| @media(max-width:600px){ | |
| .elev-row-admin { grid-template-columns:1fr auto; } | |
| .elev-row-admin .col-vpass,.elev-row-admin .col-status,.elev-row-admin .col-pos { display:none; } | |
| .prof-row { grid-template-columns:1fr auto; } | |
| .prof-row .col-mat,.prof-row .col-pin { display:none; } | |
| } | |
| .vpass-preview { background:rgba(255,255,255,0.04); border:1px solid var(--glass-border); color:var(--white); padding:10px 12px; font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:3px; min-height:42px; display:flex; align-items:center; } | |
| /* Notificari */ | |
| .notif-card { background:var(--glass); border:1px solid var(--glass-border); padding:18px 16px 16px; margin-bottom:12px; position:relative; transition:border-color 0.2s; } | |
| .notif-card.unread { border-color:rgba(255,255,255,0.18); } | |
| .notif-card .nc-type { font-size:9px; letter-spacing:2px; color:var(--white-dim); margin-bottom:8px; } | |
| .notif-card .nc-name { font-family:'Cormorant Garamond',serif; font-size:20px; font-weight:600; margin-bottom:2px; } | |
| .notif-card .nc-vpass { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-bottom:14px; } | |
| .nc-phone-block { display:flex; align-items:center; gap:10px; background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.1); padding:10px 14px; margin-bottom:14px; } | |
| .nc-phone-block .ph-label { font-size:8px; letter-spacing:2px; color:var(--white-dim); } | |
| .nc-phone-block .ph-num { font-family:'DM Mono',monospace; font-size:15px; color:var(--white); letter-spacing:2px; margin-top:3px; } | |
| .nc-code-block { display:flex; align-items:center; gap:14px; margin-bottom:14px; padding:14px 16px; background:rgba(20,18,5,0.6); border:1px solid rgba(255,220,60,0.18); } | |
| .nc-code { font-family:'Cormorant Garamond',serif; font-size:42px; letter-spacing:14px; color:var(--white); } | |
| .nc-code-info { font-size:9px; color:rgba(255,215,60,0.55); letter-spacing:1px; line-height:2.2; } | |
| .nc-code-info strong { color:rgba(255,215,60,0.85); } | |
| .nc-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; } | |
| .btn-copy-sms { background:rgba(255,255,255,0.07); border:1px solid rgba(255,255,255,0.18); color:var(--white); padding:9px 14px; font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px; cursor:pointer; transition:all 0.2s; display:flex; align-items:center; gap:7px; } | |
| .btn-copy-sms:hover { background:rgba(255,255,255,0.13); } | |
| .btn-copy-sms.copied { border-color:rgba(60,120,60,0.6); color:#5a9a5a; } | |
| .btn-copy-sms svg { width:13px; height:13px; } | |
| .notif-card .nc-time { position:absolute; top:14px; right:14px; font-size:9px; color:var(--white-faint); letter-spacing:1px; } | |
| .notif-card.done { opacity:0.32; pointer-events:none; } | |
| .notif-empty { font-size:11px; color:var(--white-dim); padding:30px 0; letter-spacing:1px; text-align:center; } | |
| .notif-badge { display:inline-flex; align-items:center; justify-content:center; width:16px; height:16px; border-radius:50%; background:rgba(200,60,60,0.9); color:#fff; font-size:8px; margin-left:5px; line-height:1; } | |
| /* Push banner */ | |
| .push-banner { position:fixed; top:62px; right:14px; z-index:9990; background:rgba(12,12,12,0.97); backdrop-filter:blur(1px); border:1px solid rgba(255,255,255,0.14); padding:14px 36px 14px 16px; max-width:290px; transform:translateX(340px); transition:transform 0.4s cubic-bezier(0.16,1,0.3,1); box-shadow:0 8px 40px rgba(0,0,0,0.7); } | |
| .push-banner.show { transform:translateX(0); } | |
| .pb-title { font-size:8px; letter-spacing:2px; color:var(--white-dim); margin-bottom:5px; } | |
| .pb-name { font-family:'Cormorant Garamond',serif; font-size:17px; font-weight:600; } | |
| .pb-sub { font-size:10px; color:var(--white-dim); margin-top:3px; letter-spacing:1px; } | |
| .pb-close { position:absolute; top:9px; right:11px; background:none; border:none; color:var(--white-dim); cursor:pointer; font-size:15px; } | |
| /* Search bar */ | |
| .search-bar { position:relative; margin-bottom:12px; } | |
| .search-bar input { padding-left:32px; } | |
| .search-bar svg { position:absolute; left:10px; top:50%; transform:translateY(-50%); opacity:0.35; pointer-events:none; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="push-banner" id="push-banner"> | |
| <button class="pb-close" onclick="this.parentElement.classList.remove('show')">✕</button> | |
| <div class="pb-title">⬤ CERERE NOUĂ — IDEA</div> | |
| <div class="pb-name" id="pb-name">—</div> | |
| <div class="pb-sub" id="pb-sub">Solicitare înregistrare cont</div> | |
| </div> | |
| <div class="loader-overlay" id="page-loader"> | |
| <div class="loader"><div class="inner one"></div><div class="inner two"></div><div class="inner three"></div></div> | |
| <div class="loader-text">ADMIN PANEL</div> | |
| </div> | |
| <div class="topbar"> | |
| <a href="index.html" class="topbar-logo"><img src="logo.svg" alt="IDEA"><span class="topbar-name">IDEA</span></a> | |
| <div class="topbar-divider"></div> | |
| <span class="topbar-section">ADMIN</span> | |
| <div class="topbar-right"> | |
| <div class="online-dot"></div> | |
| <span class="role-tag" style="color:var(--white);border-color:rgba(255,255,255,0.25);">SUPER ADMIN</span> | |
| <a href="loguri.html" class="btn-ghost" style="font-size:9px;text-decoration:none;">Loguri</a> | |
| <button class="btn-ghost" onclick="logout()" style="font-size:9px;">Ieșire</button> | |
| </div> | |
| </div> | |
| <div class="main"> | |
| <div class="stats-row fade-in"> | |
| <div class="stat-box"><div class="stat-num" id="st-elevi">0</div><div class="stat-lbl">ELEVI</div></div> | |
| <div class="stat-box"><div class="stat-num" id="st-activi">0</div><div class="stat-lbl">CONTURI ACTIVE</div></div> | |
| <div class="stat-box"><div class="stat-num" id="st-prof">0</div><div class="stat-lbl">PROFESORI</div></div> | |
| <div class="stat-box"><div class="stat-num" id="st-notif">0</div><div class="stat-lbl">NOTIFICĂRI</div></div> | |
| </div> | |
| <div class="tabs fade-in-2"> | |
| <button class="tab-btn active" onclick="showTab('t-elevi',this)">ELEVI</button> | |
| <button class="tab-btn" onclick="showTab('t-profesori',this)">PROFESORI</button> | |
| <button class="tab-btn" onclick="showTab('t-materii',this)">MATERII</button> | |
| <button class="tab-btn" onclick="showTab('t-notif',this)" id="tab-notif-btn"> | |
| NOTIFICĂRI<span class="notif-badge" id="notif-badge" style="display:none;">0</span> | |
| </button> | |
| <button class="tab-btn" onclick="showTab('t-sistem',this)">SISTEM</button> | |
| </div> | |
| <!-- ── ELEVI ── --> | |
| <div class="tab-pane active" id="t-elevi"> | |
| <div class="card fade-in-2"> | |
| <div class="card-title">Adaugă Elev</div> | |
| <div class="grid-2"> | |
| <div class="field"><label>Nume Complet</label><input type="text" id="e-nume" placeholder="Nume Prenume" oninput="genVPass()"></div> | |
| <div class="field"><label>Poziție Catalog</label><input type="number" id="e-poz" placeholder="Ex: 5" min="1" max="99" oninput="genVPass()"></div> | |
| </div> | |
| <div class="grid-2"> | |
| <div class="field"><label>PIN (opțional — lasă gol, elevul și-l setează)</label><input type="text" id="e-pin" placeholder="——————" maxlength="6" inputmode="numeric"></div> | |
| <div class="field"><label>VPass ID (generat automat)</label><div class="vpass-preview" id="vpass-prev">—</div></div> | |
| </div> | |
| <div style="display:flex;gap:10px;flex-wrap:wrap;align-items:center;"> | |
| <button class="btn-primary" onclick="addElev()">+ Adaugă Elev</button> | |
| <div style="font-size:9px;color:var(--white-dim);letter-spacing:1px;">sau</div> | |
| <label style="cursor:pointer;display:inline-flex;align-items:center;gap:8px;background:transparent;border:1px solid var(--glass-border);color:var(--white-dim);padding:12px 16px;font-family:'DM Mono',monospace;font-size:9px;letter-spacing:2px;transition:all 0.2s;" onmouseover="this.style.borderColor='rgba(255,255,255,0.28)';this.style.color='var(--white)'" onmouseout="this.style.borderColor='';this.style.color=''"> | |
| <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/></svg> | |
| IMPORT DIN .TXT | |
| <input type="file" accept=".txt" style="display:none;" onchange="importTxt(this)"> | |
| </label> | |
| </div> | |
| <div class="alert error" id="err-elev" style="margin-top:10px;"></div> | |
| <div class="alert success" id="ok-elev" style="margin-top:10px;display:none;"></div> | |
| </div> | |
| <div class="search-bar"> | |
| <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg> | |
| <input type="text" id="elev-search" placeholder="Caută elev..." oninput="filterElevi()" style="background:var(--glass);"> | |
| </div> | |
| <div class="label">Toți elevii clasei — <span id="count-lbl">0 elevi</span></div> | |
| <div class="data-table"> | |
| <div class="dt-head elev-row-admin"> | |
| <div class="col-pos">NR.</div> | |
| <div class="col-vpass">VPASS</div> | |
| <div>NUME</div> | |
| <div class="col-status">CONT</div> | |
| <div>ACȚIUNI</div> | |
| </div> | |
| <div id="elevi-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div> | |
| </div> | |
| </div> | |
| <!-- ── PROFESORI ── --> | |
| <div class="tab-pane" id="t-profesori"> | |
| <div class="card fade-in-2"> | |
| <div class="card-title">Adaugă Profesor</div> | |
| <div class="grid-2"> | |
| <div class="field"><label>Nume Complet</label><input type="text" id="p-nume" placeholder="Nume Prenume"></div> | |
| <div class="field"><label>Materie Predată</label><select id="p-mat"><option value="">— selectează —</option></select></div> | |
| </div> | |
| <div class="field" style="max-width:200px;"><label>Cod PIN</label><input type="text" id="p-pin" placeholder="Ex: 654321" maxlength="6" inputmode="numeric"></div> | |
| <button class="btn-primary" onclick="addProf()">+ Adaugă Profesor</button> | |
| </div> | |
| <div class="data-table"> | |
| <div class="dt-head prof-row"><div>NUME</div><div class="col-mat">MATERIE</div><div class="col-pin">PIN</div><div>ACȚIUNI</div></div> | |
| <div id="prof-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div> | |
| </div> | |
| </div> | |
| <!-- ── MATERII ── --> | |
| <div class="tab-pane" id="t-materii"> | |
| <div class="card fade-in-2"> | |
| <div class="card-title">Adaugă Materie</div> | |
| <div class="field" style="max-width:320px;"><label>Numele Materiei</label><input type="text" id="m-nume" placeholder="Ex: Matematică, Română..."></div> | |
| <button class="btn-primary" onclick="addMat()">+ Adaugă Materie</button> | |
| </div> | |
| <div class="label">Materii active</div> | |
| <div class="data-table"> | |
| <div class="dt-head mat-row"><div>MATERIE</div><div>ACȚIUNI</div></div> | |
| <div id="mat-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div> | |
| </div> | |
| </div> | |
| <!-- ── NOTIFICĂRI ── --> | |
| <div class="tab-pane" id="t-notif"> | |
| <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;"> | |
| <div class="label" style="margin:0;">Cereri & loguri sistem</div> | |
| <div style="display:flex;gap:8px;"> | |
| <button class="btn-outline" onclick="loadNotif()" style="font-size:9px;">↻ Reîncarcă</button> | |
| <button class="btn-outline" onclick="markAllRead()" style="font-size:9px;">Marchează citite</button> | |
| </div> | |
| </div> | |
| <div id="notif-list"><div class="notif-empty">Se încarcă...</div></div> | |
| </div> | |
| <!-- ── SISTEM ── --> | |
| <div class="tab-pane" id="t-sistem"> | |
| <div class="card fade-in-2"> | |
| <div class="card-title">Configurare Sistem</div> | |
| <div class="grid-2"> | |
| <div class="field"><label>Clasa Activă</label><input type="text" id="cfg-clasa" value="7B"></div> | |
| <div class="field"><label>Arhitect</label><input type="text" value="Victor Roșca" readonly style="opacity:0.5;cursor:not-allowed;"></div> | |
| </div> | |
| <div class="grid-2"> | |
| <div class="field"><label>Server</label><input type="text" value="93.117.161.226" readonly style="opacity:0.5;cursor:not-allowed;"></div> | |
| <div class="field"><label>Versiune</label><input type="text" value="IDEA v2.1" readonly style="opacity:0.5;cursor:not-allowed;"></div> | |
| </div> | |
| <button class="btn-primary" onclick="toast('✓ Configurare salvată.')">Salvează</button> | |
| </div> | |
| <div class="card fade-in-3"> | |
| <div class="card-title">Mod Mentenanță</div> | |
| <p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.9;"> | |
| Când activezi modul mentenanță, <strong>toate paginile</strong> sunt redirecționate automat | |
| către pagina 404 (mentenanță). Doar panoul de admin rămâne accesibil. | |
| Elevii și profesorii vor vedea un mesaj că serverul este în reparație. | |
| </p> | |
| <div style="display:flex;align-items:center;gap:14px;flex-wrap:wrap;"> | |
| <div id="maint-status-badge" style="font-size:9px;letter-spacing:2px;padding:4px 12px;border:1px solid rgba(255,255,255,0.12);color:var(--white-dim);">INACTIV</div> | |
| <button class="btn-primary" id="maint-btn" onclick="toggleMaintenance()"> | |
| ⚙ Activează Mentenanță | |
| </button> | |
| </div> | |
| <div id="maint-info" style="display:none;margin-top:14px;font-size:10px;color:rgba(220,160,60,0.9);letter-spacing:1px;line-height:1.9;border-left:2px solid rgba(220,160,60,0.4);padding-left:12px;"> | |
| Serverul este în MOD MENTENANȚĂ.<br> | |
| Toți utilizatorii văd pagina de mentenanță.<br> | |
| Apasă din nou butonul pentru a reactiva sistemul. | |
| </div> | |
| </div> | |
| </div> | |
| <footer class="footer"> | |
| <div class="footer-top"><img src="logo.svg" alt=""><span>IDEA</span></div> | |
| <div class="footer-divider"></div> | |
| <div class="footer-meta">93.117.161.226 · Telenești, Moldova</div> | |
| <div class="footer-copy">© 2026 Victor Roșca — Interfața Digitală de Educație Aplicată — v2.3 IDEA</div> | |
| </footer> | |
| </div> | |
| <div class="toast" id="toast-el"></div> | |
| <script type="module"> | |
| import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js"; | |
| import { getFirestore, collection, getDocs, addDoc, deleteDoc, updateDoc, doc, serverTimestamp } | |
| from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js"; | |
| if (sessionStorage.getItem('vs_role') !== 'admin') { window.location.href='index.html'; } | |
| const cfg = { | |
| apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com", | |
| projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app", | |
| messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf" | |
| }; | |
| const app = initializeApp(cfg); | |
| const db = getFirestore(app); | |
| // Expune pe window pentru scriptul non-module | |
| window._db=db; window._col=collection; window._getDocs=getDocs; | |
| window._addDoc=addDoc; window._deleteDoc=deleteDoc; window._updateDoc=updateDoc; window._doc=doc; | |
| let _elevCache = []; | |
| let _lastNotifCount = -1; | |
| let _pushEnabled = false; | |
| if ('Notification' in window && Notification.permission === 'default') { | |
| Notification.requestPermission().then(p => { _pushEnabled = p==='granted'; }); | |
| } else { _pushEnabled = Notification.permission === 'granted'; } | |
| function sendPush(nume, vpass, telefon) { | |
| const banner = document.getElementById('push-banner'); | |
| if (!banner) return; | |
| document.getElementById('pb-name').textContent = nume; | |
| document.getElementById('pb-sub').textContent = `${vpass} · ${telefon||''}`; | |
| banner.classList.add('show'); | |
| setTimeout(()=>banner.classList.remove('show'), 9000); | |
| if (_pushEnabled) { | |
| try { new Notification('IDEA — Cerere nouă',{ body:`${nume} (${vpass}) solicită înregistrarea.`, icon:'/favicon.svg', tag:'idea-signup' }); } catch(e){} | |
| } | |
| try { | |
| const ctx = new (window.AudioContext||window.webkitAudioContext)(); | |
| [440,660].forEach((f,i) => { | |
| const o=ctx.createOscillator(), g=ctx.createGain(); | |
| o.type='sine'; o.frequency.value=f; | |
| g.gain.setValueAtTime(0,ctx.currentTime+i*0.1); | |
| g.gain.linearRampToValueAtTime(0.08,ctx.currentTime+i*0.1+0.05); | |
| g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+i*0.1+0.4); | |
| o.connect(g); g.connect(ctx.destination); | |
| o.start(ctx.currentTime+i*0.1); o.stop(ctx.currentTime+i*0.1+0.4); | |
| }); | |
| } catch(e){} | |
| } | |
| // ══════════════════════════════════════════ | |
| // ── ELEVI ── | |
| // ══════════════════════════════════════════ | |
| async function loadElevi() { | |
| try { | |
| const snap = await getDocs(collection(db,'elevi')); | |
| _elevCache = []; | |
| snap.forEach(d => _elevCache.push({id:d.id,...d.data()})); | |
| _elevCache.sort((a,b) => (a.pozitie||0)-(b.pozitie||0)); | |
| renderElevi(_elevCache); | |
| document.getElementById('st-elevi').textContent = _elevCache.length; | |
| document.getElementById('st-activi').textContent = _elevCache.filter(e=>e.pin!=null).length; | |
| } catch(e) { console.error('loadElevi:',e); } | |
| } | |
| function renderElevi(list) { | |
| const el = document.getElementById('elevi-list'); | |
| document.getElementById('count-lbl').textContent = `${list.length} elevi`; | |
| el.innerHTML = ''; | |
| if (!list.length) { | |
| el.innerHTML = '<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun elev în baza de date.</div>'; | |
| return; | |
| } | |
| list.forEach(d => { | |
| const hasPin = d.pin != null; | |
| const r = document.createElement('div'); r.className='dt-row elev-row-admin'; | |
| r.innerHTML = ` | |
| <div class="col-pos" style="font-size:11px;color:var(--white-faint);">${String(d.pozitie||'?').padStart(2,'0')}</div> | |
| <div class="col-vpass" style="font-size:10px;color:var(--white-dim);letter-spacing:1px;">${d.vpassId||'—'}</div> | |
| <div style="font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${d.nume}</div> | |
| <div class="col-status"> | |
| <span class="pill ${hasPin?'success':'empty'}" style="font-size:8px;" data-elevid="${d.id}">${hasPin?'ACTIV':'FĂRĂ CONT'}</span> | |
| </div> | |
| <div style="display:flex;gap:5px;flex-wrap:wrap;"> | |
| ${hasPin?`<button class="btn-ghost" style="font-size:8px;padding:4px 8px;" onclick="resetElevPin('${d.id}','${d.nume}')">Reset PIN</button>`:''} | |
| <button class="btn-danger" onclick="delItem('elevi','${d.id}','${d.nume}')">✕</button> | |
| </div>`; | |
| el.appendChild(r); | |
| }); | |
| } | |
| window.filterElevi = function() { | |
| const q = document.getElementById('elev-search').value.toLowerCase(); | |
| renderElevi(_elevCache.filter(e => | |
| e.nume.toLowerCase().includes(q) || ((e.vpassId||'').toLowerCase().includes(q)) | |
| )); | |
| }; | |
| window.resetElevPin = async function(id, nume) { | |
| if (!confirm(`Resetezi parola pentru ${nume}?\nElevul va trebui să se re-înregistreze.`)) return; | |
| try { | |
| await updateDoc(doc(db,'elevi',id), {pin:null, confirmed:false}); | |
| await loadElevi(); | |
| toast(`✓ Parola resetată pentru ${nume}`); | |
| } catch(e) { toast('err-025 — Eroare resetare'); } | |
| }; | |
| // ══════════════════════════════════════════ | |
| // ── PROFESORI ── | |
| // ══════════════════════════════════════════ | |
| async function loadProf() { | |
| try { | |
| const snap = await getDocs(collection(db,'profesori')); | |
| const el = document.getElementById('prof-list'); | |
| el.innerHTML = ''; let n = 0; | |
| snap.forEach(d => { | |
| n++; | |
| const data = d.data(); | |
| const r = document.createElement('div'); r.className='dt-row prof-row'; | |
| r.innerHTML = ` | |
| <div style="font-size:13px;">${data.nume}</div> | |
| <div class="col-mat" style="font-size:11px;color:var(--white-dim);">${data.materie||'—'}</div> | |
| <div class="col-pin" style="font-size:11px;color:var(--white-dim);font-family:'DM Mono',monospace;letter-spacing:2px;">${data.pin||'—'}</div> | |
| <div style="display:flex;gap:5px;flex-wrap:wrap;"> | |
| <button class="btn-ghost" style="font-size:8px;padding:4px 8px;" onclick="resetProfPin('${d.id}','${data.nume}')">Reset PIN</button> | |
| <button class="btn-danger" onclick="delItem('profesori','${d.id}','${data.nume}')">✕</button> | |
| </div>`; | |
| el.appendChild(r); | |
| }); | |
| document.getElementById('st-prof').textContent = n; | |
| if (!n) el.innerHTML = '<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun profesor.</div>'; | |
| } catch(e) { console.error('loadProf:',e); } | |
| } | |
| window.resetProfPin = async function(id, nume) { | |
| // Dialog custom pentru PIN nou | |
| const newPin = prompt(`Introdu PIN-ul nou pentru ${nume} (6 cifre):`); | |
| if (newPin === null) return; // anulat | |
| if (!/^\d{6}$/.test(newPin)) { toast('PIN invalid — trebuie să fie exact 6 cifre.'); return; } | |
| if (!confirm(`Confirmi resetarea PIN-ului pentru ${nume} la: ${newPin}?`)) return; | |
| try { | |
| await updateDoc(doc(db,'profesori',id), {pin: newPin}); | |
| await loadProf(); | |
| toast(`✓ PIN resetat pentru ${nume} → ${newPin}`); | |
| } catch(e) { toast('err-025 — Eroare resetare PIN profesor'); } | |
| }; | |
| // ══════════════════════════════════════════ | |
| // ── MATERII ── | |
| // ══════════════════════════════════════════ | |
| async function loadMat() { | |
| try { | |
| const snap = await getDocs(collection(db,'materii')); | |
| const el = document.getElementById('mat-list'); | |
| const sel = document.getElementById('p-mat'); | |
| el.innerHTML = ''; sel.innerHTML = '<option value="">— selectează —</option>'; let n=0; | |
| snap.forEach(d => { | |
| n++; | |
| const r = document.createElement('div'); r.className='dt-row mat-row'; | |
| r.innerHTML = ` | |
| <div style="font-size:15px;font-family:'Cormorant Garamond',serif;font-weight:600;">${d.data().nume}</div> | |
| <div><button class="btn-danger" onclick="delItem('materii','${d.id}','${d.data().nume}')">✕</button></div>`; | |
| el.appendChild(r); | |
| const o = document.createElement('option'); o.value=d.data().nume; o.textContent=d.data().nume; | |
| sel.appendChild(o); | |
| }); | |
| if (!n) el.innerHTML = '<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Nicio materie.</div>'; | |
| } catch(e) { console.error('loadMat:',e); } | |
| } | |
| // ══════════════════════════════════════════ | |
| // ── NOTIFICARI ── | |
| // ══════════════════════════════════════════ | |
| async function loadNotif() { | |
| try { | |
| const snap = await getDocs(collection(db,'notificari')); | |
| const notifs = []; snap.forEach(d => notifs.push({id:d.id,...d.data()})); | |
| notifs.sort((a,b) => (b.timestamp?.seconds||0)-(a.timestamp?.seconds||0)); | |
| const unread = notifs.filter(n => !n.citita).length; | |
| // Badge | |
| const badge = document.getElementById('notif-badge'); | |
| if (badge) { badge.textContent=unread; badge.style.display=unread?'inline-flex':'none'; } | |
| // Push dacă notificare nouă | |
| if (_lastNotifCount !== -1 && unread > _lastNotifCount) { | |
| const newest = notifs.find(n => !n.citita); | |
| if (newest && newest.tip === 'signup_request') { | |
| sendPush(newest.elevNume, newest.elevVpass, newest.telefon); | |
| } | |
| } | |
| _lastNotifCount = unread; | |
| const el = document.getElementById('notif-list'); | |
| if (!el) return; | |
| el.innerHTML = ''; | |
| if (!notifs.length) { | |
| el.innerHTML = '<div class="notif-empty">Nicio notificare momentan.</div>'; return; | |
| } | |
| notifs.forEach(n => { | |
| const card = document.createElement('div'); | |
| card.className = `notif-card${!n.citita?' unread':''}${['approved','completed','rejected'].includes(n.status)?' done':''}`; | |
| const ts = n.timestamp?.seconds | |
| ? new Date(n.timestamp.seconds*1000).toLocaleString('ro',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'short'}) | |
| : '—'; | |
| const isDone = ['approved','completed','rejected'].includes(n.status); | |
| if (n.tip==='signup_request' || n.tip==='reset_request') { | |
| const tipLabel = n.tip==='reset_request' ? 'RESETARE PAROLĂ' : 'ÎNREGISTRARE CONT'; | |
| const smsMsg = `Salut ${n.elevNume}, codul de confirmare de la IDEA VPass este: ${n.confirmCode}. Te rog să nu partajezi codul nimănui! Este strict confidențial! - Victor ;)`; | |
| card.innerHTML = ` | |
| <div class="nc-type">⬤ ${tipLabel}</div> | |
| <div class="nc-name">${n.elevNume||'—'}</div> | |
| <div class="nc-vpass">${n.elevVpass||'—'}</div> | |
| <div class="nc-phone-block"> | |
| <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1.5" stroke-linecap="round"><path d="M22 16.92v3a2 2 0 01-2.18 2 19.79 19.79 0 01-8.63-3.07A19.5 19.5 0 013.07 9.8 19.79 19.79 0 01.09 1.2 2 2 0 012.07 0h3a2 2 0 012 1.72 12.84 12.84 0 00.7 2.81 2 2 0 01-.45 2.11L6.27 7.7a16 16 0 006.06 6.06l1.06-1.06a2 2 0 012.11-.45 12.84 12.84 0 002.81.7A2 2 0 0122 14.92z"/></svg> | |
| <div><div class="ph-label">NUMĂR DE TELEFON</div><div class="ph-num">${n.telefon||'—'}</div></div> | |
| </div> | |
| <div class="nc-code-block"> | |
| <div class="nc-code">${n.confirmCode||'——'}</div> | |
| <div class="nc-code-info"><strong>COD SECRET VPASS</strong><br>Vizibil doar administratorului.<br>Trimite prin SMS pe numărul de mai sus.</div> | |
| </div> | |
| <div class="nc-actions"> | |
| ${!isDone ? ` | |
| <button class="btn-copy-sms" id="cb-${n.id}" onclick="copySMS('${n.id}','${smsMsg.replace(/'/g,"\\'")}')"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg> | |
| COPIAZĂ MESAJ SMS | |
| </button> | |
| <button class="btn-primary" style="font-size:9px;padding:8px 14px;letter-spacing:1px;" onclick="approveNotif('${n.id}')">VALIDEAZĂ</button> | |
| <button class="btn-danger" style="font-size:9px;" onclick="rejectNotif('${n.id}')">Respinge</button> | |
| ` : `<span class="pill ${n.status==='completed'?'success':'empty'}" style="font-size:9px;">${n.status==='completed'?'FINALIZAT':n.status.toUpperCase()}</span>`} | |
| </div> | |
| <div class="nc-time">${ts}</div>`; | |
| } else { | |
| const tipIcon = n.tip==='upload' ? '↑ UPLOAD' : (n.tip?.toUpperCase()||'LOG'); | |
| card.innerHTML = ` | |
| <div class="nc-type">${tipIcon}</div> | |
| <div style="font-size:13px;margin-bottom:4px;">${n.mesaj||'—'}</div> | |
| <div style="font-size:10px;color:var(--white-dim);">${n.elevNume||''} ${n.elevVpass?'· '+n.elevVpass:''}</div> | |
| <div class="nc-time">${ts}</div>`; | |
| } | |
| el.appendChild(card); | |
| }); | |
| } catch(e) { console.error('loadNotif:',e); } | |
| } | |
| async function approveNotif(nid) { | |
| try { | |
| await updateDoc(doc(db,'notificari',nid), {status:'approved', citita:true}); | |
| toast('✓ Validat! Elevul poate introduce codul și seta parola.'); | |
| loadNotif(); | |
| } catch(e) { toast('err-025'); } | |
| } | |
| async function rejectNotif(nid) { | |
| if (!confirm('Respingi această cerere?')) return; | |
| try { | |
| await updateDoc(doc(db,'notificari',nid), {status:'rejected', citita:true}); | |
| toast('Cerere respinsă.'); loadNotif(); | |
| } catch(e) { toast('err-025'); } | |
| } | |
| async function markAllRead() { | |
| try { | |
| const snap = await getDocs(collection(db,'notificari')); | |
| for (const d of snap.docs) { | |
| if (!d.data().citita) await updateDoc(doc(db,'notificari',d.id), {citita:true}); | |
| } | |
| loadNotif(); toast('✓ Toate marcate ca citite.'); | |
| } catch(e) {} | |
| } | |
| window.copySMS = async function(id, msg) { | |
| try { | |
| await navigator.clipboard.writeText(msg); | |
| const btn = document.getElementById('cb-'+id); | |
| if (btn) { | |
| btn.classList.add('copied'); | |
| btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><polyline points="20 6 9 17 4 12"/></svg> COPIAT!'; | |
| setTimeout(() => { | |
| btn.classList.remove('copied'); | |
| btn.innerHTML = '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 01-2-2V4a2 2 0 012-2h9a2 2 0 012 2v1"/></svg> COPIAZĂ MESAJ SMS'; | |
| }, 3000); | |
| } | |
| } catch(e) { toast('err-027 — Nu s-a putut copia.'); } | |
| }; | |
| window.importTxt = async function(input) { | |
| const file = input.files[0]; | |
| if (!file) return; | |
| const text = await file.text(); | |
| const regex = /\{(\d+)\}\s+([A-ZĂÂÎȘȚ][a-zăâîșț]+(?:\s+[A-ZĂÂÎȘȚ][a-zăâîșț]+)+)/g; | |
| const found = []; let m; | |
| while ((m = regex.exec(text)) !== null) { | |
| const nr = parseInt(m[1]); const nume = m[2].trim(); | |
| if (nr >= 1 && nr <= 99) found.push({nr, nume}); | |
| } | |
| const errEl = document.getElementById('err-elev'); | |
| const okEl = document.getElementById('ok-elev'); | |
| if (!found.length) { | |
| errEl.textContent = 'Niciun elev găsit. Format așteptat: {1} Andronic Aniela'; | |
| errEl.classList.add('show'); return; | |
| } | |
| errEl.classList.remove('show'); | |
| okEl.style.display='block'; okEl.textContent=`Se importă ${found.length} elevi...`; | |
| let ok=0, skip=0; | |
| for (const e of found) { | |
| const parts = e.nume.split(' '); | |
| const initials = parts.map(p=>p[0]).join('').toUpperCase(); | |
| const vpassId = `${initials}-${String(e.nr).padStart(4,'0')}`; | |
| try { | |
| await addDoc(collection(db,'elevi'), { | |
| nume:e.nume, vpassId, pozitie:e.nr, pin:null, confirmed:false | |
| }); | |
| ok++; | |
| } catch(ex) { skip++; } | |
| await new Promise(r=>setTimeout(r,80)); | |
| } | |
| okEl.textContent = `✓ Import complet: ${ok} elevi adăugați${skip?', '+skip+' erori':''}.`; | |
| input.value=''; | |
| setTimeout(()=>loadElevi(), 500); | |
| }; | |
| // Expune pe window | |
| window.loadNotif=loadNotif; window.approveNotif=approveNotif; | |
| window.rejectNotif=rejectNotif; window.markAllRead=markAllRead; | |
| window._loadElevi=loadElevi; window._loadProf=loadProf; window._loadMat=loadMat; | |
| // ══════════════════════════════════════════ | |
| // ── INIT — toate functiile sunt definite, acum le apelam ── | |
| // ══════════════════════════════════════════ | |
| await Promise.all([loadElevi(), loadProf(), loadMat(), loadNotif()]); | |
| document.getElementById('page-loader').classList.add('hide'); | |
| // Polling automat | |
| setInterval(loadNotif, 12000); | |
| setInterval(async () => { | |
| if (!_elevCache.length) return; | |
| try { | |
| const snap = await getDocs(collection(db,'elevi')); | |
| const updated = {}; | |
| snap.forEach(d => { updated[d.id]=d.data(); }); | |
| _elevCache.forEach(e => { | |
| if (updated[e.id]) { | |
| const was = e.pin; e.pin=updated[e.id].pin; | |
| if (was !== e.pin) { | |
| document.querySelectorAll(`[data-elevid="${e.id}"]`).forEach(p => { | |
| p.className=`pill ${e.pin!=null?'success':'empty'}`; | |
| p.textContent=e.pin!=null?'ACTIV':'FĂRĂ CONT'; | |
| }); | |
| } | |
| } | |
| }); | |
| document.getElementById('st-activi').textContent=_elevCache.filter(e=>e.pin!=null).length; | |
| } catch(e) {} | |
| }, 5000); | |
| </script> | |
| <script> | |
| function genVPass(){ | |
| const n=document.getElementById('e-nume').value.trim(); | |
| const p=document.getElementById('e-poz').value.trim(); | |
| const el=document.getElementById('vpass-prev'); | |
| if(!n||!p){el.textContent='—';return;} | |
| const init=n.split(' ').map(w=>w[0]?.toUpperCase()||'').join(''); | |
| el.textContent=`${init}-${String(parseInt(p)).padStart(4,'0')}`; | |
| } | |
| async function addElev(){ | |
| hideError('err-elev'); | |
| const nume=document.getElementById('e-nume').value.trim(); | |
| const poz=document.getElementById('e-poz').value.trim(); | |
| const pinRaw=document.getElementById('e-pin').value.trim(); | |
| const vpassId=document.getElementById('vpass-prev').textContent; | |
| if(!nume||!poz||vpassId==='—'){showError('err-elev','err-021');return;} | |
| if(pinRaw&&pinRaw.length!==6){showError('err-elev','err-022');return;} | |
| try{ | |
| await window._addDoc(window._col(window._db,'elevi'),{ | |
| nume, pozitie:parseInt(poz), pin:pinRaw||null, vpassId, confirmed:!!pinRaw | |
| }); | |
| ['e-nume','e-poz','e-pin'].forEach(id=>document.getElementById(id).value=''); | |
| document.getElementById('vpass-prev').textContent='—'; | |
| await window._loadElevi(); toast('✓ Elev adăugat: '+vpassId); | |
| }catch(e){showError('err-elev','err-025');} | |
| } | |
| async function addProf(){ | |
| const n=document.getElementById('p-nume').value.trim(); | |
| const m=document.getElementById('p-mat').value; | |
| const p=document.getElementById('p-pin').value.trim(); | |
| if(!n||!m||p.length!==6){toast('err-021 — Completează toate câmpurile.');return;} | |
| try{ | |
| await window._addDoc(window._col(window._db,'profesori'),{nume:n,materie:m,pin:p}); | |
| ['p-nume','p-pin'].forEach(id=>document.getElementById(id).value=''); | |
| await window._loadProf(); toast('✓ Profesor adăugat: '+n); | |
| }catch(e){toast('err-025 — Eroare Firestore');} | |
| } | |
| async function addMat(){ | |
| const n=document.getElementById('m-nume').value.trim(); | |
| if(!n){toast('err-021 — Introdu numele materiei.');return;} | |
| try{ | |
| await window._addDoc(window._col(window._db,'materii'),{nume:n}); | |
| document.getElementById('m-nume').value=''; | |
| await window._loadMat(); toast('✓ Materie adăugată: '+n); | |
| }catch(e){toast('err-025');} | |
| } | |
| async function delItem(col,id,name){ | |
| if(!confirm(`Ștergi "${name}"?\nAceastă acțiune este ireversibilă.`))return; | |
| try{ | |
| await window._deleteDoc(window._doc(window._db,col,id)); | |
| if(col==='elevi') await window._loadElevi(); | |
| if(col==='profesori') await window._loadProf(); | |
| if(col==='materii') await window._loadMat(); | |
| toast('✓ Șters: '+name); | |
| }catch(e){toast('err-025');} | |
| } | |
| // 25002500 Mod Mentenanta 25002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500250025002500 | |
| let _maintActive = localStorage.getItem("idea_maintenance") === "1"; | |
| function applyMaintState() { | |
| const badge = document.getElementById("maint-status-badge"); | |
| const btn = document.getElementById("maint-btn"); | |
| const info = document.getElementById("maint-info"); | |
| if (!badge) return; | |
| if (_maintActive) { | |
| badge.textContent = "ACTIV"; badge.style.cssText="font-size:9px;letter-spacing:2px;padding:4px 12px;border:1px solid rgba(220,160,60,0.6);color:rgba(220,160,60,0.9);"; | |
| btn.textContent = "2713 Dezactiveaz0103 Mentenan021ba"; | |
| info.style.display="block"; | |
| } else { | |
| badge.textContent = "INACTIV"; badge.style.cssText="font-size:9px;letter-spacing:2px;padding:4px 12px;border:1px solid rgba(255,255,255,0.12);color:var(--white-dim);"; | |
| btn.textContent = "2699 Activeaz0103 Mentenan021b0103"; | |
| info.style.display="none"; | |
| } | |
| } | |
| function toggleMaintenance() { | |
| _maintActive = !_maintActive; | |
| localStorage.setItem("idea_maintenance", _maintActive ? "1" : "0"); | |
| applyMaintState(); | |
| toast(_maintActive ? "2699 Mentenan021b0103 activat0103." : "2713 Sistem reactivat."); | |
| } | |
| setTimeout(applyMaintState, 400); | |
| function showTab(id,btn){ | |
| document.querySelectorAll('.tab-pane').forEach(t=>t.classList.remove('active')); | |
| document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active')); | |
| document.getElementById(id).classList.add('active'); btn.classList.add('active'); | |
| if(id==='t-notif') loadNotif(); | |
| } | |
| function toast(msg){const t=document.getElementById('toast-el');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),3200);} | |
| function logout(){sessionStorage.removeItem('vs_role');window.location.href='index.html';} | |
| </script> | |
| <a href="vhelp.html" class="vhelp-fab" title="VHelp"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.9)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"> | |
| <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/> | |
| <circle cx="9" cy="10" r="0.5" fill="rgba(255,255,255,0.9)"/> | |
| <circle cx="12" cy="10" r="0.5" fill="rgba(255,255,255,0.9)"/> | |
| <circle cx="15" cy="10" r="0.5" fill="rgba(255,255,255,0.9)"/> | |
| </svg> | |
| </a> | |
| </body> | |
| </html> | |