droplyvictor89 commited on
Commit
05a543b
·
verified ·
1 Parent(s): a38d63e

Upload 12 files

Browse files
static/404.html ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ro">
3
+ <head>
4
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <title>VSERVERS | 404</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <style>
10
+ body { display:flex; align-items:center; justify-content:center; min-height:100dvh; }
11
+ .db-line { display:flex; align-items:center; gap:10px; }
12
+ .db-line span { font-size:9px; letter-spacing:2px; }
13
+ .typing::after { content:'_'; animation:blink-cur 1s step-end infinite; }
14
+ @keyframes blink-cur { 0%,100%{opacity:1;} 50%{opacity:0;} }
15
+ </style>
16
+ </head>
17
+ <body>
18
+ <div class="page-404">
19
+ <img src="logo.svg" style="width:36px;height:36px;opacity:0.2;margin-bottom:24px;" class="fade-in">
20
+
21
+ <div class="e-code fade-in-2">404</div>
22
+
23
+ <div class="e-title fade-in-2">Pagină negăsită</div>
24
+ <div class="e-sub fade-in-3">
25
+ Resursa solicitată nu există în sistemul VSERVERS.<br>
26
+ Verifică URL-ul sau revino la pagina principală.
27
+ </div>
28
+
29
+ <div class="e-db fade-in-4" id="db-log">
30
+ <div class="db-line"><span class="db-ok">✓</span><span>VSERVERS v3.0 — online</span></div>
31
+ <div class="db-line"><span class="db-ok">✓</span><span>Server: 93.117.161.226</span></div>
32
+ <div class="db-line" id="fb-line"><span>·</span><span>Firebase: verificare...</span></div>
33
+ <div class="db-line" id="b2-line" style="opacity:0.3"><span>·</span><span>Storage B2: —</span></div>
34
+ <div class="db-line" id="req-line" style="margin-top:8px;"><span class="db-err">✗</span><span class="typing" id="req-text">err-404 — rută inexistentă</span></div>
35
+ </div>
36
+
37
+ <div style="margin-top:32px;" class="fade-in-5">
38
+ <a href="index.html" class="btn-primary" style="text-decoration:none;display:inline-block;padding:11px 28px;letter-spacing:3px;font-size:10px;">← LOGIN</a>
39
+ </div>
40
+
41
+ <div class="footer-mini" style="margin-top:40px;">
42
+ VSERVERS &copy;2026 &mdash; Victor Roșca
43
+ </div>
44
+ </div>
45
+
46
+ <script type="module">
47
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
48
+ import { getFirestore, collection, getDocs } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
49
+
50
+ const cfg = {
51
+ apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
52
+ projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
53
+ messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
54
+ };
55
+
56
+ async function checkDB() {
57
+ const fbLine = document.getElementById('fb-line');
58
+ const b2Line = document.getElementById('b2-line');
59
+ try {
60
+ const app = initializeApp(cfg);
61
+ const db = getFirestore(app);
62
+ await getDocs(collection(db,'elevi'));
63
+ fbLine.innerHTML = '<span class="db-ok">✓</span><span>Firebase Firestore: conectat</span>';
64
+ b2Line.style.opacity='1';
65
+ b2Line.innerHTML='<span class="db-ok">✓</span><span>Storage B2: activ</span>';
66
+ } catch(e) {
67
+ fbLine.innerHTML = '<span class="db-err">✗</span><span>Firebase: err-001 — conexiune eșuată</span>';
68
+ b2Line.style.opacity='1';
69
+ b2Line.innerHTML='<span class="db-err">?</span><span>Storage B2: necunoscut</span>';
70
+ }
71
+ }
72
+
73
+ await new Promise(r=>setTimeout(r,800));
74
+ checkDB();
75
+ </script>
76
+ </body>
77
+ </html>
static/admin-dashboard.html CHANGED
@@ -8,68 +8,77 @@
8
  <link rel="stylesheet" href="style.css">
9
  <script src="errors.js"></script>
10
  <style>
11
- .vpass-preview { background:rgba(255,255,255,0.05); border:1px solid rgba(255,255,255,0.08); color:var(--white); padding:9px 11px; font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:3px; min-height:40px; }
12
- .danger-box { border:1px solid #3a1010; padding:20px 16px; background:rgba(30,5,5,0.4); }
13
- .danger-box h4 { font-size:10px; letter-spacing:3px; color:#cc5555; margin-bottom:8px; }
14
- .elev-row { grid-template-columns:110px 1fr 70px 70px; }
15
  .prof-row { grid-template-columns:1fr 140px 70px 70px; }
16
  .mat-row { grid-template-columns:1fr 70px; }
17
- @media(max-width:480px) {
18
- .elev-row { grid-template-columns:1fr auto; }
19
- .elev-row .col-vpass,.elev-row .col-pin { display:none; }
20
  .prof-row { grid-template-columns:1fr auto; }
21
  .prof-row .col-mat,.prof-row .col-pin { display:none; }
22
  }
 
23
 
24
- /* Notificări */
25
- .notif-badge {
26
- display:inline-flex; align-items:center; justify-content:center;
27
- width:16px; height:16px; border-radius:50%;
28
- background:rgba(200,80,80,0.85); color:#fff;
29
- font-size:8px; margin-left:5px; line-height:1;
30
- font-family:'DM Mono',monospace;
31
- }
32
- .notif-card {
33
- background:rgba(255,255,255,0.03);
34
- border:1px solid rgba(255,255,255,0.07);
35
- padding:16px 14px;
36
- margin-bottom:10px;
37
- position:relative;
38
- transition:border-color 0.2s;
39
- }
40
- .notif-card.unread { border-color:rgba(255,255,255,0.14); }
41
- .notif-card .nc-type { font-size:9px; letter-spacing:2px; color:var(--white-dim); margin-bottom:6px; }
42
- .notif-card .nc-name { font-family:'Cormorant Garamond',serif; font-size:17px; font-weight:600; margin-bottom:2px; }
43
- .notif-card .nc-vpass { font-size:10px; color:var(--white-dim); letter-spacing:1px; margin-bottom:10px; }
44
- .notif-card .nc-code-wrap { display:flex; align-items:center; gap:14px; margin-bottom:12px; }
45
- .notif-card .nc-code {
46
- font-family:'Cormorant Garamond',serif; font-size:32px; letter-spacing:8px;
47
- color:var(--white); background:rgba(255,255,255,0.05);
48
- padding:8px 18px; border:1px solid rgba(255,255,255,0.1);
49
- }
50
- .notif-card .nc-code-label { font-size:9px; color:var(--white-dim); letter-spacing:1px; line-height:1.8; }
51
- .notif-card .nc-actions { display:flex; gap:8px; flex-wrap:wrap; }
52
- .notif-card .nc-time { position:absolute; top:12px; right:12px; font-size:9px; color:var(--white-faint); letter-spacing:1px; }
53
- .notif-card.done { opacity:0.4; }
54
- .notif-card.done .nc-actions { display:none; }
55
- .notif-empty { font-size:11px; color:var(--white-dim); padding:20px 0; letter-spacing:1px; text-align:center; }
56
-
57
- .err-code { font-size:9px; opacity:0.6; margin-right:4px; letter-spacing:1px; }
58
- .tab-btn .notif-badge { vertical-align:middle; }
59
  </style>
60
  </head>
61
  <body>
62
 
 
 
 
 
 
 
 
 
 
 
 
 
63
  <div class="topbar">
64
- <a href="index.html" class="topbar-logo">
65
- <img src="logo.svg" alt="VS">
66
- <span class="topbar-name">VSERVERS</span>
67
- </a>
68
  <div class="topbar-divider"></div>
69
  <span class="topbar-section">ADMIN</span>
70
  <div class="topbar-right">
71
- <span class="role-tag" style="color:var(--white);border-color:#444;">SUPER ADMIN</span>
72
- <button class="btn-ghost" onclick="logout()">Iesire</button>
 
73
  </div>
74
  </div>
75
 
@@ -77,9 +86,9 @@
77
 
78
  <div class="stats-row fade-in">
79
  <div class="stat-box"><div class="stat-num" id="st-elevi">0</div><div class="stat-lbl">ELEVI</div></div>
 
80
  <div class="stat-box"><div class="stat-num" id="st-prof">0</div><div class="stat-lbl">PROFESORI</div></div>
81
- <div class="stat-box"><div class="stat-num" id="st-mat">0</div><div class="stat-lbl">MATERII</div></div>
82
- <div class="stat-box"><div class="stat-num" id="st-notif">0</div><div class="stat-lbl">NOTIFICARI</div></div>
83
  </div>
84
 
85
  <div class="tabs fade-in-2">
@@ -87,104 +96,117 @@
87
  <button class="tab-btn" onclick="showTab('t-profesori',this)">PROFESORI</button>
88
  <button class="tab-btn" onclick="showTab('t-materii',this)">MATERII</button>
89
  <button class="tab-btn" onclick="showTab('t-notif',this)" id="tab-notif-btn">
90
- NOTIFICARI<span class="notif-badge" id="notif-badge" style="display:none;">0</span>
91
  </button>
92
  <button class="tab-btn" onclick="showTab('t-sistem',this)">SISTEM</button>
93
  </div>
94
 
95
- <!-- ELEVI -->
96
  <div class="tab-pane active" id="t-elevi">
97
  <div class="card fade-in-2">
98
- <div class="card-title">Adauga Elev</div>
99
  <div class="grid-2">
100
  <div class="field"><label>Nume Complet</label><input type="text" id="e-nume" placeholder="Nume Prenume" oninput="genVPass()"></div>
101
- <div class="field"><label>Pozitie Catalog</label><input type="number" id="e-poz" placeholder="Ex: 5" min="1" max="99" oninput="genVPass()"></div>
102
  </div>
103
  <div class="grid-2">
104
- <div class="field"><label>Cod PIN (optional)</label><input type="text" id="e-pin" placeholder="Lasa gol = elev si-l seteaza" maxlength="6" inputmode="numeric"></div>
105
- <div class="field"><label>VPass ID</label><div class="vpass-preview" id="vpass-prev">—</div></div>
106
  </div>
107
- <button class="btn-primary" onclick="addElev()">+ Adauga Elev</button>
108
- <div class="alert error" id="err-admin-elev" style="margin-top:10px;"></div>
 
 
 
 
 
109
  </div>
110
- <div class="label">Lista elevilor</div>
 
111
  <div class="data-table">
112
- <div class="dt-head elev-row"><div class="col-vpass">VPASS</div><div>NUME</div><div class="col-pin">STATUS</div><div>ACTIUNI</div></div>
113
- <div id="elevi-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se incarca...</div></div>
 
 
 
 
 
 
114
  </div>
115
  </div>
116
 
117
- <!-- PROFESORI -->
118
  <div class="tab-pane" id="t-profesori">
119
  <div class="card fade-in-2">
120
- <div class="card-title">Adauga Profesor</div>
121
  <div class="grid-2">
122
  <div class="field"><label>Nume Complet</label><input type="text" id="p-nume" placeholder="Nume Prenume"></div>
123
- <div class="field"><label>Materie Predata</label><select id="p-mat"><option value="">-- Selecteaza --</option></select></div>
124
  </div>
125
  <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>
126
- <button class="btn-primary" onclick="addProf()">+ Adauga Profesor</button>
127
  </div>
128
- <div class="label">Lista profesorilor</div>
129
  <div class="data-table">
130
- <div class="dt-head prof-row"><div>NUME</div><div class="col-mat">MATERIE</div><div class="col-pin">PIN</div><div>ACTIUNI</div></div>
131
- <div id="prof-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se incarca...</div></div>
132
  </div>
133
  </div>
134
 
135
- <!-- MATERII -->
136
  <div class="tab-pane" id="t-materii">
137
  <div class="card fade-in-2">
138
- <div class="card-title">Adauga Materie</div>
139
- <div class="field" style="max-width:320px;"><label>Numele Materiei</label><input type="text" id="m-nume" placeholder="Ex: Matematica, Romana..."></div>
140
- <button class="btn-primary" onclick="addMat()">+ Adauga Materie</button>
141
  </div>
142
  <div class="label">Materii active</div>
143
  <div class="data-table">
144
- <div class="dt-head mat-row"><div>MATERIE</div><div>ACTIUNI</div></div>
145
- <div id="mat-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se incarca...</div></div>
146
  </div>
147
  </div>
148
 
149
- <!-- NOTIFICARI -->
150
  <div class="tab-pane" id="t-notif">
151
- <div class="label">Cereri de inregistrare & loguri sistem</div>
152
- <div id="notif-list">
153
- <div class="notif-empty">Se incarca...</div>
154
- </div>
155
- <div style="margin-top:10px;">
156
- <button class="btn-outline" onclick="loadNotif()" style="font-size:9px;">Reincarca</button>
157
- <button class="btn-outline" onclick="markAllRead()" style="font-size:9px;margin-left:8px;">Marcheaza toate citite</button>
158
  </div>
 
159
  </div>
160
 
161
- <!-- SISTEM -->
162
  <div class="tab-pane" id="t-sistem">
163
  <div class="card fade-in-2">
164
  <div class="card-title">Configurare Sistem</div>
165
  <div class="grid-2">
166
- <div class="field"><label>Clasa Activa</label><input type="text" id="cfg-clasa" value="7B"></div>
167
- <div class="field"><label>Arhitect</label><input type="text" value="Victor Rosca" readonly style="color:var(--white-dim);cursor:not-allowed;"></div>
168
  </div>
169
- <div style="margin-bottom:14px;">
170
- <a href="seed.html" class="btn-outline" style="display:inline-block;text-decoration:none;font-size:10px;padding:8px 14px;">
171
- Import elevi clasa (seed.html)
172
- </a>
173
  </div>
174
- <button class="btn-primary" onclick="toast('Configurare salvata.')">Salveaza</button>
175
  </div>
176
- <div class="danger-box fade-in-3">
177
- <h4>ZONA PERICULOASA</h4>
178
- <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.8;">Upgrade clasa reseteaza structura de date si sterge fisierele permanent.</p>
179
- <button class="btn-danger" onclick="upgradeClasa()">Initializeaza Upgrade Clasa</button>
 
 
 
180
  </div>
181
  </div>
182
 
183
  <footer class="footer">
184
  <div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</span></div>
185
  <div class="footer-divider"></div>
186
- <div class="footer-meta">93.117.161.226 &nbsp;&mdash;&nbsp; Mindresti, Telenesti, Moldova</div>
187
- <div class="footer-copy">&copy; 2026 Victor Rosca &mdash; Toate drepturile rezervate<br>Sistem Educational de Gestiune a Fisierelor &mdash; v2.0</div>
188
  </footer>
189
  </div>
190
 
@@ -192,7 +214,8 @@
192
 
193
  <script type="module">
194
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
195
- import { getFirestore, collection, getDocs, addDoc, deleteDoc, updateDoc, doc, query, where, orderBy } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
 
196
 
197
  if (sessionStorage.getItem('vs_role') !== 'admin') { window.location.href='index.html'; }
198
 
@@ -201,43 +224,103 @@ const cfg = {
201
  projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
202
  messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
203
  };
204
- const app = initializeApp(cfg); const db = getFirestore(app);
 
 
205
  window._db=db; window._col=collection; window._getDocs=getDocs;
206
  window._addDoc=addDoc; window._deleteDoc=deleteDoc; window._updateDoc=updateDoc;
207
- window._doc=doc; window._query=query; window._where=where; window._orderBy=orderBy;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
208
 
209
- await loadAll();
 
 
210
 
211
- async function loadAll() { await Promise.all([loadElevi(), loadProf(), loadMat(), loadNotif()]); }
 
212
 
 
213
  async function loadElevi() {
214
  try {
215
  const snap=await getDocs(collection(db,'elevi'));
216
- const el=document.getElementById('elevi-list'); el.innerHTML='';
217
- const elevi=[]; snap.forEach(d=>elevi.push({id:d.id,...d.data()}));
218
- elevi.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
219
- elevi.forEach(d=>{
220
- const hasPin = d.pin !== null && d.pin !== undefined;
221
- const r=document.createElement('div'); r.className='dt-row elev-row';
222
- r.innerHTML=`
223
- <div class="col-vpass" style="font-size:10px;color:var(--white-dim);letter-spacing:1px;">
224
- <span style="color:var(--white-faint);margin-right:4px;">${String(d.pozitie||'?').padStart(2,'0')}.</span>${d.vpassId||'—'}
225
- </div>
226
- <div style="font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${d.nume}</div>
227
- <div class="col-pin">
228
- <span class="pill ${hasPin?'success':'empty'}" style="font-size:9px;">${hasPin?'ACTIV':'NECONFIGURAT'}</span>
229
- </div>
230
- <div style="display:flex;gap:6px;flex-wrap:wrap;">
231
- ${hasPin?`<button class="btn-outline" style="font-size:8px;padding:3px 8px;" onclick="resetPin('${d.id}','${d.nume}')">Reset PIN</button>`:''}
232
- <button class="btn-danger" onclick="delItem('elevi','${d.id}')">Sterge</button>
233
- </div>`;
234
- el.appendChild(r);
235
- });
236
- document.getElementById('st-elevi').textContent=elevi.length;
237
- if(!elevi.length) el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun elev adaugat.</div>';
238
  }catch(e){console.error(e);}
239
  }
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  async function loadProf() {
242
  try {
243
  const snap=await getDocs(collection(db,'profesori'));
@@ -245,124 +328,134 @@ async function loadProf() {
245
  snap.forEach(d=>{
246
  n++;
247
  const r=document.createElement('div'); r.className='dt-row prof-row';
248
- 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}')">Sterge</button></div>`;
249
  el.appendChild(r);
250
  });
251
  document.getElementById('st-prof').textContent=n;
252
- if(!n) el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun profesor.</div>';
253
  }catch(e){}
254
  }
255
 
 
256
  async function loadMat() {
257
  try {
258
  const snap=await getDocs(collection(db,'materii'));
259
  const el=document.getElementById('mat-list');
260
  const sel=document.getElementById('p-mat');
261
- el.innerHTML=''; sel.innerHTML='<option value="">-- Selecteaza --</option>'; let n=0;
262
  snap.forEach(d=>{
263
  n++;
264
  const r=document.createElement('div'); r.className='dt-row mat-row';
265
- r.innerHTML=`<div style="font-size:14px;font-family:'Cormorant Garamond',serif;font-weight:600;">${d.data().nume}</div><div><button class="btn-danger" onclick="delItem('materii','${d.id}')">Sterge</button></div>`;
266
  el.appendChild(r);
267
  const o=document.createElement('option'); o.value=d.data().nume; o.textContent=d.data().nume; sel.appendChild(o);
268
  });
269
- document.getElementById('st-mat').textContent=n;
270
- if(!n) el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Nicio materie.</div>';
271
  }catch(e){}
272
  }
273
 
 
274
  async function loadNotif() {
275
  try {
276
  const snap=await getDocs(collection(db,'notificari'));
277
  const notifs=[]; snap.forEach(d=>notifs.push({id:d.id,...d.data()}));
278
- notifs.sort((a,b)=>{
279
- const ta=a.timestamp?.seconds||0, tb=b.timestamp?.seconds||0; return tb-ta;
280
- });
281
  const unread=notifs.filter(n=>!n.citita).length;
282
  document.getElementById('st-notif').textContent=unread;
283
  const badge=document.getElementById('notif-badge');
284
  if(unread>0){badge.textContent=unread;badge.style.display='inline-flex';}
285
- else{badge.style.display='none';}
 
 
 
 
 
 
 
286
 
287
  const el=document.getElementById('notif-list'); el.innerHTML='';
288
- if(!notifs.length){el.innerHTML='<div class="notif-empty">Nicio notificare.</div>';return;}
289
 
290
  notifs.forEach(n=>{
291
  const card=document.createElement('div');
292
  card.className='notif-card'+(n.citita?' done':' unread');
293
  const ts=n.timestamp?.seconds?new Date(n.timestamp.seconds*1000).toLocaleString('ro',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'short'}):'—';
294
- const isDone=n.status==='approved'||n.status==='completed'||n.status==='rejected';
295
 
296
- if(n.tip==='signup_request'){
 
 
297
  card.innerHTML=`
298
- <div class="nc-type">CERERE INREGISTRARE</div>
299
  <div class="nc-name">${n.elevNume||'—'}</div>
300
  <div class="nc-vpass">${n.elevVpass||'—'}</div>
301
- <div class="nc-code-wrap">
 
 
 
 
302
  <div class="nc-code">${n.confirmCode||'——'}</div>
303
- <div class="nc-code-label">Cod generat de<br>elev pentru<br>confirmare</div>
304
  </div>
305
  <div class="nc-actions">
306
  ${!isDone?`
307
- <button class="btn-primary" style="font-size:9px;padding:7px 14px;letter-spacing:1px;" onclick="approveSignup('${n.id}','${n.requestId||n.id}','${n.elevId}')">VALIDEAZA</button>
308
- <button class="btn-danger" style="font-size:9px;" onclick="rejectSignup('${n.id}','${n.requestId||n.id}')">Respinge</button>
309
- `:`<span class="pill ${n.status==='approved'||n.status==='completed'?'success':'empty'}" style="font-size:9px;">${n.status==='completed'?'FINALIZAT':n.status.toUpperCase()}</span>`}
 
 
 
 
310
  </div>
311
  <div class="nc-time">${ts}</div>`;
312
  } else {
 
313
  card.innerHTML=`
314
- <div class="nc-type">${n.tip?.toUpperCase()||'LOG'}</div>
315
- <div class="nc-name" style="font-size:14px;">${n.mesaj||'—'}</div>
316
- <div class="nc-vpass">${n.elevNume||''} ${n.elevVpass?'· '+n.elevVpass:''}</div>
317
  <div class="nc-time">${ts}</div>`;
318
  }
319
  el.appendChild(card);
320
  });
321
- }catch(e){console.error(e);}
322
  }
323
 
324
- async function approveSignup(notifId, requestId, elevId) {
325
  try {
326
- // Update signup_request
327
- try { await updateDoc(doc(db,'signup_requests',requestId),{status:'approved'}); } catch(e){}
328
- // Update notificare
329
- await updateDoc(doc(db,'notificari',notifId),{status:'approved',citita:true});
330
- toast('✓ Cerere validata! Elevul poate acum seta parola.');
331
  loadNotif();
332
- }catch(e){ toast('err-025 — Eroare validare'); }
333
  }
334
-
335
- async function rejectSignup(notifId, requestId) {
336
- if(!confirm('Respingi aceasta cerere?')) return;
337
  try {
338
- try { await updateDoc(doc(db,'signup_requests',requestId),{status:'rejected'}); }catch(e){}
339
- await updateDoc(doc(db,'notificari',notifId),{status:'rejected',citita:true});
340
- toast('Cerere respinsa.');
341
  loadNotif();
342
- }catch(e){ toast('err-025 — Eroare'); }
343
  }
344
-
345
  async function markAllRead() {
346
  try {
347
  const snap=await getDocs(collection(db,'notificari'));
348
- for(const d of snap.docs){
349
- if(!d.data().citita) await updateDoc(doc(db,'notificari',d.id),{citita:true});
350
- }
351
- loadNotif(); toast('Toate marcate ca citite.');
352
  }catch(e){}
353
  }
354
 
355
- window.resetPin = async function(elevId, elevNume) {
356
- if(!confirm(`Resetezi parola pentru ${elevNume}? Elevul va trebui sa se reinregistreze.`)) return;
357
- try {
358
- await updateDoc(doc(db,'elevi',elevId),{pin:null,confirmed:false});
359
- loadElevi(); toast(`✓ Parola resetata pentru ${elevNume}`);
360
- }catch(e){ toast('err-025 — Eroare reset'); }
361
- };
362
-
363
  window.loadNotif=loadNotif;
 
364
  window._loadElevi=loadElevi; window._loadProf=loadProf; window._loadMat=loadMat;
365
- window.approveSignup=approveSignup; window.rejectSignup=rejectSignup; window.markAllRead=markAllRead;
 
 
 
 
 
 
 
 
366
  </script>
367
 
368
  <script>
@@ -372,71 +465,68 @@ function genVPass(){
372
  const el=document.getElementById('vpass-prev');
373
  if(!n||!p){el.textContent='—';return;}
374
  const init=n.split(' ').map(w=>w[0]?.toUpperCase()||'').join('');
375
- el.textContent=`${init}-00${String(parseInt(p)).padStart(2,'0')}`;
376
  }
 
377
  async function addElev(){
378
- hideError('err-admin-elev');
379
  const nume=document.getElementById('e-nume').value.trim();
380
  const poz=document.getElementById('e-poz').value.trim();
381
  const pinRaw=document.getElementById('e-pin').value.trim();
382
  const vpassId=document.getElementById('vpass-prev').textContent;
383
- if(!nume||!poz||vpassId==='—'){showError('err-admin-elev','err-021');return;}
384
- if(pinRaw && pinRaw.length!==6){showError('err-admin-elev','err-022');return;}
385
  try{
386
  await window._addDoc(window._col(window._db,'elevi'),{
387
- nume, pozitie:parseInt(poz), pin: pinRaw||null, vpassId, confirmed: !!pinRaw
388
  });
389
- document.getElementById('e-nume').value=''; document.getElementById('e-poz').value='';
390
- document.getElementById('e-pin').value=''; document.getElementById('vpass-prev').textContent='—';
391
- await window._loadElevi(); toast('✓ Elev adaugat: '+vpassId);
392
- }catch(e){showError('err-admin-elev','err-025');}
393
  }
 
394
  async function addProf(){
395
- const nume=document.getElementById('p-nume').value.trim();
396
- const mat=document.getElementById('p-mat').value;
397
- const pin=document.getElementById('p-pin').value.trim();
398
- if(!nume||!mat||pin.length!==6){toast('err-021 — Completeaza toate campurile.');return;}
399
  try{
400
- await window._addDoc(window._col(window._db,'profesori'),{nume,materie:mat,pin});
401
- document.getElementById('p-nume').value=''; document.getElementById('p-pin').value='';
402
- await window._loadProf(); toast('✓ Profesor adaugat: '+nume);
403
  }catch(e){toast('err-025 — Eroare Firestore');}
404
  }
 
405
  async function addMat(){
406
- const nume=document.getElementById('m-nume').value.trim();
407
- if(!nume){toast('err-021 — Introdu numele materiei.');return;}
408
  try{
409
- await window._addDoc(window._col(window._db,'materii'),{nume});
410
  document.getElementById('m-nume').value='';
411
- await window._loadMat(); toast('✓ Materie adaugata: '+nume);
412
- }catch(e){toast('err-025 — Eroare Firestore');}
413
  }
414
- async function delItem(col,id){
415
- if(!confirm('Stergi aceasta inregistrare?'))return;
 
416
  try{
417
  await window._deleteDoc(window._doc(window._db,col,id));
418
- if(col==='elevi')await window._loadElevi();
419
- if(col==='profesori')await window._loadProf();
420
- if(col==='materii')await window._loadMat();
421
- toast('✓ Inregistrare stearsa.');
422
- }catch(e){toast('err-025 — Eroare stergere');}
423
- }
424
- function upgradeClasa(){
425
- if(confirm('ATENTIE: Sterge toate fisierele! Continui?'))
426
- toast('Upgrade initiat. Realizeaza migrarea din Firebase Console.');
427
  }
 
428
  function showTab(id,btn){
429
  document.querySelectorAll('.tab-pane').forEach(t=>t.classList.remove('active'));
430
  document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
431
  document.getElementById(id).classList.add('active'); btn.classList.add('active');
432
  if(id==='t-notif') loadNotif();
433
  }
434
- function toast(msg){
435
- const t=document.getElementById('toast-el');
436
- t.textContent=msg; t.classList.add('show');
437
- setTimeout(()=>t.classList.remove('show'),3000);
438
- }
439
- function logout(){sessionStorage.clear();window.location.href='index.html';}
440
  </script>
441
  </body>
442
  </html>
 
8
  <link rel="stylesheet" href="style.css">
9
  <script src="errors.js"></script>
10
  <style>
11
+ /* Tabel elevi cu status */
12
+ .elev-row-admin { grid-template-columns:90px 1fr 100px 80px 90px; }
 
 
13
  .prof-row { grid-template-columns:1fr 140px 70px 70px; }
14
  .mat-row { grid-template-columns:1fr 70px; }
15
+ @media(max-width:600px){
16
+ .elev-row-admin { grid-template-columns:1fr auto; }
17
+ .elev-row-admin .col-vpass,.elev-row-admin .col-status,.elev-row-admin .col-pos { display:none; }
18
  .prof-row { grid-template-columns:1fr auto; }
19
  .prof-row .col-mat,.prof-row .col-pin { display:none; }
20
  }
21
+ .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; }
22
 
