md / static /elev-dashboard.html
vsmdvic's picture
Upload 20 files
5f35ed2 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>SGE | Elev</title>
<link rel="stylesheet" href="style.css">
<script src="errors.js"></script>
<script>if(localStorage.getItem('sge_maintenance')==='1'){window.location.replace('404.html');}</script>
</head>
<body>
<!-- LOADER -->
<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" id="loader-txt">SE ÎNCARCĂ</div>
</div>
<div class="topbar">
<a href="index.html" class="topbar-logo">
<img src="logo.svg" alt="VS">
<span class="topbar-name">SGE</span>
</a>
<div class="topbar-divider"></div>
<span class="topbar-section">ELEV</span>
<div class="topbar-right">
<div class="online-dot"></div>
<span class="role-tag" id="vpass-tag"></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-name" style="font-size:16px;letter-spacing:2px;padding-top:4px;"></div><div class="stat-lbl">CONT ACTIV</div></div>
<div class="stat-box"><div class="stat-num" id="st-files">0</div><div class="stat-lbl">FIȘIERE</div></div>
<div class="stat-box"><div class="stat-num" id="st-mat">0</div><div class="stat-lbl">MATERII</div></div>
<div class="stat-box"><div class="stat-num" id="st-size">0 MB</div><div class="stat-lbl">STOCAT</div></div>
</div>
<!-- Materii -->
<div class="label fade-in-2">Selectează materia</div>
<div class="materii-grid fade-in-2" id="materii-grid">
<div style="font-size:11px;color:var(--white-dim);padding:16px 0;letter-spacing:1px;">Se încarcă materiile...</div>
</div>
<!-- Upload -->
<div class="card fade-in-3" id="upload-card">
<div class="card-title">Încarcă fișier</div>
<div id="no-mat-hint" style="font-size:11px;color:var(--white-dim);letter-spacing:1px;padding:4px 0 10px;">
Selectează mai întâi o materie din grila de mai sus.
</div>
<div id="upload-area" style="display:none;">
<div class="upload-zone" id="drop-zone">
<input type="file" id="file-input" onchange="fileSelected(this)">
<div class="uz-icon">
<svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round">
<polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/>
<path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/>
</svg>
</div>
<div class="uz-text" id="uz-text">Trage fișierul aici sau apasă pentru a selecta</div>
<div class="uz-sub">Max. 50MB &nbsp;·&nbsp; Orice format</div>
</div>
<div class="upload-progress-wrap" id="prog-wrap">
<div class="progress">
<div class="progress-value" id="prog-bar"></div>
<div class="progress-pct" id="prog-pct">0%</div>
</div>
<div class="progress-label" id="prog-label">Se pregătește...</div>
</div>
<div class="alert error" id="err-upload" style="margin-top:10px;"></div>
<div class="alert success" id="ok-upload" style="margin-top:10px;display:none;">
✓ Fișier încărcat cu succes!
</div>
</div>
</div>
<!-- Files list -->
<div class="label fade-in-4">Fișierele tale</div>
<div class="card fade-in-4" style="padding:0;">
<div id="files-list" style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;">
Selectează o materie pentru a vedea fișierele.
</div>
</div>
<footer class="footer">
<div class="footer-top"><img src="logo.svg" alt=""><span>SGE</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; Sistem Educațional de Gestiune &mdash; 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, serverTimestamp }
from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
// ── Auth check (persistent) ──
const vsRole = sessionStorage.getItem('vs_role');
const vsUid = sessionStorage.getItem('vs_uid');
const vsName = sessionStorage.getItem('vs_name');
const vsVpass = sessionStorage.getItem('vs_vpass');
if (vsRole !== 'elev' || !vsUid) { 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);
document.getElementById('vpass-tag').textContent = vsVpass || '—';
document.getElementById('st-name').textContent = vsName || '—';
// ── Ping server la fiecare 60s (sesiune activa) ──
function sendPing() {
fetch('/api/ping',{method:'POST',headers:{'Content-Type':'application/json'},
body:JSON.stringify({vpass:vsVpass,name:vsName,role:'elev'})}).catch(()=>{});
}
sendPing();
setInterval(sendPing, 60000);
let selectedMat = null;
let materii = [];
// Load materii
const loaderTxt = document.getElementById('loader-txt');
loaderTxt.textContent = 'MATERII';
try {
const snap = await getDocs(collection(db,'materii'));
snap.forEach(d => materii.push({id:d.id,...d.data()}));
document.getElementById('st-mat').textContent = materii.length;
renderMaterii();
} catch(e) { showError('err-upload','err-026'); }
function renderMaterii() {
const grid = document.getElementById('materii-grid');
grid.innerHTML = '';
if (!materii.length) {
grid.innerHTML = '<div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;padding:16px 0;grid-column:1/-1;">Nicio materie disponibilă. Contactează administratorul.</div>';
return;
}
materii.forEach(m => {
const btn = document.createElement('button');
btn.className = 'materie-btn';
btn.dataset.id = m.id;
btn.dataset.name = m.nume;
btn.innerHTML = `<div class="mb-icon">
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<path d="M2 3h6a4 4 0 014 4v14a3 3 0 00-3-3H2z"/><path d="M22 3h-6a4 4 0 00-4 4v14a3 3 0 013-3h7z"/>
</svg></div>${m.nume}`;
btn.onclick = () => selectMat(m.id, m.nume, btn);
grid.appendChild(btn);
});
}
window.selectMat = function(id, name, btn) {
selectedMat = {id, name};
document.querySelectorAll('.materie-btn').forEach(b => b.classList.remove('selected'));
btn.classList.add('selected');
document.getElementById('no-mat-hint').style.display = 'none';
document.getElementById('upload-area').style.display = 'block';
loadFiles();
};
// ── Load files ──
async function loadFiles() {
const el = document.getElementById('files-list');
el.innerHTML = '<div style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;display:flex;align-items:center;gap:10px;"><div class="pulse-dot"></div>Se încarcă fișierele...</div>';
try {
const r = await fetch(`/drive/list?elevId=${encodeURIComponent(vsVpass)}&materieId=${encodeURIComponent(selectedMat.name)}`);
const data = await r.json();
const files = data.files || [];
el.innerHTML = '';
document.getElementById('st-files').textContent = files.length;
if (!files.length) {
el.innerHTML = '<div style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;">Niciun fișier încărcat la această materie.</div>';
return;
}
let totalBytes = 0;
files.forEach(f => {
totalBytes += parseInt(f.size || 0);
const row = document.createElement('div');
row.className = 'file-row';
row.innerHTML = `
<div class="file-name">
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.4)" stroke-width="1.5" stroke-linecap="round" style="margin-right:6px;vertical-align:middle;">
<path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/><polyline points="13 2 13 9 20 9"/>
</svg>${f.name}
</div>
<div class="file-size">${f.size ? (parseInt(f.size)/1024/1024).toFixed(1)+' MB' : '—'}</div>
<div style="display:flex;gap:6px;align-items:center;">
<a href="/drive/download/${f.id}" class="btn-ghost" style="font-size:8px;padding:4px 8px;text-decoration:none;">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="vertical-align:middle;margin-right:3px;"><path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
DESCARCĂ
</a>
<button onclick="deleteFile('${f.id}', this)" class="btn-ghost" style="font-size:8px;padding:4px 8px;color:#cc5555;border-color:rgba(200,80,80,0.3);">
<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" style="vertical-align:middle;margin-right:3px;"><polyline points="3 6 5 6 21 6"/><path d="M19 6l-1 14a2 2 0 01-2 2H8a2 2 0 01-2-2L5 6"/><path d="M10 11v6"/><path d="M14 11v6"/><path d="M9 6V4h6v2"/></svg>
ȘTERGE
</button>
</div>`;
el.appendChild(row);
});
document.getElementById('st-size').textContent = (totalBytes/1024/1024).toFixed(1)+' MB';
} catch(e) { showError('err-upload','err-018'); }
}
// ── File selected ──
window.fileSelected = function(input) {
if (!input.files[0]) return;
const f = input.files[0];
if (f.size > 50 * 1024 * 1024) { showError('err-upload','err-010'); input.value=''; return; }
document.getElementById('uz-text').textContent = f.name;
document.getElementById('ok-upload').style.display = 'none';
hideError('err-upload');
uploadFile(f);
};
// ── Drag & drop ──
const dz = document.getElementById('drop-zone');
dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); });
dz.addEventListener('dragleave', () => dz.classList.remove('drag'));
dz.addEventListener('drop', e => {
e.preventDefault(); dz.classList.remove('drag');
const f = e.dataTransfer.files[0];
if (f) {
if (f.size > 50*1024*1024) { showError('err-upload','err-010'); return; }
document.getElementById('uz-text').textContent = f.name;
uploadFile(f);
}
});
// ── Upload către Google Drive (prin Flask) ──
async function uploadFile(file) {
if (!selectedMat) { showError('err-upload','err-012'); return; }
hideError('err-upload');
document.getElementById('ok-upload').style.display = 'none';
const progWrap = document.getElementById('prog-wrap');
const progBar = document.getElementById('prog-bar');
const progPct = document.getElementById('prog-pct');
const progLbl = document.getElementById('prog-label');
progWrap.classList.add('show');
progBar.style.width = '0%'; progPct.textContent = '0%';
progLbl.textContent = 'Se pregătește...';
try {
const formData = new FormData();
formData.append('file', file);
formData.append('elevId', vsVpass);
formData.append('materieId', selectedMat.name);
await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.open('POST', '/drive/upload');
xhr.upload.onprogress = e => {
if (e.lengthComputable) {
const pct = Math.round(e.loaded / e.total * 100);
progBar.style.width = pct + '%';
progPct.textContent = pct + '%';
if (pct < 30) progLbl.textContent = 'Se conectează...';
else if (pct < 70) progLbl.textContent = 'Se transferă fișierul...';
else if (pct < 95) progLbl.textContent = 'Se procesează...';
else progLbl.textContent = 'Aproape gata...';
}
};
xhr.onload = () => {
if (xhr.status === 200) {
try { resolve(JSON.parse(xhr.responseText)); }
catch(e) { reject(new Error('Răspuns invalid de la server')); }
} else {
let errMsg = `HTTP ${xhr.status}`;
try { const d = JSON.parse(xhr.responseText); errMsg = d.error || errMsg; } catch(e) {}
reject(new Error(errMsg));
}
};
xhr.onerror = () => reject(new Error('Eroare rețea — serverul nu răspunde'));
xhr.send(formData);
});
progBar.style.width = '100%'; progPct.textContent = '100%';
progLbl.textContent = 'Finalizat!';
// Notificare Firestore
try {
await addDoc(collection(db,'notificari'),{
tip:'upload', elevId:vsUid, elevVpass:vsVpass, elevNume:vsName,
mesaj:`${vsName} a încărcat: ${file.name} (${selectedMat.name})`,
citita:false, timestamp:serverTimestamp()
});
} catch(e) {}
await new Promise(r=>setTimeout(r,500));
progWrap.classList.remove('show');
document.getElementById('ok-upload').style.display = 'block';
document.getElementById('uz-text').textContent = 'Trage fișierul aici sau apasă pentru a selecta';
document.getElementById('file-input').value = '';
await loadFiles();
} catch(e) {
progWrap.classList.remove('show');
const msg = e.message || 'Eroare necunoscută';
showError('err-upload', 'err-009', `Upload eșuat: ${msg}`);
console.error('[UPLOAD ERROR]', e);
}
}
// ── Ștergere fișier ──
window.deleteFile = async function(fileId, btn) {
if (!confirm('Ești sigur că vrei să ștergi acest fișier?')) return;
btn.disabled = true; btn.textContent = '...';
try {
const r = await fetch(`/drive/delete/${fileId}`, { method: 'DELETE' });
if (r.ok) { await loadFiles(); }
else { btn.disabled = false; btn.textContent = 'ȘTERGE'; alert('Eroare la ștergere.'); }
} catch(e) { btn.disabled = false; btn.textContent = 'ȘTERGE'; }
};
// ── Hide loader ──
await new Promise(r=>setTimeout(r,600));
document.getElementById('page-loader').classList.add('hide');
</script>
<script>
function logout() {
sessionStorage.removeItem('vs_role'); sessionStorage.removeItem('vs_uid');
sessionStorage.removeItem('vs_name'); sessionStorage.removeItem('vs_vpass');
window.location.href = 'index.html';
}
function toast(msg){
const t=document.getElementById('toast-el');
t.textContent=msg; t.classList.add('show');
setTimeout(()=>t.classList.remove('show'),3000);
}
// Expose pulse-dot style for loading
document.head.insertAdjacentHTML('beforeend',`<style>.pulse-dot{width:7px;height:7px;border-radius:50%;background:rgba(255,255,255,0.4);animation:pulse 2s ease-in-out infinite;display:inline-block;}@keyframes pulse{0%,100%{opacity:0.3;}50%{opacity:1;}}</style>`);
</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>