···
+
import { css, html, LitElement } from "lit";
+
import { customElement, state } from "lit/decorators.js";
+
interface WaitlistEntry {
+
additional_info: string | null;
+
meeting_times: string | null;
+
@customElement("admin-classes")
+
export class AdminClasses extends LitElement {
+
@state() classes: Class[] = [];
+
@state() waitlist: WaitlistEntry[] = [];
+
@state() isLoading = true;
+
@state() searchTerm = "";
+
@state() showCreateModal = false;
+
@state() activeTab: "classes" | "waitlist" = "classes";
+
@state() approvingEntry: WaitlistEntry | null = null;
+
@state() meetingTimes: string[] = [""];
+
static override styles = css`
+
justify-content: space-between;
+
padding: 0.5rem 0.75rem;
+
border: 2px solid var(--secondary);
+
background: var(--background);
+
border-color: var(--primary);
+
padding: 0.75rem 1.5rem;
+
background: var(--primary);
+
border: 2px solid var(--primary);
+
background: var(--gunmetal);
+
border-color: var(--gunmetal);
+
background: var(--background);
+
border: 2px solid var(--secondary);
+
border-color: var(--primary);
+
justify-content: space-between;
+
align-items: flex-start;
+
margin-bottom: 0.75rem;
+
text-transform: uppercase;
+
color: var(--paynes-gray);
+
padding: 0.25rem 0.5rem;
+
text-transform: uppercase;
+
background: var(--secondary);
+
background: transparent;
+
color: var(--paynes-gray);
+
border-color: var(--secondary);
+
border-color: var(--paynes-gray);
+
background: transparent;
+
color: var(--paynes-gray);
+
color: var(--paynes-gray);
+
border-bottom: 2px solid var(--secondary);
+
padding: 0.75rem 1.5rem;
+
background: transparent;
+
border-bottom: 2px solid transparent;
+
border-bottom-color: var(--primary);
+
padding: 0.125rem 0.5rem;
+
background: var(--accent);
+
background: rgba(0, 0, 0, 0.5);
+
justify-content: center;
+
background: var(--background);
+
border: 2px solid var(--secondary);
+
border: 2px solid var(--secondary);
+
background: var(--background);
+
box-sizing: border-box;
+
.form-group input:focus {
+
border-color: var(--primary);
+
flex-direction: column;
+
.meeting-time-row input {
+
background: transparent;
+
border: 2px solid #dc2626;
+
background: transparent;
+
border: 2px solid var(--primary);
+
background: var(--primary);
+
justify-content: flex-end;
+
padding: 0.75rem 1.5rem;
+
background: var(--primary);
+
border: 2px solid var(--primary);
+
.btn-submit:hover:not(:disabled) {
+
background: var(--gunmetal);
+
border-color: var(--gunmetal);
+
padding: 0.75rem 1.5rem;
+
background: transparent;
+
border: 2px solid var(--secondary);
+
border-color: var(--primary);
+
override async connectedCallback() {
+
super.connectedCallback();
+
private async loadData() {
+
const [classesRes, waitlistRes] = await Promise.all([
+
fetch("/api/admin/classes"),
+
fetch("/api/admin/waitlist"),
+
if (!classesRes.ok || !waitlistRes.ok) {
+
throw new Error("Failed to load data");
+
const classesData = await classesRes.json();
+
const waitlistData = await waitlistRes.json();
+
this.classes = classesData.classes || [];
+
this.waitlist = waitlistData.waitlist || [];
+
this.error = "Failed to load data. Please try again.";
+
this.isLoading = false;
+
private handleSearch(e: Event) {
+
this.searchTerm = (e.target as HTMLInputElement).value.toLowerCase();
+
private async handleToggleArchive(classId: string) {
+
const response = await fetch(`/api/classes/${classId}/archive`, {
+
throw new Error("Failed to update class");
+
this.error = "Failed to update class. Please try again.";
+
private async handleDelete(classId: string, courseName: string) {
+
`Are you sure you want to delete ${courseName}? This will remove all associated data and cannot be undone.`,
+
const response = await fetch(`/api/classes/${classId}`, {
+
throw new Error("Failed to delete class");
+
this.error = "Failed to delete class. Please try again.";
+
private handleCreateClass() {
+
this.showCreateModal = true;
+
private async handleDeleteWaitlist(id: string, courseCode: string) {
+
`Are you sure you want to delete this waitlist request for ${courseCode}?`,
+
const response = await fetch(`/api/admin/waitlist/${id}`, {
+
throw new Error("Failed to delete waitlist entry");
+
this.error = "Failed to delete waitlist entry. Please try again.";
+
private getFilteredClasses() {
+
if (!this.searchTerm) return this.classes;
+
return this.classes.filter((cls) => {
+
const searchStr = this.searchTerm;
+
cls.course_code.toLowerCase().includes(searchStr) ||
+
cls.name.toLowerCase().includes(searchStr) ||
+
cls.professor.toLowerCase().includes(searchStr)
+
return html`<div class="loading">Loading...</div>`;
+
const filteredClasses = this.getFilteredClasses();
+
${this.error ? html`<div class="error-message">${this.error}</div>` : ""}
+
class="tab ${this.activeTab === "classes" ? "active" : ""}"
+
@click=${() => { this.activeTab = "classes"; }}
+
class="tab ${this.activeTab === "waitlist" ? "active" : ""}"
+
@click=${() => { this.activeTab = "waitlist"; }}
+
${this.waitlist.length > 0 ? html`<span class="tab-badge">${this.waitlist.length}</span>` : ""}
+
this.activeTab === "classes"
+
? this.renderClasses(filteredClasses)
+
: this.renderWaitlist()
+
${this.approvingEntry ? this.renderApprovalModal() : ""}
+
private renderClasses(filteredClasses: Class[]) {
+
placeholder="Search classes..."
+
@input=${this.handleSearch}
+
.value=${this.searchTerm}
+
<button class="create-btn" @click=${this.handleCreateClass}>
+
filteredClasses.length === 0
+
<div class="empty-state">
+
${this.searchTerm ? "No classes found matching your search" : "No classes yet"}
+
<div class="classes-grid">
+
<div class="class-card ${cls.archived ? "archived" : ""}">
+
<div class="class-header">
+
<div class="class-info">
+
<div class="course-code">${cls.course_code}</div>
+
<div class="class-name">${cls.name}</div>
+
<div class="class-meta">
+
<span>👤 ${cls.professor}</span>
+
<span>📅 ${cls.semester} ${cls.year}</span>
+
${cls.archived ? html`<span class="badge archived">Archived</span>` : ""}
+
@click=${() => this.handleToggleArchive(cls.id)}
+
${cls.archived ? "Unarchive" : "Archive"}
+
@click=${() => this.handleDelete(cls.id, cls.course_code)}
+
private renderWaitlist() {
+
this.waitlist.length === 0
+
<div class="empty-state">No waitlist requests yet</div>
+
<div class="classes-grid">
+
<div class="class-card">
+
<div class="class-header">
+
<div class="class-info">
+
<div class="course-code">${entry.course_code}</div>
+
<div class="class-name">${entry.course_name}</div>
+
<div class="class-meta">
+
<span>👤 ${entry.professor}</span>
+
<span>📅 ${entry.semester} ${entry.year}</span>
+
<p style="margin-top: 0.75rem; font-size: 0.875rem; color: var(--paynes-gray);">
+
${entry.additional_info}
+
@click=${() => this.handleApproveWaitlist(entry)}
+
@click=${() => this.handleDeleteWaitlist(entry.id, entry.course_code)}
+
private handleApproveWaitlist(entry: WaitlistEntry) {
+
this.approvingEntry = entry;
+
// Parse meeting times from JSON if available, otherwise use empty array
+
if (entry.meeting_times) {
+
const parsed = JSON.parse(entry.meeting_times);
+
this.meetingTimes = Array.isArray(parsed) && parsed.length > 0 ? parsed : [""];
+
this.meetingTimes = [""];
+
this.meetingTimes = [""];
+
private addMeetingTime() {
+
this.meetingTimes = [...this.meetingTimes, ""];
+
private removeMeetingTime(index: number) {
+
this.meetingTimes = this.meetingTimes.filter((_, i) => i !== index);
+
private updateMeetingTime(index: number, value: string) {
+
this.meetingTimes = this.meetingTimes.map((time, i) =>
+
i === index ? value : time,
+
private cancelApproval() {
+
this.approvingEntry = null;
+
this.meetingTimes = [""];
+
private async submitApproval() {
+
if (!this.approvingEntry) return;
+
const entry = this.approvingEntry;
+
const times = this.meetingTimes.filter((t) => t.trim() !== "");
+
if (times.length === 0) {
+
this.error = "Please add at least one meeting time";
+
const response = await fetch("/api/classes", {
+
headers: { "Content-Type": "application/json" },
+
course_code: entry.course_code,
+
name: entry.course_name,
+
professor: entry.professor,
+
semester: entry.semester,
+
throw new Error("Failed to create class");
+
await fetch(`/api/admin/waitlist/${entry.id}`, {
+
this.activeTab = "classes";
+
this.approvingEntry = null;
+
this.meetingTimes = [""];
+
this.error = "Failed to approve waitlist entry. Please try again.";
+
private renderApprovalModal() {
+
if (!this.approvingEntry) return "";
+
const entry = this.approvingEntry;
+
<div class="modal-overlay" @click=${this.cancelApproval}>
+
<div class="modal" @click=${(e: Event) => e.stopPropagation()}>
+
<h2 class="modal-title">Add Meeting Times</h2>
+
<p style="margin-bottom: 1.5rem; color: var(--paynes-gray);">
+
Creating class: <strong>${entry.course_code} - ${entry.course_name}</strong>
+
<div class="form-group">
+
<label>Meeting Times</label>
+
<div class="meeting-times-list">
+
${this.meetingTimes.map(
+
<div class="meeting-time-row">
+
placeholder="e.g., Monday Lecture, Wednesday Lab"
+
this.updateMeetingTime(
+
(e.target as HTMLInputElement).value,
+
this.meetingTimes.length > 1
+
@click=${() => this.removeMeetingTime(index)}
+
<button class="btn-add" @click=${this.addMeetingTime}>
+
<div class="modal-actions">
+
<button class="btn-cancel" @click=${this.cancelApproval}>
+
@click=${this.submitApproval}
+
?disabled=${this.meetingTimes.every((t) => t.trim() === "")}