23
+ /* Notificari */
24
+ .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; }
25
+ .notif-card.unread { border-color:rgba(255,255,255,0.18); }
26
+ .notif-card .nc-type { font-size:9px; letter-spacing:2px; color:var(--white-dim); margin-bottom:8px; }
27
+ .notif-card .nc-name { font-family:'Cormorant Garamond',serif; font-size:20px; font-weight:600; margin-bottom:2px; }
28
+ .notif-card .nc-vpass { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-bottom:14px; }
29
+ .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; }
30
+ .nc-phone-block .ph-label { font-size:8px; letter-spacing:2px; color:var(--white-dim); }
31
+ .nc-phone-block .ph-num { font-family:'DM Mono',monospace; font-size:15px; color:var(--white); letter-spacing:2px; margin-top:3px; }
32
+ .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); }
33
+ .nc-code { font-family:'Cormorant Garamond',serif; font-size:42px; letter-spacing:14px; color:var(--white); }
34
+ .nc-code-info { font-size:9px; color:rgba(255,215,60,0.55); letter-spacing:1px; line-height:2.2; }
35
+ .nc-code-info strong { color:rgba(255,215,60,0.85); }
36
+ .nc-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
37
+ .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; }
38
+ .btn-copy-sms:hover { background:rgba(255,255,255,0.13); }
39
+ .btn-copy-sms.copied { border-color:rgba(60,120,60,0.6); color:#5a9a5a; }
40
+ .btn-copy-sms svg { width:13px; height:13px; }
41
+ .notif-card .nc-time { position:absolute; top:14px; right:14px; font-size:9px; color:var(--white-faint); letter-spacing:1px; }
42
+ .notif-card.done { opacity:0.32; pointer-events:none; }
43
+ .notif-empty { font-size:11px; color:var(--white-dim); padding:30px 0; letter-spacing:1px; text-align:center; }
44
+ .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; }
45
+
46
+ /* Push banner */
47
+ .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); }
48
+ .push-banner.show { transform:translateX(0); }
49
+ .pb-title { font-size:8px; letter-spacing:2px; color:var(--white-dim); margin-bottom:5px; }
50
+ .pb-name { font-family:'Cormorant Garamond',serif; font-size:17px; font-weight:600; }
51
+ .pb-sub { font-size:10px; color:var(--white-dim); margin-top:3px; letter-spacing:1px; }
52
+ .pb-close { position:absolute; top:9px; right:11px; background:none; border:none; color:var(--white-dim); cursor:pointer; font-size:15px; }
53
+
54
+ /* Search bar */
55
+ .search-bar { position:relative; margin-bottom:12px; }
56
+ .search-bar input { padding-left:32px; }
57
+ .search-bar svg { position:absolute; left:10px; top:50%; transform:translateY(-50%); opacity:0.35; pointer-events:none; }
58
  </style>
59
  </head>
60
  <body>
61
 
62
+ <div class="push-banner" id="push-banner">
63
+ <button class="pb-close" onclick="this.parentElement.classList.remove('show')">✕</button>
64
+ <div class="pb-title">⬤ CERERE NOUĂ — VSERVERS</div>
65
+ <div class="pb-name" id="pb-name">—</div>
66
+ <div class="pb-sub" id="pb-sub">Solicitare înregistrare cont</div>
67
+ </div>
68
+
69
+ <div class="loader-overlay" id="page-loader">
70
+ <div class="loader"><div class="inner one"></div><div class="inner two"></div><div class="inner three"></div></div>
71
+ <div class="loader-text">ADMIN PANEL</div>
72
+ </div>
73
+
74
  <div class="topbar">
75
+ <a href="index.html" class="topbar-logo"><img src="logo.svg" alt="VS"><span class="topbar-name">VSERVERS</span></a>
 
 
 
76
  <div class="topbar-divider"></div>
77
  <span class="topbar-section">ADMIN</span>
78
  <div class="topbar-right">
79
+ <div class="online-dot"></div>
80
+ <span class="role-tag" style="color:var(--white);border-color:rgba(255,255,255,0.25);">SUPER ADMIN</span>
81
+ <button class="btn-ghost" onclick="logout()" style="font-size:9px;">Ieșire</button>
82
  </div>
83
  </div>
84
 
 
86
 
87
  <div class="stats-row fade-in">
88
  <div class="stat-box"><div class="stat-num" id="st-elevi">0</div><div class="stat-lbl">ELEVI</div></div>
89
+ <div class="stat-box"><div class="stat-num" id="st-activi">0</div><div class="stat-lbl">CONTURI ACTIVE</div></div>
90
  <div class="stat-box"><div class="stat-num" id="st-prof">0</div><div class="stat-lbl">PROFESORI</div></div>
91
+ <div class="stat-box"><div class="stat-num" id="st-notif">0</div><div class="stat-lbl">NOTIFICĂRI</div></div>
 
92
  </div>
93
 
94
  <div class="tabs fade-in-2">
 
96
  <button class="tab-btn" onclick="showTab('t-profesori',this)">PROFESORI</button>
97
  <button class="tab-btn" onclick="showTab('t-materii',this)">MATERII</button>
98
  <button class="tab-btn" onclick="showTab('t-notif',this)" id="tab-notif-btn">
99
+ NOTIFICĂRI<span class="notif-badge" id="notif-badge" style="display:none;">0</span>
100
  </button>
101
  <button class="tab-btn" onclick="showTab('t-sistem',this)">SISTEM</button>
102
  </div>
103
 
104
+ <!-- ── ELEVI ── -->
105
  <div class="tab-pane active" id="t-elevi">
106
  <div class="card fade-in-2">
107
+ <div class="card-title">Adaugă Elev</div>
108
  <div class="grid-2">
109
  <div class="field"><label>Nume Complet</label><input type="text" id="e-nume" placeholder="Nume Prenume" oninput="genVPass()"></div>
110
+ <div class="field"><label>Poziție Catalog</label><input type="number" id="e-poz" placeholder="Ex: 5" min="1" max="99" oninput="genVPass()"></div>
111
  </div>
112
  <div class="grid-2">
113
+ <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>
114
+ <div class="field"><label>VPass ID (generat automat)</label><div class="vpass-preview" id="vpass-prev">—</div></div>
115
  </div>
116
+ <button class="btn-primary" onclick="addElev()">+ Adaugă Elev</button>
117
+ <div class="alert error" id="err-elev" style="margin-top:10px;"></div>
118
+ </div>
119
+
120
+ <div class="search-bar">
121
+ <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>
122
+ <input type="text" id="elev-search" placeholder="Caută elev..." oninput="filterElevi()" style="background:var(--glass);">
123
  </div>
124
+
125
+ <div class="label">Toți elevii clasei — <span id="count-lbl">0 elevi</span></div>
126
  <div class="data-table">
127
+ <div class="dt-head elev-row-admin">
128
+ <div class="col-pos">NR.</div>
129
+ <div class="col-vpass">VPASS</div>
130
+ <div>NUME</div>
131
+ <div class="col-status">CONT</div>
132
+ <div>ACȚIUNI</div>
133
+ </div>
134
+ <div id="elevi-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div>
135
  </div>
136
  </div>
137
 
138
+ <!-- ── PROFESORI ── -->
139
  <div class="tab-pane" id="t-profesori">
140
  <div class="card fade-in-2">
141
+ <div class="card-title">Adaugă Profesor</div>
142
  <div class="grid-2">
143
  <div class="field"><label>Nume Complet</label><input type="text" id="p-nume" placeholder="Nume Prenume"></div>
144
+ <div class="field"><label>Materie Predată</label><select id="p-mat"><option value=""> selectează </option></select></div>
145
  </div>
146
  <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>
147
+ <button class="btn-primary" onclick="addProf()">+ Adaugă Profesor</button>
148
  </div>
 
149
  <div class="data-table">
150
+ <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>
151
+ <div id="prof-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div>
152
  </div>
153
  </div>
154
 
155
+ <!-- ── MATERII ── -->
156
  <div class="tab-pane" id="t-materii">
157
  <div class="card fade-in-2">
158
+ <div class="card-title">Adaugă Materie</div>
159
+ <div class="field" style="max-width:320px;"><label>Numele Materiei</label><input type="text" id="m-nume" placeholder="Ex: Matematică, Română..."></div>
160
+ <button class="btn-primary" onclick="addMat()">+ Adaugă Materie</button>
161
  </div>
162
  <div class="label">Materii active</div>
163
  <div class="data-table">
164
+ <div class="dt-head mat-row"><div>MATERIE</div><div>ACȚIUNI</div></div>
165
+ <div id="mat-list"><div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Se încarcă...</div></div>
166
  </div>
167
  </div>
168
 
169
+ <!-- ── NOTIFICĂRI ── -->
170
  <div class="tab-pane" id="t-notif">
171
+ <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:12px;">
172
+ <div class="label" style="margin:0;">Cereri & loguri sistem</div>
173
+ <div style="display:flex;gap:8px;">
174
+ <button class="btn-outline" onclick="loadNotif()" style="font-size:9px;">↻ Reîncarcă</button>
175
+ <button class="btn-outline" onclick="markAllRead()" style="font-size:9px;">Marchează citite</button>
176
+ </div>
 
177
  </div>
178
+ <div id="notif-list"><div class="notif-empty">Se încarcă...</div></div>
179
  </div>
180
 
181
+ <!-- ── SISTEM ── -->
182
  <div class="tab-pane" id="t-sistem">
183
  <div class="card fade-in-2">
184
  <div class="card-title">Configurare Sistem</div>
185
  <div class="grid-2">
186
+ <div class="field"><label>Clasa Activă</label><input type="text" id="cfg-clasa" value="7B"></div>
187
+ <div class="field"><label>Arhitect</label><input type="text" value="Victor Roșca" readonly style="opacity:0.5;cursor:not-allowed;"></div>
188
  </div>
189
+ <div class="grid-2">
190
+ <div class="field"><label>Server</label><input type="text" value="93.117.161.226" readonly style="opacity:0.5;cursor:not-allowed;"></div>
191
+ <div class="field"><label>Versiune</label><input type="text" value="VSERVERS v3.0" readonly style="opacity:0.5;cursor:not-allowed;"></div>
 
192
  </div>
193
+ <button class="btn-primary" onclick="toast('Configurare salvată.')">Salvează</button>
194
  </div>
195
+
196
+ <div class="card fade-in-3">
197
+ <div class="card-title">Firebase Storage</div>
198
+ <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.8;">
199
+ Fișierele elevilor sunt stocate în Firebase Storage (5GB gratuit). Gestionarea se face direct din Firebase Console.
200
+ </p>
201
+ <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>
202
  </div>
203
  </div>
204
 
205
  <footer class="footer">
206
  <div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</span></div>
207
  <div class="footer-divider"></div>
208
+ <div class="footer-meta">93.117.161.226 &nbsp;·&nbsp; Mîndrești, Telenești, Moldova</div>
209
+ <div class="footer-copy">&copy; 2026 Victor Roșca &mdash; Sistem Educațional de Gestiune &mdash; v3.0</div>
210
  </footer>
211
  </div>
212
 
 
214
 
215
  <script type="module">
216
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
217
+ import { getFirestore, collection, getDocs, addDoc, deleteDoc, updateDoc, doc, query, where, serverTimestamp }
218
+ from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
219
 
220
  if (sessionStorage.getItem('vs_role') !== 'admin') { window.location.href='index.html'; }
221
 
 
224
  projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
225
  messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
226
  };
227
+ const app = initializeApp(cfg);
228
+ const db = getFirestore(app);
229
+
230
  window._db=db; window._col=collection; window._getDocs=getDocs;
231
  window._addDoc=addDoc; window._deleteDoc=deleteDoc; window._updateDoc=updateDoc;
232
+ window._doc=doc; window._query=query; window._where=where;
233
+
234
+ let _elevCache = [];
235
+ let _lastNotifCount = -1;
236
+ let _pushEnabled = false;
237
+
238
+ // Push notifications init
239
+ if ('Notification' in window && Notification.permission === 'default') {
240
+ Notification.requestPermission().then(p => { _pushEnabled = p==='granted'; });
241
+ } else { _pushEnabled = Notification.permission === 'granted'; }
242
+
243
+ function sendPush(nume, vpass, telefon) {
244
+ const banner = document.getElementById('push-banner');
245
+ document.getElementById('pb-name').textContent = nume;
246
+ document.getElementById('pb-sub').textContent = `${vpass} · ${telefon||''}`;
247
+ banner.classList.add('show');
248
+ setTimeout(()=>banner.classList.remove('show'), 9000);
249
+ if (_pushEnabled) {
250
+ try { new Notification('VSERVERS — Cerere nouă',{ body:`${nume} (${vpass}) solicită înregistrarea.`, icon:'/favicon.svg', tag:'vservers-signup' }); }
251
+ catch(e){}
252
+ }
253
+ try {
254
+ const ctx = new (window.AudioContext||window.webkitAudioContext)();
255
+ [440,660].forEach((f,i) => {
256
+ const o=ctx.createOscillator(), g=ctx.createGain();
257
+ o.type='sine'; o.frequency.value=f;
258
+ g.gain.setValueAtTime(0,ctx.currentTime+i*0.1);
259
+ g.gain.linearRampToValueAtTime(0.08,ctx.currentTime+i*0.1+0.05);
260
+ g.gain.exponentialRampToValueAtTime(0.001,ctx.currentTime+i*0.1+0.4);
261
+ o.connect(g); g.connect(ctx.destination);
262
+ o.start(ctx.currentTime+i*0.1); o.stop(ctx.currentTime+i*0.1+0.4);
263
+ });
264
+ } catch(e){}
265
+ }
266
 
267
+ // ── LOAD ALL ──
268
+ await Promise.all([loadElevi(), loadProf(), loadMat(), loadNotif()]);
269
+ document.getElementById('page-loader').classList.add('hide');
270
 
271
+ // ── Auto-polling notificari ──
272
+ setInterval(loadNotif, 12000);
273
 
274
+ // ── ELEVI ──
275
  async function loadElevi() {
276
  try {
277
  const snap=await getDocs(collection(db,'elevi'));
278
+ _elevCache=[];
279
+ snap.forEach(d=>_elevCache.push({id:d.id,...d.data()}));
280
+ _elevCache.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
281
+ renderElevi(_elevCache);
282
+ document.getElementById('st-elevi').textContent=_elevCache.length;
283
+ document.getElementById('st-activi').textContent=_elevCache.filter(e=>e.pin!=null).length;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  }catch(e){console.error(e);}
285
  }
286
 
287
+ function renderElevi(list) {
288
+ const el=document.getElementById('elevi-list');
289
+ document.getElementById('count-lbl').textContent=`${list.length} elevi`;
290
+ el.innerHTML='';
291
+ 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;}
292
+ list.forEach(d=>{
293
+ const hasPin = d.pin!=null;
294
+ const r=document.createElement('div'); r.className='dt-row elev-row-admin';
295
+ r.innerHTML=`
296
+ <div class="col-pos" style="font-size:11px;color:var(--white-faint);">${String(d.pozitie||'?').padStart(2,'0')}</div>
297
+ <div class="col-vpass" style="font-size:10px;color:var(--white-dim);letter-spacing:1px;">${d.vpassId||'—'}</div>
298
+ <div style="font-size:13px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;">${d.nume}</div>
299
+ <div class="col-status">
300
+ <span class="pill ${hasPin?'success':'empty'}" style="font-size:8px;">${hasPin?'ACTIV':'FĂRĂ CONT'}</span>
301
+ </div>
302
+ <div style="display:flex;gap:5px;flex-wrap:wrap;">
303
+ ${hasPin?`<button class="btn-ghost" style="font-size:8px;padding:4px 8px;" onclick="resetPin('${d.id}','${d.nume}')">Reset PIN</button>`:''}
304
+ <button class="btn-danger" onclick="delItem('elevi','${d.id}','${d.nume}')">✕</button>
305
+ </div>`;
306
+ el.appendChild(r);
307
+ });
308
+ }
309
+
310
+ window.filterElevi = function() {
311
+ const q = document.getElementById('elev-search').value.toLowerCase();
312
+ renderElevi(_elevCache.filter(e=>e.nume.toLowerCase().includes(q)||((e.vpassId||'').toLowerCase().includes(q))));
313
+ };
314
+
315
+ window.resetPin = async function(id, nume) {
316
+ if(!confirm(`Resetezi parola pentru ${nume}?\nElevul va trebui să se re-înregistreze.`))return;
317
+ try {
318
+ await updateDoc(doc(db,'elevi',id),{pin:null,confirmed:false});
319
+ await loadElevi(); toast(`✓ Parola resetată pentru ${nume}`);
320
+ }catch(e){toast('err-025 — Eroare resetare');}
321
+ };
322
+
323
+ // ── PROFESORI ──
324
  async function loadProf() {
325
  try {
326
  const snap=await getDocs(collection(db,'profesori'));
 
328
  snap.forEach(d=>{
329
  n++;
330
  const r=document.createElement('div'); r.className='dt-row prof-row';
331
+ 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>`;
332
  el.appendChild(r);
333
  });
334
  document.getElementById('st-prof').textContent=n;
335
+ if(!n)el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Niciun profesor.</div>';
336
  }catch(e){}
337
  }
338
 
339
+ // ── MATERII ──
340
  async function loadMat() {
341
  try {
342
  const snap=await getDocs(collection(db,'materii'));
343
  const el=document.getElementById('mat-list');
344
  const sel=document.getElementById('p-mat');
345
+ el.innerHTML=''; sel.innerHTML='<option value=""> selectează </option>'; let n=0;
346
  snap.forEach(d=>{
347
  n++;
348
  const r=document.createElement('div'); r.className='dt-row mat-row';
349
+ 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>`;
350
  el.appendChild(r);
351
  const o=document.createElement('option'); o.value=d.data().nume; o.textContent=d.data().nume; sel.appendChild(o);
352
  });
353
+ if(!n)el.innerHTML='<div style="padding:16px 12px;font-size:11px;color:var(--white-dim);">Nicio materie.</div>';
 
354
  }catch(e){}
355
  }
356
 
