π File Manager
π /
/
home
/
u857492117
/
domains
/
mangaldaicollege.org
/
public_html
/
app
/
Teacher
Dosya DΓΌzenle: takeAttendance.php
<?php session_set_cookie_params(86400 * 30); session_start(); date_default_timezone_set('Asia/Kolkata'); include "db.php"; if (!isset($_SESSION['user_id']) || $_SESSION['role'] != "teacher") { header("Location: ../login.php"); exit(); } $user_id = $_SESSION['user_id']; $routine_id = (int)($_GET['routine_id'] ?? 0); if (!$routine_id) { header("Location: index.php"); exit(); } // ββ Ensure attendance tables exist ββββββββββββββββββββ $conn->query(" CREATE TABLE IF NOT EXISTS attendance_sessions ( id INT AUTO_INCREMENT PRIMARY KEY, routine_id INT NOT NULL, teacher_id INT NOT NULL, topic TEXT DEFAULT NULL, pin CHAR(4) DEFAULT NULL, pin_expires DATETIME DEFAULT NULL, date DATE NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, CONSTRAINT fk_as_routine FOREIGN KEY (routine_id) REFERENCES routine(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 "); // Ensure pin column has a unique index so DB itself rejects duplicates as a final safety net $conn->query(" ALTER TABLE attendance_sessions ADD UNIQUE INDEX IF NOT EXISTS uq_active_pin (pin, pin_expires) "); // Ensure unit column exists (dropdown value 1-10, saved alongside topic) $conn->query(" ALTER TABLE attendance_sessions ADD COLUMN IF NOT EXISTS unit TINYINT DEFAULT NULL AFTER topic "); $conn->query(" CREATE TABLE IF NOT EXISTS attendance ( id INT AUTO_INCREMENT PRIMARY KEY, session_id INT NOT NULL, student_id INT NOT NULL, marked_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uq_attendance (session_id, student_id), CONSTRAINT fk_att_session FOREIGN KEY (session_id) REFERENCES attendance_sessions(id) ON DELETE CASCADE, CONSTRAINT fk_att_student FOREIGN KEY (student_id) REFERENCES students(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 "); // ββ Fetch routine + subject details βββββββββββββββββββ $stmt = $conn->prepare( "SELECT r.id, r.day, r.time_start, r.time_end, r.paper_type, r.stream, r.semester, r.room_no, r.department, s.name AS subject_name, s.code AS subject_code, t.id AS teacher_id, t.name AS teacher_name, t.department AS t_dept FROM routine r JOIN subjects s ON s.id = r.subject_id JOIN routine_teachers rt ON rt.routine_id = r.id JOIN teachers t ON t.id = rt.teacher_id WHERE r.id = ? AND t.user_id = ? LIMIT 1" ); $stmt->bind_param("ii", $routine_id, $user_id); $stmt->execute(); $routine = $stmt->get_result()->fetch_assoc(); $stmt->close(); if (!$routine) { header("Location: index.php"); exit(); } $teacher_id = $routine['teacher_id']; $today = date('Y-m-d'); // ββ Check if session already exists for today βββββββββ $ses = $conn->prepare( "SELECT * FROM attendance_sessions WHERE routine_id=? AND date=? LIMIT 1" ); $ses->bind_param("is", $routine_id, $today); $ses->execute(); $att_session = $ses->get_result()->fetch_assoc(); $ses->close(); $toast = null; $toast_type = 'success'; // ββ Generate PIN (topic auto-saved here too) ββββββββββ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['generate_pin'])) { $topic = trim($_POST['topic'] ?? ''); $unit = $_POST['unit'] ?? ''; $unit = ($unit !== '' && (int)$unit >= 1 && (int)$unit <= 10) ? (int)$unit : null; $pin = null; $expires = date('Y-m-d H:i:s', strtotime('+1 minute')); $attempts = 0; do { $candidate = str_pad(rand(1000, 9999), 4, '0', STR_PAD_LEFT); $existing_id = $att_session ? $att_session['id'] : 0; $chk = $conn->prepare( "SELECT id FROM attendance_sessions WHERE pin = ? AND pin_expires > NOW() AND id != ? LIMIT 1" ); $chk->bind_param("si", $candidate, $existing_id); $chk->execute(); $conflict = $chk->get_result()->num_rows; $chk->close(); if ($conflict === 0) $pin = $candidate; $attempts++; } while ($pin === null && $attempts < 20); if ($pin === null) { // Redirect with error flag header("Location: takeAttendance.php?routine_id=$routine_id&err=pin"); exit(); } if ($att_session) { $upd = $conn->prepare( "UPDATE attendance_sessions SET pin=?, pin_expires=?, topic=?, unit=? WHERE id=?" ); $upd->bind_param("sssii", $pin, $expires, $topic, $unit, $att_session['id']); $upd->execute(); $upd->close(); } else { $ins = $conn->prepare( "INSERT INTO attendance_sessions (routine_id, teacher_id, topic, unit, pin, pin_expires, date) VALUES (?,?,?,?,?,?,?)" ); $ins->bind_param("iisisss", $routine_id, $teacher_id, $topic, $unit, $pin, $expires, $today); $ins->execute(); $ins->close(); } // PRG β redirect so browser refresh never re-submits the form header("Location: takeAttendance.php?routine_id=$routine_id&pinok=1"); exit(); } // ββ Save topic only (no PIN) ββββββββββββββββββββββββββ if ($_SERVER['REQUEST_METHOD'] === 'POST' && isset($_POST['save_topic'])) { $topic = trim($_POST['topic'] ?? ''); $unit = $_POST['unit'] ?? ''; $unit = ($unit !== '' && (int)$unit >= 1 && (int)$unit <= 10) ? (int)$unit : null; if ($att_session) { $upd = $conn->prepare("UPDATE attendance_sessions SET topic=?, unit=? WHERE id=?"); $upd->bind_param("sii", $topic, $unit, $att_session['id']); $upd->execute(); $upd->close(); } else { $ins = $conn->prepare( "INSERT INTO attendance_sessions (routine_id, teacher_id, topic, unit, date) VALUES (?,?,?,?,?)" ); $ins->bind_param("iisis", $routine_id, $teacher_id, $topic, $unit, $today); $ins->execute(); $ins->close(); } header("Location: takeAttendance.php?routine_id=$routine_id&saved=1"); exit(); } // ββ Read flash messages from redirect params ββββββββββ if (isset($_GET['pinok'])) { $toast = 'PIN generated!'; $toast_type = 'success'; } if (isset($_GET['saved'])) { $toast = 'Topic saved!'; $toast_type = 'success'; } if (isset($_GET['err']) && $_GET['err'] === 'pin') { $toast = 'Could not generate a unique PIN. Try again.'; $toast_type = 'danger'; } // Re-fetch session after redirect so page has latest data $ses = $conn->prepare("SELECT * FROM attendance_sessions WHERE routine_id=? AND date=? LIMIT 1"); $ses->bind_param("is", $routine_id, $today); $ses->execute(); $att_session = $ses->get_result()->fetch_assoc(); $ses->close(); // ββ Fetch attendance list for today's session βββββββββ $attendance_list = []; $present_count = 0; if ($att_session) { $al = $conn->prepare( "SELECT st.name, st.roll_no, a.marked_at FROM attendance a JOIN students st ON st.id = a.student_id WHERE a.session_id = ? ORDER BY a.marked_at ASC" ); $al->bind_param("i", $att_session['id']); $al->execute(); $attendance_list = $al->get_result()->fetch_all(MYSQLI_ASSOC); $al->close(); $present_count = count($attendance_list); } // Total students for this stream+semester $tot = $conn->prepare( "SELECT COUNT(*) as cnt FROM students WHERE stream=? AND semester=?" ); $tot->bind_param("ss", $routine['stream'], $routine['semester']); $tot->execute(); $total_students = $tot->get_result()->fetch_assoc()['cnt'] ?? 0; $tot->close(); // Check if PIN is still valid $pin_valid = false; $pin_secs_left = 0; if (!empty($att_session['pin_expires'])) { $expires_ts = strtotime($att_session['pin_expires']); $now_ts = time(); $pin_valid = $now_ts < $expires_ts; $pin_secs_left = max(0, $expires_ts - $now_ts); } $sem_labels = ['1'=>'1st','2'=>'2nd','3'=>'3rd','4'=>'4th','5'=>'5th','6'=>'6th']; ?> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>MConnect β Take Attendance</title> <link href="https://fonts.googleapis.com/css2?family=DM+Sans:ital,opsz,wght@0,9..40,300;0,9..40,400;0,9..40,500;1,9..40,300&family=DM+Mono:wght@400;500&display=swap" rel="stylesheet"> <style> :root { --bg:#f5f4f0; --surface:#fff; --surface-2:#f0ede8; --border:rgba(0,0,0,0.08); --border-md:rgba(0,0,0,0.14); --text-1:#1a1917; --text-2:#5a5751; --text-3:#9a9590; --accent:#2e2a7a; --accent-soft:#eceaff; --accent-mid:#6b65d4; --success:#1a6b4a; --success-bg:#e5f4ec; --success-border:rgba(26,107,74,0.2); --danger:#9b2a2a; --danger-bg:#fdf0f0; --danger-border:rgba(155,42,42,0.2); --shadow-sm:0 1px 3px rgba(0,0,0,0.06); --shadow-md:0 4px 12px rgba(0,0,0,0.07); --radius-sm:8px; --radius-md:12px; --radius-lg:18px; --radius-xl:24px; --nav-h:64px; } *,*::before,*::after{margin:0;padding:0;box-sizing:border-box;} body{font-family:'DM Sans',sans-serif;font-size:15px;line-height:1.6; background:var(--bg);color:var(--text-1);min-height:100vh; padding-bottom:calc(var(--nav-h) + 40px);} /* TOAST */ .toast{position:fixed;top:68px;left:50%;transform:translateX(-50%) translateY(-8px); z-index:999;min-width:280px;max-width:calc(100vw - 32px); padding:12px 16px;border-radius:var(--radius-md); display:flex;align-items:center;gap:10px;font-size:13.5px; box-shadow:0 8px 24px rgba(0,0,0,0.12); opacity:0;transition:opacity 0.25s,transform 0.25s;pointer-events:none;} .toast.show{opacity:1;transform:translateX(-50%) translateY(0);pointer-events:all;} .toast.success{background:var(--success-bg);border:1px solid var(--success-border);color:var(--success);} .toast.danger{background:var(--danger-bg);border:1px solid var(--danger-border);color:var(--danger);} .toast-close{margin-left:auto;background:none;border:none;cursor:pointer;color:inherit;opacity:0.6;font-size:16px;} /* TOPBAR */ .topbar{position:sticky;top:0;z-index:200;background:rgba(245,244,240,0.88); backdrop-filter:blur(12px);-webkit-backdrop-filter:blur(12px); border-bottom:1px solid var(--border); display:flex;align-items:center;justify-content:space-between;padding:0 20px;height:56px;} .topbar-logo{display:flex;align-items:center;gap:9px;font-size:17px;font-weight:500; letter-spacing:-0.3px;color:var(--text-1);text-decoration:none;} .topbar-logo span{color:var(--accent-mid);} .topbar-logo-icon{width:32px;height:32px;border-radius:9px;background:var(--accent); display:flex;align-items:center;justify-content:center;flex-shrink:0; box-shadow:0 2px 8px rgba(46,42,122,0.28);} .back-btn{display:flex;align-items:center;gap:6px;font-size:14px; color:var(--text-2);text-decoration:none;transition:color 0.15s;} .back-btn:hover{color:var(--text-1);} /* PAGE */ .page{max-width:520px;margin:0 auto;padding:24px 16px 0;} .page-heading{margin-bottom:18px;} .page-heading h1{font-size:21px;font-weight:500;letter-spacing:-0.4px;margin-bottom:2px;} .page-heading p{font-size:14px;color:var(--text-2);} /* INFO CARD */ .info-card{background:var(--surface);border-radius:var(--radius-xl); border:1px solid var(--border);box-shadow:var(--shadow-md); overflow:hidden;margin-bottom:16px;} .info-card-head{padding:14px 18px;border-bottom:1px solid var(--border); background:var(--surface-2);display:flex;align-items:center;gap:10px;} .info-card-icon{width:36px;height:36px;border-radius:var(--radius-md); background:var(--accent-soft);display:flex;align-items:center;justify-content:center;flex-shrink:0;} .info-card-title{font-size:15px;font-weight:500;color:var(--text-1);} .info-card-sub{font-size:12px;color:var(--text-3);} .info-row{display:flex;align-items:center;gap:12px;padding:11px 18px; border-bottom:1px solid var(--border);} .info-row:last-child{border-bottom:none;} .info-icon-sm{width:28px;height:28px;border-radius:var(--radius-sm); background:var(--surface-2);display:flex;align-items:center;justify-content:center;flex-shrink:0;} .info-label{font-size:11.5px;color:var(--text-3);margin-bottom:1px;} .info-value{font-size:14px;font-weight:500;color:var(--text-1);} .info-value.mono{font-family:'DM Mono',monospace;font-size:13px;} .lock-icon{margin-left:auto;opacity:0.35;} /* FORM CARD */ .form-card{background:var(--surface);border-radius:var(--radius-xl); border:1px solid var(--border);box-shadow:var(--shadow-md); overflow:hidden;margin-bottom:16px;} .form-card-head{padding:14px 18px 0;display:flex;align-items:center;gap:10px;} .form-card-icon{width:36px;height:36px;border-radius:var(--radius-md); background:var(--accent-soft);display:flex;align-items:center;justify-content:center;flex-shrink:0;} .form-card-title{font-size:15px;font-weight:500;color:var(--text-1);} .form-card-sub{font-size:12px;color:var(--text-3);} .form-card-body{padding:14px 18px 18px;} /* TEXTAREA */ .topic-area{width:100%;padding:11px 14px;border-radius:var(--radius-md); border:1px solid var(--border-md);background:var(--bg); font-family:inherit;font-size:14px;color:var(--text-1); outline:none;resize:vertical;min-height:90px;line-height:1.55; transition:border-color 0.15s,box-shadow 0.15s;} .topic-area:focus{border-color:var(--accent-mid);box-shadow:0 0 0 3px rgba(107,101,212,0.12);} #unit:focus{border-color:var(--accent-mid);box-shadow:0 0 0 3px rgba(107,101,212,0.12);} .topic-area::placeholder{color:var(--text-3);} .btn-save-topic{margin-top:10px;padding:10px 20px;border-radius:var(--radius-md); background:var(--surface-2);border:1px solid var(--border-md); font-family:inherit;font-size:14px;font-weight:500;color:var(--text-2); cursor:pointer;transition:background 0.15s;} .btn-save-topic:hover{background:var(--accent-soft);color:var(--accent);border-color:var(--accent-mid);} /* PIN CARD */ .pin-card{background:var(--surface);border-radius:var(--radius-xl); border:1px solid var(--border);box-shadow:var(--shadow-md); overflow:hidden;margin-bottom:16px;} .pin-card-body{padding:20px 18px 22px;text-align:center;} .pin-display{ font-size:52px;font-weight:500;letter-spacing:16px; font-family:'DM Mono',monospace; color:var(--accent);margin:4px 0 8px; line-height:1; } .pin-display.expired{color:var(--text-3);} /* countdown ring */ .countdown-wrap{position:relative;width:64px;height:64px;margin:0 auto 10px;} .countdown-wrap svg{transform:rotate(-90deg);} .c-bg{fill:none;stroke:var(--surface-2);stroke-width:5;} .c-fg{fill:none;stroke:var(--accent-mid);stroke-width:5;stroke-linecap:round; stroke-dasharray:176;transition:stroke-dashoffset 1s linear;} .c-fg.warning{stroke:#ba7517;} .c-fg.expired{stroke:#ccc;} .c-text{position:absolute;inset:0;display:flex;align-items:center;justify-content:center; font-size:15px;font-weight:500;color:var(--text-1);} .pin-hint{font-size:13px;color:var(--text-2);margin-bottom:16px;} .pin-status{font-size:12.5px;margin-bottom:14px;} .pin-status.valid{color:var(--success);} .pin-status.expired-txt{color:var(--text-3);} .btn-generate{padding:12px 28px;border-radius:var(--radius-md); background:var(--accent);color:#fff;border:none; font-family:inherit;font-size:15px;font-weight:500;cursor:pointer; transition:background 0.15s,transform 0.1s; display:inline-flex;align-items:center;gap:8px;} .btn-generate:hover{background:#231d6a;} .btn-generate:active{transform:scale(0.99);} /* ATTENDANCE LIST */ .att-card{background:var(--surface);border-radius:var(--radius-xl); border:1px solid var(--border);box-shadow:var(--shadow-sm);overflow:hidden;margin-bottom:16px;} .att-head{padding:13px 18px;border-bottom:1px solid var(--border); background:var(--surface-2);display:flex;align-items:center;justify-content:space-between;} .att-head-title{font-size:14px;font-weight:500;color:var(--text-1);} .att-count{font-size:12px;font-weight:500;background:var(--accent-soft); color:var(--accent-mid);border-radius:99px;padding:2px 10px;} .att-row{display:flex;align-items:center;gap:12px;padding:11px 18px; border-bottom:1px solid var(--border);transition:background 0.13s;} .att-row:last-child{border-bottom:none;} .att-row:hover{background:var(--surface-2);} .att-avatar{width:32px;height:32px;border-radius:50%;background:var(--success-bg); color:var(--success);display:flex;align-items:center;justify-content:center; font-size:12px;font-weight:500;flex-shrink:0;} .att-name{font-size:14px;font-weight:500;color:var(--text-1);} .att-roll{font-size:12px;color:var(--text-3);font-family:'DM Mono',monospace;} .att-time{font-size:12px;color:var(--text-3);margin-left:auto;white-space:nowrap;} .present-badge{font-size:11px;font-weight:500;background:var(--success-bg); color:var(--success);border-radius:99px;padding:2px 8px;} .empty-att{text-align:center;padding:28px 20px;color:var(--text-3);font-size:14px;} /* ATTENDANCE GRAPH */ .att-graph-wrap{padding:18px 18px 14px;border-bottom:1px solid var(--border);} .att-graph-numbers{display:flex;justify-content:space-between;align-items:flex-end;margin-bottom:10px;} .att-num-big{font-size:28px;font-weight:500;color:var(--accent);letter-spacing:-0.5px;line-height:1;} .att-num-label{font-size:11.5px;color:var(--text-3);margin-top:2px;} .att-num-right{text-align:right;} .att-num-absent{font-size:20px;font-weight:500;color:var(--text-3);letter-spacing:-0.3px;line-height:1;} /* segmented bar */ .att-bar-wrap{position:relative;height:10px;background:var(--surface-2); border-radius:99px;overflow:hidden;margin-bottom:8px;} .att-bar-fill{height:100%;border-radius:99px; background:linear-gradient(90deg,var(--accent-mid),#a56eff); transition:width 0.7s cubic-bezier(0.34,1.56,0.64,1);} .att-bar-labels{display:flex;justify-content:space-between;} .att-bar-lbl{font-size:11px;color:var(--text-3);} .att-pct{font-size:13px;font-weight:500;color:var(--accent-mid);} /* legend dots */ .att-legend{display:flex;gap:14px;margin-top:10px;} .legend-dot{width:8px;height:8px;border-radius:50%;flex-shrink:0;margin-top:3px;} .legend-item{display:flex;align-items:flex-start;gap:5px;font-size:12px;color:var(--text-2);} .bottom-nav{position:fixed;bottom:0;left:0;right:0;height:var(--nav-h); background:rgba(255,255,255,0.9);backdrop-filter:blur(14px); -webkit-backdrop-filter:blur(14px);border-top:1px solid var(--border); display:flex;justify-content:space-around;align-items:center;padding:0 8px;z-index:90;} .nav-item{flex:1;display:flex;flex-direction:column;align-items:center; gap:3px;padding:8px 0;border-radius:var(--radius-md);cursor:pointer; text-decoration:none;color:var(--text-3);transition:background 0.15s;} .nav-item:hover{background:var(--surface-2);} .nav-item svg{width:22px;height:22px;} .nav-label{font-size:11px;font-weight:500;letter-spacing:0.2px;} .nav-item.active{color:var(--accent);} </style> </head> <body> <?php if ($toast): ?> <div class="toast <?= $toast_type ?>" id="toast"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg> <span><?= htmlspecialchars($toast) ?></span> <button class="toast-close" onclick="this.parentElement.classList.remove('show')">Γ</button> </div> <?php endif; ?> <!-- TOPBAR --> <header class="topbar"> <a class="topbar-logo" href="index.php"> <div class="topbar-logo-icon"> <svg width="18" height="18" viewBox="0 0 24 24" fill="none"> <path d="M12 3L2 8.5l10 5.5 10-5.5L12 3z" fill="rgba(255,255,255,0.95)"/> <path d="M6 11.2V16c0 0 2 2.5 6 2.5S18 16 18 16v-4.8" stroke="rgba(255,255,255,0.75)" stroke-width="1.6" stroke-linecap="round"/> <line x1="22" y1="8.5" x2="22" y2="13.5" stroke="rgba(255,255,255,0.6)" stroke-width="1.6" stroke-linecap="round"/> </svg> </div> M<span>Connect</span> </a> <a class="back-btn" href="index.php"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"/></svg> Back </a> </header> <main class="page"> <div class="page-heading"> <h1>Take Attendance</h1> <p><?= date('l, d M Y') ?></p> </div> <!-- CLASS INFO β locked from DB --> <div class="info-card"> <div class="info-card-head"> <div class="info-card-icon"> <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><path d="M2 3h6a4 4 0 0 1 4 4v14a3 3 0 0 0-3-3H2z"/><path d="M22 3h-6a4 4 0 0 0-4 4v14a3 3 0 0 1 3-3h7z"/></svg> </div> <div> <div class="info-card-title"><?= htmlspecialchars($routine['subject_name']) ?></div> <div class="info-card-sub"><?= htmlspecialchars($routine['subject_code'] ?? '') ?></div> </div> </div> <div class="info-row"> <div class="info-icon-sm"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-2)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 7V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v2"/></svg> </div> <div> <div class="info-label">Department</div> <div class="info-value"><?= htmlspecialchars($routine['department']) ?></div> </div> <div class="lock-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--text-3)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div> </div> <div class="info-row"> <div class="info-icon-sm"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-2)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M22 10v6M2 10l10-5 10 5-10 5z"/></svg> </div> <div> <div class="info-label">Stream Β· Semester</div> <div class="info-value"><?= htmlspecialchars($routine['stream']) ?> Β· <?= $sem_labels[$routine['semester']] ?? $routine['semester'] ?> Sem</div> </div> <div class="lock-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--text-3)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div> </div> <div class="info-row"> <div class="info-icon-sm"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="var(--text-2)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg> </div> <div> <div class="info-label">Time Β· Room</div> <div class="info-value"><?= date('g:i A', strtotime($routine['time_start'])) ?> β <?= date('g:i A', strtotime($routine['time_end'])) ?> <?php if ($routine['room_no']): ?> Β· Room <?= htmlspecialchars($routine['room_no']) ?><?php endif; ?> </div> </div> <div class="lock-icon"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="var(--text-3)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg></div> </div> </div> <!-- TOPIC + PIN β one single form --> <form method="POST" id="mainForm"> <!-- TOPIC --> <div class="form-card" style="margin-bottom:16px;"> <div class="form-card-head"> <div class="form-card-icon"> <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><line x1="17" y1="10" x2="3" y2="10"/><line x1="21" y1="6" x2="3" y2="6"/><line x1="21" y1="14" x2="3" y2="14"/><line x1="17" y1="18" x2="3" y2="18"/></svg> </div> <div> <div class="form-card-title">Today's Topic</div> <div class="form-card-sub">Saved automatically when you generate PIN</div> </div> </div> <div class="form-card-body"> <div style="margin-bottom:12px;"> <label for="unit" style="display:block;font-size:12px;color:var(--text-3);margin-bottom:6px;font-weight:500;">Unit</label> <select name="unit" id="unit" style="width:100%;padding:11px 14px;border-radius:var(--radius-md); border:1px solid var(--border-md);background:var(--bg); font-family:inherit;font-size:14px;color:var(--text-1);outline:none; transition:border-color 0.15s,box-shadow 0.15s;"> <option value="">Select unitβ¦</option> <?php $selected_unit = $att_session['unit'] ?? ''; for ($u = 1; $u <= 10; $u++): ?> <option value="<?= $u ?>" <?= ((string)$selected_unit === (string)$u) ? 'selected' : '' ?>>Unit <?= $u ?></option> <?php endfor; ?> </select> </div> <textarea class="topic-area" name="topic" placeholder="e.g. Introduction to Linked Lists, Binary Search Treesβ¦"><?= htmlspecialchars($att_session['topic'] ?? '') ?></textarea> </div> </div> <!-- PIN GENERATOR --> <div class="pin-card"> <div class="info-card-head" style="padding:14px 18px;"> <div class="info-card-icon"> <svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="var(--accent)" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="11" width="18" height="11" rx="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg> </div> <div> <div class="info-card-title">Attendance PIN</div> <div class="info-card-sub">Share with students β valid for 1 minute</div> </div> </div> <div class="pin-card-body"> <?php if (!empty($att_session['pin'])): ?> <div class="countdown-wrap"> <svg width="64" height="64" viewBox="0 0 64 64"> <circle class="c-bg" cx="32" cy="32" r="28"/> <circle class="c-fg <?= $pin_valid ? ($pin_secs_left <= 15 ? 'warning' : '') : 'expired' ?>" cx="32" cy="32" r="28" id="countRing" style="stroke-dashoffset:<?= $pin_valid ? round(176 * (1 - $pin_secs_left/60)) : 176 ?>"/> </svg> <div class="c-text" id="countText"><?= $pin_valid ? $pin_secs_left : '0' ?></div> </div> <div class="pin-display <?= $pin_valid ? '' : 'expired' ?>" id="pinDisplay"> <?= htmlspecialchars($att_session['pin']) ?> </div> <?php if ($pin_valid): ?> <div class="pin-status valid" id="pinStatus"> β Active β expires in <span id="expireText"><?= $pin_secs_left ?>s</span> </div> <?php else: ?> <div class="pin-status expired-txt" id="pinStatus">PIN expired</div> <?php endif; ?> <?php else: ?> <div style="font-size:52px;color:var(--surface-2);letter-spacing:16px;font-family:'DM Mono',monospace;margin-bottom:12px;">β’β’β’β’</div> <div class="pin-hint">No PIN generated yet</div> <?php endif; ?> <button class="btn-generate" type="submit" name="generate_pin" id="generateBtn"> <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><polyline points="23 4 23 10 17 10"/><path d="M20.49 15a9 9 0 1 1-2.12-9.36L23 10"/></svg> <?= ($att_session && $att_session['pin']) ? 'Regenerate PIN' : 'Generate PIN' ?> </button> </div> </div> </form> <!-- ATTENDANCE CARD with live graph --> <div class="att-card" id="attCard"> <div class="att-head"> <div class="att-head-title">Present Students</div> <div style="display:flex;align-items:center;gap:8px;"> <span class="att-count" id="attCount"><?= $present_count ?> / <?= $total_students ?: '?' ?></span> <span style="font-size:11px;color:var(--text-3);"> Live <span id="liveDot" style="display:inline-block;width:6px;height:6px;border-radius:50%;background:var(--success);vertical-align:middle;animation:pulse-dot 1.4s ease-in-out infinite;"></span> </span> </div> </div> <!-- GRAPH --> <div class="att-graph-wrap" id="graphWrap"> <?php $pct = $total_students > 0 ? round($present_count / $total_students * 100) : 0; $absent = $total_students - $present_count; ?> <div class="att-graph-numbers"> <div> <div class="att-num-big" id="numPresent"><?= $present_count ?></div> <div class="att-num-label">Present</div> </div> <div style="text-align:center;"> <div class="att-pct" id="numPct"><?= $pct ?>%</div> <div class="att-num-label">Attendance</div> </div> <div class="att-num-right"> <div class="att-num-absent" id="numAbsent"><?= $absent ?></div> <div class="att-num-label">Not yet</div> </div> </div> <div class="att-bar-wrap"> <div class="att-bar-fill" id="attBar" style="width:<?= $pct ?>%"></div> </div> <div class="att-bar-labels"> <span class="att-bar-lbl">0</span> <span class="att-bar-lbl"><?= $total_students ?> enrolled (<?= htmlspecialchars($routine['stream']) ?> Β· <?= ($sem_labels[$routine['semester']] ?? $routine['semester']) ?> Sem)</span> </div> <div class="att-legend"> <div class="legend-item"> <div class="legend-dot" style="background:var(--accent-mid);"></div> <span>Present</span> </div> <div class="legend-item"> <div class="legend-dot" style="background:var(--surface-2);border:1px solid var(--border-md);"></div> <span>Not yet marked</span> </div> </div> </div> <!-- VIEW REPORT BUTTON --> <div style="padding:14px 18px 18px;"> <button onclick="openReport()" style=" width:100%;padding:11px;border-radius:var(--radius-md); background:var(--accent-soft);color:var(--accent); border:1px solid rgba(107,101,212,0.2); font-family:inherit;font-size:14px;font-weight:500;cursor:pointer; display:flex;align-items:center;justify-content:center;gap:8px; transition:background 0.15s;"> <svg width="15" height="15" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/></svg> View Full Report </button> </div> </div> </main> <!-- ββ REPORT MODAL βββββββββββββββββββββββββββββββββββ --> <div id="reportOverlay" style=" position:fixed;inset:0;z-index:500; background:rgba(0,0,0,0.5); display:none;align-items:flex-end;justify-content:center;"> <div id="reportSheet" style=" width:100%;max-width:520px; background:var(--surface); border-radius:var(--radius-xl) var(--radius-xl) 0 0; max-height:90vh;display:flex;flex-direction:column; transform:translateY(100%);transition:transform 0.3s ease;"> <!-- Sheet header --> <div style="padding:16px 20px;border-bottom:1px solid var(--border); display:flex;align-items:center;justify-content:space-between;flex-shrink:0;"> <div> <div style="font-size:16px;font-weight:500;color:var(--text-1);">Attendance Report</div> <div style="font-size:12px;color:var(--text-3);"> <?= htmlspecialchars($routine['subject_name']) ?> Β· <?= date('d M Y') ?> </div> </div> <div style="display:flex;align-items:center;gap:8px;"> <button onclick="downloadCSV()" style=" display:flex;align-items:center;gap:6px;padding:8px 14px; border-radius:var(--radius-md);background:var(--accent);color:#fff; border:none;font-family:inherit;font-size:13px;font-weight:500;cursor:pointer;"> <svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg> CSV </button> <button onclick="closeReport()" style=" width:32px;height:32px;border-radius:50%; border:1px solid var(--border);background:var(--surface-2); font-size:18px;cursor:pointer;color:var(--text-2); display:flex;align-items:center;justify-content:center;">Γ</button> </div> </div> <!-- Filter tabs --> <div style="padding:10px 20px 0;display:flex;gap:8px;flex-shrink:0;"> <button onclick="filterReport('all')" id="tab_all" style="flex:1;padding:8px;border-radius:var(--radius-md);border:none; font-family:inherit;font-size:13px;font-weight:500;cursor:pointer; background:var(--accent);color:#fff;">All</button> <button onclick="filterReport('present')" id="tab_present" style="flex:1;padding:8px;border-radius:var(--radius-md);border:none; font-family:inherit;font-size:13px;font-weight:500;cursor:pointer; background:var(--surface-2);color:var(--text-2);">Present</button> <button onclick="filterReport('absent')" id="tab_absent" style="flex:1;padding:8px;border-radius:var(--radius-md);border:none; font-family:inherit;font-size:13px;font-weight:500;cursor:pointer; background:var(--surface-2);color:var(--text-2);">Absent</button> </div> <!-- Student rows --> <div id="reportList" style="overflow-y:auto;flex:1;padding:8px 0;"></div> </div> </div> <!-- BOTTOM NAV --> <?php include "botNav.php"; ?> <script> // Toast <?php if ($toast): ?> window.addEventListener('DOMContentLoaded', () => { const t = document.getElementById('toast'); if (t) { requestAnimationFrame(() => t.classList.add('show')); setTimeout(() => t.classList.remove('show'), 4000); } }); <?php endif; ?> // ββ PIN Countdown ββββββββββββββββββββββββββββββββββββββ <?php if (!empty($att_session['pin']) && $pin_valid): ?> let secsLeft = <?= $pin_secs_left ?>; const ring = document.getElementById('countRing'); const countTxt = document.getElementById('countText'); const statusEl = document.getElementById('pinStatus'); const expireTxt = document.getElementById('expireText'); const pinEl = document.getElementById('pinDisplay'); const timer = setInterval(() => { secsLeft--; if (secsLeft <= 0) { clearInterval(timer); countTxt.textContent = '0'; ring.style.strokeDashoffset = '176'; ring.classList.remove('warning'); ring.classList.add('expired'); pinEl.classList.add('expired'); statusEl.className = 'pin-status expired-txt'; statusEl.textContent = 'PIN expired'; document.getElementById('generateBtn').textContent = 'Regenerate PIN'; return; } // Update ring (176 = full circumference) ring.style.strokeDashoffset = Math.round(176 * (1 - secsLeft / 60)); countTxt.textContent = secsLeft; if (expireTxt) expireTxt.textContent = secsLeft + 's'; if (secsLeft <= 15) ring.classList.add('warning'); }, 1000); <?php endif; ?> // ββ Live attendance polling via AJAX βββββββββββββββββββ const SESSION_ID = <?= $att_session ? (int)$att_session['id'] : 0 ?>; const TOTAL_STU = <?= (int)$total_students ?>; const ROUTINE_ID = <?= $routine_id ?>; function fetchAttendance() { if (!SESSION_ID) return; fetch(`attendancePoll.php?session_id=${SESSION_ID}&total=${TOTAL_STU}`) .then(r => r.json()) .then(data => { const present = data.present; const total = data.total; const pct = total > 0 ? Math.round(present / total * 100) : 0; const absent = total - present; // Update header count document.getElementById('attCount').textContent = present + ' / ' + total; // Update graph numbers document.getElementById('numPresent').textContent = present; document.getElementById('numAbsent').textContent = absent; document.getElementById('numPct').textContent = pct + '%'; // Animate bar document.getElementById('attBar').style.width = pct + '%'; // Rebuild student rows const listEl = document.getElementById('attList'); if (data.students.length === 0) { listEl.innerHTML = `<div class="empty-att"> <svg width="28" height="28" viewBox="0 0 24 24" fill="none" stroke="var(--text-3)" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" style="display:block;margin:0 auto 10px;"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg> No students marked present yet.<br>Share the PIN to start.</div>`; } else { listEl.innerHTML = data.students.map(s => ` <div class="att-row"> <div class="att-avatar">${s.initial}</div> <div> <div class="att-name">${s.name}</div> <div class="att-roll">${s.roll_no}</div> </div> <div class="att-time"> <span class="present-badge">Present</span><br> <span style="font-size:11px;">${s.time}</span> </div> </div>`).join(''); } }) .catch(() => {}); // silent fail β no network error shown to teacher } // Poll every 5 seconds if (SESSION_ID) { setInterval(fetchAttendance, 5000); } // ββ Report modal βββββββββββββββββββββββββββββββββββββββ let reportData = { present: [], absent: [] }; let activeTab = 'all'; async function openReport() { // Fetch full report const res = await fetch(`attendancePoll.php?session_id=${SESSION_ID}&total=${TOTAL_STU}&report=1&routine_id=${ROUTINE_ID}`); const data = await res.json(); reportData = data; activeTab = 'all'; renderReport(); const overlay = document.getElementById('reportOverlay'); const sheet = document.getElementById('reportSheet'); overlay.style.display = 'flex'; requestAnimationFrame(() => sheet.style.transform = 'translateY(0)'); } function closeReport() { const sheet = document.getElementById('reportSheet'); const overlay = document.getElementById('reportOverlay'); sheet.style.transform = 'translateY(100%)'; setTimeout(() => overlay.style.display = 'none', 300); } function filterReport(tab) { activeTab = tab; ['all','present','absent'].forEach(t => { const btn = document.getElementById('tab_' + t); btn.style.background = t === tab ? 'var(--accent)' : 'var(--surface-2)'; btn.style.color = t === tab ? '#fff' : 'var(--text-2)'; }); renderReport(); } function renderReport() { const list = document.getElementById('reportList'); const present = reportData.present || []; const absent = reportData.absent || []; let rows = []; if (activeTab === 'all') { rows = [ ...present.map(s => ({...s, status:'present'})), ...absent.map(s => ({...s, status:'absent'})) ].sort((a,b) => (a.roll_no||'').localeCompare(b.roll_no||'', undefined, {numeric:true})); } else if (activeTab === 'present') { rows = present.map(s => ({...s, status:'present'})); } else { rows = absent.map(s => ({...s, status:'absent'})); } if (rows.length === 0) { list.innerHTML = `<div style="text-align:center;padding:32px 20px;font-size:14px;color:var(--text-3);">No students in this category.</div>`; return; } list.innerHTML = rows.map((s, i) => ` <div style="display:flex;align-items:center;gap:12px;padding:11px 20px; border-bottom:${i < rows.length-1 ? '1px solid var(--border)' : 'none'};"> <div style="width:32px;height:32px;border-radius:50%;flex-shrink:0; display:flex;align-items:center;justify-content:center; font-size:12px;font-weight:500; background:${s.status==='present' ? 'var(--success-bg)' : 'var(--surface-2)'}; color:${s.status==='present' ? 'var(--success)' : 'var(--text-3)'};"> ${s.initial || s.name.charAt(0).toUpperCase()} </div> <div style="flex:1;min-width:0;"> <div style="font-size:14px;font-weight:500;color:var(--text-1); white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">${s.name}</div> <div style="font-size:12px;color:var(--text-3);font-family:'DM Mono',monospace;">${s.roll_no}</div> </div> <div style="text-align:right;flex-shrink:0;"> ${s.status === 'present' ? `<span onclick="toggleStatus(${s.id}, 'present')" style="cursor:pointer;user-select:none;font-size:11px;font-weight:500;background:var(--success-bg);color:var(--success);border-radius:99px;padding:2px 9px;">Present</span> <div style="font-size:11px;color:var(--text-3);margin-top:2px;">${s.time || ''}</div>` : `<span onclick="toggleStatus(${s.id}, 'absent')" style="cursor:pointer;user-select:none;font-size:11px;font-weight:500;background:var(--danger-bg);color:var(--danger);border-radius:99px;padding:2px 9px;">Absent</span>` } </div> </div>`).join(''); } // ββ Manual present/absent toggle (new) βββββββββββββββββ async function toggleStatus(studentId, currentStatus) { const newAction = currentStatus === 'present' ? 'absent' : 'present'; const res = await fetch('toggleAttendance.php', { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, body: `session_id=${SESSION_ID}&student_id=${studentId}&action=${newAction}` }); const data = await res.json(); if (!data.ok) return; // Move student between present/absent arrays in local reportData const fromArr = currentStatus === 'present' ? reportData.present : reportData.absent; const toArr = currentStatus === 'present' ? reportData.absent : reportData.present; const idx = fromArr.findIndex(s => s.id === studentId); if (idx !== -1) { const [student] = fromArr.splice(idx, 1); if (newAction === 'present') student.time = data.time || ''; else student.time = ''; toArr.push(student); } renderReport(); fetchAttendance(); // refresh live counts/graph on the main page } function downloadCSV() { const present = reportData.present || []; const absent = reportData.absent || []; const rows = [ ['Name', 'Roll No', 'Status', 'Time Marked'], ...present.map(s => [s.name, s.roll_no, 'Present', s.time || '']), ...absent.map(s => [s.name, s.roll_no, 'Absent', '']), ]; const csv = rows.map(r => r.map(c => `"${(c||'').replace(/"/g,'""')}"`).join(',')).join('\n'); const blob = new Blob([csv], {type:'text/csv'}); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `attendance_<?= date('Y-m-d') ?>_<?= addslashes($routine['subject_name']) ?>.csv`; a.click(); URL.revokeObjectURL(url); } // Close on backdrop click document.getElementById('reportOverlay').addEventListener('click', function(e) { if (e.target === this) closeReport(); }); </script> </body> </html>
πΎ Kaydet
Δ°ptal