Team Ai
Apppublic

Nimish-ctrl/AttendX

sourceHugging Faceupdated 3mo agoView on Hugging Face
1likes
admin.html399 linesDownload Raw Back to root
1<!DOCTYPE html>2<html lang="en">3<head>4<link rel="icon" type="image/png" href="/favicon.png">5<meta charset="UTF-8">6<meta name="viewport" content="width=device-width, initial-scale=1.0">7<title>AttendX โ€” Admin</title>8<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600&family=Playfair+Display:wght@400;500&display=swap" rel="stylesheet">9<style>10  *, *::before, *::after { box-sizing:border-box; margin:0; padding:0; }11  :root {12    --bg:#f8f7f4; --surface:#fff; --surface2:#f3f2ee;13    --text:#1a1a1a; --muted:#8a8a8a; --border:#e8e6e1;14    --green:#22a06b; --red:#d94f4f; --blue:#2563eb;15  }16  html,body { min-height:100vh; background:var(--bg); color:var(--text); font-family:'Inter',sans-serif; }17 18  body::before {19    content:''; position:fixed; inset:0; z-index:0;20    background-image:radial-gradient(circle at 1px 1px,rgba(0,0,0,0.04) 1px,transparent 0);21    background-size:28px 28px; pointer-events:none;22  }23 24  /* gate */25  #gate {26    position:fixed; inset:0; z-index:200;27    display:flex; align-items:center; justify-content:center;28    background:var(--bg);29  }30  .gate-box {31    background:var(--surface); border:1px solid var(--border); border-radius:20px;32    padding:2.5rem; width:340px; text-align:center;33  }34  .gate-box .g-eye { font-size:2rem; margin-bottom:1.25rem; }35  .gate-box h2 { font-family:'Playfair Display',serif; font-size:1.5rem; font-weight:400; margin-bottom:0.35rem; }36  .gate-box p  { font-size:0.8rem; color:var(--muted); margin-bottom:1.5rem; line-height:1.6; }37  .gate-box input {38    width:100%; padding:0.75rem 1rem; background:var(--surface2); border:1px solid var(--border);39    border-radius:10px; color:var(--text); font-family:'Inter',sans-serif; font-size:0.88rem;40    outline:none; text-align:center; letter-spacing:0.08em; margin-bottom:0.75rem;41    transition:border-color 0.2s;42  }43  .gate-box input:focus { border-color:var(--text); }44  .gate-err { font-size:0.78rem; color:var(--red); margin-bottom:0.75rem; display:none; }45 46  nav {47    position:fixed; top:0; left:0; right:0; z-index:100;48    display:flex; align-items:center; justify-content:space-between;49    padding:1.1rem 2.5rem;50    background:rgba(248,247,244,0.92); backdrop-filter:blur(16px);51    border-bottom:1px solid var(--border);52  }53  .logo { font-family:'Playfair Display',serif; font-size:1.15rem; font-weight:400; }54  .logo em { font-style:italic; color:var(--muted); }55  .nav-right { display:flex; align-items:center; gap:1.25rem; }56  .admin-chip { font-size:0.68rem; font-weight:500; letter-spacing:0.08em; text-transform:uppercase; padding:3px 10px; border-radius:20px; border:1px solid var(--border); color:var(--muted); }57  .nav-back { font-size:0.8rem; color:var(--muted); text-decoration:none; transition:color 0.2s; }58  .nav-back:hover { color:var(--text); }59 60  .page { position:relative; z-index:1; padding:6rem 2rem 4rem; max-width:1000px; margin:0 auto; }61 62  /* tabs */63  .tabs { display:flex; gap:0; margin-bottom:2.5rem; border-bottom:1px solid var(--border); }64  .tab {65    padding:0.65rem 1.25rem; border:none; background:transparent;66    font-family:'Inter',sans-serif; font-size:0.85rem; font-weight:500;67    color:var(--muted); cursor:pointer; position:relative; transition:color 0.2s;68  }69  .tab.active { color:var(--text); }70  .tab.active::after { content:''; position:absolute; bottom:-1px; left:0; right:0; height:2px; background:var(--text); }71 72  .tab-panel { display:none; animation:fadeIn 0.25s ease; }73  .tab-panel.active { display:block; }74  @keyframes fadeIn { from{opacity:0;transform:translateY(6px)} to{opacity:1;transform:translateY(0)} }75 76  .section-title { font-family:'Playfair Display',serif; font-size:1.8rem; font-weight:400; margin-bottom:0.4rem; letter-spacing:-0.01em; }77  .section-sub   { font-size:0.82rem; color:var(--muted); line-height:1.7; margin-bottom:1.75rem; }78 79  /* controls */80  .controls {81    display:flex; align-items:center; gap:0.75rem; flex-wrap:wrap;82    padding:1rem 1.25rem; background:var(--surface); border:1px solid var(--border);83    border-radius:12px; margin-bottom:1.75rem;84  }85  .controls label { font-size:0.72rem; font-weight:500; color:var(--muted); text-transform:uppercase; letter-spacing:0.08em; white-space:nowrap; }86  input[type=date] {87    padding:0.6rem 0.85rem; background:var(--surface2); border:1px solid var(--border);88    border-radius:8px; color:var(--text); font-family:'Inter',sans-serif; font-size:0.82rem;89    outline:none; cursor:pointer; color-scheme:light; transition:border-color 0.2s;90  }91  input[type=date]:focus { border-color:var(--text); }92 93  /* buttons */94  .btn {95    display:inline-flex; align-items:center; gap:0.4rem;96    padding:0.6rem 1.25rem; border-radius:8px; border:1px solid transparent;97    font-family:'Inter',sans-serif; font-size:0.8rem; font-weight:500;98    cursor:pointer; transition:all 0.2s; text-decoration:none;99  }100  .btn-dark  { background:var(--text); color:#fff; border-color:var(--text); }101  .btn-dark:hover  { background:#333; }102  .btn-ghost { background:transparent; color:var(--text); border-color:var(--border); }103  .btn-ghost:hover { border-color:var(--text); }104  .btn-danger { background:transparent; color:var(--red); border-color:#f0b8b8; }105  .btn-danger:hover { background:#fdf2f2; }106  .btn:disabled { opacity:0.35; cursor:not-allowed; }107 108  /* stats */109  .stats { display:grid; grid-template-columns:repeat(4,1fr); gap:0.75rem; margin-bottom:1.75rem; }110  .stat { background:var(--surface); border:1px solid var(--border); border-radius:12px; padding:1.1rem 1.25rem; }111  .stat .n { font-family:'Playfair Display',serif; font-size:1.9rem; font-weight:400; }112  .stat .l { font-size:0.7rem; color:var(--muted); text-transform:uppercase; letter-spacing:0.08em; margin-top:2px; }113  .stat.g .n { color:var(--green); }114  .stat.r .n { color:var(--red); }115  .stat.b .n { color:var(--blue); }116 117  /* table */118  .tbl-wrap { background:var(--surface); border:1px solid var(--border); border-radius:14px; overflow:hidden; }119  .tbl-head {120    display:grid; grid-template-columns:1.8fr 2.5fr 1fr 1.8fr 1.2fr 0.6fr;121    padding:0.65rem 1.25rem;122    background:var(--surface2); border-bottom:1px solid var(--border);123    font-size:0.68rem; color:var(--muted); text-transform:uppercase; letter-spacing:0.08em; font-weight:500;124  }125  .tbl-row {126    display:grid; grid-template-columns:1.8fr 2.5fr 1fr 1.8fr 1.2fr 0.6fr;127    padding:0.8rem 1.25rem; border-bottom:1px solid var(--border);128    align-items:center; transition:background 0.15s; animation:fadeIn 0.2s ease;129  }130  .tbl-row:last-child { border-bottom:none; }131  .tbl-row:hover { background:var(--surface2); }132  .tbl-row .roll { font-size:0.75rem; color:var(--muted); font-family:monospace; }133  .tbl-row .name { font-size:0.88rem; font-weight:500; }134  .tbl-row .time { font-size:0.75rem; color:var(--muted); }135  .tbl-row .conf { font-size:0.75rem; color:var(--muted); }136 137  .chip { font-size:0.68rem; font-weight:500; padding:3px 9px; border-radius:20px; display:inline-flex; align-items:center; gap:4px; }138  .chip.p { background:#f0faf5; color:var(--green); border:1px solid #a7e0c4; }139  .chip.a { background:#fdf2f2; color:var(--red);   border:1px solid #f0b8b8; }140 141  /* enrolled grid */142  .enrolled-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(240px,1fr)); gap:0.75rem; }143  .e-card {144    background:var(--surface); border:1px solid var(--border); border-radius:12px;145    padding:1rem 1.1rem; display:flex; align-items:center; gap:0.85rem;146    animation:fadeIn 0.25s ease;147  }148  .avatar {149    width:36px; height:36px; border-radius:50%; background:var(--surface2);150    border:1px solid var(--border); display:flex; align-items:center; justify-content:center;151    font-size:0.78rem; font-weight:600; color:var(--muted); flex-shrink:0;152  }153  .e-card .info { flex:1; min-width:0; }154  .e-card .ename { font-size:0.88rem; font-weight:500; white-space:nowrap; overflow:hidden; text-overflow:ellipsis; }155  .e-card .eroll { font-size:0.72rem; color:var(--muted); font-family:monospace; }156  .e-card .edate { font-size:0.7rem; color:var(--muted); margin-top:1px; }157 158  .del-btn {159    background:transparent; border:none; cursor:pointer; color:var(--muted);160    padding:4px; border-radius:6px; line-height:1; font-size:0.9rem;161    transition:color 0.2s, background 0.2s; flex-shrink:0;162  }163  .del-btn:hover { color:var(--red); background:#fdf2f2; }164 165  /* confirm modal */166  #modal-bg {167    position:fixed; inset:0; z-index:300; background:rgba(0,0,0,0.25); backdrop-filter:blur(4px);168    display:flex; align-items:center; justify-content:center; display:none;169  }170  .modal {171    background:var(--surface); border:1px solid var(--border); border-radius:16px;172    padding:2rem; width:320px; text-align:center;173  }174  .modal h3 { font-family:'Playfair Display',serif; font-size:1.2rem; font-weight:400; margin-bottom:0.5rem; }175  .modal p  { font-size:0.82rem; color:var(--muted); line-height:1.6; margin-bottom:1.5rem; }176  .modal-btns { display:flex; gap:0.6rem; justify-content:center; }177 178  /* empty / loading */179  .empty { text-align:center; padding:3.5rem 1rem; color:var(--muted); font-size:0.85rem; }180 181  .spin { width:13px;height:13px;border:2px solid rgba(255,255,255,0.3);border-top-color:#fff;border-radius:50%;animation:sp 0.7s linear infinite;display:none; }182  @keyframes sp { to{transform:rotate(360deg)} }183</style>184</head>185<body>186 187<!-- gate -->188<div id="gate">189  <div class="gate-box">190    <div class="g-eye">๐Ÿ”</div>191    <h2>Admin access</h2>192    <p>Enter the admin key to continue.</p>193    <p class="gate-err" id="gate-err">Incorrect key. Try again.</p>194    <input type="password" id="gate-input" placeholder="Admin key" />195    <button class="btn btn-dark" style="width:100%" onclick="checkKey()">Enter โ†’</button>196  </div>197</div>198 199<!-- confirm delete modal -->200<div id="modal-bg">201  <div class="modal">202    <h3>Remove student?</h3>203    <p id="modal-msg">This will delete the student and their face data. Attendance logs are kept.</p>204    <div class="modal-btns">205      <button class="btn btn-ghost" onclick="closeModal()">Cancel</button>206      <button class="btn btn-danger" id="modal-confirm">Delete</button>207    </div>208  </div>209</div>210 211<nav>212  <div class="logo">Attend<em>X</em></div>213  <div class="nav-right">214    <span class="admin-chip">Admin</span>215    <a href="index.html" class="nav-back">โ† Home</a>216  </div>217</nav>218 219<div class="page">220  <div class="tabs">221    <button class="tab active" onclick="sw('report')">Daily Report</button>222    <button class="tab" onclick="sw('enrolled'); loadEnrolled()">Students</button>223  </div>224 225  <!-- REPORT TAB -->226  <div class="tab-panel active" id="tab-report">227    <h2 class="section-title">Attendance report</h2>228    <p class="section-sub">Select a date to see who attended and who didn't.</p>229 230    <div class="controls">231      <label>Date</label>232      <input type="date" id="date-pick" />233      <button class="btn btn-dark" id="load-btn" onclick="loadReport()">Load <div class="spin" id="load-spin"></div></button>234      <button class="btn btn-ghost" onclick="setToday()">Today</button>235    </div>236 237    <div id="report-stats" style="display:none">238      <div class="stats">239        <div class="stat">  <div class="n" id="s-tot">0</div><div class="l">Enrolled</div></div>240        <div class="stat g"><div class="n" id="s-pre">0</div><div class="l">Present</div></div>241        <div class="stat r"><div class="n" id="s-abs">0</div><div class="l">Absent</div></div>242        <div class="stat b"><div class="n" id="s-pct">โ€”</div><div class="l">Attendance</div></div>243      </div>244      <div class="tbl-wrap">245        <div class="tbl-head">246          <span>Roll No.</span><span>Name</span><span>Status</span>247          <span>Marked at</span><span>Confidence</span><span></span>248        </div>249        <div id="tbl-body"></div>250      </div>251    </div>252 253    <div class="empty" id="report-empty">Select a date and load the report.</div>254  </div>255 256  <!-- STUDENTS TAB -->257  <div class="tab-panel" id="tab-enrolled">258    <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:1.5rem;flex-wrap:wrap;gap:0.75rem;">259      <div>260        <h2 class="section-title">Students</h2>261        <p class="section-sub" style="margin-bottom:0">All enrolled students. Delete removes face data.</p>262      </div>263      <button class="btn btn-ghost" onclick="loadEnrolled()">โ†ป Refresh</button>264    </div>265    <div class="enrolled-grid" id="enrolled-grid">266      <div class="empty">Loading...</div>267    </div>268  </div>269</div>270 271<script>272const API = window.location.origin;273let KEY = '';274let pendingDelete = null;275 276// gate277document.getElementById('gate-input').addEventListener('keydown', e => { if(e.key==='Enter') checkKey(); });278 279async function checkKey() {280  const k = document.getElementById('gate-input').value.trim();281  if (!k) return;282  const r = await fetch(`${API}/students?key=${encodeURIComponent(k)}`);283  if (r.ok) {284    KEY = k;285    document.getElementById('gate').style.display = 'none';286    setToday(); loadReport();287  } else {288    document.getElementById('gate-err').style.display = 'block';289  }290}291 292function sw(t) {293  document.querySelectorAll('.tab').forEach((el,i) => el.classList.toggle('active', ['report','enrolled'][i]===t));294  document.querySelectorAll('.tab-panel').forEach(p => p.classList.remove('active'));295  document.getElementById('tab-'+t).classList.add('active');296}297 298function setToday() {299  document.getElementById('date-pick').value = new Date().toISOString().split('T')[0];300}301 302async function loadReport() {303  const date = document.getElementById('date-pick').value;304  if (!date) return;305  const btn  = document.getElementById('load-btn');306  const spin = document.getElementById('load-spin');307  btn.disabled = true; spin.style.display = 'block';308 309  try {310    const r = await fetch(`${API}/attendance?date=${date}&key=${encodeURIComponent(KEY)}`);311    const d = await r.json();312    if (!r.ok) { alert(d.detail); return; }313 314    document.getElementById('s-tot').textContent = d.total;315    document.getElementById('s-pre').textContent = d.attended;316    document.getElementById('s-abs').textContent = d.absent;317    const pct = d.total > 0 ? Math.round((d.attended/d.total)*100) : 0;318    document.getElementById('s-pct').textContent = pct + '%';319 320    document.getElementById('tbl-body').innerHTML = d.report.map(row => {321      const time = row.present322        ? new Date(row.marked_at).toLocaleTimeString('en-IN',{hour:'2-digit',minute:'2-digit'})323        : 'โ€”';324      const conf = row.present ? `${(row.similarity*100).toFixed(1)}%` : 'โ€”';325      return `<div class="tbl-row">326        <span class="roll">${row.roll_no}</span>327        <span class="name">${row.name}</span>328        <span><span class="chip ${row.present?'p':'a'}">${row.present?'Present':'Absent'}</span></span>329        <span class="time">${time}</span>330        <span class="conf">${conf}</span>331        <span></span>332      </div>`;333    }).join('');334 335    document.getElementById('report-empty').style.display  = 'none';336    document.getElementById('report-stats').style.display  = 'block';337  } catch(e) { alert('Cannot reach server.'); }338 339  btn.disabled = false; spin.style.display = 'none';340}341 342async function loadEnrolled() {343  const grid = document.getElementById('enrolled-grid');344  grid.innerHTML = '<div class="empty">Loading...</div>';345  try {346    const r = await fetch(`${API}/students?key=${encodeURIComponent(KEY)}`);347    const d = await r.json();348    if (!d.students.length) { grid.innerHTML='<div class="empty">No students enrolled yet.</div>'; return; }349    grid.innerHTML = d.students.map(s => {350      const initials = s.name.split(' ').map(n=>n[0]).join('').slice(0,2).toUpperCase();351      const date = new Date(s.enrolled_at).toLocaleDateString('en-IN',{day:'numeric',month:'short',year:'numeric'});352      return `<div class="e-card" id="card-${s.roll_no}">353        <div class="avatar">${initials}</div>354        <div class="info">355          <div class="ename">${s.name}</div>356          <div class="eroll">${s.roll_no}</div>357          <div class="edate">Enrolled ${date}</div>358        </div>359        <button class="del-btn" onclick="askDelete('${s.roll_no}','${s.name}')" title="Remove student">โœ•</button>360      </div>`;361    }).join('');362  } catch(e) { grid.innerHTML='<div class="empty">Could not load students.</div>'; }363}364 365// delete flow366function askDelete(roll, name) {367  pendingDelete = roll;368  document.getElementById('modal-msg').textContent =369    `Remove "${name}" (${roll})? Their face data will be deleted. Attendance logs are kept.`;370  document.getElementById('modal-bg').style.display = 'flex';371  document.getElementById('modal-confirm').onclick = confirmDelete;372}373 374function closeModal() {375  document.getElementById('modal-bg').style.display = 'none';376  pendingDelete = null;377}378 379async function confirmDelete() {380  if (!pendingDelete) return;381  const roll = pendingDelete;382  closeModal();383  try {384    const r = await fetch(`${API}/students/${encodeURIComponent(roll)}?key=${encodeURIComponent(KEY)}`, {method:'DELETE'});385    if (r.ok) {386      const card = document.getElementById(`card-${roll}`);387      if (card) { card.style.opacity='0'; card.style.transform='scale(0.95)'; card.style.transition='all 0.25s'; setTimeout(()=>card.remove(), 250); }388    } else {389      const d = await r.json(); alert(d.detail || 'Delete failed.');390    }391  } catch(e) { alert('Cannot reach server.'); }392}393 394// close modal on bg click395document.getElementById('modal-bg').addEventListener('click', e => { if(e.target===e.currentTarget) closeModal(); });396</script>397</body>398</html>399