# dashboards/teacher_db.py import os import io import csv import datetime import streamlit as st import plotly.graph_objects as go from utils import db as dbapi import utils.api as api # backend Space client # If DISABLE_DB=1 (default), don't call MySQL at all USE_LOCAL_DB = os.getenv("DISABLE_DB", "1") != "1" def load_css(file_name): try: with open(file_name, 'r', encoding="utf-8") as f: st.markdown(f"", unsafe_allow_html=True) except FileNotFoundError: st.warning("โš ๏ธ Stylesheet not found. Please ensure 'assets/styles.css' exists.") def tile(icon, label, value): return f"""
{icon}
{value}
{label}
""" def _level_from_xp(xp: int) -> int: """ Prefer backend/db helper if available, else simple fallback (every 500 XP = +1 level). """ try: if USE_LOCAL_DB and hasattr(dbapi, "level_from_xp"): return int(dbapi.level_from_xp(xp)) if hasattr(api, "level_from_xp"): # if you add the endpoint return int(api.level_from_xp(int(xp))) except Exception: pass xp = int(xp or 0) return 1 + (xp // 500) def _safe_get_tiles(teacher_id: int) -> dict: if USE_LOCAL_DB and hasattr(dbapi, "teacher_tiles"): return dbapi.teacher_tiles(teacher_id) try: return api.teacher_tiles(teacher_id) except Exception: return { "total_students": 0, "class_avg": 0.0, "lessons_created": 0, "active_students": 0 } def _safe_list_classes(teacher_id: int) -> list: if USE_LOCAL_DB and hasattr(dbapi, "list_classes_by_teacher"): return dbapi.list_classes_by_teacher(teacher_id) try: return api.list_classes_by_teacher(teacher_id) except Exception: return [] def _safe_create_class(teacher_id: int, name: str) -> dict: if USE_LOCAL_DB and hasattr(dbapi, "create_class"): return dbapi.create_class(teacher_id, name) return api.create_class(teacher_id, name) def _safe_class_student_metrics(class_id: int) -> list: if USE_LOCAL_DB and hasattr(dbapi, "class_student_metrics"): return dbapi.class_student_metrics(class_id) try: return api.class_student_metrics(class_id) except Exception: return [] def _safe_weekly_activity(class_id: int) -> list: if USE_LOCAL_DB and hasattr(dbapi, "class_weekly_activity"): return dbapi.class_weekly_activity(class_id) try: return api.class_weekly_activity(class_id) except Exception: return [] def _safe_progress_overview(class_id: int) -> dict: if USE_LOCAL_DB and hasattr(dbapi, "class_progress_overview"): return dbapi.class_progress_overview(class_id) try: return api.class_progress_overview(class_id) except Exception: return { "overall_progress": 0.0, "quiz_performance": 0.0, "lessons_completed": 0, "class_xp": 0 } def _safe_recent_activity(class_id: int, limit=6, days=30) -> list: if USE_LOCAL_DB and hasattr(dbapi, "class_recent_activity"): return dbapi.class_recent_activity(class_id, limit=limit, days=days) try: return api.class_recent_activity(class_id, limit=limit, days=days) except Exception: return [] def _safe_list_students(class_id: int) -> list: if USE_LOCAL_DB and hasattr(dbapi, "list_students_in_class"): return dbapi.list_students_in_class(class_id) try: return api.list_students_in_class(class_id) except Exception: return [] def show_teacher_dashboard(): css_path = os.path.join("assets", "styles.css") load_css(css_path) user = st.session_state.user teacher_id = user["user_id"] name = user["name"] # ========== HEADER / HERO ========== colH1, colH2 = st.columns([5, 2]) with colH1: st.markdown(f"""
Welcome back, Teacher {name}!
Managing your classrooms
""", unsafe_allow_html=True) with colH2: with st.popover("โž• Create Classroom"): new_class_name = st.text_input("Classroom Name", key="new_class_name") if st.button("Create Classroom", key="create_classroom_btn"): if new_class_name.strip(): try: out = _safe_create_class(teacher_id, new_class_name.strip()) code = out.get("code") or out.get("class_code") or "โ€”" st.success(f"Classroom created. Code: **{code}**") except Exception as e: st.error(f"Create failed: {e}") # ========== TILES ========== tiles = _safe_get_tiles(teacher_id) c1,c2,c3,c4 = st.columns(4) c1.markdown(tile("๐Ÿ‘ฅ","Total Students", tiles.get("total_students", 0)), unsafe_allow_html=True) c2.markdown(tile("๐Ÿ“Š","Class Average", f"{int((tiles.get('class_avg') or 0)*100)}%"), unsafe_allow_html=True) c3.markdown(tile("๐Ÿ“š","Lessons Created", tiles.get("lessons_created", 0)), unsafe_allow_html=True) c4.markdown(tile("๐Ÿ“ˆ","Active Students", tiles.get("active_students", 0)), unsafe_allow_html=True) # ========== CLASS PICKER ========== classes = _safe_list_classes(teacher_id) if not classes: st.info("No classes yet. Create one above, then share the code with students.") return idx = st.selectbox( "Choose a class", list(range(len(classes))), index=0, format_func=lambda i: f"{classes[i].get('name','Class')} (Code: {classes[i].get('code','')})" ) selected = classes[idx] class_id = selected.get("class_id") or selected.get("id") class_code = selected.get("code","") # secondary hero controls cTop1, cTop2, cTop3 = st.columns([2,1,1]) with cTop1: st.button(f"Class Code: {class_code}", disabled=True) with cTop2: if st.button("๐Ÿ“‹ Copy Code"): st.toast("Code copied. Paste it anywhere your heart desires.") with cTop3: rows = _safe_class_student_metrics(class_id) if rows: headers = [] for r in rows: for k in r.keys(): if k not in headers: headers.append(k) buf = io.StringIO() writer = csv.DictWriter(buf, fieldnames=headers) writer.writeheader() for r in rows: writer.writerow(r) st.download_button( "๐Ÿ“ค Export Class Report", data=buf.getvalue(), file_name=f"class_{class_id}_report.csv", mime="text/csv" ) else: st.button("๐Ÿ“ค Export Class Report", disabled=True) # ========== TOP ROW: WEEKLY ACTIVITY + CLASS PROGRESS ========== left, right = st.columns([3,2]) with left: st.subheader("Weekly Activity") st.caption("Student engagement throughout the week") activity = _safe_weekly_activity(class_id) if activity: days, lessons, quizzes, games = [], [], [], [] for row in activity: date_str = row.get("date") try: # Support ISO date or datetime day = datetime.datetime.fromisoformat(str(date_str)).strftime("%a") except Exception: day = str(date_str) days.append(day) lessons.append(row.get("lessons",0)) quizzes.append(row.get("quizzes",0)) games.append(row.get("games",0)) fig = go.Figure(data=[ go.Bar(name="Lessons", x=days, y=lessons), go.Bar(name="Quizzes", x=days, y=quizzes), go.Bar(name="Games", x=days, y=games), ]) fig.update_layout(barmode="group", xaxis_title="Day", yaxis_title="Count") st.plotly_chart(fig, use_container_width=True) else: st.info("No activity in the last 7 days.") with right: st.subheader("Class Progress Overview") st.caption("How your students are performing") prog = _safe_progress_overview(class_id) overall_pct = int(round((prog.get("overall_progress") or 0) * 100)) quiz_pct = int(round((prog.get("quiz_performance") or 0) * 100)) st.text("Overall Progress") st.progress(min(1.0, overall_pct/100.0)) st.caption(f"{overall_pct}%") st.text("Quiz Performance") st.progress(min(1.0, quiz_pct/100.0)) st.caption(f"{quiz_pct}%") k1, k2 = st.columns(2) k1.metric("๐Ÿ“– Lessons Completed", prog.get("lessons_completed", 0)) k2.metric("๐Ÿช™ Total Class XP", prog.get("class_xp", 0)) # ========== BOTTOM ROW: RECENT ACTIVITY + QUICK ACTIONS ========== b1, b2 = st.columns([3,2]) with b1: st.subheader("Recent Student Activity") st.caption("Latest activity from your students") feed = _safe_recent_activity(class_id, limit=6, days=30) if not feed: st.caption("Nothing yet. Assign something, chief.") else: for r in feed: kind = str(r.get("kind","")).lower() icon = "๐Ÿ“˜" if kind == "lesson" else "๐Ÿ†" if kind == "quiz" else "๐ŸŽฎ" lvl = r.get("level") or _level_from_xp(r.get("total_xp", 0)) tail = f" ยท {r['extra']}" if r.get("extra") else "" st.write(f"{icon} **{r.get('student_name','(unknown)')}** โ€” {r.get('item_title','(untitled)')}{tail} \n" f"*Level {lvl}*") with b2: st.subheader("Quick Actions") st.caption("Manage your classroom") if st.button("๐Ÿ“– Create New Lesson", use_container_width=True): st.session_state.current_page = "Content Management" st.rerun() if st.button("๐Ÿ† Create New Quiz", use_container_width=True): st.session_state.current_page = "Content Management" st.rerun() if st.button("๐Ÿ—“๏ธ Schedule Assignment", use_container_width=True): st.session_state.current_page = "Class management" st.rerun() if st.button("๐Ÿ“„ Generate Reports", use_container_width=True): st.session_state.current_page = "Students List" st.rerun() # optional: keep your per-class expanders below for c in classes: with st.expander(f"{c.get('name','Class')} ยท Code **{c.get('code','')}**"): st.write(f"Students: {c.get('total_students', 0)}") avg = c.get("class_avg", 0.0) st.write(f"Average score: {round(float(avg)*100) if avg is not None else 0}%") roster = _safe_list_students(c.get("class_id") or c.get("id")) if roster: for s in roster: lvl_slug = (s.get("level_slug") or s.get("level") or "beginner") st.write(f"- {s.get('name','(unknown)')} ยท {s.get('email','โ€”')} ยท Level {str(lvl_slug).capitalize()}") else: st.caption("No students yet. Share the code.")