idea / static /admin-dashboard.html
vsmdvic's picture
Upload 20 files
9b1e4bc verified
<!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>
<!-- ── 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 &nbsp;·&nbsp; Telenești, Moldova</div>
<div class="footer-copy">&copy; 2026 Victor Roșca &mdash; Interfața Digitală de Educație Aplicată &mdash; 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>