馃 distributed transcription service thistle.dunkirk.sh
1import { nanoid } from "nanoid"; 2import db from "../db/schema"; 3 4export interface Class { 5 id: string; 6 course_code: string; 7 name: string; 8 professor: string; 9 semester: string; 10 year: number; 11 archived: boolean; 12 created_at: number; 13} 14 15export interface MeetingTime { 16 id: string; 17 class_id: string; 18 label: string; 19 created_at: number; 20} 21 22export interface ClassMember { 23 class_id: string; 24 user_id: number; 25 enrolled_at: number; 26} 27 28export interface ClassWithStats extends Class { 29 student_count?: number; 30 transcript_count?: number; 31} 32 33/** 34 * Get all classes for a user (either enrolled or admin sees all) 35 */ 36export function getClassesForUser( 37 userId: number, 38 isAdmin: boolean, 39): ClassWithStats[] { 40 if (isAdmin) { 41 return db 42 .query<ClassWithStats, []>( 43 `SELECT 44 c.*, 45 (SELECT COUNT(*) FROM class_members WHERE class_id = c.id) as student_count, 46 (SELECT COUNT(*) FROM transcriptions WHERE class_id = c.id) as transcript_count 47 FROM classes c 48 ORDER BY c.year DESC, c.semester DESC, c.course_code ASC`, 49 ) 50 .all(); 51 } 52 53 return db 54 .query<ClassWithStats, [number]>( 55 `SELECT c.* FROM classes c 56 INNER JOIN class_members cm ON c.id = cm.class_id 57 WHERE cm.user_id = ? 58 ORDER BY c.year DESC, c.semester DESC, c.course_code ASC`, 59 ) 60 .all(userId); 61} 62 63/** 64 * Get a single class by ID 65 */ 66export function getClassById(classId: string): Class | null { 67 const result = db 68 .query<Class, [string]>("SELECT * FROM classes WHERE id = ?") 69 .get(classId); 70 return result ?? null; 71} 72 73/** 74 * Check if user is enrolled in a class 75 */ 76export function isUserEnrolledInClass( 77 userId: number, 78 classId: string, 79): boolean { 80 const result = db 81 .query<{ count: number }, [string, number]>( 82 "SELECT COUNT(*) as count FROM class_members WHERE class_id = ? AND user_id = ?", 83 ) 84 .get(classId, userId); 85 return (result?.count ?? 0) > 0; 86} 87 88/** 89 * Create a new class 90 */ 91export function createClass(data: { 92 course_code: string; 93 name: string; 94 professor: string; 95 semester: string; 96 year: number; 97 meeting_times?: string[]; 98}): Class { 99 const id = nanoid(); 100 const now = Math.floor(Date.now() / 1000); 101 102 db.run( 103 "INSERT INTO classes (id, course_code, name, professor, semester, year, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)", 104 [ 105 id, 106 data.course_code, 107 data.name, 108 data.professor, 109 data.semester, 110 data.year, 111 now, 112 ], 113 ); 114 115 // Create meeting times if provided 116 if (data.meeting_times && data.meeting_times.length > 0) { 117 for (const label of data.meeting_times) { 118 createMeetingTime(id, label); 119 } 120 } 121 122 return { 123 id, 124 course_code: data.course_code, 125 name: data.name, 126 professor: data.professor, 127 semester: data.semester, 128 year: data.year, 129 archived: false, 130 created_at: now, 131 }; 132} 133 134/** 135 * Archive or unarchive a class 136 */ 137export function toggleClassArchive(classId: string, archived: boolean): void { 138 db.run("UPDATE classes SET archived = ? WHERE id = ?", [ 139 archived ? 1 : 0, 140 classId, 141 ]); 142} 143 144/** 145 * Delete a class (cascades to members, meeting times, and transcriptions) 146 */ 147export function deleteClass(classId: string): void { 148 db.run("DELETE FROM classes WHERE id = ?", [classId]); 149} 150 151/** 152 * Enroll a user in a class 153 */ 154export function enrollUserInClass(userId: number, classId: string): void { 155 const now = Math.floor(Date.now() / 1000); 156 db.run( 157 "INSERT OR IGNORE INTO class_members (class_id, user_id, enrolled_at) VALUES (?, ?, ?)", 158 [classId, userId, now], 159 ); 160} 161 162/** 163 * Remove a user from a class 164 */ 165export function removeUserFromClass(userId: number, classId: string): void { 166 db.run("DELETE FROM class_members WHERE class_id = ? AND user_id = ?", [ 167 classId, 168 userId, 169 ]); 170} 171 172/** 173 * Get all members of a class 174 */ 175export function getClassMembers(classId: string) { 176 return db 177 .query< 178 { 179 user_id: number; 180 email: string; 181 name: string | null; 182 avatar: string; 183 enrolled_at: number; 184 }, 185 [string] 186 >( 187 `SELECT cm.user_id, u.email, u.name, u.avatar, cm.enrolled_at 188 FROM class_members cm 189 INNER JOIN users u ON cm.user_id = u.id 190 WHERE cm.class_id = ? 191 ORDER BY cm.enrolled_at DESC`, 192 ) 193 .all(classId); 194} 195 196/** 197 * Create a meeting time for a class 198 */ 199export function createMeetingTime(classId: string, label: string): MeetingTime { 200 const id = nanoid(); 201 const now = Math.floor(Date.now() / 1000); 202 203 db.run( 204 "INSERT INTO meeting_times (id, class_id, label, created_at) VALUES (?, ?, ?, ?)", 205 [id, classId, label, now], 206 ); 207 208 return { 209 id, 210 class_id: classId, 211 label, 212 created_at: now, 213 }; 214} 215 216/** 217 * Get all meeting times for a class 218 */ 219export function getMeetingTimesForClass(classId: string): MeetingTime[] { 220 return db 221 .query<MeetingTime, [string]>( 222 "SELECT * FROM meeting_times WHERE class_id = ? ORDER BY created_at ASC", 223 ) 224 .all(classId); 225} 226 227/** 228 * Update a meeting time label 229 */ 230export function updateMeetingTime(meetingId: string, label: string): void { 231 db.run("UPDATE meeting_times SET label = ? WHERE id = ?", [label, meetingId]); 232} 233 234/** 235 * Delete a meeting time 236 */ 237export function deleteMeetingTime(meetingId: string): void { 238 db.run("DELETE FROM meeting_times WHERE id = ?", [meetingId]); 239} 240 241/** 242 * Get transcriptions for a class 243 */ 244export function getTranscriptionsForClass(classId: string) { 245 return db 246 .query< 247 { 248 id: string; 249 user_id: number; 250 meeting_time_id: string | null; 251 filename: string; 252 original_filename: string; 253 status: string; 254 progress: number; 255 error_message: string | null; 256 created_at: number; 257 updated_at: number; 258 }, 259 [string] 260 >( 261 `SELECT id, user_id, meeting_time_id, filename, original_filename, status, progress, error_message, created_at, updated_at 262 FROM transcriptions 263 WHERE class_id = ? 264 ORDER BY created_at DESC`, 265 ) 266 .all(classId); 267} 268 269/** 270 * Search for classes by course code 271 */ 272export function searchClassesByCourseCode(courseCode: string): Class[] { 273 return db 274 .query<Class, [string]>( 275 `SELECT * FROM classes 276 WHERE UPPER(course_code) LIKE UPPER(?) 277 AND archived = 0 278 ORDER BY year DESC, semester DESC, professor ASC`, 279 ) 280 .all(`%${courseCode}%`); 281} 282 283/** 284 * Join a class by class ID 285 */ 286export function joinClass( 287 classId: string, 288 userId: number, 289): { success: boolean; error?: string } { 290 // Find class by ID 291 const cls = db 292 .query<Class, [string]>("SELECT * FROM classes WHERE id = ?") 293 .get(classId); 294 295 if (!cls) { 296 return { success: false, error: "Class not found" }; 297 } 298 299 if (cls.archived) { 300 return { success: false, error: "This class is archived" }; 301 } 302 303 // Check if already enrolled 304 const existing = db 305 .query<ClassMember, [string, number]>( 306 "SELECT * FROM class_members WHERE class_id = ? AND user_id = ?", 307 ) 308 .get(cls.id, userId); 309 310 if (existing) { 311 return { success: false, error: "Already enrolled in this class" }; 312 } 313 314 // Enroll user 315 db.query( 316 "INSERT INTO class_members (class_id, user_id, enrolled_at) VALUES (?, ?, ?)", 317 ).run(cls.id, userId, Math.floor(Date.now() / 1000)); 318 319 return { success: true }; 320} 321 322/** 323 * Waitlist entry interface 324 */ 325export interface WaitlistEntry { 326 id: string; 327 user_id: number; 328 course_code: string; 329 course_name: string; 330 professor: string; 331 semester: string; 332 year: number; 333 additional_info: string | null; 334 meeting_times: string | null; 335 created_at: number; 336} 337 338/** 339 * Add a class to the waitlist 340 */ 341export function addToWaitlist( 342 userId: number, 343 courseCode: string, 344 courseName: string, 345 professor: string, 346 semester: string, 347 year: number, 348 additionalInfo: string | null, 349 meetingTimes: string[] | null, 350): string { 351 const id = nanoid(); 352 const meetingTimesJson = meetingTimes ? JSON.stringify(meetingTimes) : null; 353 354 db.query( 355 `INSERT INTO class_waitlist 356 (id, user_id, course_code, course_name, professor, semester, year, additional_info, meeting_times, created_at) 357 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, 358 ).run( 359 id, 360 userId, 361 courseCode, 362 courseName, 363 professor, 364 semester, 365 year, 366 additionalInfo, 367 meetingTimesJson, 368 Math.floor(Date.now() / 1000), 369 ); 370 return id; 371} 372 373/** 374 * Get all waitlist entries 375 */ 376export function getAllWaitlistEntries(): WaitlistEntry[] { 377 return db 378 .query<WaitlistEntry, []>( 379 "SELECT * FROM class_waitlist ORDER BY created_at DESC", 380 ) 381 .all(); 382} 383 384/** 385 * Delete a waitlist entry 386 */ 387export function deleteWaitlistEntry(id: string): void { 388 db.query("DELETE FROM class_waitlist WHERE id = ?").run(id); 389}