Spaces:
Sleeping
Sleeping
| <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>VSERVERS | 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(20px); 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Ă — VSERVERS</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="VS"><span class="topbar-name">VSERVERS</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> | |
| <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> | |
| <button class="btn-primary" onclick="addElev()">+ Adaugă Elev</button> | |
| <div class="alert error" id="err-elev" style="margin-top:10px;"></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="VSERVERS v3.0" 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">Firebase Storage</div> | |
| <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.8;"> | |
| Fișierele elevilor sunt stocate în Firebase Storage (5GB gratuit). Gestionarea se face direct din Firebase Console. | |
| </p> | |
| <a href="https://console.firebase.google.com/project/vservers1/storage" target="_blank" class="btn-outline" style="text-decoration:none;display:inline-block;font-size:9px;">Deschide Firebase Console →</a> | |
| </div> | |
| </div> | |
| <footer class="footer"> | |
| <div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</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 — Sistem Educațional de Gestiune — v3.0</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, query, where, 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; window._query=query; window._where=where; | |
| let _elevCache = []; | |
| let _lastNotifCount = -1; | |
| let _pushEnabled = false; | |
| // Push notifications init | |
| 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'); | |
| 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('VSERVERS — Cerere nouă',{ body:`${nume} (${vpass}) solicită înregistrarea.`, icon:'/favicon.svg', tag:'vservers-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){} | |
| } | |
| // ── LOAD ALL ── | |
| await Promise.all([loadElevi(), loadProf(), loadMat(), loadNotif()]); | |
| document.getElementById('page-loader').classList.add('hide'); | |
| // ── Auto-polling notificari ── | |
| setInterval(loadNotif, 12000); | |
| // ── 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(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;">${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="resetPin('${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.resetPin = 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 r=document.createElement('div'); r.className='dt-row prof-row'; | |
| r.innerHTML=`<div style="font-size:13px;">${d.data().nume}</div><div class="col-mat" style="font-size:11px;color:var(--white-dim);">${d.data().materie||'—'}</div><div class="col-pin" style="font-size:11px;color:var(--white-dim);">${d.data().pin}</div><div><button class="btn-danger" onclick="delItem('profesori','${d.id}','${d.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){} | |
| } | |
| // ── 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){} | |
| } | |
| // ── 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; | |
| document.getElementById('st-notif').textContent=unread; | |
| const badge=document.getElementById('notif-badge'); | |
| if(unread>0){badge.textContent=unread;badge.style.display='inline-flex';} | |
| else badge.style.display='none'; | |
| // Push daca notificari noi | |
| if(_lastNotifCount>=0 && unread>_lastNotifCount){ | |
| const newest=notifs.find(n=>!n.citita&&(n.tip==='signup_request'||n.tip==='reset_request')); | |
| if(newest) sendPush(newest.elevNume||'—',newest.elevVpass||'—',newest.telefon||''); | |
| } | |
| _lastNotifCount=unread; | |
| const el=document.getElementById('notif-list'); el.innerHTML=''; | |
| if(!notifs.length){el.innerHTML='<div class="notif-empty">Nicio notificare. Totul e în regulă.</div>';return;} | |
| notifs.forEach(n=>{ | |
| const card=document.createElement('div'); | |
| card.className='notif-card'+(n.citita?' done':' unread'); | |
| 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=`Bună, ${n.elevNume}!\n\nCodul de confirmare VPass pentru contul ${n.elevVpass} este:\n\n${n.confirmCode}\n\nAtenție! Nu partajați nimănui acest cod, este personal și confidențial.\n\n— Roșca 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('notif:',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.loadNotif=loadNotif; | |
| window.approveNotif=approveNotif; window.rejectNotif=rejectNotif; window.markAllRead=markAllRead; | |
| window._loadElevi=loadElevi; window._loadProf=loadProf; window._loadMat=loadMat; | |
| 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.'); } | |
| }; | |
| </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');} | |
| } | |
| 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> | |
| </body> | |
| </html> | |