| <!DOCTYPE html> |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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> |
|
|
| |
| <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); |
| |
| |
| 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){} |
| } |
| |
| |
| |
| |
| 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'); } |
| }; |
| |
| |
| |
| |
| 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) { |
| |
| const newPin = prompt(`Introdu PIN-ul nou pentru ${nume} (6 cifre):`); |
| if (newPin === null) return; |
| 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'); } |
| }; |
| |
| |
| |
| |
| 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); } |
| } |
| |
| |
| |
| |
| 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; |
| |
| |
| const badge = document.getElementById('notif-badge'); |
| if (badge) { badge.textContent=unread; badge.style.display=unread?'inline-flex':'none'; } |
| |
| |
| 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');} |
| } |
| |
| |
| 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> |
|
|