357
+ // ── NOTIFICARI ──
358
  async function loadNotif() {
359
  try {
360
  const snap=await getDocs(collection(db,'notificari'));
361
  const notifs=[]; snap.forEach(d=>notifs.push({id:d.id,...d.data()}));
362
+ notifs.sort((a,b)=>(b.timestamp?.seconds||0)-(a.timestamp?.seconds||0));
 
 
363
  const unread=notifs.filter(n=>!n.citita).length;
364
  document.getElementById('st-notif').textContent=unread;
365
  const badge=document.getElementById('notif-badge');
366
  if(unread>0){badge.textContent=unread;badge.style.display='inline-flex';}
367
+ else badge.style.display='none';
368
+
369
+ // Push daca notificari noi
370
+ if(_lastNotifCount>=0 && unread>_lastNotifCount){
371
+ const newest=notifs.find(n=>!n.citita&&(n.tip==='signup_request'||n.tip==='reset_request'));
372
+ if(newest) sendPush(newest.elevNume||'—',newest.elevVpass||'—',newest.telefon||'');
373
+ }
374
+ _lastNotifCount=unread;
375
 
376
  const el=document.getElementById('notif-list'); el.innerHTML='';
377
+ if(!notifs.length){el.innerHTML='<div class="notif-empty">Nicio notificare. Totul e în regulă.</div>';return;}
378
 
379
  notifs.forEach(n=>{
380
  const card=document.createElement('div');
381
  card.className='notif-card'+(n.citita?' done':' unread');
382
  const ts=n.timestamp?.seconds?new Date(n.timestamp.seconds*1000).toLocaleString('ro',{hour:'2-digit',minute:'2-digit',day:'2-digit',month:'short'}):'—';
383
+ const isDone=['approved','completed','rejected'].includes(n.status);
384
 
385
+ if(n.tip==='signup_request'||n.tip==='reset_request'){
386
+ const tipLabel = n.tip==='reset_request' ? 'RESETARE PAROLĂ' : 'ÎNREGISTRARE CONT';
387
+ 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— Echipa VSERVERS`;
388
  card.innerHTML=`
389
+ <div class="nc-type"> ${tipLabel}</div>
390
  <div class="nc-name">${n.elevNume||'—'}</div>
391
  <div class="nc-vpass">${n.elevVpass||'—'}</div>
392
+ <div class="nc-phone-block">
393
+ <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>
394
+ <div><div class="ph-label">NUMĂR DE TELEFON</div><div class="ph-num">${n.telefon||'—'}</div></div>
395
+ </div>
396
+ <div class="nc-code-block">
397
  <div class="nc-code">${n.confirmCode||'——'}</div>
398
+ <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>
399
  </div>
400
  <div class="nc-actions">
401
  ${!isDone?`
402
+ <button class="btn-copy-sms" id="cb-${n.id}" onclick="copySMS('${n.id}',\`${smsMsg.replace(/`/g,"'")}\`)">
403
+ <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>
404
+ COPIAZĂ MESAJ SMS
405
+ </button>
406
+ <button class="btn-primary" style="font-size:9px;padding:8px 14px;letter-spacing:1px;" onclick="approveNotif('${n.id}')">VALIDEAZĂ</button>
407
+ <button class="btn-danger" style="font-size:9px;" onclick="rejectNotif('${n.id}')">Respinge</button>
408
+ `:`<span class="pill ${n.status==='completed'?'success':'empty'}" style="font-size:9px;">${n.status==='completed'?'FINALIZAT':n.status.toUpperCase()}</span>`}
409
  </div>
410
  <div class="nc-time">${ts}</div>`;
411
  } else {
412
+ const tipIcon = n.tip==='upload'?'↑ UPLOAD':n.tip?.toUpperCase()||'LOG';
413
  card.innerHTML=`
414
+ <div class="nc-type">${tipIcon}</div>
415
+ <div style="font-size:13px;margin-bottom:4px;">${n.mesaj||'—'}</div>
416
+ <div style="font-size:10px;color:var(--white-dim);">${n.elevNume||''} ${n.elevVpass?'· '+n.elevVpass:''}</div>
417
  <div class="nc-time">${ts}</div>`;
418
  }
419
  el.appendChild(card);
420
  });
421
+ }catch(e){console.error('notif:',e);}
422
  }
423
 
424
+ async function approveNotif(nid) {
425
  try {
426
+ await updateDoc(doc(db,'notificari',nid),{status:'approved',citita:true});
427
+ toast('✓ Validat! Elevul poate introduce codul și seta parola.');
 
 
 
428
  loadNotif();
429
+ }catch(e){toast('err-025');}
430
  }
431
+ async function rejectNotif(nid) {
432
+ if(!confirm('Respingi această cerere?'))return;
 
433
  try {
434
+ await updateDoc(doc(db,'notificari',nid),{status:'rejected',citita:true});
435
+ toast('Cerere respinsă.');
 
436
  loadNotif();
437
+ }catch(e){toast('err-025');}
438
  }
 
439
  async function markAllRead() {
440
  try {
441
  const snap=await getDocs(collection(db,'notificari'));
442
+ for(const d of snap.docs){ if(!d.data().citita) await updateDoc(doc(db,'notificari',d.id),{citita:true}); }
443
+ loadNotif(); toast('✓ Toate marcate ca citite.');
 
 
444
  }catch(e){}
445
  }
446
 
 
 
 
 
 
 
 
 
447
  window.loadNotif=loadNotif;
448
+ window.approveNotif=approveNotif; window.rejectNotif=rejectNotif; window.markAllRead=markAllRead;
449
  window._loadElevi=loadElevi; window._loadProf=loadProf; window._loadMat=loadMat;
450
+
451
+ window.copySMS = async function(id, msg) {
452
+ try {
453
+ await navigator.clipboard.writeText(msg);
454
+ const btn=document.getElementById('cb-'+id);
455
+ 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!';
456
+ 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);}
457
+ } catch(e){ toast('err-027 — Nu s-a putut copia.'); }
458
+ };
459
  </script>
460
 
461
  <script>
 
465
  const el=document.getElementById('vpass-prev');
466
  if(!n||!p){el.textContent='—';return;}
467
  const init=n.split(' ').map(w=>w[0]?.toUpperCase()||'').join('');
468
+ el.textContent=`${init}-${String(parseInt(p)).padStart(4,'0')}`;
469
  }
470
+
471
  async function addElev(){
472
+ hideError('err-elev');
473
  const nume=document.getElementById('e-nume').value.trim();
474
  const poz=document.getElementById('e-poz').value.trim();
475
  const pinRaw=document.getElementById('e-pin').value.trim();
476
  const vpassId=document.getElementById('vpass-prev').textContent;
477
+ if(!nume||!poz||vpassId==='—'){showError('err-elev','err-021');return;}
478
+ if(pinRaw&&pinRaw.length!==6){showError('err-elev','err-022');return;}
479
  try{
480
  await window._addDoc(window._col(window._db,'elevi'),{
481
+ nume, pozitie:parseInt(poz), pin:pinRaw||null, vpassId, confirmed:!!pinRaw
482
  });
483
+ ['e-nume','e-poz','e-pin'].forEach(id=>document.getElementById(id).value='');
484
+ document.getElementById('vpass-prev').textContent='—';
485
+ await window._loadElevi(); toast('✓ Elev adăugat: '+vpassId);
486
+ }catch(e){showError('err-elev','err-025');}
487
  }
488
+
489
  async function addProf(){
490
+ const n=document.getElementById('p-nume').value.trim();
491
+ const m=document.getElementById('p-mat').value;
492
+ const p=document.getElementById('p-pin').value.trim();
493
+ if(!n||!m||p.length!==6){toast('err-021 — Completează toate câmpurile.');return;}
494
  try{
495
+ await window._addDoc(window._col(window._db,'profesori'),{nume:n,materie:m,pin:p});
496
+ ['p-nume','p-pin'].forEach(id=>document.getElementById(id).value='');
497
+ await window._loadProf(); toast('✓ Profesor adăugat: '+n);
498
  }catch(e){toast('err-025 — Eroare Firestore');}
499
  }
500
+
501
  async function addMat(){
502
+ const n=document.getElementById('m-nume').value.trim();
503
+ if(!n){toast('err-021 — Introdu numele materiei.');return;}
504
  try{
505
+ await window._addDoc(window._col(window._db,'materii'),{nume:n});
506
  document.getElementById('m-nume').value='';
507
+ await window._loadMat(); toast('✓ Materie adăugată: '+n);
508
+ }catch(e){toast('err-025');}
509
  }
510
+
511
+ async function delItem(col,id,name){
512
+ if(!confirm(`Ștergi "${name}"?\nAceastă acțiune este ireversibilă.`))return;
513
  try{
514
  await window._deleteDoc(window._doc(window._db,col,id));
515
+ if(col==='elevi') await window._loadElevi();
516
+ if(col==='profesori') await window._loadProf();
517
+ if(col==='materii') await window._loadMat();
518
+ toast('✓ Șters: '+name);
519
+ }catch(e){toast('err-025');}
 
 
 
 
520
  }
521
+
522
  function showTab(id,btn){
523
  document.querySelectorAll('.tab-pane').forEach(t=>t.classList.remove('active'));
524
  document.querySelectorAll('.tab-btn').forEach(b=>b.classList.remove('active'));
525
  document.getElementById(id).classList.add('active'); btn.classList.add('active');
526
  if(id==='t-notif') loadNotif();
527
  }
528
+ function toast(msg){const t=document.getElementById('toast-el');t.textContent=msg;t.classList.add('show');setTimeout(()=>t.classList.remove('show'),3200);}
529
+ function logout(){sessionStorage.removeItem('vs_role');window.location.href='index.html';}
 
 
 
 
530
  </script>
531
  </body>
532
  </html>
static/elev-dashboard.html CHANGED
@@ -4,255 +4,315 @@
4
  <link rel="icon" type="image/svg+xml" href="favicon.svg">
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
- <title>VSERVERS | Camera Elevilor</title>
8
  <link rel="stylesheet" href="style.css">
9
- <script src="b2.js"></script>
10
- <style>
11
- .subject-grid {
12
- display:grid; grid-template-columns:repeat(2,1fr);
13
- gap:1px; background:var(--border); border:1px solid var(--border); margin-bottom:14px;
14
- }
15
- @media(min-width:440px){.subject-grid{grid-template-columns:repeat(3,1fr);}}
16
- @media(min-width:640px){.subject-grid{grid-template-columns:repeat(4,1fr);}}
17
- .subj-card {
18
- background:var(--surface); padding:14px 12px; cursor:pointer;
19
- transition:background 0.15s; border:none; color:var(--white);
20
- text-align:left; font-family:'DM Mono',monospace; min-width:0;
21
- -webkit-tap-highlight-color:transparent;
22
- }
23
- .subj-card:hover{background:var(--surface2);}
24
- .subj-card.selected{background:var(--white);color:var(--black);}
25
- .subj-card svg{width:16px;height:16px;display:block;margin-bottom:8px;stroke:var(--white-dim);}
26
- .subj-card.selected svg{stroke:#333;}
27
- .subj-card .sn{font-size:11px;font-weight:500;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
28
- .subj-card .sc{font-size:9px;opacity:0.45;letter-spacing:1px;margin-top:3px;}
29
- .upload-panel{display:none;}
30
- .upload-panel.show{display:block;}
31
- .fp-bar{display:none;align-items:center;gap:9px;padding:9px 12px;border:1px solid var(--border-light);margin-top:9px;}
32
- .fp-bar.show{display:flex;}
33
- .fp-bar svg{width:14px;height:14px;stroke:var(--white-dim);flex-shrink:0;}
34
- .fp-name{flex:1;font-size:11px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:0;}
35
- .fp-size{font-size:9px;color:var(--white-dim);white-space:nowrap;flex-shrink:0;}
36
- .fp-rm{cursor:pointer;color:var(--white-dim);font-size:14px;line-height:1;flex-shrink:0;padding:2px;}
37
- .fp-rm:hover{color:#cc5555;}
38
- </style>
39
  </head>
40
  <body>
41
 
 
 
 
 
 
 
42
  <div class="topbar">
43
  <a href="index.html" class="topbar-logo">
44
  <img src="logo.svg" alt="VS">
45
  <span class="topbar-name">VSERVERS</span>
46
  </a>
47
  <div class="topbar-divider"></div>
48
- <span class="topbar-section" id="tb-name"></span>
49
  <div class="topbar-right">
50
- <span class="role-tag">ELEV</span>
51
- <button class="btn-ghost" onclick="logout()">Iesire</button>
 
52
  </div>
53
  </div>
54
 
55
  <div class="main">
56
 
57
  <div class="stats-row fade-in">
58
- <div class="stat-box">
59
- <div class="stat-num" id="s-name" style="font-size:14px;padding-top:5px;"></div>
60
- <div class="stat-lbl">ELEV</div>
61
- </div>
62
- <div class="stat-box">
63
- <div class="stat-num" id="s-vpass" style="font-size:16px;">—</div>
64
- <div class="stat-lbl">VPASS ID</div>
65
- </div>
66
- <div class="stat-box">
67
- <div class="stat-num">7B</div>
68
- <div class="stat-lbl">CLASA</div>
69
- </div>
70
- <div class="stat-box">
71
- <div class="stat-num" id="s-total">0</div>
72
- <div class="stat-lbl">FISIERE</div>
73
- </div>
74
  </div>
75
 
76
- <div class="label fade-in-2">Materii disponibile</div>
77
- <div class="subject-grid fade-in-2" id="subj-grid">
78
- <div style="padding:14px;font-size:11px;color:var(--white-dim);grid-column:1/-1;">Se incarca...</div>
 
79
  </div>
80
 
81
- <div class="upload-panel card fade-in-3" id="upload-panel">
82
- <div class="card-title">Incarca &rarr; <span id="up-materie">—</span></div>
83
- <div class="upload-zone" id="uz">
84
- <input type="file" id="file-input" onchange="onFile(event)">
85
- <svg class="uz-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
86
- <path d="M21 15v4a2 2 0 01-2 2H5a2 2 0 01-2-2v-4"/>
87
- <polyline points="17 8 12 3 7 8"/><line x1="12" y1="3" x2="12" y2="15"/>
88
- </svg>
89
- <p>Trage fisierul sau <strong>click</strong> pentru selectare</p>
90
- <span>PDF, DOCX, PNG, JPG, ZIP &mdash; Max 50MB</span>
91
- </div>
92
- <div class="fp-bar" id="fp-bar">
93
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
94
- <path d="M13 2H6a2 2 0 00-2 2v16a2 2 0 002 2h12a2 2 0 002-2V9z"/>
95
- <polyline points="13 2 13 9 20 9"/>
96
- </svg>
97
- <span class="fp-name" id="fp-name">—</span>
98
- <span class="fp-size" id="fp-size">—</span>
99
- <span class="fp-rm" onclick="clearFile()">&#x2715;</span>
100
  </div>
101
- <div class="progress-wrap" id="prog-wrap">
102
- <div class="progress-info"><span>Upload &rarr; B2</span><span id="prog-pct">0%</span></div>
103
- <div class="progress-track"><div class="progress-fill" id="prog-fill"></div></div>
104
- </div>
105
- <div class="alert success" id="up-ok">&#x2713; Fisier stocat cu succes.</div>
106
- <div class="alert error" id="up-err"></div>
107
- <div style="margin-top:13px;">
108
- <button class="btn-primary" id="btn-up" onclick="doUpload()" disabled>Incarca in VSERVERS</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
  </div>
110
  </div>
111
 
112
- <div class="label fade-in-4">Registru fisiere</div>
113
- <div id="files-container" class="fade-in-4">
114
- <div style="padding:18px 0;font-size:11px;color:var(--white-dim);letter-spacing:1px;">Niciun fisier detectat.</div>
 
 
 
115
  </div>
116
 
117
  <footer class="footer">
118
- <div class="footer-top">
119
- <img src="logo.svg" alt="">
120
- <span>VSERVERS</span>
121
- </div>
122
  <div class="footer-divider"></div>
123
- <div class="footer-meta">93.117.161.226 &nbsp;&mdash;&nbsp; Mindresti, Telenesti, Moldova</div>
124
- <div class="footer-copy">
125
- &copy; 2026 Victor Rosca &mdash; Toate drepturile rezervate<br>
126
- Sistem Educatonal de Gestiune a Fisierelor &mdash; v1.0
127
- </div>
128
  </footer>
129
-
130
  </div>
131
 
 
 
132
  <script type="module">
133
- import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
134
- import { getFirestore, collection, getDocs } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
135
- const cfg={apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU",authDomain:"vservers1.firebaseapp.com",projectId:"vservers1",storageBucket:"vservers1.firebasestorage.app",messagingSenderId:"42433037358",appId:"1:42433037358:web:fde70fec79542428b60bbf"};
136
- const app=initializeApp(cfg); const db=getFirestore(app);
137
- const uid=sessionStorage.getItem('vs_uid');
138
- if(!uid){window.location.href='index.html';}
139
- const name=sessionStorage.getItem('vs_name')||'—';
140
- const vpass=sessionStorage.getItem('vs_vpass')||'—';
141
- document.getElementById('tb-name').textContent=name;
142
- document.getElementById('s-name').textContent=name;
143
- document.getElementById('s-vpass').textContent=vpass;
144
-
145
- // SVG icons pool — fara emoji
146
- const ICONS=[
147
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M4 19.5A2.5 2.5 0 016.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 014 19.5v-15A2.5 2.5 0 016.5 2z"/></svg>',
148
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><line x1="19" y1="5" x2="5" y2="19"/><circle cx="6.5" cy="6.5" r="2.5"/><circle cx="17.5" cy="17.5" r="2.5"/></svg>',
149
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 014 10 15.3 15.3 0 01-4 10 15.3 15.3 0 01-4-10 15.3 15.3 0 014-10z"/></svg>',
150
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M9 3H5a2 2 0 00-2 2v4m6-6h10a2 2 0 012 2v4M9 3v18m0 0h10a2 2 0 002-2V9M9 21H5a2 2 0 01-2-2V9m0 0h18"/></svg>',
151
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><polyline points="16 18 22 12 16 6"/><polyline points="8 6 2 12 8 18"/></svg>',
152
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/></svg>',
153
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="12" cy="8" r="6"/><path d="M15.477 12.89L17 22l-5-3-5 3 1.523-9.11"/></svg>',
154
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>',
155
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="12" cy="12" r="3"/><path d="M12 2v2M12 20v2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M2 12h2M20 12h2M4.93 19.07l1.41-1.41M17.66 6.34l1.41-1.41"/></svg>',
156
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M1 6l11 6 11-6-11-6-11 6z"/><path d="M1 12l11 6 11-6"/><path d="M1 18l11 6 11-6"/></svg>',
157
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="2" y="3" width="20" height="14" rx="2"/><path d="M8 21h8M12 17v4"/></svg>',
158
- '<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M18 20V10M12 20V4M6 20v-6"/></svg>'
159
- ];
160
-
161
- let idx=0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
162
  try {
163
- const snap=await getDocs(collection(db,'materii'));
164
- const grid=document.getElementById('subj-grid'); grid.innerHTML='';
165
- snap.forEach(d=>{
166
- const btn=document.createElement('button'); btn.className='subj-card';
167
- btn.innerHTML=`${ICONS[idx%ICONS.length]}<span class="sn">${d.data().nume}</span><span class="sc" id="fc-${d.id}">—</span>`;
168
- btn.onclick=()=>selectMaterie(d.id,d.data().nume,btn);
169
- grid.appendChild(btn); idx++;
170
- loadCount(uid,d.id);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
171
  });
172
- if(!snap.size) grid.innerHTML='<div style="padding:14px;font-size:11px;color:var(--white-dim);grid-column:1/-1;">Nicio materie adaugata.</div>';
173
- } catch(e){console.error(e);}
174
 
175
- await loadAllFiles(uid);
 
 
 
 
 
 
 
 
 
176
 
177
- async function loadCount(uid,mid){
178
- try{const f=await b2List(`elevi/${uid}/${mid}/`);const el=document.getElementById(`fc-${mid}`);if(el)el.textContent=f.length+' fis.';}catch(e){}
 
 
 
 
 
 
 
 
 
179
  }
180
- async function loadAllFiles(uid){
181
- try{
182
- const files=await b2List(`elevi/${uid}/`);
183
- document.getElementById('s-total').textContent=files.length;
184
- renderFiles(files);
185
- }catch(e){}
186
- }
187
- function renderFiles(files){
188
- const c=document.getElementById('files-container');
189
- if(!files.length){c.innerHTML='<div style="padding:18px 0;font-size:11px;color:var(--white-dim);letter-spacing:1px;">Niciun fisier detectat.</div>';return;}
190
- c.innerHTML='';
191
- files.forEach(f=>{
192
- const parts=f.key.split('/'); const mat=parts[2]||'—';
193
- const row=document.createElement('div'); row.className='file-row';
194
- row.innerHTML=`
195
- <svg class="fr-svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
196
- <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"/>
197
- </svg>
198
- <span class="fr-name">${f.name}</span>
199
- <span class="fr-meta">${mat}</span>
200
- <a href="${f.url}" target="_blank" class="pill active" style="text-decoration:none;">DL</a>`;
201
- c.appendChild(row);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
202
  });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
203
  }
204
- window._loadAllFiles=loadAllFiles; window._loadCount=loadCount; window._uid=uid;
 
 
 
 
205
  </script>
 
206
  <script>
207
- let selMaterieId=null,selFile=null;
208
- function selectMaterie(id,name,btn){
209
- document.querySelectorAll('.subj-card').forEach(b=>b.classList.remove('selected'));
210
- btn.classList.add('selected'); selMaterieId=id;
211
- const panel=document.getElementById('upload-panel'); panel.classList.add('show');
212
- document.getElementById('up-materie').textContent=name;
213
- panel.scrollIntoView({behavior:'smooth',block:'nearest'});
214
- clearFile();
215
- document.getElementById('up-ok').classList.remove('show');
216
- document.getElementById('up-err').classList.remove('show');
217
- }
218
- function onFile(e){
219
- selFile=e.target.files[0]; if(!selFile)return;
220
- document.getElementById('fp-bar').classList.add('show');
221
- document.getElementById('fp-name').textContent=selFile.name;
222
- document.getElementById('fp-size').textContent=fmtSize(selFile.size);
223
- document.getElementById('btn-up').disabled=false;
224
- }
225
- function clearFile(){
226
- selFile=null; document.getElementById('file-input').value='';
227
- document.getElementById('fp-bar').classList.remove('show');
228
- document.getElementById('btn-up').disabled=true;
229
- document.getElementById('prog-wrap').classList.remove('show');
230
- }
231
- function fmtSize(b){if(b<1024)return b+' B';if(b<1048576)return (b/1024).toFixed(1)+' KB';return (b/1048576).toFixed(1)+' MB';}
232
- async function doUpload(){
233
- if(!selFile||!selMaterieId)return;
234
- const uid=window._uid||sessionStorage.getItem('vs_uid');
235
- const path=`elevi/${uid}/${selMaterieId}/${selFile.name}`;
236
- document.getElementById('prog-wrap').classList.add('show');
237
- document.getElementById('btn-up').disabled=true;
238
- document.getElementById('up-ok').classList.remove('show');
239
- document.getElementById('up-err').classList.remove('show');
240
- try{
241
- await b2Upload(selFile,path,pct=>{
242
- document.getElementById('prog-fill').style.width=pct+'%';
243
- document.getElementById('prog-pct').textContent=pct+'%';
244
- });
245
- document.getElementById('up-ok').classList.add('show');
246
- clearFile();
247
- await window._loadAllFiles(uid);
248
- window._loadCount(uid,selMaterieId);
249
- }catch(e){
250
- const err=document.getElementById('up-err');
251
- err.textContent='Eroare: '+e.message; err.classList.add('show');
252
- document.getElementById('btn-up').disabled=false;
253
- }
254
- }
255
- function logout(){sessionStorage.clear();window.location.href='index.html';}
256
  </script>
257
  </body>
258
  </html>
 
4
  <link rel="icon" type="image/svg+xml" href="favicon.svg">
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
+ <title>VSERVERS | Elev</title>
8
  <link rel="stylesheet" href="style.css">
9
+ <script src="errors.js"></script>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  </head>
11
  <body>
12
 
13
+ <!-- LOADER -->
14
+ <div class="loader-overlay" id="page-loader">
15
+ <div class="loader"><div class="inner one"></div><div class="inner two"></div><div class="inner three"></div></div>
16
+ <div class="loader-text" id="loader-txt">SE ÎNCARCĂ</div>
17
+ </div>
18
+
19
  <div class="topbar">
20
  <a href="index.html" class="topbar-logo">
21
  <img src="logo.svg" alt="VS">
22
  <span class="topbar-name">VSERVERS</span>
23
  </a>
24
  <div class="topbar-divider"></div>
25
+ <span class="topbar-section">ELEV</span>
26
  <div class="topbar-right">
27
+ <div class="online-dot"></div>
28
+ <span class="role-tag" id="vpass-tag"></span>
29
+ <button class="btn-ghost" onclick="logout()" style="font-size:9px;">Ieșire</button>
30
  </div>
31
  </div>
32
 
33
  <div class="main">
34
 
35
  <div class="stats-row fade-in">
36
+ <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>
37
+ <div class="stat-box"><div class="stat-num" id="st-files">0</div><div class="stat-lbl">FIȘIERE</div></div>
38
+ <div class="stat-box"><div class="stat-num" id="st-mat">0</div><div class="stat-lbl">MATERII</div></div>
39
+ <div class="stat-box"><div class="stat-num" id="st-size">0 MB</div><div class="stat-lbl">STOCAT</div></div>
 
 
 
 
 
 
 
 
 
 
 
 
40
  </div>
41
 
42
+ <!-- Materii -->
43
+ <div class="label fade-in-2">Selectează materia</div>
44
+ <div class="materii-grid fade-in-2" id="materii-grid">
45
+ <div style="font-size:11px;color:var(--white-dim);padding:16px 0;letter-spacing:1px;">Se încarcă materiile...</div>
46
  </div>
47
 
48
+ <!-- Upload -->
49
+ <div class="card fade-in-3" id="upload-card">
50
+ <div class="card-title">Încarcă fișier</div>
51
+ <div id="no-mat-hint" style="font-size:11px;color:var(--white-dim);letter-spacing:1px;padding:4px 0 10px;">
52
+ Selectează mai întâi o materie din grila de mai sus.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
53
  </div>
54
+ <div id="upload-area" style="display:none;">
55
+ <div class="upload-zone" id="drop-zone">
56
+ <input type="file" id="file-input" onchange="fileSelected(this)">
57
+ <div class="uz-icon">
58
+ <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">
59
+ <polyline points="16 16 12 12 8 16"/><line x1="12" y1="12" x2="12" y2="21"/>
60
+ <path d="M20.39 18.39A5 5 0 0018 9h-1.26A8 8 0 103 16.3"/>
61
+ </svg>
62
+ </div>
63
+ <div class="uz-text" id="uz-text">Trage fișierul aici sau apasă pentru a selecta</div>
64
+ <div class="uz-sub">Max. 50MB &nbsp;·&nbsp; Orice format</div>
65
+ </div>
66
+
67
+ <div class="upload-progress-wrap" id="prog-wrap">
68
+ <div class="progress">
69
+ <div class="progress-value" id="prog-bar"></div>
70
+ <div class="progress-pct" id="prog-pct">0%</div>
71
+ </div>
72
+ <div class="progress-label" id="prog-label">Se pregătește...</div>
73
+ </div>
74
+
75
+ <div class="alert error" id="err-upload" style="margin-top:10px;"></div>
76
+ <div class="alert success" id="ok-upload" style="margin-top:10px;display:none;">
77
+ ✓ Fișier încărcat cu succes!
78
+ </div>
79
  </div>
80
  </div>
81
 
82
+ <!-- Files list -->
83
+ <div class="label fade-in-4">Fișierele tale</div>
84
+ <div class="card fade-in-4" style="padding:0;">
85
+ <div id="files-list" style="padding:20px;font-size:11px;color:var(--white-dim);letter-spacing:1px;">
86
+ Selectează o materie pentru a vedea fișierele.
87
+ </div>
88
  </div>
89
 
90
  <footer class="footer">
91
+ <div class="footer-top"><img src="logo.svg" alt=""><span>VSERVERS</span></div>
 
 
 
92
  <div class="footer-divider"></div>
93
+ <div class="footer-meta">93.117.161.226 &nbsp;·&nbsp; Mîndrești, Telenești, Moldova</div>
94
+ <div class="footer-copy">&copy; 2026 Victor Roșca &mdash; Sistem Educațional de Gestiune &mdash; v3.0</div>
 
 
 
95
  </footer>
 
96
  </div>
97
 
98
+ <div class="toast" id="toast-el"></div>
99
+
100
  <script type="module">
101
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
102
+ import { getFirestore, collection, getDocs, addDoc, serverTimestamp }
103
+ from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
104
+
105
+ // ── Auth check (persistent) ──
106
+ const vsRole = localStorage.getItem('vs_role');
107
+ const vsUid = localStorage.getItem('vs_uid');
108
+ const vsName = localStorage.getItem('vs_name');
109
+ const vsVpass = localStorage.getItem('vs_vpass');
110
+ if (vsRole !== 'elev' || !vsUid) { window.location.href='index.html'; }
111
+
112
+ const cfg = {
113
+ apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
114
+ projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
115
+ messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
116
+ };
117
+ const app = initializeApp(cfg);
118
+ const db = getFirestore(app);
119
+
120
+ document.getElementById('vpass-tag').textContent = vsVpass || '—';
121
+ document.getElementById('st-name').textContent = vsName || '—';
122
+
123
+ let selectedMat = null;
124
+ let materii = [];
125
+
126
+ // Load materii
127
+ const loaderTxt = document.getElementById('loader-txt');
128
+ loaderTxt.textContent = 'MATERII';
129
+ try {
130
+ const snap = await getDocs(collection(db,'materii'));
131
+ snap.forEach(d => materii.push({id:d.id,...d.data()}));
132
+ document.getElementById('st-mat').textContent = materii.length;
133
+ renderMaterii();
134
+ } catch(e) { showError('err-upload','err-026'); }
135
+
136
+ function renderMaterii() {
137
+ const grid = document.getElementById('materii-grid');
138
+ grid.innerHTML = '';
139
+ if (!materii.length) {
140
+ 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>';
141
+ return;
142
+ }
143
+ materii.forEach(m => {
144
+ const btn = document.createElement('button');
145
+ btn.className = 'materie-btn';
146
+ btn.dataset.id = m.id;
147
+ btn.dataset.name = m.nume;
148
+ btn.innerHTML = `<div class="mb-icon">
149
+ <svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
150
+ <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"/>
151
+ </svg></div>${m.nume}`;
152
+ btn.onclick = () => selectMat(m.id, m.nume, btn);
153
+ grid.appendChild(btn);
154
+ });
155
+ }
156
+
157
+ window.selectMat = function(id, name, btn) {
158
+ selectedMat = {id, name};
159
+ document.querySelectorAll('.materie-btn').forEach(b => b.classList.remove('selected'));
160
+ btn.classList.add('selected');
161
+ document.getElementById('no-mat-hint').style.display = 'none';
162
+ document.getElementById('upload-area').style.display = 'block';
163
+ loadFiles();
164
+ };
165
+
166
+ // ── Load files ──
167
+ async function loadFiles() {
168
+ const el = document.getElementById('files-list');
169
+ 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>';
170
  try {
171
+ const prefix = `elevi/${vsUid}/${selectedMat.id}/`;
172
+ const files = await b2List(prefix);
173
+ el.innerHTML = '';
174
+ document.getElementById('st-files').textContent = files.length;
175
+ if (!files.length) {
176
+ 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>';
177
+ return;
178
+ }
179
+ let totalBytes = 0;
180
+ files.forEach(f => {
181
+ const row = document.createElement('div');
182
+ row.className = 'file-row';
183
+ row.innerHTML = `
184
+ <div class="file-name">
185
+ <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;">
186
+ <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"/>
187
+ </svg>${f.name}
188
+ </div>
189
+ <div class="file-size">—</div>
190
+ <div>
191
+ <a href="${f.url}" target="_blank" class="btn-ghost" style="font-size:8px;padding:4px 8px;text-decoration:none;">
192
+ <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>
193
+ DESCARCĂ
194
+ </a>
195
+ </div>`;
196
+ el.appendChild(row);
197
  });
198
+ } catch(e) { showError('err-upload','err-018'); }
199
+ }
200
 
201
+ // ── File selected ──
202
+ window.fileSelected = function(input) {
203
+ if (!input.files[0]) return;
204
+ const f = input.files[0];
205
+ if (f.size > 50 * 1024 * 1024) { showError('err-upload','err-010'); input.value=''; return; }
206
+ document.getElementById('uz-text').textContent = f.name;
207
+ document.getElementById('ok-upload').style.display = 'none';
208
+ hideError('err-upload');
209
+ uploadFile(f);
210
+ };
211
 
212
+ // ── Drag & drop ──
213
+ const dz = document.getElementById('drop-zone');
214
+ dz.addEventListener('dragover', e => { e.preventDefault(); dz.classList.add('drag'); });
215
+ dz.addEventListener('dragleave', () => dz.classList.remove('drag'));
216
+ dz.addEventListener('drop', e => {
217
+ e.preventDefault(); dz.classList.remove('drag');
218
+ const f = e.dataTransfer.files[0];
219
+ if (f) {
220
+ if (f.size > 50*1024*1024) { showError('err-upload','err-010'); return; }
221
+ document.getElementById('uz-text').textContent = f.name;
222
+ uploadFile(f);
223
  }
224
+ });
225
+
226
+ // ── Upload ──
227
+ async function uploadFile(file) {
228
+ if (!selectedMat) { showError('err-upload','err-012'); return; }
229
+ hideError('err-upload');
230
+ document.getElementById('ok-upload').style.display = 'none';
231
+
232
+ const progWrap = document.getElementById('prog-wrap');
233
+ const progBar = document.getElementById('prog-bar');
234
+ const progPct = document.getElementById('prog-pct');
235
+ const progLbl = document.getElementById('prog-label');
236
+
237
+ progWrap.classList.add('show');
238
+ progBar.style.width = '0%'; progPct.textContent = '0%';
239
+ progLbl.textContent = 'Se pregătește...';
240
+
241
+ // Simuleaza progres realist: incet, cu pauze naturale
242
+ let fakeProgress = 0;
243
+ const fakeIv = setInterval(() => {
244
+ if (fakeProgress < 15) fakeProgress += 0.8;
245
+ else if (fakeProgress < 40) fakeProgress += 0.4;
246
+ else if (fakeProgress < 70) fakeProgress += 0.2;
247
+ else if (fakeProgress < 88) fakeProgress += 0.05;
248
+ progBar.style.width = fakeProgress + '%';
249
+ progPct.textContent = Math.round(fakeProgress) + '%';
250
+ if (fakeProgress < 20) progLbl.textContent = 'Se conectează la server...';
251
+ else if (fakeProgress < 50) progLbl.textContent = 'Se transferă fișierul...';
252
+ else if (fakeProgress < 80) progLbl.textContent = 'Se procesează...';
253
+ else progLbl.textContent = 'Aproape gata...';
254
+ }, 80);
255
+
256
+ try {
257
+ const path = `elevi/${vsUid}/${selectedMat.id}/${file.name}`;
258
+ await b2Upload(file, path, (pct) => {
259
+ clearInterval(fakeIv);
260
+ fakeProgress = pct;
261
+ progBar.style.width = pct + '%';
262
+ progPct.textContent = pct + '%';
263
  });
264
+
265
+ clearInterval(fakeIv);
266
+ // Smooth fill to 100
267
+ for (let p = fakeProgress; p <= 100; p += 2) {
268
+ await new Promise(r=>setTimeout(r,18));
269
+ progBar.style.width = p + '%';
270
+ progPct.textContent = Math.round(p) + '%';
271
+ }
272
+ progBar.style.width = '100%'; progPct.textContent = '100%';
273
+ progLbl.textContent = 'Finalizat!';
274
+
275
+ // Log upload to Firestore
276
+ try {
277
+ await addDoc(collection(db,'notificari'),{
278
+ tip:'upload', elevId:vsUid, elevVpass:vsVpass, elevNume:vsName,
279
+ mesaj:`${vsName} a încărcat: ${file.name} (${selectedMat.name})`,
280
+ citita:false, timestamp:serverTimestamp()
281
+ });
282
+ } catch(e) {}
283
+
284
+ await new Promise(r=>setTimeout(r,600));
285
+ progWrap.classList.remove('show');
286
+ document.getElementById('ok-upload').style.display = 'block';
287
+ document.getElementById('uz-text').textContent = 'Trage fișierul aici sau apasă pentru a selecta';
288
+ document.getElementById('file-input').value = '';
289
+
290
+ await loadFiles();
291
+ } catch(e) {
292
+ clearInterval(fakeIv);
293
+ progWrap.classList.remove('show');
294
+ showError('err-upload','err-009');
295
  }
296
+ }
297
+
298
+ // ── Hide loader ──
299
+ await new Promise(r=>setTimeout(r,600));
300
+ document.getElementById('page-loader').classList.add('hide');
301
  </script>
302
+
303
  <script>
304
+ function logout() {
305
+ localStorage.removeItem('vs_role'); localStorage.removeItem('vs_uid');
306
+ localStorage.removeItem('vs_name'); localStorage.removeItem('vs_vpass');
307
+ window.location.href = 'index.html';
308
+ }
309
+ function toast(msg){
310
+ const t=document.getElementById('toast-el');
311
+ t.textContent=msg; t.classList.add('show');
312
+ setTimeout(()=>t.classList.remove('show'),3000);
313
+ }
314
+ // Expose pulse-dot style for loading
315
+ 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>`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  </script>
317
  </body>
318
  </html>
static/index.html CHANGED
@@ -4,171 +4,157 @@
4
  <link rel="icon" type="image/svg+xml" href="favicon.svg">
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
- <title>VSERVERS | Log-in</title>
8
  <link rel="stylesheet" href="style.css">
9
  <script src="errors.js"></script>
10
  <style>
11
- body { display:flex; flex-direction:column; align-items:center; justify-content:center; padding:20px 14px; min-height:100dvh; }
12
- .login-wrap { width:100%; max-width:360px; }
13
- .login-header { text-align:center; margin-bottom:22px; }
14
- .login-header img { width:40px; height:40px; margin:0 auto 12px; display:block; }
15
- .login-header h1 { font-family:'Cormorant Garamond',serif; font-size:26px; font-weight:600; letter-spacing:4px; color:var(--white); margin-bottom:4px; }
16
- .login-header p { font-size:9px; letter-spacing:2px; color:var(--white-dim); }
17
- .server-status { display:flex; align-items:center; justify-content:center; gap:7px; margin-bottom:16px; font-size:9px; color:var(--white-dim); letter-spacing:1px; }
18
- .login-card { padding:20px 16px; }
19
- .role-tabs { display:grid; grid-template-columns:1fr 1fr 1fr; border:1px solid rgba(255,255,255,0.08); margin-bottom:18px; }
20
- .role-tab {
21
- background:transparent; border:none; border-right:1px solid rgba(255,255,255,0.07);
22
- color:var(--white-dim); padding:10px 6px;
23
- font-family:'DM Mono',monospace; font-size:9px; letter-spacing:1px;
24
- cursor:pointer; transition:all 0.15s;
25
- display:flex; flex-direction:column; align-items:center; gap:5px;
26
- -webkit-tap-highlight-color:transparent;
27
- }
28
- .role-tab:last-child { border-right:none; }
29
- .role-tab:hover { color:var(--white); background:rgba(255,255,255,0.03); }
30
- .role-tab.active { background:var(--white); color:var(--black); }
31
- .role-tab svg { width:15px; height:15px; }
32
- .fields { display:none; }
33
- .fields.show { display:block; }
34
-
35
- /* SIGNUP button — different style */
36
- .btn-signup {
37
- width:100%; background:transparent;
38
- border:1px solid rgba(255,255,255,0.2);
39
- color:var(--white); padding:11px;
40
- font-family:'DM Mono',monospace; font-size:11px; letter-spacing:3px;
41
- cursor:pointer; transition:all 0.2s; display:none; margin-top:8px;
42
- position:relative; overflow:hidden;
43
- }
44
- .btn-signup::before {
45
- content:''; position:absolute; inset:0;
46
- background:linear-gradient(90deg, transparent, rgba(255,255,255,0.05), transparent);
47
- transform:translateX(-100%); transition:transform 0.5s;
48
- }
49
- .btn-signup:hover::before { transform:translateX(100%); }
50
- .btn-signup:hover { border-color:rgba(255,255,255,0.4); }
51
- .btn-signup.show { display:block; }
52
 
53
- /* Signup hint */
54
- .signup-hint { font-size:10px; color:var(--white-dim); letter-spacing:1px; text-align:center; margin-top:10px; display:none; }
55
- .signup-hint.show { display:block; }
56
 
57
- .err-code { font-size:9px; opacity:0.6; margin-right:4px; letter-spacing:1px; }
58
-
59
- .loc-overlay {
60
- display:none; position:fixed; inset:0; background:rgba(10,10,10,0.98);
61
- z-index:200; flex-direction:column; align-items:center; justify-content:center;
62
- gap:16px; padding:20px; text-align:center;
63
- }
64
- .loc-overlay.show { display:flex; }
65
- .loc-overlay img { width:36px; height:36px; }
66
- .loc-title { font-family:'Cormorant Garamond',serif; font-size:18px; letter-spacing:3px; color:var(--white); }
67
- .loc-steps { font-size:10px; color:var(--white-dim); line-height:2.4; letter-spacing:1px; }
68
- .loc-steps .done { color:var(--white); }
69
- .footer-mini { text-align:center; margin-top:18px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2; }
70
  </style>
71
  </head>
72
  <body>
73
 
74
- <div class="login-wrap">
 
 
 
 
 
 
 
 
 
 
 
75
  <div class="login-header fade-in">
76
  <img src="logo.svg" alt="VSERVERS">
77
  <h1>VSERVERS</h1>
78
- <p>Sistem de Gestiune Educationala</p>
79
  </div>
80
 
81
  <div class="server-status fade-in-2">
82
  <span class="status-dot online"></span>
83
- <span>SERVER ACTIV &nbsp;&mdash;&nbsp; 93.117.161.226</span>
84
  </div>
85
 
86
- <div class="login-card card fade-in-3">
87
- <div class="label">Selecteaza rolul</div>
88
  <div class="role-tabs">
89
- <button class="role-tab active" onclick="switchRole('elev',this)">
90
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
91
- <circle cx="12" cy="7" r="4"/><path d="M4 21c0-4 3.58-7 8-7s8 3 8 7"/>
92
- </svg>ELEV
93
  </button>
94
- <button class="role-tab" onclick="switchRole('profesor',this)">
95
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
96
- <rect x="3" y="3" width="18" height="13" rx="1"/><path d="M8 21h8M12 16v5"/>
97
- </svg>PROF
98
  </button>
99
- <button class="role-tab" onclick="switchRole('admin',this)">
100
- <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
101
- <path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/>
102
- </svg>ADMIN
103
  </button>
104
  </div>
105
 
106
- <div class="fields show" id="f-elev">
107
- <div class="field"><label>Identificator Elev</label>
108
- <select id="elev-id" onchange="checkElevPin()"><option value="">-- Selecteaza --</option></select>
109
- </div>
110
- <div id="pin-field" class="field" style="display:none;"><label>Cod VPass</label>
111
- <input type="password" id="elev-pin" maxlength="6" placeholder="------" inputmode="numeric" autocomplete="off">
112
- </div>
113
- <div id="btn-login-wrap" style="margin-top:14px;display:none;">
114
- <button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
115
- </div>
116
- <button class="btn-signup" id="btn-signup" onclick="goSignup()">ÎNREGISTRARE →</button>
117
- <div class="signup-hint" id="signup-hint">Contul tău nu are încă o parolă.<br>Apasă ÎNREGISTRARE pentru a-l activa.</div>
118
- </div>
119
 
120
- <div class="fields" id="f-profesor">
121
- <div class="field"><label>Identificator Profesor</label>
122
- <select id="prof-id"><option value="">-- Selecteaza --</option></select>
123
- </div>
124
- <div class="field"><label>Cod de Acces</label>
125
- <input type="password" id="prof-pin" maxlength="6" placeholder="------" inputmode="numeric" autocomplete="off">
126
- </div>
127
- <div style="margin-top:14px;">
128
- <button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
 
 
 
 
 
 
 
 
 
 
 
129
  </div>
130
- </div>
131
 
132
- <div class="fields" id="f-admin">
133
- <div class="field"><label>Parola Administrator</label>
134
- <input type="password" id="admin-pass" placeholder="------------" autocomplete="off">
 
 
 
 
 
 
 
 
 
 
135
  </div>
136
- <div style="margin-top:14px;">
137
- <button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
 
 
 
 
 
 
 
 
 
 
 
138
  </div>
 
139
  </div>
140
 
141
- <div class="alert error" id="err-msg" style="margin-top:10px;"></div>
142
  </div>
143
 
144
- <div class="footer-mini fade-in-4">VSERVERS &copy;2026 &mdash; Victor Rosca<br>Mindresti, Telenesti, Moldova</div>
145
- </div>
146
-
147
- <div class="loc-overlay" id="locating">
148
- <img src="logo.svg" alt="">
149
- <div class="loc-title">VSERVERS</div>
150
- <div class="loc-steps">
151
- <div id="ls1">Initializare VPass...</div>
152
- <div id="ls2" style="opacity:0.3">Actualizare informatii...</div>
153
- <div id="ls3" style="opacity:0.3">Autentificare identitate...</div>
154
- <div id="ls4" style="opacity:0.3">Acces acordat...</div>
155
  </div>
156
  </div>
157
 
158
  <script type="module">
159
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
160
- import { getFirestore, collection, getDocs, doc, getDoc } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
 
161
 
162
  const cfg = {
163
- apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
164
- projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
 
165
  messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
166
  };
167
  const app = initializeApp(cfg);
168
  const db = getFirestore(app);
169
  window._db=db; window._doc=doc; window._getDoc=getDoc;
170
 
171
- // elevi map: id {pin, nume, vpassId}
 
 
 
 
 
 
 
 
 
 
 
 
 
172
  window._elevMap = {};
173
 
174
  try {
@@ -181,59 +167,90 @@ try {
181
  });
182
  elevi.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
183
  elevi.forEach(e => {
184
- const o = document.createElement('option'); o.value=e.id;
 
185
  o.textContent = `${String(e.pozitie||'').padStart(2,'0')}. ${e.nume}`;
186
- o.dataset.hasPin = e.pin !== null && e.pin !== undefined ? '1' : '0';
187
  sel.appendChild(o);
188
  });
189
- } catch(e) { showError('err-msg','err-001'); }
190
 
191
  try {
192
  const snap = await getDocs(collection(db,'profesori'));
193
  const sel = document.getElementById('prof-id');
194
  snap.forEach(d => {
195
- const o = document.createElement('option'); o.value=d.id;
196
- o.textContent=`${d.data().nume} — ${d.data().materie}`;
 
197
  sel.appendChild(o);
198
  });
199
  } catch(e) {}
 
 
 
 
 
 
 
200
  </script>
201
 
202
  <script>
203
  let role = 'elev';
204
  const ADMIN_PASS = '122012';
 
205
 
206
- function switchRole(r,btn) {
207
- role=r;
208
- document.querySelectorAll('.role-tab').forEach(b=>b.classList.remove('active'));
209
- btn.classList.add('active');
210
- ['elev','profesor','admin'].forEach(x=>document.getElementById('f-'+x).classList.toggle('show',x===r));
211
- hideError('err-msg');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  }
213
 
214
  function checkElevPin() {
215
- const sel = document.getElementById('elev-id');
216
- const id = sel.value;
217
- const pinField = document.getElementById('pin-field');
218
- const btnLogin = document.getElementById('btn-login-wrap');
219
- const btnSignup = document.getElementById('btn-signup');
220
- const signupHint = document.getElementById('signup-hint');
221
-
222
- if (!id) {
223
- pinField.style.display='none'; btnLogin.style.display='none';
224
- btnSignup.classList.remove('show'); signupHint.classList.remove('show'); return;
225
- }
226
-
227
  const elev = window._elevMap && window._elevMap[id];
228
- const hasPin = elev && elev.pin !== null && elev.pin !== undefined;
229
-
230
- if (hasPin) {
231
- pinField.style.display='block'; btnLogin.style.display='block';
232
- btnSignup.classList.remove('show'); signupHint.classList.remove('show');
233
- } else {
234
- pinField.style.display='none'; btnLogin.style.display='none';
235
- btnSignup.classList.add('show'); signupHint.classList.add('show');
236
- }
237
  hideError('err-msg');
238
  }
239
 
@@ -249,52 +266,75 @@ function goSignup() {
249
 
250
  async function doLogin() {
251
  hideError('err-msg');
252
- await showLocating();
 
 
 
 
 
 
 
 
 
253
  try {
254
  if (role === 'elev') {
255
  const id = document.getElementById('elev-id').value;
256
  const pin = document.getElementById('elev-pin').value;
257
- if (!id) { showError('err-msg','err-002'); return; }
258
- if (pin.length !== 6) { showError('err-msg','err-022'); return; }
259
  const snap = await window._getDoc(window._doc(window._db,'elevi',id));
260
  if (!snap.exists()) { showError('err-msg','err-002'); return; }
261
  const data = snap.data();
262
- if (data.pin === null || data.pin === undefined) { showError('err-msg','err-004'); return; }
263
  if (data.pin !== pin) { showError('err-msg','err-003'); return; }
264
- sessionStorage.setItem('vs_role','elev'); sessionStorage.setItem('vs_uid',id);
265
- sessionStorage.setItem('vs_name',data.nume); sessionStorage.setItem('vs_vpass',data.vpassId||id);
266
- window.location.href='elev-dashboard.html';
 
 
 
 
 
 
267
  } else if (role === 'profesor') {
268
  const id = document.getElementById('prof-id').value;
269
  const pin = document.getElementById('prof-pin').value;
270
- if (!id) { showError('err-msg','err-002'); return; }
271
- if (pin.length !== 6) { showError('err-msg','err-022'); return; }
272
  const snap = await window._getDoc(window._doc(window._db,'profesori',id));
273
- if (snap.exists() && snap.data().pin === pin) {
274
- sessionStorage.setItem('vs_role','profesor'); sessionStorage.setItem('vs_uid',id);
275
- sessionStorage.setItem('vs_name',snap.data().nume); sessionStorage.setItem('vs_materie',snap.data().materie);
276
- window.location.href='profesor-dashboard.html';
277
- } else { showError('err-msg','err-003'); }
 
 
 
 
 
278
  } else {
279
- if (document.getElementById('admin-pass').value === ADMIN_PASS) {
280
- sessionStorage.setItem('vs_role','admin'); window.location.href='admin-dashboard.html';
281
- } else { showError('err-msg','err-020'); }
 
 
 
 
 
282
  }
283
- } catch(e) { showError('err-msg','err-001'); }
284
- finally { document.getElementById('locating').classList.remove('show'); }
285
- }
286
-
287
- async function showLocating() {
288
- document.getElementById('locating').classList.add('show');
289
- for (let i=1;i<=4;i++) {
290
- await delay(420);
291
- const el=document.getElementById('ls'+i);
292
- el.style.opacity='1'; el.classList.add('done');
293
- el.textContent='✓ '+el.textContent;
294
  }
295
- await delay(240);
296
  }
297
- function delay(ms){return new Promise(r=>setTimeout(r,ms));}
 
 
 
 
 
 
298
  </script>
299
  </body>
300
  </html>
 
4
  <link rel="icon" type="image/svg+xml" href="favicon.svg">
5
  <meta charset="UTF-8">
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
+ <title>VSERVERS | Login</title>
8
  <link rel="stylesheet" href="style.css">
9
  <script src="errors.js"></script>
10
  <style>
11
+ body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
 
13
+ /* Loader overlay pentru login */
14
+ #login-loader { background:var(--black); }
 
15
 
16
+ /* Role fields animatie */
17
+ .role-fields-wrap { position:relative; min-height:200px; }
18
+ .fields { position:absolute; inset:0; padding:20px 16px; }
19
+ .fields.current { position:relative; }
 
 
 
 
 
 
 
 
 
20
  </style>
21
  </head>
22
  <body>
23
 
24
+ <!-- LOADER -->
25
+ <div class="loader-overlay" id="login-loader">
26
+ <div class="loader">
27
+ <div class="inner one"></div>
28
+ <div class="inner two"></div>
29
+ <div class="inner three"></div>
30
+ </div>
31
+ <div class="loader-text" id="loader-text">INIȚIALIZARE</div>
32
+ </div>
33
+
34
+ <div class="login-wrap" id="login-wrap" style="opacity:0">
35
+
36
  <div class="login-header fade-in">
37
  <img src="logo.svg" alt="VSERVERS">
38
  <h1>VSERVERS</h1>
39
+ <p>Sistem de Gestiune Educațională</p>
40
  </div>
41
 
42
  <div class="server-status fade-in-2">
43
  <span class="status-dot online"></span>
44
+ <span>SERVER ACTIV &nbsp;·&nbsp; 93.117.161.226</span>
45
  </div>
46
 
47
+ <div class="fade-in-3">
48
+ <!-- Role tabs -->
49
  <div class="role-tabs">
50
+ <button class="role-tab active" id="tab-elev" onclick="switchRole('elev',this)">
51
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><circle cx="12" cy="7" r="4"/><path d="M4 21c0-4 3.58-7 8-7s8 3 8 7"/></svg>
52
+ ELEV
 
53
  </button>
54
+ <button class="role-tab" id="tab-profesor" onclick="switchRole('profesor',this)">
55
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><rect x="3" y="3" width="18" height="13" rx="1"/><path d="M8 21h8M12 16v5"/></svg>
56
+ PROFESOR
 
57
  </button>
58
+ <button class="role-tab" id="tab-admin" onclick="switchRole('admin',this)">
59
+ <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"><path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z"/></svg>
60
+ ADMIN
 
61
  </button>
62
  </div>
63
 
64
+ <!-- Fields wrapper -->
65
+ <div class="role-fields-wrap" id="fields-wrap">
 
 
 
 
 
 
 
 
 
 
 
66
 
67
+ <!-- ELEV -->
68
+ <div class="fields current show" id="f-elev" style="padding:20px 16px;border:1px solid var(--glass-border);border-top:none;">
69
+ <div class="field">
70
+ <label>Identificator Elev</label>
71
+ <select id="elev-id" onchange="checkElevPin()">
72
+ <option value="">— selectează —</option>
73
+ </select>
74
+ </div>
75
+ <div id="pin-field" class="field" style="display:none;">
76
+ <label>Cod VPass</label>
77
+ <input type="password" id="elev-pin" maxlength="6" placeholder="——————" inputmode="numeric" autocomplete="current-password">
78
+ </div>
79
+ <div id="btn-login-wrap" style="margin-top:14px;display:none;">
80
+ <button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
81
+ </div>
82
+ <button class="btn-signup" id="btn-signup" onclick="goSignup()">ÎNREGISTRARE →</button>
83
+ <div class="signup-hint" id="signup-hint">Contul tău nu are încă o parolă.<br>Apasă ÎNREGISTRARE pentru a-l activa.</div>
84
+ <div style="text-align:center;margin-top:12px;display:none;" id="reset-link-wrap">
85
+ <a href="reset.html" style="font-size:9px;color:var(--white-dim);letter-spacing:1px;text-decoration:none;">Am uitat parola →</a>
86
+ </div>
87
  </div>
 
88
 
89
+ <!-- PROFESOR -->
90
+ <div class="fields" id="f-profesor" style="padding:20px 16px;border:1px solid var(--glass-border);border-top:none;">
91
+ <div class="field">
92
+ <label>Identificator Profesor</label>
93
+ <select id="prof-id"><option value="">�� selecteaz㠗</option></select>
94
+ </div>
95
+ <div class="field">
96
+ <label>Cod de Acces</label>
97
+ <input type="password" id="prof-pin" maxlength="6" placeholder="——————" inputmode="numeric">
98
+ </div>
99
+ <div style="margin-top:14px;">
100
+ <button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
101
+ </div>
102
  </div>
103
+
104
+ <!-- ADMIN -->
105
+ <div class="fields" id="f-admin" style="padding:20px 16px;border:1px solid var(--glass-border);border-top:none;">
106
+ <div class="field">
107
+ <label>Parolă Administrator</label>
108
+ <input type="password" id="admin-pass" placeholder="—————————————" autocomplete="off">
109
+ </div>
110
+ <div style="margin-top:14px;">
111
+ <button class="btn-primary" style="width:100%;letter-spacing:3px;" onclick="doLogin()">AUTENTIFICARE</button>
112
+ </div>
113
+ <div style="margin-top:10px;font-size:9px;color:var(--white-faint);letter-spacing:1px;text-align:center;">
114
+ Sesiunea admin nu este persistentă
115
+ </div>
116
  </div>
117
+
118
  </div>
119
 
120
+ <div class="alert error" id="err-msg" style="margin-top:10px;border-top:none;"></div>
121
  </div>
122
 
123
+ <div class="footer-mini fade-in-4">
124
+ VSERVERS &copy;2026 &mdash; Victor Roșca<br>
125
+ Mîndrești, Telenești, Moldova
 
 
 
 
 
 
 
 
126
  </div>
127
  </div>
128
 
129
  <script type="module">
130
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
131
+ import { getFirestore, collection, getDocs, doc, getDoc }
132
+ from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
133
 
134
  const cfg = {
135
+ apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU",
136
+ authDomain:"vservers1.firebaseapp.com", projectId:"vservers1",
137
+ storageBucket:"vservers1.firebasestorage.app",
138
  messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
139
  };
140
  const app = initializeApp(cfg);
141
  const db = getFirestore(app);
142
  window._db=db; window._doc=doc; window._getDoc=getDoc;
143
 
144
+ // ── Verifica sesiune persistenta ──
145
+ const role = localStorage.getItem('vs_role');
146
+ if (role === 'elev') {
147
+ window.location.href = 'elev-dashboard.html'; // redirect direct
148
+ } else if (role === 'profesor') {
149
+ window.location.href = 'profesor-dashboard.html';
150
+ }
151
+
152
+ // ── Loader sequence ──
153
+ const loaderTexts = ['INIȚIALIZARE','FIREBASE','ELEVI','GATA'];
154
+ let li = 0;
155
+ const ltEl = document.getElementById('loader-text');
156
+ const ltIv = setInterval(()=>{ li++; if(li<loaderTexts.length) ltEl.textContent=loaderTexts[li]; },350);
157
+
158
  window._elevMap = {};
159
 
160
  try {
 
167
  });
168
  elevi.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
169
  elevi.forEach(e => {
170
+ const o = document.createElement('option');
171
+ o.value = e.id;
172
  o.textContent = `${String(e.pozitie||'').padStart(2,'0')}. ${e.nume}`;
 
173
  sel.appendChild(o);
174
  });
175
+ } catch(e) { console.error('err-001',e); }
176
 
177
  try {
178
  const snap = await getDocs(collection(db,'profesori'));
179
  const sel = document.getElementById('prof-id');
180
  snap.forEach(d => {
181
+ const o = document.createElement('option');
182
+ o.value = d.id;
183
+ o.textContent = `${d.data().nume} — ${d.data().materie||''}`;
184
  sel.appendChild(o);
185
  });
186
  } catch(e) {}
187
+
188
+ // ── Hide loader ──
189
+ clearInterval(ltIv);
190
+ await new Promise(r=>setTimeout(r,400));
191
+ document.getElementById('login-loader').classList.add('hide');
192
+ document.getElementById('login-wrap').style.opacity='1';
193
+ document.getElementById('login-wrap').style.transition='opacity 0.4s ease';
194
  </script>
195
 
196
  <script>
197
  let role = 'elev';
198
  const ADMIN_PASS = '122012';
199
+ let _switching = false;
200
 
201
+ function switchRole(r, btn) {
202
+ if (_switching || r === role) return;
203
+ _switching = true;
204
+
205
+ const currentEl = document.getElementById('f-'+role);
206
+ const nextEl = document.getElementById('f-'+r);
207
+
208
+ // Fade out current
209
+ currentEl.style.transition = 'opacity 0.18s ease, transform 0.18s ease';
210
+ currentEl.style.opacity = '0';
211
+ currentEl.style.transform = 'translateY(-5px)';
212
+
213
+ setTimeout(() => {
214
+ currentEl.classList.remove('show','current');
215
+ currentEl.style.position = 'absolute';
216
+ currentEl.style.opacity = '';
217
+ currentEl.style.transform= '';
218
+
219
+ // Fade in next
220
+ nextEl.style.opacity = '0';
221
+ nextEl.style.transform = 'translateY(5px)';
222
+ nextEl.classList.add('show','current');
223
+ nextEl.style.position = 'relative';
224
+
225
+ requestAnimationFrame(() => {
226
+ nextEl.style.transition = 'opacity 0.22s ease, transform 0.22s ease';
227
+ nextEl.style.opacity = '1';
228
+ nextEl.style.transform = 'translateY(0)';
229
+ });
230
+
231
+ // Update tab styles
232
+ document.querySelectorAll('.role-tab').forEach(b => b.classList.remove('active'));
233
+ btn.classList.add('active');
234
+ role = r;
235
+ hideError('err-msg');
236
+
237
+ setTimeout(() => {
238
+ nextEl.style.transition = '';
239
+ _switching = false;
240
+ }, 250);
241
+ }, 200);
242
  }
243
 
244
  function checkElevPin() {
245
+ const id = document.getElementById('elev-id').value;
 
 
 
 
 
 
 
 
 
 
 
246
  const elev = window._elevMap && window._elevMap[id];
247
+ const hasPin = elev && elev.pin != null;
248
+
249
+ document.getElementById('pin-field').style.display = hasPin ? 'block' : 'none';
250
+ document.getElementById('btn-login-wrap').style.display = hasPin ? 'block' : 'none';
251
+ document.getElementById('btn-signup').classList.toggle('show', !hasPin && !!id);
252
+ document.getElementById('signup-hint').classList.toggle('show', !hasPin && !!id);
253
+ document.getElementById('reset-link-wrap').style.display = hasPin ? 'block' : 'none';
 
 
254
  hideError('err-msg');
255
  }
256
 
 
266
 
267
  async function doLogin() {
268
  hideError('err-msg');
269
+
270
+ // Show loader
271
+ const loader = document.getElementById('login-loader');
272
+ loader.classList.remove('hide');
273
+ loader.style.opacity = '1';
274
+ loader.style.visibility = 'visible';
275
+ document.getElementById('loader-text').textContent = 'AUTENTIFICARE';
276
+
277
+ await delay(300);
278
+
279
  try {
280
  if (role === 'elev') {
281
  const id = document.getElementById('elev-id').value;
282
  const pin = document.getElementById('elev-pin').value;
283
+ if (!id) { showError('err-msg','err-002'); return; }
284
+ if (pin.length < 6) { showError('err-msg','err-022'); return; }
285
  const snap = await window._getDoc(window._doc(window._db,'elevi',id));
286
  if (!snap.exists()) { showError('err-msg','err-002'); return; }
287
  const data = snap.data();
288
+ if (data.pin == null) { showError('err-msg','err-004'); return; }
289
  if (data.pin !== pin) { showError('err-msg','err-003'); return; }
290
+ // Sesiune persistenta pentru elevi
291
+ localStorage.setItem('vs_role','elev');
292
+ localStorage.setItem('vs_uid', id);
293
+ localStorage.setItem('vs_name', data.nume);
294
+ localStorage.setItem('vs_vpass', data.vpassId||id);
295
+ document.getElementById('loader-text').textContent = 'ACCES ACORDAT';
296
+ await delay(400);
297
+ window.location.href = 'elev-dashboard.html';
298
+
299
  } else if (role === 'profesor') {
300
  const id = document.getElementById('prof-id').value;
301
  const pin = document.getElementById('prof-pin').value;
302
+ if (!id) { showError('err-msg','err-002'); return; }
303
+ if (pin.length < 6) { showError('err-msg','err-022'); return; }
304
  const snap = await window._getDoc(window._doc(window._db,'profesori',id));
305
+ if (!snap.exists() || snap.data().pin !== pin) { showError('err-msg','err-003'); return; }
306
+ // Sesiune persistenta pentru profesori
307
+ localStorage.setItem('vs_role','profesor');
308
+ localStorage.setItem('vs_uid', id);
309
+ localStorage.setItem('vs_name', snap.data().nume);
310
+ localStorage.setItem('vs_materie', snap.data().materie||'');
311
+ document.getElementById('loader-text').textContent = 'ACCES ACORDAT';
312
+ await delay(400);
313
+ window.location.href = 'profesor-dashboard.html';
314
+
315
  } else {
316
+ // ADMIN fara sesiune persistenta (sessionStorage doar)
317
+ if (document.getElementById('admin-pass').value !== ADMIN_PASS) {
318
+ showError('err-msg','err-020'); return;
319
+ }
320
+ sessionStorage.setItem('vs_role','admin');
321
+ document.getElementById('loader-text').textContent = 'ACCES ACORDAT';
322
+ await delay(400);
323
+ window.location.href = 'admin-dashboard.html';
324
  }
325
+ } catch(e) {
326
+ showError('err-msg','err-001');
327
+ } finally {
328
+ loader.classList.add('hide');
 
 
 
 
 
 
 
329
  }
 
330
  }
331
+
332
+ // Enter key support
333
+ document.addEventListener('keydown', e => {
334
+ if (e.key === 'Enter') doLogin();
335
+ });
336
+
337
+ function delay(ms) { return new Promise(r=>setTimeout(r,ms)); }
338
  </script>
339
  </body>
340
  </html>
static/reset.html ADDED
@@ -0,0 +1,267 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="ro">
3
+ <head>
4
+ <link rel="icon" type="image/svg+xml" href="favicon.svg">
5
+ <meta charset="UTF-8">
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover">
7
+ <title>VSERVERS | Resetare Parolă</title>
8
+ <link rel="stylesheet" href="style.css">
9
+ <script src="errors.js"></script>
10
+ <style>
11
+ body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
12
+ .wrap { width:100%; max-width:360px; }
13
+ .phone-wrap { display:flex; border:1px solid rgba(255,255,255,0.12); }
14
+ .phone-prefix { background:rgba(255,255,255,0.06); border:none; border-right:1px solid rgba(255,255,255,0.1); color:var(--white); padding:10px 12px; font-family:'DM Mono',monospace; font-size:12px; flex-shrink:0; display:flex; align-items:center; }
15
+ .phone-input { flex:1; background:transparent; border:none; color:var(--white); padding:10px 12px; font-family:'DM Mono',monospace; font-size:13px; letter-spacing:2px; outline:none; }
16
+ .phone-input::placeholder { color:var(--white-faint); font-size:11px; }
17
+ .phase { display:none; animation:fadeSlideIn 0.3s ease forwards; }
18
+ .phase.active { display:block; }
19
+ @keyframes fadeSlideIn { from{opacity:0;transform:translateY(6px);}to{opacity:1;transform:translateY(0);} }
20
+ .sms-box { text-align:center; padding:20px 16px; background:var(--glass); border:1px solid var(--glass-border); margin-bottom:14px; }
21
+ .sms-box .sb-title { font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:2px; margin-bottom:6px; }
22
+ .sms-box .sb-num { font-family:'DM Mono',monospace; font-size:14px; letter-spacing:2px; margin-top:8px; }
23
+ .waiting-indicator { display:flex; align-items:center; gap:10px; padding:12px 14px; background:var(--glass); border:1px solid var(--glass-border); margin-bottom:14px; }
24
+ .pulse-dot { width:8px; height:8px; border-radius:50%; background:rgba(255,255,255,0.4); animation:pulse 2s ease-in-out infinite; flex-shrink:0; }
25
+ @keyframes pulse { 0%,100%{opacity:0.3;transform:scale(0.8);}50%{opacity:1;transform:scale(1.2);} }
26
+ </style>
27
+ </head>
28
+ <body>
29
+
30
+ <div class="wrap">
31
+ <div style="text-align:center;margin-bottom:22px;" class="fade-in">
32
+ <img src="logo.svg" style="width:34px;height:34px;margin:0 auto 10px;display:block;">
33
+ <div style="font-family:'Cormorant Garamond',serif;font-size:22px;letter-spacing:3px;">VSERVERS</div>
34
+ <div style="font-size:9px;letter-spacing:2px;color:var(--white-dim);margin-top:3px;">RESETARE PAROLĂ</div>
35
+ </div>
36
+
37
+ <!-- STEPS -->
38
+ <div class="steps fade-in-2">
39
+ <div class="step"><div class="step-dot active" id="s1-dot">1</div><div class="step-label active">IDENTIFICARE</div></div>
40
+ <div class="step-line"></div>
41
+ <div class="step"><div class="step-dot" id="s2-dot">2</div><div class="step-label" id="s2-lbl">SMS</div></div>
42
+ <div class="step-line"></div>
43
+ <div class="step"><div class="step-dot" id="s3-dot">3</div><div class="step-label" id="s3-lbl">PAROLĂ NOUĂ</div></div>
44
+ </div>
45
+
46
+ <!-- PHASE 1 -->
47
+ <div class="phase active fade-in-3" id="phase-1">
48
+ <div class="card" style="padding:18px 16px;">
49
+ <div class="card-title" style="font-size:15px;">Identificare cont</div>
50
+ <p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.9;">
51
+ Selectează contul tău și introdu numărul de telefon pentru a primi un cod de resetare.
52
+ </p>
53
+ <div class="field">
54
+ <label>Contul tău</label>
55
+ <select id="elev-sel"><option value="">— selectează —</option></select>
56
+ </div>
57
+ <div class="field">
58
+ <label>Număr de telefon</label>
59
+ <div class="phone-wrap">
60
+ <div class="phone-prefix">+373</div>
61
+ <input class="phone-input" type="tel" id="phone-in" maxlength="9" placeholder="(69) 048 176" inputmode="tel" oninput="fmtPhone(this)">
62
+ </div>
63
+ <div style="font-size:9px;color:var(--white-faint);margin-top:5px;letter-spacing:1px;">Ex: +373 (69) 048 176</div>
64
+ </div>
65
+ <button class="btn-primary" onclick="requestReset()" id="btn-reset" style="width:100%;letter-spacing:2px;margin-top:4px;">SOLICITĂ RESETARE →</button>
66
+ <div class="alert error" id="err-1" style="margin-top:10px;"></div>
67
+ </div>
68
+ <div style="text-align:center;margin-top:12px;">
69
+ <a href="index.html" style="font-size:10px;color:var(--white-dim);letter-spacing:1px;text-decoration:none;">← înapoi la login</a>
70
+ </div>
71
+ </div>
72
+
73
+ <!-- PHASE 2 -->
74
+ <div class="phase" id="phase-2">
75
+ <div class="sms-box">
76
+ <div class="sb-title">Cod trimis prin SMS</div>
77
+ <div style="font-size:10px;color:var(--white-dim);letter-spacing:1px;line-height:1.9;">Administratorul ți-a transmis un cod de resetare pe numărul:</div>
78
+ <div class="sb-num" id="phone-display">—</div>
79
+ </div>
80
+ <div class="waiting-indicator">
81
+ <div class="pulse-dot"></div>
82
+ <div>
83
+ <div class="wi-text">Așteptăm confirmarea adminului...</div>
84
+ <div class="wi-code" id="wait-timer">—</div>
85
+ </div>
86
+ </div>
87
+ <div class="card" style="padding:18px 16px;">
88
+ <div class="card-title" style="font-size:15px;">Introdu codul din SMS</div>
89
+ <div class="field">
90
+ <input type="text" id="code-in" maxlength="4" placeholder="• • • •"
91
+ inputmode="numeric" autocomplete="one-time-code"
92
+ style="font-size:28px;letter-spacing:12px;text-align:center;padding:14px;">
93
+ </div>
94
+ <button class="btn-primary" onclick="verifyReset()" style="width:100%;letter-spacing:2px;margin-top:4px;">VERIFICĂ COD →</button>
95
+ <div class="alert error" id="err-2" style="margin-top:10px;"></div>
96
+ <div style="text-align:center;margin-top:12px;">
97
+ <button class="btn-ghost" onclick="backToStep1()" style="font-size:9px;">Nu am primit SMS — Înapoi</button>
98
+ </div>
99
+ </div>
100
+ </div>
101
+
102
+ <!-- PHASE 3 -->
103
+ <div class="phase" id="phase-3">
104
+ <div class="card" style="padding:18px 16px;">
105
+ <div class="card-title" style="font-size:15px;">Parolă nouă</div>
106
+ <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.9;">
107
+ Alege o parolă nouă de minimum 6 cifre.
108
+ </p>
109
+ <div class="field">
110
+ <label>Parolă nouă (min. 6 cifre)</label>
111
+ <input type="password" id="new-pin1" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
112
+ </div>
113
+ <div class="field">
114
+ <label>Confirmă parola</label>
115
+ <input type="password" id="new-pin2" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
116
+ </div>
117
+ <button class="btn-primary" onclick="saveNewPin()" style="width:100%;letter-spacing:2px;margin-top:4px;">SALVEAZĂ PAROLA →</button>
118
+ <div class="alert error" id="err-3" style="margin-top:10px;"></div>
119
+ </div>
120
+ </div>
121
+
122
+ <!-- PHASE 4: Done -->
123
+ <div class="phase" id="phase-4">
124
+ <div class="card" style="padding:28px 20px;">
125
+ <div class="success-anim">
126
+ <svg viewBox="0 0 24 24" fill="none" stroke="#5a9a5a" stroke-width="1.5" stroke-linecap="round">
127
+ <path d="M22 11.08V12a10 10 0 11-5.93-9.14"/>
128
+ <polyline points="22 4 12 14.01 9 11.01"/>
129
+ </svg>
130
+ <div class="sa-title">Parolă resetată</div>
131
+ <div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;" id="done-name">—</div>
132
+ </div>
133
+ <div style="margin-top:20px;">
134
+ <button class="btn-primary" onclick="window.location.href='index.html'" style="width:100%;letter-spacing:2px;">MERGI LA LOGIN →</button>
135
+ </div>
136
+ </div>
137
+ </div>
138
+
139
+ <div class="footer-mini">VSERVERS &copy;2026 &mdash; Victor Roșca</div>
140
+ </div>
141
+
142
+ <script type="module">
143
+ import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
144
+ import { getFirestore, collection, getDocs, addDoc, updateDoc, doc, query, where, serverTimestamp }
145
+ from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
146
+
147
+ const cfg = {
148
+ apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
149
+ projectId:"vservers1", storageBucket:"vservers1.firebasestorage.app",
150
+ messagingSenderId:"42433037358", appId:"1:42433037358:web:fde70fec79542428b60bbf"
151
+ };
152
+ const app = initializeApp(cfg);
153
+ const db = getFirestore(app);
154
+
155
+ let selectedElevId = null, selectedVpass = null, selectedNume = null;
156
+ let resetCode = null, resetDocId = null, codeExpiry = null;
157
+
158
+ // Load elevi
159
+ try {
160
+ const snap = await getDocs(collection(db,'elevi'));
161
+ const sel = document.getElementById('elev-sel');
162
+ const elevi = [];
163
+ snap.forEach(d => elevi.push({id:d.id,...d.data()}));
164
+ elevi.sort((a,b)=>(a.pozitie||0)-(b.pozitie||0));
165
+ elevi.forEach(e => {
166
+ if (!e.pin) return; // doar cei cu cont activ
167
+ const o = document.createElement('option');
168
+ o.value = e.id;
169
+ o.textContent = `${String(e.pozitie||'').padStart(2,'0')}. ${e.nume}`;
170
+ sel.appendChild(o);
171
+ });
172
+ } catch(e) {}
173
+
174
+ function setPhase(n) {
175
+ document.querySelectorAll('.phase').forEach(p => p.classList.remove('active'));
176
+ document.getElementById('phase-'+n).classList.add('active');
177
+ window.scrollTo({top:0,behavior:'smooth'});
178
+ for (let i=1;i<=3;i++) {
179
+ const d = document.getElementById(`s${i}-dot`);
180
+ if (i < n) { d.classList.add('done'); d.innerHTML='✓'; }
181
+ else if (i===n) { d.classList.add('active'); }
182
+ else { d.classList.remove('active','done'); }
183
+ }
184
+ }
185
+
186
+ window.fmtPhone = function(input) {
187
+ let v = input.value.replace(/\D/g,'');
188
+ if(v.length>8)v=v.slice(0,8);
189
+ let out='';
190
+ if(v.length>=2)out='('+v.slice(0,2)+') '; else out=v;
191
+ if(v.length>2)out+=v.slice(2,5);
192
+ if(v.length>5)out+=' '+v.slice(5,8);
193
+ input.value=out;
194
+ };
195
+
196
+ window.requestReset = async function() {
197
+ hideError('err-1');
198
+ const id = document.getElementById('elev-sel').value;
199
+ const rawPh = document.getElementById('phone-in').value.replace(/\D/g,'');
200
+ if (!id) { showError('err-1','err-021','Selectează contul.'); return; }
201
+ if (rawPh.length<8){ showError('err-1','err-021','Număr de telefon incomplet.'); return; }
202
+
203
+ document.getElementById('btn-reset').disabled=true;
204
+ const phone = '+373 ('+rawPh.slice(0,2)+') '+rawPh.slice(2,5)+' '+rawPh.slice(5,8);
205
+
206
+ try {
207
+ const snap = await getDocs(query(collection(db,'elevi')));
208
+ const elevDoc = snap.docs.find(d=>d.id===id);
209
+ if (!elevDoc) { showError('err-1','err-002'); document.getElementById('btn-reset').disabled=false; return; }
210
+ selectedElevId = id;
211
+ selectedVpass = elevDoc.data().vpassId;
212
+ selectedNume = elevDoc.data().nume;
213
+
214
+ resetCode = String(Math.floor(1000+Math.random()*9000));
215
+
216
+ const ref = await addDoc(collection(db,'notificari'),{
217
+ tip:'reset_request', elevId:id, elevVpass:selectedVpass, elevNume:selectedNume,
218
+ telefon:phone, confirmCode:resetCode,
219
+ status:'pending', citita:false, timestamp:serverTimestamp()
220
+ });
221
+ resetDocId = ref.id;
222
+
223
+ document.getElementById('phone-display').textContent = phone;
224
+ codeExpiry = Date.now() + 10*60*1000;
225
+ const tv = document.getElementById('wait-timer');
226
+ const iv = setInterval(()=>{
227
+ const l=Math.max(0,codeExpiry-Date.now());
228
+ const m=Math.floor(l/60000), s=Math.floor((l%60000)/1000);
229
+ tv.textContent=`Expiră în ${m}:${String(s).padStart(2,'0')}`;
230
+ if(l<=0){clearInterval(iv);tv.textContent='Cod expirat';}
231
+ },1000);
232
+ setPhase(2);
233
+ } catch(e){ showError('err-1','err-025'); document.getElementById('btn-reset').disabled=false; }
234
+ };
235
+
236
+ window.verifyReset = function() {
237
+ hideError('err-2');
238
+ const input = document.getElementById('code-in').value.replace(/\s/g,'');
239
+ if(!/^\d{4}$/.test(input)){ showError('err-2','err-029'); return; }
240
+ if(codeExpiry && Date.now()>codeExpiry){ showError('err-2','err-006'); return; }
241
+ if(input !== resetCode){ showError('err-2','err-007'); return; }
242
+ setPhase(3);
243
+ };
244
+
245
+ window.backToStep1 = function() {
246
+ document.getElementById('btn-reset').disabled=false;
247
+ setPhase(1);
248
+ };
249
+
250
+ window.saveNewPin = async function() {
251
+ hideError('err-3');
252
+ const p1 = document.getElementById('new-pin1').value;
253
+ const p2 = document.getElementById('new-pin2').value;
254
+ if(p1.length<6){ showError('err-3','err-008'); return; }
255
+ if(p1!==p2){ showError('err-3','err-008','Parolele nu coincid.'); return; }
256
+ try {
257
+ await updateDoc(doc(db,'elevi',selectedElevId),{pin:p1});
258
+ if(resetDocId) await updateDoc(doc(db,'notificari',resetDocId),{status:'completed',citita:true});
259
+ document.getElementById('done-name').textContent=selectedNume;
260
+ // Sterge sesiunea veche
261
+ localStorage.removeItem('vs_role');
262
+ setPhase(4);
263
+ } catch(e){ showError('err-3','err-025'); }
264
+ };
265
+ </script>
266
+ </body>
267
+ </html>
static/signup.html CHANGED
@@ -11,7 +11,6 @@
11
  body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
12
  .wrap { width:100%; max-width:380px; }
13
 
14
- /* Steps indicator */
15
  .steps { display:flex; align-items:center; justify-content:center; gap:0; margin-bottom:28px; }
16
  .step { display:flex; flex-direction:column; align-items:center; gap:5px; }
17
  .step-dot {
@@ -22,39 +21,31 @@
22
  transition:all 0.4s ease;
23
  }
24
  .step-dot.active { border-color:var(--white); color:var(--white); background:rgba(255,255,255,0.08); }
25
- .step-dot.done { border-color:rgba(60,120,60,0.6); background:rgba(20,50,20,0.4); color:#5a9a5a; }
26
  .step-label { font-size:8px; letter-spacing:1px; color:var(--white-faint); white-space:nowrap; }
27
  .step-label.active { color:var(--white-dim); }
28
- .step-line { width:36px; height:1px; background:rgba(255,255,255,0.08); margin:0 4px; margin-bottom:14px; }
29
 
30
- /* VPass card — identity display */
31
  .vpass-card {
32
  background:rgba(255,255,255,0.04);
33
- backdrop-filter:blur(20px);
34
- -webkit-backdrop-filter:blur(20px);
35
  border:1px solid rgba(255,255,255,0.09);
36
- padding:18px 16px;
37
- margin-bottom:16px;
38
  display:flex; align-items:center; gap:14px;
39
  }
40
- .vpass-card .vc-icon { flex-shrink:0; opacity:0.6; }
41
- .vpass-card .vc-icon img { width:28px; height:28px; }
42
  .vpass-card .vc-name { font-family:'Cormorant Garamond',serif; font-size:18px; font-weight:600; }
43
- .vpass-card .vc-id { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-top:2px; }
44
 
45
- /* Waiting pulse */
46
  .waiting-indicator {
47
  display:flex; align-items:center; gap:10px;
48
- padding:14px 16px;
49
- background:rgba(255,255,255,0.03);
50
- border:1px solid rgba(255,255,255,0.07);
51
- margin-bottom:14px;
52
  }
53
  .pulse-dot {
54
  width:8px; height:8px; border-radius:50%;
55
  background:rgba(255,255,255,0.4);
56
- animation:pulse 2s ease-in-out infinite;
57
- flex-shrink:0;
58
  }
59
  @keyframes pulse {
60
  0%,100% { opacity:0.3; transform:scale(0.8); }
@@ -63,32 +54,43 @@
63
  .waiting-indicator .wi-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
64
  .waiting-indicator .wi-code { font-size:10px; color:var(--white-faint); margin-top:2px; }
65
 
66
- /* Code display for waiting */
67
- .generated-code-box {
68
- text-align:center; padding:16px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
69
  background:rgba(255,255,255,0.03);
70
- border:1px solid rgba(255,255,255,0.07);
71
- margin-bottom:14px;
72
  }
73
- .generated-code-box .gc-label { font-size:9px; letter-spacing:3px; color:var(--white-dim); margin-bottom:8px; }
74
- .generated-code-box .gc-code { font-family:'Cormorant Garamond',serif; font-size:36px; letter-spacing:8px; color:var(--white); }
 
 
75
 
76
  .phase { display:none; }
77
  .phase.active { display:block; }
78
 
79
- /* Success state */
80
- .success-anim {
81
- text-align:center; padding:20px 0;
82
- }
83
  .success-anim svg { width:40px; height:40px; margin:0 auto 12px; display:block; }
84
 
85
  .err-code { font-size:9px; opacity:0.6; margin-right:4px; letter-spacing:1px; }
86
-
87
  .footer-mini { text-align:center; margin-top:18px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2; }
88
  </style>
89
  </head>
90
  <body>
91
-
92
  <div class="wrap">
93
 
94
  <div style="text-align:center;margin-bottom:22px;" class="fade-in">
@@ -97,25 +99,30 @@
97
  <div style="font-size:9px;letter-spacing:2px;color:var(--white-dim);margin-top:3px;">ÎNREGISTRARE CONT</div>
98
  </div>
99
 
100
- <!-- STEPS -->
101
  <div class="steps fade-in-2">
102
  <div class="step">
103
  <div class="step-dot active" id="s1-dot">1</div>
104
- <div class="step-label active">IDENTITATE</div>
105
  </div>
106
  <div class="step-line"></div>
107
  <div class="step">
108
  <div class="step-dot" id="s2-dot">2</div>
109
- <div class="step-label" id="s2-lbl">CONFIRMARE</div>
110
  </div>
111
  <div class="step-line"></div>
112
  <div class="step">
113
  <div class="step-dot" id="s3-dot">3</div>
114
- <div class="step-label" id="s3-lbl">PAROLĂ</div>
 
 
 
 
 
115
  </div>
116
  </div>
117
 
118
- <!-- PHASE 1: Identity confirm -->
119
  <div class="phase active fade-in-3" id="phase-1">
120
  <div class="vpass-card">
121
  <div class="vc-icon"><img src="logo.svg" alt=""></div>
@@ -126,11 +133,10 @@
126
  </div>
127
  <div class="card" style="padding:18px 16px;">
128
  <div class="card-title" style="font-size:15px;">Ești tu?</div>
129
- <p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.8;">
130
- Dacă datele de mai sus sunt ale tale, apasă <strong style="color:var(--white);">SOLICITĂ COD</strong>.
131
- VPass va genera un cod de 4 cifre pe care adminul ți-l va transmite.
132
  </p>
133
- <button class="btn-primary" onclick="requestCode()" id="btn-req" style="width:100%;letter-spacing:2px;">SOLICITĂ COD DE CONFIRMARE</button>
134
  <div class="alert error" id="err-1" style="margin-top:10px;"></div>
135
  </div>
136
  <div style="text-align:center;margin-top:12px;">
@@ -138,50 +144,83 @@
138
  </div>
139
  </div>
140
 
141
- <!-- PHASE 2: Waiting + Enter code -->
142
  <div class="phase" id="phase-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  <div class="waiting-indicator">
144
  <div class="pulse-dot"></div>
145
  <div>
146
- <div class="wi-text">Așteptăm validarea adminului...</div>
147
  <div class="wi-code" id="wait-timer">—</div>
148
  </div>
149
  </div>
150
- <div class="generated-code-box">
151
- <div class="gc-label">CODUL TĂU DE SOLICITARE</div>
152
- <div class="gc-code" id="display-code">——</div>
153
- <div style="font-size:9px;color:var(--white-faint);margin-top:8px;letter-spacing:1px;">Arată acest cod adminului sau transmite-i verbal</div>
154
- </div>
155
  <div class="card" style="padding:18px 16px;">
156
- <div class="card-title" style="font-size:15px;">Cod de confirmare</div>
157
- <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.8;">
158
- Introduceți codul de 4 cifre primit de la administrator după validare.
159
  </p>
160
  <div class="field">
161
- <label>Cod confirmare (4 cifre)</label>
162
- <input type="text" id="confirm-input" maxlength="4" placeholder="••••" inputmode="numeric" style="font-size:24px;letter-spacing:8px;text-align:center;">
 
163
  </div>
164
- <button class="btn-primary" onclick="verifyCode()" style="width:100%;letter-spacing:2px;margin-top:4px;">VERIFICĂ COD</button>
165
- <div class="alert error" id="err-2" style="margin-top:10px;"></div>
166
  <div style="margin-top:12px;text-align:center;">
167
- <button class="btn-ghost" onclick="requestNewCode()" style="font-size:9px;">Cod nou</button>
168
  </div>
169
  </div>
170
  </div>
171
 
172
- <!-- PHASE 3: Set password -->
173
- <div class="phase" id="phase-3">
174
  <div class="vpass-card">
175
  <div class="vc-icon"><img src="logo.svg" alt=""></div>
176
  <div>
177
- <div class="vc-name" id="ph3-name">—</div>
178
- <div class="vc-id" id="ph3-vpass" style="color:#5a9a5a;">✓ CONFIRMAT</div>
179
  </div>
180
  </div>
181
  <div class="card" style="padding:18px 16px;">
182
  <div class="card-title" style="font-size:15px;">Setează parola VPass</div>
183
- <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.8;">
184
- Alege o parolă de minimum 6 cifre. Aceasta va fi codul tău permanent de autentificare.
185
  </p>
186
  <div class="field">
187
  <label>Parolă nouă (min. 6 cifre)</label>
@@ -191,13 +230,13 @@
191
  <label>Confirmă parola</label>
192
  <input type="password" id="new-pin2" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
193
  </div>
194
- <button class="btn-primary" onclick="setPassword()" style="width:100%;letter-spacing:2px;margin-top:4px;">ACTIVEAZĂ CONTUL</button>
195
- <div class="alert error" id="err-3" style="margin-top:10px;"></div>
196
  </div>
197
  </div>
198
 
199
- <!-- PHASE 4: Done -->
200
- <div class="phase" id="phase-4">
201
  <div class="card" style="padding:28px 20px;">
202
  <div class="success-anim">
203
  <svg viewBox="0 0 24 24" fill="none" stroke="#5a9a5a" stroke-width="1.5" stroke-linecap="round">
@@ -205,8 +244,8 @@
205
  <polyline points="22 4 12 14.01 9 11.01"/>
206
  </svg>
207
  <div style="font-family:'Cormorant Garamond',serif;font-size:22px;letter-spacing:2px;margin-bottom:6px;">Cont activat</div>
208
- <div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;" id="ph4-name">—</div>
209
- <div style="font-size:10px;color:var(--white-faint);letter-spacing:2px;margin-top:4px;" id="ph4-vpass">—</div>
210
  </div>
211
  <div style="margin-top:20px;">
212
  <button class="btn-primary" onclick="window.location.href='index.html'" style="width:100%;letter-spacing:2px;">MERGI LA LOGIN →</button>
@@ -219,7 +258,8 @@
219
 
220
  <script type="module">
221
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
222
- import { getFirestore, collection, addDoc, getDocs, updateDoc, doc, query, where, serverTimestamp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
 
223
 
224
  const cfg = {
225
  apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
@@ -229,167 +269,175 @@ const cfg = {
229
  const app = initializeApp(cfg);
230
  const db = getFirestore(app);
231
 
232
- // Get elev from sessionStorage (set de index.html)
233
  const elevId = sessionStorage.getItem('su_elevId');
234
  const elevNume = sessionStorage.getItem('su_name');
235
  const elevVpass = sessionStorage.getItem('su_vpass');
236
-
237
  if (!elevId || !elevNume) { window.location.href = 'index.html'; }
238
 
239
- document.getElementById('ph1-name').textContent = elevNume;
240
- document.getElementById('ph1-vpass').textContent = elevVpass;
241
- document.getElementById('ph3-name').textContent = elevNume;
242
- document.getElementById('ph3-vpass').textContent = elevVpass;
243
- document.getElementById('ph4-name').textContent = elevNume;
244
- document.getElementById('ph4-vpass').textContent = elevVpass;
 
245
 
246
- let generatedCode = null;
247
  let requestDocId = null;
248
  let pollInterval = null;
249
  let codeExpiry = null;
 
250
 
 
251
  function setPhase(n) {
252
  document.querySelectorAll('.phase').forEach(p => p.classList.remove('active'));
253
  document.getElementById('phase-'+n).classList.add('active');
254
- // Update steps
255
- for (let i=1; i<=3; i++) {
256
  const dot = document.getElementById(`s${i}-dot`);
257
- const lbl = document.getElementById(`s${i}-lbl`) || dot.nextElementSibling;
258
- if (i < n) { dot.classList.add('done'); dot.innerHTML=''; }
259
- else if (i === n) { dot.classList.add('active'); }
260
  }
261
  }
262
 
263
- function genCode4() {
264
- return String(Math.floor(1000 + Math.random() * 9000));
265
- }
 
 
 
 
 
 
 
 
266
 
267
- function startTimer() {
268
- codeExpiry = Date.now() + 10 * 60 * 1000; // 10 min
269
- const el = document.getElementById('wait-timer');
270
- const iv = setInterval(() => {
271
- const left = Math.max(0, codeExpiry - Date.now());
272
- const m = Math.floor(left/60000);
273
- const s = Math.floor((left%60000)/1000);
274
- el.textContent = `Expiră în ${m}:${String(s).padStart(2,'0')}`;
275
- if (left <= 0) { clearInterval(iv); el.textContent = 'Cod expirat — solicită unul nou'; }
276
- }, 1000);
277
- }
278
 
 
279
  window.requestCode = async function() {
280
- hideError('err-1');
 
 
 
281
  document.getElementById('btn-req').disabled = true;
282
 
283
- // Check existing pending
284
  try {
285
  const q = query(collection(db,'signup_requests'), where('elevId','==',elevId), where('status','==','pending'));
286
  const ex = await getDocs(q);
287
- if (!ex.empty) { showError('err-1','err-005'); document.getElementById('btn-req').disabled=false; return; }
288
- } catch(e) { showError('err-1','err-026'); document.getElementById('btn-req').disabled=false; return; }
289
 
290
- // Check already registered
291
  try {
292
  const q2 = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
293
  const snap = await getDocs(q2);
294
  if (!snap.empty && snap.docs[0].data().pin !== null) {
295
- showError('err-1','err-023'); document.getElementById('btn-req').disabled=false; return;
296
  }
297
  } catch(e) {}
298
 
299
- generatedCode = genCode4();
 
300
 
301
  try {
302
  const ref = await addDoc(collection(db,'signup_requests'), {
303
- elevId, elevVpass, elevNume,
304
  confirmCode: generatedCode,
305
  status: 'pending',
306
  timestamp: serverTimestamp()
307
  });
308
  requestDocId = ref.id;
309
 
310
- // Add to notificari
311
  await addDoc(collection(db,'notificari'), {
312
  tip: 'signup_request',
313
  elevId, elevVpass, elevNume,
314
- confirmCode: generatedCode,
 
315
  requestId: ref.id,
316
  status: 'pending',
317
  citita: false,
318
  timestamp: serverTimestamp()
319
  });
320
 
321
- document.getElementById('display-code').textContent = generatedCode;
322
- setPhase(2);
 
323
  startTimer();
324
  startPolling();
325
 
326
- } catch(e) { showError('err-1','err-025'); document.getElementById('btn-req').disabled=false; }
327
  };
328
 
 
 
 
 
 
 
 
 
 
 
 
 
329
  function startPolling() {
330
  if (pollInterval) clearInterval(pollInterval);
331
  pollInterval = setInterval(async () => {
332
  if (!requestDocId) return;
333
  try {
334
- const q = query(collection(db,'signup_requests'), where('elevId','==',elevId), where('status','!=','pending'));
 
335
  const snap = await getDocs(q);
336
- if (!snap.empty) {
337
- const data = snap.docs[0].data();
338
- if (data.status === 'approved') {
339
- clearInterval(pollInterval);
340
- // auto-fill if code matches
341
- sessionStorage.setItem('su_approved','1');
342
- } else if (data.status === 'rejected') {
343
- clearInterval(pollInterval);
344
- showError('err-2','err-024');
345
- }
346
- }
347
  } catch(e) {}
348
- }, 3000);
349
  }
350
 
 
351
  window.verifyCode = function() {
352
- hideError('err-2');
353
- const input = document.getElementById('confirm-input').value.trim();
354
- if (!/^\d{4}$/.test(input)) { showError('err-2','err-029'); return; }
355
- if (codeExpiry && Date.now() > codeExpiry) { showError('err-2','err-006'); return; }
356
- if (input !== generatedCode) { showError('err-2','err-007'); return; }
357
  if (pollInterval) clearInterval(pollInterval);
358
- setPhase(3);
359
  };
360
 
361
  window.requestNewCode = async function() {
362
  if (pollInterval) clearInterval(pollInterval);
363
- // Reset
364
  generatedCode = null; requestDocId = null;
365
  document.getElementById('confirm-input').value = '';
366
- setPhase(1);
367
  document.getElementById('btn-req').disabled = false;
 
368
  };
369
 
 
370
  window.setPassword = async function() {
371
- hideError('err-3');
372
  const p1 = document.getElementById('new-pin').value;
373
  const p2 = document.getElementById('new-pin2').value;
374
- if (p1.length < 6) { showError('err-3','err-008'); return; }
375
- if (p1 !== p2) { showError('err-3','err-008','Parolele nu coincid.'); return; }
376
  try {
377
- // Update elev document
378
  const q = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
379
  const snap = await getDocs(q);
380
- if (snap.empty) { showError('err-3','err-002'); return; }
381
  await updateDoc(doc(db,'elevi',snap.docs[0].id), { pin: p1, confirmed: true });
382
-
383
- // Mark request done
384
  if (requestDocId) {
385
  try { await updateDoc(doc(db,'signup_requests',requestDocId),{ status:'completed' }); } catch(e){}
386
  }
387
-
388
  sessionStorage.removeItem('su_elevId');
389
  sessionStorage.removeItem('su_name');
390
  sessionStorage.removeItem('su_vpass');
391
- setPhase(4);
392
- } catch(e) { showError('err-3','err-025'); }
393
  };
394
  </script>
395
  </body>
 
11
  body { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
12
  .wrap { width:100%; max-width:380px; }
13
 
 
14
  .steps { display:flex; align-items:center; justify-content:center; gap:0; margin-bottom:28px; }
15
  .step { display:flex; flex-direction:column; align-items:center; gap:5px; }
16
  .step-dot {
 
21
  transition:all 0.4s ease;
22
  }
23
  .step-dot.active { border-color:var(--white); color:var(--white); background:rgba(255,255,255,0.08); }
24
+ .step-dot.done { border-color:rgba(60,120,60,0.6); background:rgba(20,50,20,0.4); color:#5a9a5a; }
25
  .step-label { font-size:8px; letter-spacing:1px; color:var(--white-faint); white-space:nowrap; }
26
  .step-label.active { color:var(--white-dim); }
27
+ .step-line { width:28px; height:1px; background:rgba(255,255,255,0.08); margin:0 4px; margin-bottom:14px; }
28
 
 
29
  .vpass-card {
30
  background:rgba(255,255,255,0.04);
31
+ backdrop-filter:blur(20px); -webkit-backdrop-filter:blur(20px);
 
32
  border:1px solid rgba(255,255,255,0.09);
33
+ padding:18px 16px; margin-bottom:16px;
 
34
  display:flex; align-items:center; gap:14px;
35
  }
36
+ .vpass-card .vc-icon img { width:28px; height:28px; opacity:0.6; }
 
37
  .vpass-card .vc-name { font-family:'Cormorant Garamond',serif; font-size:18px; font-weight:600; }
38
+ .vpass-card .vc-id { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-top:2px; }
39
 
 
40
  .waiting-indicator {
41
  display:flex; align-items:center; gap:10px;
42
+ padding:14px 16px; background:rgba(255,255,255,0.03);
43
+ border:1px solid rgba(255,255,255,0.07); margin-bottom:14px;
 
 
44
  }
45
  .pulse-dot {
46
  width:8px; height:8px; border-radius:50%;
47
  background:rgba(255,255,255,0.4);
48
+ animation:pulse 2s ease-in-out infinite; flex-shrink:0;
 
49
  }
50
  @keyframes pulse {
51
  0%,100% { opacity:0.3; transform:scale(0.8); }
 
54
  .waiting-indicator .wi-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
55
  .waiting-indicator .wi-code { font-size:10px; color:var(--white-faint); margin-top:2px; }
56
 
57
+ /* Telefon input */
58
+ .phone-wrap { display:flex; gap:0; border:1px solid rgba(255,255,255,0.12); }
59
+ .phone-prefix {
60
+ background:rgba(255,255,255,0.06); border:none; border-right:1px solid rgba(255,255,255,0.1);
61
+ color:var(--white); padding:10px 12px; font-family:'DM Mono',monospace;
62
+ font-size:12px; letter-spacing:1px; flex-shrink:0; display:flex; align-items:center;
63
+ }
64
+ .phone-input {
65
+ flex:1; background:transparent; border:none; color:var(--white);
66
+ padding:10px 12px; font-family:'DM Mono',monospace; font-size:13px;
67
+ letter-spacing:2px; outline:none;
68
+ }
69
+ .phone-input::placeholder { color:var(--white-faint); letter-spacing:1px; font-size:11px; }
70
+ .phone-format { font-size:9px; color:var(--white-faint); letter-spacing:1px; margin-top:5px; }
71
+
72
+ /* Waiting SMS box */
73
+ .sms-waiting-box {
74
+ text-align:center; padding:20px 16px;
75
  background:rgba(255,255,255,0.03);
76
+ border:1px solid rgba(255,255,255,0.07); margin-bottom:14px;
 
77
  }
78
+ .sms-waiting-box .sw-icon { font-size:28px; margin-bottom:10px; }
79
+ .sms-waiting-box .sw-title { font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:2px; margin-bottom:6px; }
80
+ .sms-waiting-box .sw-sub { font-size:10px; color:var(--white-dim); letter-spacing:1px; line-height:1.8; }
81
+ .sms-waiting-box .sw-phone { font-size:13px; color:var(--white); letter-spacing:2px; margin-top:8px; font-family:'DM Mono',monospace; }
82
 
83
  .phase { display:none; }
84
  .phase.active { display:block; }
85
 
86
+ .success-anim { text-align:center; padding:20px 0; }
 
 
 
87
  .success-anim svg { width:40px; height:40px; margin:0 auto 12px; display:block; }
88
 
89
  .err-code { font-size:9px; opacity:0.6; margin-right:4px; letter-spacing:1px; }
 
90
  .footer-mini { text-align:center; margin-top:18px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2; }
91
  </style>
92
  </head>
93
  <body>
 
94
  <div class="wrap">
95
 
96
  <div style="text-align:center;margin-bottom:22px;" class="fade-in">
 
99
  <div style="font-size:9px;letter-spacing:2px;color:var(--white-dim);margin-top:3px;">ÎNREGISTRARE CONT</div>
100
  </div>
101
 
102
+ <!-- STEPS: 4 pasi -->
103
  <div class="steps fade-in-2">
104
  <div class="step">
105
  <div class="step-dot active" id="s1-dot">1</div>
106
+ <div class="step-label active" id="s1-lbl">IDENTITATE</div>
107
  </div>
108
  <div class="step-line"></div>
109
  <div class="step">
110
  <div class="step-dot" id="s2-dot">2</div>
111
+ <div class="step-label" id="s2-lbl">TELEFON</div>
112
  </div>
113
  <div class="step-line"></div>
114
  <div class="step">
115
  <div class="step-dot" id="s3-dot">3</div>
116
+ <div class="step-label" id="s3-lbl">COD SMS</div>
117
+ </div>
118
+ <div class="step-line"></div>
119
+ <div class="step">
120
+ <div class="step-dot" id="s4-dot">4</div>
121
+ <div class="step-label" id="s4-lbl">PAROLĂ</div>
122
  </div>
123
  </div>
124
 
125
+ <!-- PHASE 1: Confirmare identitate -->
126
  <div class="phase active fade-in-3" id="phase-1">
127
  <div class="vpass-card">
128
  <div class="vc-icon"><img src="logo.svg" alt=""></div>
 
133
  </div>
134
  <div class="card" style="padding:18px 16px;">
135
  <div class="card-title" style="font-size:15px;">Ești tu?</div>
136
+ <p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.9;">
137
+ Verifică datele de mai sus îți aparțin. La pasul următor vei introduce numărul tău de telefon pentru a primi codul de confirmare prin SMS.
 
138
  </p>
139
+ <button class="btn-primary" onclick="goToPhone()" style="width:100%;letter-spacing:2px;">DA, SUNT EU — CONTINUĂ →</button>
140
  <div class="alert error" id="err-1" style="margin-top:10px;"></div>
141
  </div>
142
  <div style="text-align:center;margin-top:12px;">
 
144
  </div>
145
  </div>
146
 
147
+ <!-- PHASE 2: Introducere număr telefon -->
148
  <div class="phase" id="phase-2">
149
+ <div class="vpass-card">
150
+ <div class="vc-icon"><img src="logo.svg" alt=""></div>
151
+ <div>
152
+ <div class="vc-name" id="ph2-name">—</div>
153
+ <div class="vc-id" id="ph2-vpass">—</div>
154
+ </div>
155
+ </div>
156
+ <div class="card" style="padding:18px 16px;">
157
+ <div class="card-title" style="font-size:15px;">Număr de telefon</div>
158
+ <p style="font-size:11px;color:var(--white-dim);margin-bottom:16px;line-height:1.9;">
159
+ Introdu numărul tău de telefon. Administratorul îți va transmite codul de confirmare VPass pe acest număr.
160
+ </p>
161
+ <div class="field">
162
+ <label>Număr de telefon</label>
163
+ <div class="phone-wrap">
164
+ <div class="phone-prefix">+373</div>
165
+ <input class="phone-input" type="tel" id="phone-input" maxlength="9" placeholder="(69) 048 176" inputmode="tel" oninput="formatPhone(this)">
166
+ </div>
167
+ <div class="phone-format">Format: +373 (##) ### ### &nbsp;·&nbsp; Ex: +373 (69) 048 176</div>
168
+ </div>
169
+ <button class="btn-primary" onclick="requestCode()" id="btn-req" style="width:100%;letter-spacing:2px;margin-top:4px;">SOLICITĂ COD →</button>
170
+ <div class="alert error" id="err-2" style="margin-top:10px;"></div>
171
+ </div>
172
+ </div>
173
+
174
+ <!-- PHASE 3: Asteapta SMS + introdu codul -->
175
+ <div class="phase" id="phase-3">
176
+ <div class="sms-waiting-box">
177
+ <div class="sw-icon">
178
+ <svg width="32" height="32" viewBox="0 0 24 24" fill="none" stroke="rgba(255,255,255,0.5)" stroke-width="1.5" stroke-linecap="round">
179
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
180
+ </svg>
181
+ </div>
182
+ <div class="sw-title">SMS trimis de administrator</div>
183
+ <div class="sw-sub">Administratorul a primit cererea ta și va trimite<br>codul de confirmare pe numărul:</div>
184
+ <div class="sw-phone" id="phone-display">—</div>
185
+ </div>
186
  <div class="waiting-indicator">
187
  <div class="pulse-dot"></div>
188
  <div>
189
+ <div class="wi-text">Așteptăm validarea administratorului...</div>
190
  <div class="wi-code" id="wait-timer">—</div>
191
  </div>
192
  </div>
 
 
 
 
 
193
  <div class="card" style="padding:18px 16px;">
194
+ <div class="card-title" style="font-size:15px;">Introdu codul primit prin SMS</div>
195
+ <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.9;">
196
+ Introduceți cele 4 cifre primite în SMS-ul de confirmare VPass.
197
  </p>
198
  <div class="field">
199
+ <input type="text" id="confirm-input" maxlength="4" placeholder="• • • •"
200
+ inputmode="numeric" autocomplete="one-time-code"
201
+ style="font-size:28px;letter-spacing:12px;text-align:center;padding:14px;">
202
  </div>
203
+ <button class="btn-primary" onclick="verifyCode()" style="width:100%;letter-spacing:2px;margin-top:4px;">VERIFICĂ COD</button>
204
+ <div class="alert error" id="err-3" style="margin-top:10px;"></div>
205
  <div style="margin-top:12px;text-align:center;">
206
+ <button class="btn-ghost" onclick="requestNewCode()" style="font-size:9px;letter-spacing:1px;">Nu am primit SMS — Cod nou</button>
207
  </div>
208
  </div>
209
  </div>
210
 
211
+ <!-- PHASE 4: Setare parola -->
212
+ <div class="phase" id="phase-4">
213
  <div class="vpass-card">
214
  <div class="vc-icon"><img src="logo.svg" alt=""></div>
215
  <div>
216
+ <div class="vc-name" id="ph4-name">—</div>
217
+ <div class="vc-id" id="ph4-vpass" style="color:#5a9a5a;letter-spacing:2px;">✓ CONFIRMAT PRIN SMS</div>
218
  </div>
219
  </div>
220
  <div class="card" style="padding:18px 16px;">
221
  <div class="card-title" style="font-size:15px;">Setează parola VPass</div>
222
+ <p style="font-size:11px;color:var(--white-dim);margin-bottom:14px;line-height:1.9;">
223
+ Alege o parolă de minimum 6 cifre. Aceasta va fi codul tău permanent de autentificare în sistem.
224
  </p>
225
  <div class="field">
226
  <label>Parolă nouă (min. 6 cifre)</label>
 
230
  <label>Confirmă parola</label>
231
  <input type="password" id="new-pin2" maxlength="6" placeholder="••••••" inputmode="numeric" autocomplete="new-password">
232
  </div>
233
+ <button class="btn-primary" onclick="setPassword()" style="width:100%;letter-spacing:2px;margin-top:4px;">ACTIVEAZĂ CONTUL</button>
234
+ <div class="alert error" id="err-4" style="margin-top:10px;"></div>
235
  </div>
236
  </div>
237
 
238
+ <!-- PHASE 5: Done -->
239
+ <div class="phase" id="phase-5">
240
  <div class="card" style="padding:28px 20px;">
241
  <div class="success-anim">
242
  <svg viewBox="0 0 24 24" fill="none" stroke="#5a9a5a" stroke-width="1.5" stroke-linecap="round">
 
244
  <polyline points="22 4 12 14.01 9 11.01"/>
245
  </svg>
246
  <div style="font-family:'Cormorant Garamond',serif;font-size:22px;letter-spacing:2px;margin-bottom:6px;">Cont activat</div>
247
+ <div style="font-size:11px;color:var(--white-dim);letter-spacing:1px;" id="ph5-name">—</div>
248
+ <div style="font-size:10px;color:var(--white-faint);letter-spacing:2px;margin-top:4px;" id="ph5-vpass">—</div>
249
  </div>
250
  <div style="margin-top:20px;">
251
  <button class="btn-primary" onclick="window.location.href='index.html'" style="width:100%;letter-spacing:2px;">MERGI LA LOGIN →</button>
 
258
 
259
  <script type="module">
260
  import { initializeApp } from "https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js";
261
+ import { getFirestore, collection, addDoc, getDocs, updateDoc, doc, query, where, serverTimestamp }
262
+ from "https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js";
263
 
264
  const cfg = {
265
  apiKey:"AIzaSyB9--Onx3-_YjD-YzblhZjaWSVVqTQJ1lU", authDomain:"vservers1.firebaseapp.com",
 
269
  const app = initializeApp(cfg);
270
  const db = getFirestore(app);
271
 
 
272
  const elevId = sessionStorage.getItem('su_elevId');
273
  const elevNume = sessionStorage.getItem('su_name');
274
  const elevVpass = sessionStorage.getItem('su_vpass');
 
275
  if (!elevId || !elevNume) { window.location.href = 'index.html'; }
276
 
277
+ // Populate identity fields
278
+ ['ph1-name','ph2-name','ph4-name','ph5-name'].forEach(id => {
279
+ const el = document.getElementById(id); if(el) el.textContent = elevNume;
280
+ });
281
+ ['ph1-vpass','ph2-vpass','ph5-vpass'].forEach(id => {
282
+ const el = document.getElementById(id); if(el) el.textContent = elevVpass;
283
+ });
284
 
285
+ let generatedCode = null; // SECRET — nu e afișat elevului
286
  let requestDocId = null;
287
  let pollInterval = null;
288
  let codeExpiry = null;
289
+ let phoneNumber = null;
290
 
291
+ // ── STEPS ──
292
  function setPhase(n) {
293
  document.querySelectorAll('.phase').forEach(p => p.classList.remove('active'));
294
  document.getElementById('phase-'+n).classList.add('active');
295
+ window.scrollTo({top:0,behavior:'smooth'});
296
+ for (let i=1; i<=4; i++) {
297
  const dot = document.getElementById(`s${i}-dot`);
298
+ if (i < n) { dot.classList.remove('active'); dot.classList.add('done'); dot.innerHTML='✓'; }
299
+ else if (i===n) { dot.classList.add('active'); dot.classList.remove('done'); }
300
+ else { dot.classList.remove('active','done'); }
301
  }
302
  }
303
 
304
+ // ── PHONE FORMAT ──
305
+ window.formatPhone = function(input) {
306
+ let v = input.value.replace(/\D/g,'');
307
+ if (v.length > 8) v = v.slice(0,8);
308
+ let out = '';
309
+ if (v.length >= 2) out = '(' + v.slice(0,2) + ') ';
310
+ else out = v;
311
+ if (v.length > 2) out += v.slice(2,5);
312
+ if (v.length > 5) out += ' ' + v.slice(5,8);
313
+ input.value = out;
314
+ };
315
 
316
+ // ── PHASE 1 → 2 ──
317
+ window.goToPhone = function() { setPhase(2); };
 
 
 
 
 
 
 
 
 
318
 
319
+ // ── SOLICITA COD (PHASE 2) ──
320
  window.requestCode = async function() {
321
+ hideError('err-2');
322
+ const raw = document.getElementById('phone-input').value.replace(/\D/g,'');
323
+ if (raw.length < 8) { showError('err-2','err-021','Număr de telefon incomplet.'); return; }
324
+ phoneNumber = '+373 (' + raw.slice(0,2) + ') ' + raw.slice(2,5) + ' ' + raw.slice(5,8);
325
  document.getElementById('btn-req').disabled = true;
326
 
327
+ // Verifica pending
328
  try {
329
  const q = query(collection(db,'signup_requests'), where('elevId','==',elevId), where('status','==','pending'));
330
  const ex = await getDocs(q);
331
+ if (!ex.empty) { showError('err-2','err-005'); document.getElementById('btn-req').disabled=false; return; }
332
+ } catch(e) { showError('err-2','err-026'); document.getElementById('btn-req').disabled=false; return; }
333
 
334
+ // Verifica deja inregistrat
335
  try {
336
  const q2 = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
337
  const snap = await getDocs(q2);
338
  if (!snap.empty && snap.docs[0].data().pin !== null) {
339
+ showError('err-2','err-023'); document.getElementById('btn-req').disabled=false; return;
340
  }
341
  } catch(e) {}
342
 
343
+ // Genereaza cod SECRET
344
+ generatedCode = String(Math.floor(1000 + Math.random() * 9000));
345
 
346
  try {
347
  const ref = await addDoc(collection(db,'signup_requests'), {
348
+ elevId, elevVpass, elevNume, telefon: phoneNumber,
349
  confirmCode: generatedCode,
350
  status: 'pending',
351
  timestamp: serverTimestamp()
352
  });
353
  requestDocId = ref.id;
354
 
355
+ // Notificare pentru admin (cu codul + telefonul)
356
  await addDoc(collection(db,'notificari'), {
357
  tip: 'signup_request',
358
  elevId, elevVpass, elevNume,
359
+ telefon: phoneNumber,
360
+ confirmCode: generatedCode, // secret — vizibil doar adminului
361
  requestId: ref.id,
362
  status: 'pending',
363
  citita: false,
364
  timestamp: serverTimestamp()
365
  });
366
 
367
+ // Afiseaza ecranul de asteptare
368
+ document.getElementById('phone-display').textContent = phoneNumber;
369
+ setPhase(3);
370
  startTimer();
371
  startPolling();
372
 
373
+ } catch(e) { showError('err-2','err-025'); document.getElementById('btn-req').disabled=false; }
374
  };
375
 
376
+ function startTimer() {
377
+ codeExpiry = Date.now() + 10 * 60 * 1000;
378
+ const el = document.getElementById('wait-timer');
379
+ const iv = setInterval(() => {
380
+ const left = Math.max(0, codeExpiry - Date.now());
381
+ const m = Math.floor(left/60000);
382
+ const s = Math.floor((left%60000)/1000);
383
+ el.textContent = `Codul expiră în ${m}:${String(s).padStart(2,'0')}`;
384
+ if (left <= 0) { clearInterval(iv); el.textContent = 'Cod expirat — solicită unul nou'; }
385
+ }, 1000);
386
+ }
387
+
388
  function startPolling() {
389
  if (pollInterval) clearInterval(pollInterval);
390
  pollInterval = setInterval(async () => {
391
  if (!requestDocId) return;
392
  try {
393
+ const q = query(collection(db,'signup_requests'),
394
+ where('elevId','==',elevId), where('status','==','rejected'));
395
  const snap = await getDocs(q);
396
+ if (!snap.empty) { clearInterval(pollInterval); showError('err-3','err-024'); }
 
 
 
 
 
 
 
 
 
 
397
  } catch(e) {}
398
+ }, 4000);
399
  }
400
 
401
+ // ── VERIFICA COD SMS ──
402
  window.verifyCode = function() {
403
+ hideError('err-3');
404
+ const input = document.getElementById('confirm-input').value.replace(/\s/g,'');
405
+ if (!/^\d{4}$/.test(input)) { showError('err-3','err-029'); return; }
406
+ if (codeExpiry && Date.now() > codeExpiry) { showError('err-3','err-006'); return; }
407
+ if (input !== generatedCode) { showError('err-3','err-007'); return; }
408
  if (pollInterval) clearInterval(pollInterval);
409
+ setPhase(4);
410
  };
411
 
412
  window.requestNewCode = async function() {
413
  if (pollInterval) clearInterval(pollInterval);
 
414
  generatedCode = null; requestDocId = null;
415
  document.getElementById('confirm-input').value = '';
416
+ document.getElementById('phone-input').value = '';
417
  document.getElementById('btn-req').disabled = false;
418
+ setPhase(2);
419
  };
420
 
421
+ // ── SETEAZA PAROLA ──
422
  window.setPassword = async function() {
423
+ hideError('err-4');
424
  const p1 = document.getElementById('new-pin').value;
425
  const p2 = document.getElementById('new-pin2').value;
426
+ if (p1.length < 6) { showError('err-4','err-008'); return; }
427
+ if (p1 !== p2) { showError('err-4','err-008','Parolele nu coincid.'); return; }
428
  try {
 
429
  const q = query(collection(db,'elevi'), where('vpassId','==',elevVpass));
430
  const snap = await getDocs(q);
431
+ if (snap.empty) { showError('err-4','err-002'); return; }
432
  await updateDoc(doc(db,'elevi',snap.docs[0].id), { pin: p1, confirmed: true });
 
 
433
  if (requestDocId) {
434
  try { await updateDoc(doc(db,'signup_requests',requestDocId),{ status:'completed' }); } catch(e){}
435
  }
 
436
  sessionStorage.removeItem('su_elevId');
437
  sessionStorage.removeItem('su_name');
438
  sessionStorage.removeItem('su_vpass');
439
+ setPhase(5);
440
+ } catch(e) { showError('err-4','err-025'); }
441
  };
442
  </script>
443
  </body>
static/style.css CHANGED
@@ -1,421 +1,744 @@
1
- @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@300;400;600;700&family=DM+Mono:wght@300;400;500&display=swap');
 
 
 
2
 
3
- :root {
4
- --bg: #0a0a0a;
5
- --surface: #111111;
6
- --surface2: #181818;
7
- --border: #242424;
8
- --border-light: #333333;
9
- --white: #efefef;
10
- --white-dim: #666666;
11
- --white-faint: #2a2a2a;
12
- --black: #0a0a0a;
13
- --danger: #cc3333;
14
- }
15
 
16
- *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; }
17
- html { -webkit-text-size-adjust: 100%; }
18
 
19
- body {
20
- background: var(--bg);
21
- color: var(--white);
22
- font-family: 'DM Mono', monospace;
23
- min-height: 100vh;
24
- min-height: 100dvh;
25
- -webkit-font-smoothing: antialiased;
26
- overflow-x: hidden;
27
- }
 
 
 
 
 
 
 
 
 
 
28
 
29
- /* ── TOPBAR ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
30
  .topbar {
31
- position: sticky; top: 0; z-index: 100;
32
- background: rgba(10,10,10,0.97);
33
- backdrop-filter: blur(10px);
34
- -webkit-backdrop-filter: blur(10px);
35
- border-bottom: 1px solid var(--border);
36
- height: 50px;
37
- display: flex;
38
- align-items: center;
39
- padding: 0 14px;
40
- gap: 10px;
41
- overflow: hidden;
42
- }
43
- .topbar-logo { display: flex; align-items: center; gap: 7px; text-decoration: none; flex-shrink: 0; }
44
- .topbar-logo img { width: 20px; height: 20px; object-fit: contain; flex-shrink: 0; }
45
- .topbar-name { font-family: 'Cormorant Garamond', serif; font-size: 17px; font-weight: 600; letter-spacing: 2px; color: var(--white); white-space: nowrap; }
46
- .topbar-divider { width: 1px; height: 16px; background: var(--border); flex-shrink: 0; }
47
- .topbar-section { font-size: 9px; color: var(--white-dim); letter-spacing: 1px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100px; }
48
- .topbar-right { margin-left: auto; display: flex; align-items: center; gap: 6px; flex-shrink: 0; }
49
- .role-tag { border: 1px solid var(--border-light); color: var(--white-dim); padding: 2px 7px; font-size: 9px; letter-spacing: 1px; white-space: nowrap; }
50
-
51
- .btn-ghost {
52
- background: transparent; border: 1px solid var(--border);
53
- color: var(--white-dim); padding: 4px 9px;
54
- font-family: 'DM Mono', monospace; font-size: 9px; letter-spacing: 1px;
55
- cursor: pointer; transition: all 0.15s; white-space: nowrap;
56
- }
57
- .btn-ghost:hover { border-color: var(--white); color: var(--white); }
58
-
59
- /* ── MAIN ── */
60
- .main { max-width: 860px; margin: 0 auto; padding: 16px 14px 0; }
61
-
62
- /* ── CARD ── */
63
- .card { background: var(--surface); border: 1px solid var(--border); padding: 18px 16px; margin-bottom: 14px; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  .card-title {
65
- font-family: 'Cormorant Garamond', serif; font-size: 18px; font-weight: 600;
66
- letter-spacing: 1px; margin-bottom: 14px; padding-bottom: 12px;
67
- border-bottom: 1px solid var(--border); color: var(--white);
 
68
  }
69
 
70
  /* ── LABEL ── */
71
  .label {
72
- font-size: 9px; letter-spacing: 3px; color: var(--white-dim);
73
- text-transform: uppercase; margin-bottom: 10px;
74
- display: flex; align-items: center; gap: 10px;
75
- }
76
- .label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
77
-
78
- /* ── FORM ── */
79
- .field { margin-bottom: 12px; }
80
- .field > label { display: block; font-size: 9px; letter-spacing: 2px; color: var(--white-dim); text-transform: uppercase; margin-bottom: 5px; }
81
- .field input, .field select {
82
- width: 100%; background: var(--surface2); border: 1px solid var(--border);
83
- color: var(--white); padding: 9px 11px;
84
- font-family: 'DM Mono', monospace; font-size: 13px;
85
- outline: none; transition: border-color 0.15s;
86
- appearance: none; -webkit-appearance: none; border-radius: 0;
87
- }
88
- .field input:focus, .field select:focus { border-color: var(--white); }
89
- .field select option { background: #111; }
90
- .field input::placeholder { color: var(--white-faint); }
91
-
92
- /* ── BUTTONS ── */
93
- .btn-primary {
94
- background: var(--white); border: none; color: var(--black);
95
- padding: 10px 20px; font-family: 'DM Mono', monospace;
96
- font-size: 11px; letter-spacing: 2px; cursor: pointer;
97
- transition: all 0.15s; text-transform: uppercase; border-radius: 0;
98
- -webkit-tap-highlight-color: transparent;
99
- }
100
- .btn-primary:hover { background: #d0d0d0; }
101
- .btn-primary:active { background: #aaa; }
102
- .btn-primary:disabled { background: var(--border); color: var(--white-dim); cursor: not-allowed; }
103
-
104
- .btn-outline {
105
- background: transparent; border: 1px solid var(--border-light);
106
- color: var(--white-dim); padding: 6px 12px;
107
- font-family: 'DM Mono', monospace; font-size: 10px; letter-spacing: 1px;
108
- cursor: pointer; transition: all 0.15s; border-radius: 0;
109
- -webkit-tap-highlight-color: transparent;
110
- }
111
- .btn-outline:hover { border-color: var(--white); color: var(--white); }
112
-
113
- .btn-danger {
114
- background: transparent; border: 1px solid #3a1111; color: #994444;
115
- padding: 5px 10px; font-family: 'DM Mono', monospace;
116
- font-size: 9px; letter-spacing: 1px; cursor: pointer;
117
- transition: all 0.15s; border-radius: 0;
118
- -webkit-tap-highlight-color: transparent;
119
  }
120
- .btn-danger:hover { border-color: var(--danger); color: var(--danger); }
121
 
122
  /* ── GRID ── */
123
- .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; }
124
- @media(max-width: 500px) { .grid-2 { grid-template-columns: 1fr; gap: 10px; } }
125
-
126
- /* ── STATUS ── */
127
- .status-dot { display: inline-block; width: 6px; height: 6px; border-radius: 50%; background: var(--white-dim); flex-shrink: 0; }
128
- .status-dot.online { background: #fff; box-shadow: 0 0 5px rgba(255,255,255,0.5); }
129
-
130
- /* ── TABLE ── */
131
- .data-table { border: 1px solid var(--border); overflow: hidden; margin-bottom: 14px; overflow-x: auto; }
132
- .dt-head { display: grid; padding: 8px 12px; border-bottom: 1px solid var(--border); font-size: 9px; letter-spacing: 2px; color: var(--white-dim); background: var(--surface2); min-width: 0; }
133
- .dt-row { display: grid; padding: 10px 12px; border-bottom: 1px solid rgba(36,36,36,0.7); align-items: center; transition: background 0.1s; gap: 8px; min-width: 0; }
134
- .dt-row:last-child { border-bottom: none; }
135
- .dt-row:hover { background: rgba(255,255,255,0.02); }
136
-
137
- /* ── PILL ── */
138
- .pill { display: inline-block; padding: 2px 7px; border: 1px solid var(--border-light); font-size: 9px; letter-spacing: 1px; color: var(--white-dim); white-space: nowrap; }
139
- .pill.active { border-color: #444; color: var(--white); }
140
- .pill.success { border-color: #2a4a2a; color: #5a9a5a; }
141
- .pill.empty { border-color: #1e1e1e; color: #333; }
142
-
143
- /* ── ALERTS ── */
144
- .alert { padding: 10px 13px; border: 1px solid var(--border); font-size: 11px; margin-top: 10px; display: none; line-height: 1.5; }
145
- .alert.show { display: block; }
146
- .alert.error { border-color: #4a1a1a; color: #cc5555; background: rgba(50,10,10,0.3); }
147
- .alert.success { border-color: #1a3a1a; color: #5a9a5a; background: rgba(10,25,10,0.3); }
148
-
149
- /* ── PROGRESS ── */
150
- .progress-wrap { margin-top: 10px; display: none; }
151
- .progress-wrap.show { display: block; }
152
- .progress-info { display: flex; justify-content: space-between; font-size: 10px; color: var(--white-dim); margin-bottom: 5px; }
153
- .progress-track { height: 2px; background: var(--border); }
154
- .progress-fill { height: 100%; background: var(--white); width: 0%; transition: width 0.2s; }
155
-
156
- /* ── UPLOAD ZONE ── */
157
- .upload-zone {
158
- border: 1px dashed var(--border-light); padding: 26px 14px;
159
- text-align: center; cursor: pointer; transition: border-color 0.15s; position: relative;
160
- }
161
- .upload-zone:hover, .upload-zone.over { border-color: var(--white); }
162
- .upload-zone input { position: absolute; inset: 0; opacity: 0; cursor: pointer; width: 100%; height: 100%; }
163
- .upload-zone .uz-svg { margin: 0 auto 8px; width: 26px; height: 26px; stroke: var(--white-dim); display: block; }
164
- .upload-zone p { font-size: 12px; color: var(--white-dim); }
165
- .upload-zone span { font-size: 10px; color: var(--white-faint); display: block; margin-top: 3px; }
166
-
167
- /* ── FILE ROW ── */
168
- .file-row { display: flex; align-items: center; gap: 10px; padding: 9px 12px; border: 1px solid var(--border); margin-bottom: 5px; transition: border-color 0.15s; }
169
- .file-row:hover { border-color: var(--border-light); }
170
- .file-row .fr-svg { width: 15px; height: 15px; stroke: var(--white-dim); flex-shrink: 0; }
171
- .file-row .fr-name { flex: 1; font-size: 12px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
172
- .file-row .fr-meta { font-size: 9px; color: var(--white-dim); white-space: nowrap; flex-shrink: 0; }
173
-
174
- /* ── TOAST ── */
175
- .toast {
176
- position: fixed; bottom: 16px; left: 12px; right: 12px;
177
- background: var(--surface); border: 1px solid var(--border-light);
178
- color: var(--white); padding: 10px 14px; font-size: 11px; letter-spacing: 1px;
179
- transform: translateY(80px); opacity: 0; transition: all 0.25s; z-index: 500; text-align: center;
180
- }
181
- @media(min-width: 500px) { .toast { left: auto; right: 20px; width: auto; max-width: 320px; text-align: left; } }
182
- .toast.show { transform: translateY(0); opacity: 1; }
183
-
184
- /* ── TABS ── */
185
- .tabs { display: flex; border-bottom: 1px solid var(--border); margin-bottom: 18px; overflow-x: auto; -webkit-overflow-scrolling: touch; scrollbar-width: none; }
186
- .tabs::-webkit-scrollbar { display: none; }
187
- .tab-btn {
188
- background: transparent; border: none; color: var(--white-dim);
189
- padding: 10px 14px; font-family: 'DM Mono', monospace;
190
- font-size: 9px; letter-spacing: 2px; cursor: pointer; transition: color 0.15s;
191
- border-bottom: 2px solid transparent; margin-bottom: -1px; white-space: nowrap; flex-shrink: 0;
192
- -webkit-tap-highlight-color: transparent;
193
- }
194
- .tab-btn:hover { color: var(--white); }
195
- .tab-btn.active { color: var(--white); border-bottom-color: var(--white); }
196
- .tab-pane { display: none; }
197
- .tab-pane.active { display: block; }
198
 
199
  /* ── STATS ROW ── */
200
- .stats-row { display: grid; grid-template-columns: 1fr 1fr; gap: 1px; background: var(--border); border: 1px solid var(--border); margin-bottom: 14px; }
201
- @media(min-width: 560px) { .stats-row { grid-template-columns: repeat(4, 1fr); } }
202
- .stat-box { background: var(--surface); padding: 14px 12px; min-width: 0; }
203
- .stat-num { font-family: 'Cormorant Garamond', serif; font-size: 24px; font-weight: 300; color: var(--white); line-height: 1; margin-bottom: 3px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
204
- .stat-lbl { font-size: 8px; letter-spacing: 2px; color: var(--white-dim); }
205
-
206
- /* ── FOOTER ── */
207
- .footer {
208
- margin-top: 40px;
209
- border-top: 1px solid var(--border);
210
- padding: 28px 14px 36px;
211
- display: flex; flex-direction: column; align-items: center; gap: 14px; text-align: center;
212
- }
213
- .footer-top { display: flex; align-items: center; gap: 8px; }
214
- .footer-top img { width: 16px; height: 16px; object-fit: contain; opacity: 0.7; }
215
- .footer-top span { font-family: 'Cormorant Garamond', serif; font-size: 15px; font-weight: 600; letter-spacing: 3px; color: var(--white); }
216
- .footer-meta { font-size: 9px; letter-spacing: 2px; color: var(--white-dim); }
217
- .footer-copy { font-size: 9px; color: var(--white-faint); letter-spacing: 1px; line-height: 2; }
218
- .footer-divider { width: 40px; height: 1px; background: var(--border); }
219
-
220
- /* ── ANIMS ── */
221
- @keyframes fadeUp { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } }
222
- .fade-in { animation: fadeUp 0.3s ease both; }
223
- .fade-in-2 { animation: fadeUp 0.3s ease 0.06s both; }
224
- .fade-in-3 { animation: fadeUp 0.3s ease 0.12s both; }
225
- .fade-in-4 { animation: fadeUp 0.3s ease 0.18s both; }
226
-
227
- /* ══════════════════════════════════════════════
228
- LIQUID GLASS — global overlay effects
229
- ══════════════════════════════════════════════ */
230
-
231
- /* Topbar glass — toate paginile */
232
- .topbar {
233
- background: rgba(12,12,12,0.55) !important;
234
- backdrop-filter: blur(32px) saturate(180%) !important;
235
- -webkit-backdrop-filter: blur(32px) saturate(180%) !important;
236
- border-bottom: 1px solid rgba(255,255,255,0.07) !important;
237
- box-shadow: 0 1px 0 rgba(255,255,255,0.04) inset, 0 4px 24px rgba(0,0,0,0.45);
238
- }
239
-
240
- /* Stat boxes glass */
241
  .stats-row {
242
- background: transparent !important;
243
- border: none !important;
244
- gap: 8px !important;
245
  }
 
246
  .stat-box {
247
- background: rgba(255,255,255,0.04) !important;
248
- backdrop-filter: blur(20px) saturate(150%);
249
- -webkit-backdrop-filter: blur(20px) saturate(150%);
250
- border: 1px solid rgba(255,255,255,0.07) !important;
251
- box-shadow: 0 4px 20px rgba(0,0,0,0.3), 0 1px 0 rgba(255,255,255,0.05) inset;
252
- }
253
-
254
- /* Cards glass */
255
- .card {
256
- background: rgba(255,255,255,0.04) !important;
257
- backdrop-filter: blur(24px) saturate(160%);
258
- -webkit-backdrop-filter: blur(24px) saturate(160%);
259
- border: 1px solid rgba(255,255,255,0.08) !important;
260
- box-shadow: 0 6px 28px rgba(0,0,0,0.35), 0 1px 0 rgba(255,255,255,0.06) inset;
261
  }
 
 
 
262
 
263
- /* Data table glass */
264
- .data-table {
265
- background: rgba(255,255,255,0.02) !important;
266
- backdrop-filter: blur(16px);
267
- -webkit-backdrop-filter: blur(16px);
268
- border: 1px solid rgba(255,255,255,0.06) !important;
269
- box-shadow: 0 4px 20px rgba(0,0,0,0.28);
270
- }
271
- .dt-head {
272
- background: rgba(255,255,255,0.05) !important;
273
- border-bottom: 1px solid rgba(255,255,255,0.05) !important;
274
- }
275
- .dt-row { border-bottom: 1px solid rgba(255,255,255,0.035) !important; }
276
- .dt-row:hover { background: rgba(255,255,255,0.04) !important; }
277
-
278
- /* Login card glass */
279
- .login-card {
280
- background: rgba(255,255,255,0.05) !important;
281
- backdrop-filter: blur(28px) saturate(170%);
282
- -webkit-backdrop-filter: blur(28px) saturate(170%);
283
- border: 1px solid rgba(255,255,255,0.1) !important;
284
- box-shadow: 0 8px 40px rgba(0,0,0,0.5), 0 1px 0 rgba(255,255,255,0.08) inset;
285
- }
286
-
287
- /* Role tabs in login */
288
- .role-tabs {
289
- border: 1px solid rgba(255,255,255,0.08) !important;
290
- background: rgba(255,255,255,0.02);
291
- }
292
- .role-tab { border-right: 1px solid rgba(255,255,255,0.07) !important; }
293
-
294
- /* Input fields glass */
295
- .field input, .field select {
296
- background: rgba(255,255,255,0.05) !important;
297
- border: 1px solid rgba(255,255,255,0.09) !important;
298
- backdrop-filter: blur(8px);
299
- -webkit-backdrop-filter: blur(8px);
300
- }
301
- .field input:focus, .field select:focus {
302
- border-color: rgba(255,255,255,0.35) !important;
303
- background: rgba(255,255,255,0.07) !important;
304
- }
305
 
306
- /* Tabs glass */
307
- .tabs {
308
- border-bottom: 1px solid rgba(255,255,255,0.06) !important;
 
 
 
 
 
309
  }
310
-
311
- /* Toast glass */
312
- .toast {
313
- background: rgba(20,20,20,0.75) !important;
314
- backdrop-filter: blur(20px) !important;
315
- -webkit-backdrop-filter: blur(20px) !important;
316
- border: 1px solid rgba(255,255,255,0.1) !important;
317
- box-shadow: 0 8px 32px rgba(0,0,0,0.5);
318
  }
 
 
 
319
 
320
- /* Footer glass border */
321
- .footer {
322
- border-top: 1px solid rgba(255,255,255,0.06) !important;
 
 
323
  }
 
324
 
325
- /* VPass preview glass */
326
- .vpass-preview {
327
- background: rgba(255,255,255,0.05) !important;
328
- border: 1px solid rgba(255,255,255,0.08) !important;
329
- backdrop-filter: blur(8px);
330
- -webkit-backdrop-filter: blur(8px);
331
  }
 
332
 
333
- /* Danger box glass */
334
- .danger-box {
335
- background: rgba(40,5,5,0.35) !important;
336
- border: 1px solid rgba(120,20,20,0.4) !important;
337
- backdrop-filter: blur(12px);
338
- -webkit-backdrop-filter: blur(12px);
339
  }
 
340
 
341
- /* Pill glass */
342
- .pill.success {
343
- background: rgba(20,50,20,0.3);
344
- border-color: rgba(60,120,60,0.4) !important;
345
- backdrop-filter: blur(8px);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  }
347
- .pill.empty {
348
- background: rgba(255,255,255,0.02);
349
- border-color: rgba(255,255,255,0.06) !important;
 
350
  }
 
 
351
 
352
- /* Drawer glass */
353
- .drawer {
354
- background: rgba(255,255,255,0.03) !important;
355
- backdrop-filter: blur(24px) saturate(150%);
356
- -webkit-backdrop-filter: blur(24px) saturate(150%);
357
- border: 1px solid rgba(255,255,255,0.07) !important;
358
- box-shadow: 0 8px 36px rgba(0,0,0,0.45);
359
- }
360
- .drawer-head {
361
- background: rgba(255,255,255,0.04) !important;
362
- border-bottom: 1px solid rgba(255,255,255,0.06) !important;
363
  }
 
 
364
 
365
- /* File row glass */
366
- .file-row {
367
- background: rgba(255,255,255,0.03) !important;
368
- border: 1px solid rgba(255,255,255,0.06) !important;
369
- backdrop-filter: blur(8px);
370
- -webkit-backdrop-filter: blur(8px);
371
- }
372
- .file-row:hover {
373
- background: rgba(255,255,255,0.06) !important;
374
- border-color: rgba(255,255,255,0.12) !important;
375
  }
 
376
 
377
- /* Upload zone glass */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
378
  .upload-zone {
379
- border: 1px dashed rgba(255,255,255,0.18) !important;
380
- background: rgba(255,255,255,0.025);
381
- }
382
- .upload-zone:hover, .upload-zone.over {
383
- border-color: rgba(255,255,255,0.5) !important;
384
- background: rgba(255,255,255,0.05);
385
- }
386
-
387
- /* Alert glass */
388
- .alert.error {
389
- background: rgba(60,5,5,0.35) !important;
390
- border-color: rgba(100,30,30,0.5) !important;
391
- backdrop-filter: blur(8px);
392
- }
393
- .alert.success {
394
- background: rgba(5,30,5,0.35) !important;
395
- border-color: rgba(30,80,30,0.5) !important;
396
- backdrop-filter: blur(8px);
397
- }
398
-
399
- /* Subject grid glass */
400
- .subj-card {
401
- background: rgba(255,255,255,0.04) !important;
402
- backdrop-filter: blur(12px);
403
- -webkit-backdrop-filter: blur(12px);
404
- border-right: 1px solid rgba(255,255,255,0.04);
405
- border-bottom: 1px solid rgba(255,255,255,0.04);
406
- }
407
- .subj-card:hover { background: rgba(255,255,255,0.08) !important; }
408
- .subj-card.selected {
409
- background: rgba(245,245,245,0.95) !important;
410
- backdrop-filter: none;
411
- box-shadow: 0 2px 12px rgba(255,255,255,0.15);
412
- }
413
-
414
- /* ── ERROR CODES ── */
415
- .alert.error .err-code {
416
- font-size:9px; opacity:0.55; margin-right:5px;
417
- letter-spacing:1px; font-family:'DM Mono',monospace;
418
- }
419
- /* Signup steps */
420
- .step-dot.done { border-color:rgba(60,120,60,0.6) !important; background:rgba(20,50,20,0.4) !important; color:#5a9a5a !important; }
421
- .step-dot.active { border-color:var(--white) !important; color:var(--white) !important; background:rgba(255,255,255,0.07) !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* ════════════════════════════════════════════════
2
+ VSERVERS v3.0 — Master Stylesheet
3
+ © 2026 Victor Roșca — Mîndrești, Telenești, MD
4
+ ════════════════════════════════════════════════ */
5
 
6
+ @import url('https://fonts.googleapis.com/css2?family=Cormorant+Garamond:wght@400;500;600;700&family=DM+Mono:wght@300;400;500&display=swap');
 
 
 
 
 
 
 
 
 
 
 
7
 
8
+ /* ── RESET & BASE ── */
9
+ *, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }
10
 
11
+ :root {
12
+ --black: #0a0a0a;
13
+ --black-2: #111111;
14
+ --black-3: #161616;
15
+ --white: #efefef;
16
+ --white-dim: rgba(239,239,239,0.55);
17
+ --white-faint: rgba(239,239,239,0.22);
18
+ --white-ghost: rgba(239,239,239,0.08);
19
+ --accent: rgba(239,239,239,0.9);
20
+ --success: #4a8a4a;
21
+ --success-bg: rgba(20,50,20,0.4);
22
+ --error: #cc4444;
23
+ --error-bg: rgba(40,10,10,0.5);
24
+ --glass: rgba(255,255,255,0.04);
25
+ --glass-border:rgba(255,255,255,0.09);
26
+ --radius: 0px;
27
+ }
28
+
29
+ html { height:100%; scroll-behavior:smooth; }
30
 
31
+ body {
32
+ background:#0a0a0a;
33
+ color:var(--white);
34
+ font-family:'DM Mono',monospace;
35
+ font-size:13px;
36
+ line-height:1.6;
37
+ min-height:100dvh;
38
+ -webkit-font-smoothing:antialiased;
39
+ overflow-x:hidden;
40
+ }
41
+
42
+ /* ── TYPOGRAPHY ── */
43
+ h1,h2,h3,.serif { font-family:'Cormorant Garamond',serif; }
44
+ .mono { font-family:'DM Mono',monospace; }
45
+
46
+ /* ── SCROLLBAR ── */
47
+ ::-webkit-scrollbar { width:3px; height:3px; }
48
+ ::-webkit-scrollbar-track { background:transparent; }
49
+ ::-webkit-scrollbar-thumb { background:rgba(255,255,255,0.12); }
50
+ ::-webkit-scrollbar-thumb:hover { background:rgba(255,255,255,0.25); }
51
+
52
+ /* ════════════════════════════════════════════════
53
+ LOADER — 3 inele concentrice
54
+ ════════════════════════════════════════════════ */
55
+ .loader-overlay {
56
+ position:fixed; inset:0; background:var(--black);
57
+ display:flex; flex-direction:column;
58
+ align-items:center; justify-content:center;
59
+ z-index:9999;
60
+ transition:opacity 0.5s ease, visibility 0.5s ease;
61
+ }
62
+ .loader-overlay.hide { opacity:0; visibility:hidden; pointer-events:none; }
63
+
64
+ .loader {
65
+ position:relative;
66
+ width:64px; height:64px;
67
+ border-radius:50%;
68
+ perspective:800px;
69
+ }
70
+ .inner {
71
+ position:absolute;
72
+ box-sizing:border-box;
73
+ width:100%; height:100%;
74
+ border-radius:50%;
75
+ }
76
+ .inner.one {
77
+ left:0%; top:0%;
78
+ animation:rotate-one 1.4s linear infinite;
79
+ border-bottom:3px solid rgba(239,239,239,0.7);
80
+ }
81
+ .inner.two {
82
+ right:0%; top:0%;
83
+ animation:rotate-two 1.9s linear infinite;
84
+ border-right:3px solid rgba(239,239,239,0.5);
85
+ }
86
+ .inner.three {
87
+ right:0%; bottom:0%;
88
+ animation:rotate-three 2.6s linear infinite;
89
+ border-top:3px solid rgba(239,239,239,0.3);
90
+ }
91
+
92
+ @keyframes rotate-one {
93
+ 0% { transform:rotateX(35deg) rotateY(-45deg) rotateZ(0deg); }
94
+ 100% { transform:rotateX(35deg) rotateY(-45deg) rotateZ(360deg); }
95
+ }
96
+ @keyframes rotate-two {
97
+ 0% { transform:rotateX(50deg) rotateY(10deg) rotateZ(0deg); }
98
+ 100% { transform:rotateX(50deg) rotateY(10deg) rotateZ(360deg); }
99
+ }
100
+ @keyframes rotate-three {
101
+ 0% { transform:rotateX(35deg) rotateY(55deg) rotateZ(0deg); }
102
+ 100% { transform:rotateX(35deg) rotateY(55deg) rotateZ(-360deg); }
103
+ }
104
+
105
+ .loader-text {
106
+ margin-top:28px;
107
+ font-size:10px;
108
+ letter-spacing:3px;
109
+ color:var(--white-dim);
110
+ animation:pulse-text 2s ease-in-out infinite;
111
+ }
112
+ @keyframes pulse-text {
113
+ 0%,100% { opacity:0.4; }
114
+ 50% { opacity:1; }
115
+ }
116
+
117
+ /* ════════════════════════════════════════════════
118
+ PROGRESS BAR — upload files
119
+ ════════════════════════════════════════════════ */
120
+ .upload-progress-wrap {
121
+ margin:14px 0;
122
+ display:none;
123
+ }
124
+ .upload-progress-wrap.show { display:block; }
125
+ .progress {
126
+ background:rgba(255,255,255,0.07);
127
+ border-radius:100px;
128
+ align-items:center;
129
+ position:relative;
130
+ padding:0 5px;
131
+ display:flex;
132
+ height:36px;
133
+ width:100%;
134
+ overflow:hidden;
135
+ }
136
+ .progress-value {
137
+ box-shadow:0 0 20px rgba(255,255,255,0.3), 0 0 40px rgba(255,255,255,0.1);
138
+ border-radius:100px;
139
+ background:var(--white);
140
+ height:26px;
141
+ width:0%;
142
+ transition:width 0.6s cubic-bezier(0.25,0.46,0.45,0.94);
143
+ position:relative;
144
+ overflow:hidden;
145
+ }
146
+ .progress-value::after {
147
+ content:'';
148
+ position:absolute; inset:0;
149
+ background:linear-gradient(90deg, transparent 0%, rgba(255,255,255,0.4) 50%, transparent 100%);
150
+ animation:shimmer 2s ease-in-out infinite;
151
+ }
152
+ @keyframes shimmer {
153
+ 0% { transform:translateX(-100%); }
154
+ 100% { transform:translateX(100%); }
155
+ }
156
+ .progress-label {
157
+ font-size:9px; letter-spacing:2px; color:var(--white-dim);
158
+ margin-top:7px; text-align:center;
159
+ }
160
+ .progress-pct {
161
+ position:absolute; right:14px; top:50%;
162
+ transform:translateY(-50%);
163
+ font-size:10px; letter-spacing:1px; color:var(--black);
164
+ mix-blend-mode:difference;
165
+ pointer-events:none;
166
+ }
167
+
168
+ /* ════════════════════════════════════════════════
169
+ TOPBAR
170
+ ════════════════════════════════════════════════ */
171
  .topbar {
172
+ position:sticky; top:0; z-index:100;
173
+ height:52px;
174
+ background:rgba(10,10,10,0.92);
175
+ backdrop-filter:blur(20px) saturate(180%);
176
+ -webkit-backdrop-filter:blur(20px) saturate(180%);
177
+ border-bottom:1px solid var(--glass-border);
178
+ display:flex; align-items:center; padding:0 16px; gap:12px;
179
+ }
180
+ .topbar-logo {
181
+ display:flex; align-items:center; gap:8px;
182
+ text-decoration:none; color:var(--white);
183
+ flex-shrink:0;
184
+ }
185
+ .topbar-logo img { width:22px; height:22px; }
186
+ .topbar-name { font-family:'Cormorant Garamond',serif; font-size:16px; letter-spacing:3px; }
187
+ .topbar-divider { width:1px; height:18px; background:var(--glass-border); }
188
+ .topbar-section { font-size:9px; letter-spacing:3px; color:var(--white-dim); }
189
+ .topbar-right { margin-left:auto; display:flex; align-items:center; gap:8px; }
190
+ .role-tag {
191
+ font-size:8px; letter-spacing:2px; padding:3px 8px;
192
+ border:1px solid var(--glass-border); color:var(--white-dim);
193
+ }
194
+ .online-dot {
195
+ width:6px; height:6px; border-radius:50%;
196
+ background:#4a8a4a;
197
+ box-shadow:0 0 6px rgba(74,138,74,0.8);
198
+ animation:blink 3s ease-in-out infinite;
199
+ }
200
+ @keyframes blink {
201
+ 0%,100% { opacity:1; } 50% { opacity:0.4; }
202
+ }
203
+
204
+ /* ════════════════════════════════════════════════
205
+ MAIN CONTENT
206
+ ════════════════════════════════════════════════ */
207
+ .main {
208
+ max-width:960px; margin:0 auto;
209
+ padding:20px 14px 60px;
210
+ }
211
+
212
+ /* ── CARDS ── */
213
+ .card {
214
+ background:var(--glass);
215
+ backdrop-filter:blur(20px) saturate(150%);
216
+ -webkit-backdrop-filter:blur(20px) saturate(150%);
217
+ border:1px solid var(--glass-border);
218
+ padding:20px 18px;
219
+ margin-bottom:14px;
220
+ transition:border-color 0.2s;
221
+ }
222
+ .card:hover { border-color:rgba(255,255,255,0.14); }
223
  .card-title {
224
+ font-family:'Cormorant Garamond',serif;
225
+ font-size:17px; font-weight:600;
226
+ letter-spacing:1px; margin-bottom:14px;
227
+ color:var(--white);
228
  }
229
 
230
  /* ── LABEL ── */
231
  .label {
232
+ font-size:9px; letter-spacing:3px; color:var(--white-dim);
233
+ margin:20px 0 8px; text-transform:uppercase;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
  }
 
235
 
236
  /* ── GRID ── */
237
+ .grid-2 { display:grid; grid-template-columns:1fr 1fr; gap:12px; }
238
+ @media(max-width:480px) { .grid-2 { grid-template-columns:1fr; } }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
239
 
240
  /* ── STATS ROW ── */
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
  .stats-row {
242
+ display:grid; grid-template-columns:repeat(4,1fr); gap:10px;
243
+ margin-bottom:20px;
 
244
  }
245
+ @media(max-width:500px) { .stats-row { grid-template-columns:repeat(2,1fr); } }
246
  .stat-box {
247
+ background:var(--glass); border:1px solid var(--glass-border);
248
+ padding:14px 12px; text-align:center;
249
+ transition:border-color 0.2s, transform 0.2s;
 
 
 
 
 
 
 
 
 
 
 
250
  }
251
+ .stat-box:hover { border-color:rgba(255,255,255,0.18); transform:translateY(-1px); }
252
+ .stat-num { font-family:'Cormorant Garamond',serif; font-size:28px; font-weight:600; }
253
+ .stat-lbl { font-size:8px; letter-spacing:3px; color:var(--white-dim); margin-top:2px; }
254
 
255
+ /* ── TABS ── */
256
+ .tabs { display:flex; gap:0; border-bottom:1px solid var(--glass-border); margin-bottom:18px; overflow-x:auto; scrollbar-width:none; }
257
+ .tabs::-webkit-scrollbar { display:none; }
258
+ .tab-btn {
259
+ background:transparent; border:none; border-bottom:2px solid transparent;
260
+ color:var(--white-dim); padding:10px 14px;
261
+ font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
262
+ cursor:pointer; white-space:nowrap; margin-bottom:-1px;
263
+ transition:all 0.2s; -webkit-tap-highlight-color:transparent;
264
+ }
265
+ .tab-btn:hover { color:var(--white); }
266
+ .tab-btn.active { color:var(--white); border-bottom-color:var(--white); }
267
+ .tab-pane { display:none; }
268
+ .tab-pane.active { display:block; }
269
+
270
+ /* ── FIELDS ── */
271
+ .field { margin-bottom:12px; }
272
+ .field label {
273
+ display:block; font-size:9px; letter-spacing:2px;
274
+ color:var(--white-dim); margin-bottom:6px;
275
+ }
276
+ input, select, textarea {
277
+ width:100%; background:rgba(255,255,255,0.04);
278
+ border:1px solid rgba(255,255,255,0.1);
279
+ color:var(--white); padding:10px 12px;
280
+ font-family:'DM Mono',monospace; font-size:12px;
281
+ outline:none; transition:border-color 0.2s;
282
+ -webkit-appearance:none; appearance:none;
283
+ border-radius:0;
284
+ }
285
+ input:focus, select:focus { border-color:rgba(255,255,255,0.35); }
286
+ input::placeholder { color:var(--white-faint); }
287
+ select option { background:#111; color:var(--white); }
 
 
 
 
 
 
 
 
 
288
 
289
+ /* ── BUTTONS ── */
290
+ .btn-primary {
291
+ background:var(--white); color:var(--black);
292
+ border:none; padding:11px 20px;
293
+ font-family:'DM Mono',monospace; font-size:10px; letter-spacing:3px;
294
+ cursor:pointer; transition:all 0.2s; font-weight:500;
295
+ -webkit-tap-highlight-color:transparent;
296
+ position:relative; overflow:hidden;
297
  }
298
+ .btn-primary::after {
299
+ content:''; position:absolute; inset:0;
300
+ background:rgba(0,0,0,0.1); opacity:0; transition:opacity 0.15s;
 
 
 
 
 
301
  }
302
+ .btn-primary:hover::after { opacity:1; }
303
+ .btn-primary:active { transform:scale(0.98); }
304
+ .btn-primary:disabled { opacity:0.4; cursor:not-allowed; }
305
 
306
+ .btn-ghost {
307
+ background:transparent; border:1px solid var(--glass-border);
308
+ color:var(--white-dim); padding:8px 14px;
309
+ font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
310
+ cursor:pointer; transition:all 0.2s;
311
  }
312
+ .btn-ghost:hover { border-color:rgba(255,255,255,0.3); color:var(--white); }
313
 
314
+ .btn-danger {
315
+ background:transparent; border:1px solid rgba(120,30,30,0.5);
316
+ color:#cc5555; padding:6px 12px;
317
+ font-family:'DM Mono',monospace; font-size:9px; letter-spacing:1px;
318
+ cursor:pointer; transition:all 0.2s;
 
319
  }
320
+ .btn-danger:hover { background:rgba(40,10,10,0.5); border-color:#cc4444; }
321
 
322
+ .btn-outline {
323
+ background:transparent; border:1px solid var(--glass-border);
324
+ color:var(--white-dim); padding:8px 14px;
325
+ font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
326
+ cursor:pointer; transition:all 0.2s; display:inline-block;
327
+ text-decoration:none;
328
  }
329
+ .btn-outline:hover { border-color:rgba(255,255,255,0.3); color:var(--white); }
330
 
331
+ /* ── ALERTS ── */
332
+ .alert {
333
+ font-size:10px; letter-spacing:1px; padding:10px 12px;
334
+ display:none; line-height:1.7;
335
+ }
336
+ .alert.show { display:block; }
337
+ .alert.error { border:1px solid rgba(120,30,30,0.4); color:#e06060; background:var(--error-bg); }
338
+ .alert.success { border:1px solid rgba(40,90,40,0.5); color:#6ab06a; background:var(--success-bg); }
339
+ .alert.info { border:1px solid rgba(255,255,255,0.1); color:var(--white-dim); background:var(--glass); }
340
+ .err-code { font-size:8px; opacity:0.55; margin-right:5px; letter-spacing:1px; }
341
+
342
+ /* ── PILLS ── */
343
+ .pill {
344
+ display:inline-block; font-size:8px; letter-spacing:2px;
345
+ padding:3px 8px; border:1px solid var(--glass-border); color:var(--white-dim);
346
+ }
347
+ .pill.success { border-color:rgba(40,90,40,0.5); color:#6ab06a; background:var(--success-bg); }
348
+ .pill.empty { border-color:rgba(100,60,20,0.4); color:rgba(200,140,60,0.8); background:rgba(30,15,5,0.4); }
349
+ .pill.pending { border-color:rgba(80,80,20,0.5); color:rgba(200,200,60,0.8); background:rgba(20,20,5,0.4); }
350
+
351
+ /* ── DATA TABLE ── */
352
+ .data-table { border:1px solid var(--glass-border); }
353
+ .dt-head {
354
+ display:grid; gap:8px; padding:8px 12px;
355
+ font-size:8px; letter-spacing:3px; color:var(--white-faint);
356
+ background:rgba(255,255,255,0.02); border-bottom:1px solid var(--glass-border);
357
  }
358
+ .dt-row {
359
+ display:grid; gap:8px; padding:11px 12px;
360
+ border-bottom:1px solid rgba(255,255,255,0.03);
361
+ align-items:center; transition:background 0.15s;
362
  }
363
+ .dt-row:last-child { border-bottom:none; }
364
+ .dt-row:hover { background:rgba(255,255,255,0.025); }
365
 
366
+ /* ── STATUS DOT ── */
367
+ .status-dot {
368
+ width:7px; height:7px; border-radius:50%; display:inline-block;
369
+ margin-right:6px; vertical-align:middle;
 
 
 
 
 
 
 
370
  }
371
+ .status-dot.online { background:#4a8a4a; box-shadow:0 0 6px rgba(74,138,74,0.7); }
372
+ .status-dot.offline { background:rgba(255,255,255,0.2); }
373
 
374
+ /* ── TOAST ── */
375
+ .toast {
376
+ position:fixed; bottom:20px; left:50%; transform:translateX(-50%) translateY(80px);
377
+ background:var(--white); color:var(--black);
378
+ padding:10px 20px; font-size:10px; letter-spacing:2px;
379
+ transition:transform 0.35s cubic-bezier(0.16,1,0.3,1), opacity 0.35s;
380
+ opacity:0; z-index:9998; white-space:nowrap; pointer-events:none;
 
 
 
381
  }
382
+ .toast.show { transform:translateX(-50%) translateY(0); opacity:1; }
383
 
384
+ /* ── FOOTER ── */
385
+ .footer {
386
+ margin-top:60px; padding-top:24px;
387
+ border-top:1px solid var(--glass-border); text-align:center;
388
+ }
389
+ .footer-top { display:flex; align-items:center; justify-content:center; gap:8px; margin-bottom:10px; }
390
+ .footer-top img { width:16px; height:16px; opacity:0.4; }
391
+ .footer-top span { font-family:'Cormorant Garamond',serif; font-size:14px; letter-spacing:3px; opacity:0.4; }
392
+ .footer-divider { width:40px; height:1px; background:var(--glass-border); margin:0 auto 10px; }
393
+ .footer-meta { font-size:9px; letter-spacing:2px; color:var(--white-faint); margin-bottom:4px; }
394
+ .footer-copy { font-size:8px; letter-spacing:1px; color:rgba(239,239,239,0.15); line-height:2; }
395
+
396
+ /* ════════════════════════════════════════════════
397
+ LOGIN PAGE
398
+ ════════════════════════════════════════════════ */
399
+ .login-page {
400
+ display:flex; flex-direction:column;
401
+ align-items:center; justify-content:center;
402
+ min-height:100dvh; padding:24px 16px;
403
+ }
404
+ .login-wrap { width:100%; max-width:360px; }
405
+
406
+ .login-header { text-align:center; margin-bottom:28px; }
407
+ .login-header img { width:40px; height:40px; margin:0 auto 14px; display:block; }
408
+ .login-header h1 {
409
+ font-family:'Cormorant Garamond',serif; font-size:28px;
410
+ font-weight:600; letter-spacing:5px; margin-bottom:5px;
411
+ }
412
+ .login-header p { font-size:9px; letter-spacing:2px; color:var(--white-dim); }
413
+
414
+ /* Role tabs — with fade animation */
415
+ .role-tabs { display:grid; grid-template-columns:1fr 1fr 1fr; border:1px solid var(--glass-border); margin-bottom:0; }
416
+ .role-tab {
417
+ background:transparent; border:none; border-right:1px solid var(--glass-border);
418
+ color:var(--white-dim); padding:12px 6px;
419
+ font-family:'DM Mono',monospace; font-size:9px; letter-spacing:1px;
420
+ cursor:pointer; transition:all 0.2s;
421
+ display:flex; flex-direction:column; align-items:center; gap:6px;
422
+ -webkit-tap-highlight-color:transparent;
423
+ }
424
+ .role-tab:last-child { border-right:none; }
425
+ .role-tab:hover { color:var(--white); background:rgba(255,255,255,0.03); }
426
+ .role-tab.active { background:var(--white); color:var(--black); }
427
+ .role-tab svg { width:16px; height:16px; transition:transform 0.2s; }
428
+ .role-tab.active svg { transform:scale(1.1); }
429
+
430
+ /* Role fields container */
431
+ .role-fields-wrap {
432
+ border:1px solid var(--glass-border); border-top:none;
433
+ padding:20px 16px;
434
+ position:relative; overflow:hidden;
435
+ min-height:180px;
436
+ }
437
+ .fields { display:none; animation:fadeSlideIn 0.3s ease forwards; }
438
+ .fields.show { display:block; }
439
+ .fields.out { animation:fadeSlideOut 0.2s ease forwards; }
440
+
441
+ @keyframes fadeSlideIn {
442
+ from { opacity:0; transform:translateY(6px); }
443
+ to { opacity:1; transform:translateY(0); }
444
+ }
445
+ @keyframes fadeSlideOut {
446
+ from { opacity:1; transform:translateY(0); }
447
+ to { opacity:0; transform:translateY(-6px); }
448
+ }
449
+
450
+ /* Signup button */
451
+ .btn-signup {
452
+ width:100%; background:transparent;
453
+ border:1px solid rgba(255,255,255,0.18);
454
+ color:var(--white); padding:11px;
455
+ font-family:'DM Mono',monospace; font-size:10px; letter-spacing:3px;
456
+ cursor:pointer; transition:all 0.25s; display:none; margin-top:10px;
457
+ position:relative; overflow:hidden;
458
+ }
459
+ .btn-signup.show { display:block; }
460
+ .btn-signup:hover { border-color:rgba(255,255,255,0.4); background:rgba(255,255,255,0.04); }
461
+
462
+ .signup-hint {
463
+ font-size:10px; color:var(--white-dim); letter-spacing:1px;
464
+ text-align:center; margin-top:10px; line-height:1.8; display:none;
465
+ }
466
+ .signup-hint.show { display:block; }
467
+
468
+ .server-status {
469
+ display:flex; align-items:center; justify-content:center;
470
+ gap:7px; margin-bottom:16px; font-size:9px;
471
+ color:var(--white-dim); letter-spacing:1px;
472
+ }
473
+
474
+ /* ════════════════════════════════════════════════
475
+ SIGNUP PAGE — Steps
476
+ ════════════════════════════════════════════════ */
477
+ .steps { display:flex; align-items:center; justify-content:center; gap:0; margin-bottom:28px; }
478
+ .step { display:flex; flex-direction:column; align-items:center; gap:5px; }
479
+ .step-dot {
480
+ width:28px; height:28px; border-radius:50%;
481
+ border:1.5px solid rgba(255,255,255,0.15);
482
+ display:flex; align-items:center; justify-content:center;
483
+ font-size:10px; color:var(--white-dim);
484
+ transition:all 0.4s ease;
485
+ }
486
+ .step-dot.active { border-color:var(--white); color:var(--white); background:rgba(255,255,255,0.08); }
487
+ .step-dot.done { border-color:rgba(60,120,60,0.6); background:rgba(20,50,20,0.4); color:#5a9a5a; }
488
+ .step-label { font-size:8px; letter-spacing:1px; color:var(--white-faint); white-space:nowrap; }
489
+ .step-label.active { color:var(--white-dim); }
490
+ .step-line { width:24px; height:1px; background:rgba(255,255,255,0.08); margin:0 3px; margin-bottom:14px; }
491
+
492
+ .phase { display:none; animation:fadeSlideIn 0.3s ease forwards; }
493
+ .phase.active { display:block; }
494
+
495
+ /* VPass identity card */
496
+ .vpass-card {
497
+ background:var(--glass); backdrop-filter:blur(20px);
498
+ -webkit-backdrop-filter:blur(20px);
499
+ border:1px solid var(--glass-border);
500
+ padding:16px 16px; margin-bottom:16px;
501
+ display:flex; align-items:center; gap:14px;
502
+ }
503
+ .vpass-card .vc-icon img { width:26px; height:26px; opacity:0.5; }
504
+ .vpass-card .vc-name { font-family:'Cormorant Garamond',serif; font-size:19px; font-weight:600; }
505
+ .vpass-card .vc-id { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-top:2px; }
506
+
507
+ /* Phone input */
508
+ .phone-wrap { display:flex; border:1px solid rgba(255,255,255,0.12); }
509
+ .phone-prefix {
510
+ background:rgba(255,255,255,0.06); border:none; border-right:1px solid rgba(255,255,255,0.1);
511
+ color:var(--white); padding:10px 12px;
512
+ font-family:'DM Mono',monospace; font-size:12px; letter-spacing:1px;
513
+ flex-shrink:0; display:flex; align-items:center;
514
+ }
515
+ .phone-input {
516
+ flex:1; background:transparent; border:none; color:var(--white);
517
+ padding:10px 12px; font-family:'DM Mono',monospace; font-size:13px;
518
+ letter-spacing:2px; outline:none;
519
+ }
520
+ .phone-input::placeholder { color:var(--white-faint); letter-spacing:1px; font-size:11px; }
521
+ .phone-format { font-size:9px; color:var(--white-faint); letter-spacing:1px; margin-top:5px; }
522
+
523
+ /* Waiting pulse */
524
+ .waiting-indicator {
525
+ display:flex; align-items:center; gap:10px;
526
+ padding:14px 16px; background:var(--glass);
527
+ border:1px solid var(--glass-border); margin-bottom:14px;
528
+ }
529
+ .pulse-dot {
530
+ width:8px; height:8px; border-radius:50%;
531
+ background:rgba(255,255,255,0.4);
532
+ animation:pulse 2s ease-in-out infinite; flex-shrink:0;
533
+ }
534
+ @keyframes pulse {
535
+ 0%,100% { opacity:0.3; transform:scale(0.8); }
536
+ 50% { opacity:1; transform:scale(1.2); }
537
+ }
538
+ .wi-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
539
+ .wi-code { font-size:10px; color:var(--white-faint); margin-top:2px; }
540
+
541
+ /* SMS waiting box */
542
+ .sms-waiting-box {
543
+ text-align:center; padding:20px 16px;
544
+ background:var(--glass); border:1px solid var(--glass-border); margin-bottom:14px;
545
+ }
546
+ .sms-waiting-box .sw-title { font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:2px; margin-bottom:8px; }
547
+ .sms-waiting-box .sw-sub { font-size:10px; color:var(--white-dim); letter-spacing:1px; line-height:1.9; }
548
+ .sms-waiting-box .sw-phone { font-size:14px; color:var(--white); letter-spacing:2px; margin-top:10px; font-family:'DM Mono',monospace; }
549
+
550
+ /* Success animation */
551
+ .success-anim { text-align:center; padding:22px 0; }
552
+ .success-anim svg { width:44px; height:44px; margin:0 auto 14px; display:block; }
553
+ .success-anim .sa-title { font-family:'Cormorant Garamond',serif; font-size:24px; letter-spacing:2px; margin-bottom:6px; }
554
+
555
+ /* ════════════════════════════════════════════════
556
+ ADMIN — Notifications
557
+ ════════════════════════════════════════════════ */
558
+ .notif-badge {
559
+ display:inline-flex; align-items:center; justify-content:center;
560
+ width:16px; height:16px; border-radius:50%;
561
+ background:rgba(200,60,60,0.9); color:#fff;
562
+ font-size:8px; margin-left:5px; line-height:1;
563
+ }
564
+
565
+ .notif-card {
566
+ background:var(--glass); border:1px solid var(--glass-border);
567
+ padding:18px 16px 16px; margin-bottom:12px; position:relative;
568
+ transition:border-color 0.2s;
569
+ }
570
+ .notif-card.unread { border-color:rgba(255,255,255,0.18); }
571
+ .notif-card .nc-type { font-size:9px; letter-spacing:2px; color:var(--white-dim); margin-bottom:8px; }
572
+ .notif-card .nc-name { font-family:'Cormorant Garamond',serif; font-size:20px; font-weight:600; margin-bottom:2px; }
573
+ .notif-card .nc-vpass { font-size:10px; color:var(--white-dim); letter-spacing:2px; margin-bottom:14px; }
574
+ .nc-phone-block {
575
+ display:flex; align-items:center; gap:10px;
576
+ background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.1);
577
+ padding:10px 14px; margin-bottom:14px;
578
+ }
579
+ .nc-phone-block .ph-label { font-size:8px; letter-spacing:2px; color:var(--white-dim); }
580
+ .nc-phone-block .ph-num { font-family:'DM Mono',monospace; font-size:15px; color:var(--white); letter-spacing:2px; margin-top:3px; }
581
+ .nc-code-block {
582
+ display:flex; align-items:center; gap:14px; margin-bottom:14px;
583
+ padding:14px 16px;
584
+ background:rgba(20,18,5,0.6); border:1px solid rgba(255,220,60,0.18);
585
+ }
586
+ .nc-code-block .nc-code {
587
+ font-family:'Cormorant Garamond',serif; font-size:40px; letter-spacing:12px; color:var(--white);
588
+ }
589
+ .nc-code-block .nc-code-info { font-size:9px; color:rgba(255,215,60,0.55); letter-spacing:1px; line-height:2.2; }
590
+ .nc-code-block .nc-code-info strong { color:rgba(255,215,60,0.85); }
591
+ .nc-actions { display:flex; gap:8px; flex-wrap:wrap; align-items:center; }
592
+ .btn-copy-sms {
593
+ background:rgba(255,255,255,0.07); border:1px solid rgba(255,255,255,0.18);
594
+ color:var(--white); padding:9px 14px;
595
+ font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
596
+ cursor:pointer; transition:all 0.2s; display:flex; align-items:center; gap:7px;
597
+ }
598
+ .btn-copy-sms:hover { background:rgba(255,255,255,0.13); }
599
+ .btn-copy-sms.copied { border-color:rgba(60,120,60,0.6); color:#5a9a5a; }
600
+ .btn-copy-sms svg { width:13px; height:13px; }
601
+ .notif-card .nc-time { position:absolute; top:14px; right:14px; font-size:9px; color:var(--white-faint); letter-spacing:1px; }
602
+ .notif-card.done { opacity:0.35; pointer-events:none; }
603
+ .notif-empty { font-size:11px; color:var(--white-dim); padding:30px 0; letter-spacing:1px; text-align:center; }
604
+
605
+ /* Push banner */
606
+ .push-banner {
607
+ position:fixed; top:62px; right:14px; z-index:9990;
608
+ background:rgba(12,12,12,0.97); backdrop-filter:blur(20px);
609
+ border:1px solid rgba(255,255,255,0.14);
610
+ padding:14px 36px 14px 16px; max-width:290px;
611
+ transform:translateX(340px); transition:transform 0.4s cubic-bezier(0.16,1,0.3,1);
612
+ box-shadow:0 8px 40px rgba(0,0,0,0.7);
613
+ }
614
+ .push-banner.show { transform:translateX(0); }
615
+ .pb-dot { display:inline-block; width:6px; height:6px; border-radius:50%; background:#cc4444; margin-right:6px; vertical-align:middle; animation:blink 1.5s ease-in-out infinite; }
616
+ .pb-title { font-size:8px; letter-spacing:2px; color:var(--white-dim); margin-bottom:5px; }
617
+ .pb-name { font-family:'Cormorant Garamond',serif; font-size:17px; font-weight:600; }
618
+ .pb-sub { font-size:10px; color:var(--white-dim); margin-top:3px; letter-spacing:1px; }
619
+ .pb-close { position:absolute; top:9px; right:11px; background:none; border:none; color:var(--white-dim); cursor:pointer; font-size:15px; line-height:1; }
620
+
621
+ /* ════════════════════════════════════════════════
622
+ ELEV DASHBOARD — Materii grid
623
+ ════════════════════════════════════════════════ */
624
+ .materii-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(120px,1fr)); gap:10px; margin-bottom:20px; }
625
+ .materie-btn {
626
+ background:var(--glass); border:1px solid var(--glass-border);
627
+ color:var(--white-dim); padding:16px 10px;
628
+ font-family:'DM Mono',monospace; font-size:9px; letter-spacing:2px;
629
+ cursor:pointer; transition:all 0.2s; text-align:center; line-height:1.6;
630
+ -webkit-tap-highlight-color:transparent;
631
+ }
632
+ .materie-btn:hover { border-color:rgba(255,255,255,0.3); color:var(--white); background:rgba(255,255,255,0.06); }
633
+ .materie-btn.selected { background:var(--white); color:var(--black); border-color:var(--white); }
634
+ .materie-btn .mb-icon { font-size:20px; margin-bottom:7px; opacity:0.7; }
635
+
636
+ /* Upload zone */
637
  .upload-zone {
638
+ border:1px dashed rgba(255,255,255,0.15); padding:28px 20px;
639
+ text-align:center; cursor:pointer; transition:all 0.25s;
640
+ position:relative;
641
+ }
642
+ .upload-zone:hover, .upload-zone.drag { border-color:rgba(255,255,255,0.4); background:rgba(255,255,255,0.03); }
643
+ .upload-zone input[type=file] { position:absolute; inset:0; opacity:0; cursor:pointer; }
644
+ .upload-zone .uz-icon { margin-bottom:10px; opacity:0.5; }
645
+ .upload-zone .uz-text { font-size:11px; color:var(--white-dim); letter-spacing:1px; }
646
+ .upload-zone .uz-sub { font-size:9px; color:var(--white-faint); margin-top:5px; letter-spacing:1px; }
647
+
648
+ /* Files list */
649
+ .file-row { display:grid; grid-template-columns:1fr auto auto; gap:10px; padding:10px 12px; border-bottom:1px solid rgba(255,255,255,0.04); align-items:center; transition:background 0.15s; }
650
+ .file-row:hover { background:rgba(255,255,255,0.02); }
651
+ .file-row:last-child { border-bottom:none; }
652
+ .file-name { font-size:12px; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
653
+ .file-size { font-size:9px; color:var(--white-faint); letter-spacing:1px; }
654
+
655
+ /* ═════��══════════════════════════════════════════
656
+ PROFESOR DASHBOARD — spinner
657
+ ════════════════════════════════════════════════ */
658
+ .prof-spinner { display:none; flex-direction:column; align-items:center; padding:40px 20px; gap:20px; }
659
+ .prof-spinner.show { display:flex; }
660
+ .ps-text { font-size:10px; letter-spacing:2px; color:var(--white-dim); text-align:center; }
661
+
662
+ /* Elev row in profesor dashboard */
663
+ .elev-card {
664
+ background:var(--glass); border:1px solid var(--glass-border);
665
+ padding:14px 16px; margin-bottom:8px; cursor:pointer;
666
+ transition:all 0.2s; display:flex; justify-content:space-between; align-items:center;
667
+ }
668
+ .elev-card:hover { border-color:rgba(255,255,255,0.2); background:rgba(255,255,255,0.05); }
669
+ .elev-card .ec-name { font-family:'Cormorant Garamond',serif; font-size:16px; font-weight:600; }
670
+ .elev-card .ec-vpass { font-size:9px; color:var(--white-dim); letter-spacing:2px; margin-top:2px; }
671
+ .elev-card .ec-count { font-size:11px; color:var(--white-dim); }
672
+
673
+ /* ════════════════════════════════════════════════
674
+ FADE ANIMATIONS
675
+ ════════════════════════════════════════════════ */
676
+ .fade-in { animation:fadeIn 0.5s ease forwards; }
677
+ .fade-in-2 { animation:fadeIn 0.5s ease 0.1s both; }
678
+ .fade-in-3 { animation:fadeIn 0.5s ease 0.2s both; }
679
+ .fade-in-4 { animation:fadeIn 0.5s ease 0.3s both; }
680
+ .fade-in-5 { animation:fadeIn 0.5s ease 0.4s both; }
681
+
682
+ @keyframes fadeIn {
683
+ from { opacity:0; transform:translateY(8px); }
684
+ to { opacity:1; transform:translateY(0); }
685
+ }
686
+
687
+ /* ════════════════════════════════════════════════
688
+ ADMIN — Elevi tabel cu status cont
689
+ ════════════════════════════════════════════════ */
690
+ .elev-row { grid-template-columns:100px 1fr 90px 80px 80px; }
691
+ .prof-row { grid-template-columns:1fr 140px 70px 70px; }
692
+ .mat-row { grid-template-columns:1fr 70px; }
693
+ @media(max-width:600px) {
694
+ .elev-row { grid-template-columns:1fr auto; }
695
+ .elev-row .col-vpass, .elev-row .col-pin, .elev-row .col-status { display:none; }
696
+ .prof-row { grid-template-columns:1fr auto; }
697
+ .prof-row .col-mat, .prof-row .col-pin { display:none; }
698
+ }
699
+
700
+ /* VPass preview input */
701
+ .vpass-preview {
702
+ background:rgba(255,255,255,0.04); border:1px solid rgba(255,255,255,0.08);
703
+ color:var(--white); padding:10px 12px;
704
+ font-family:'Cormorant Garamond',serif; font-size:17px; letter-spacing:3px;
705
+ min-height:42px; display:flex; align-items:center;
706
+ }
707
+
708
+ /* Danger zone */
709
+ .danger-box { border:1px solid rgba(120,30,30,0.4); padding:20px 16px; background:rgba(20,5,5,0.5); }
710
+ .danger-box h4 { font-size:9px; letter-spacing:3px; color:#cc5555; margin-bottom:10px; }
711
+
712
+ /* ════════════════════════════════════════════════
713
+ 404 PAGE
714
+ ════════════════════════════════════════════════ */
715
+ .page-404 {
716
+ min-height:100dvh; display:flex; flex-direction:column;
717
+ align-items:center; justify-content:center; text-align:center; padding:24px;
718
+ }
719
+ .page-404 .e-code {
720
+ font-family:'Cormorant Garamond',serif; font-size:80px; font-weight:600;
721
+ line-height:1; letter-spacing:8px; color:rgba(255,255,255,0.06);
722
+ margin-bottom:0;
723
+ }
724
+ .page-404 .e-title { font-family:'Cormorant Garamond',serif; font-size:22px; letter-spacing:3px; margin-bottom:8px; }
725
+ .page-404 .e-sub { font-size:10px; color:var(--white-dim); letter-spacing:2px; line-height:2; margin-bottom:28px; }
726
+ .page-404 .e-db { font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2.2; font-family:'DM Mono',monospace; }
727
+ .page-404 .e-db .db-ok { color:rgba(74,138,74,0.7); }
728
+ .page-404 .e-db .db-err { color:rgba(180,60,60,0.7); }
729
+
730
+ /* ════════════════════════════════════════════════
731
+ RESET PASSWORD PAGE
732
+ ════════════════════════════════════════════════ */
733
+ .reset-page { display:flex; flex-direction:column; align-items:center; justify-content:center; min-height:100dvh; padding:20px 14px; }
734
+ .reset-wrap { width:100%; max-width:360px; }
735
+
736
+ /* ════════════════════════════════════════════════
737
+ MISC
738
+ ═════════════════════════════════��══════════════ */
739
+ .footer-mini { text-align:center; margin-top:20px; font-size:9px; color:var(--white-faint); letter-spacing:1px; line-height:2.2; }
740
+ .sep { height:1px; background:var(--glass-border); margin:16px 0; }
741
+ .text-dim { color:var(--white-dim); }
742
+ .text-faint{ color:var(--white-faint); }
743
+ .mt-10 { margin-top:10px; }
744
+ .mt-16 { margin-top:16px; }