馃 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 const result = db.run("UPDATE classes SET archived = ? WHERE id = ?", [
139 archived ? 1 : 0,
140 classId,
141 ]);
142
143 if (result.changes === 0) {
144 throw new Error("Class not found");
145 }
146}
147
148/**
149 * Delete a class (cascades to members, meeting times, and transcriptions)
150 */
151export function deleteClass(classId: string): void {
152 db.run("DELETE FROM classes WHERE id = ?", [classId]);
153}
154
155/**
156 * Enroll a user in a class
157 */
158export function enrollUserInClass(userId: number, classId: string): void {
159 const now = Math.floor(Date.now() / 1000);
160 db.run(
161 "INSERT OR IGNORE INTO class_members (class_id, user_id, enrolled_at) VALUES (?, ?, ?)",
162 [classId, userId, now],
163 );
164}
165
166/**
167 * Remove a user from a class
168 */
169export function removeUserFromClass(userId: number, classId: string): void {
170 db.run("DELETE FROM class_members WHERE class_id = ? AND user_id = ?", [
171 classId,
172 userId,
173 ]);
174}
175
176/**
177 * Get all members of a class
178 */
179export function getClassMembers(classId: string) {
180 return db
181 .query<
182 {
183 user_id: number;
184 email: string;
185 name: string | null;
186 avatar: string;
187 enrolled_at: number;
188 },
189 [string]
190 >(
191 `SELECT cm.user_id, u.email, u.name, u.avatar, cm.enrolled_at
192 FROM class_members cm
193 INNER JOIN users u ON cm.user_id = u.id
194 WHERE cm.class_id = ?
195 ORDER BY cm.enrolled_at DESC`,
196 )
197 .all(classId);
198}
199
200/**
201 * Create a meeting time for a class
202 */
203export function createMeetingTime(classId: string, label: string): MeetingTime {
204 const id = nanoid();
205 const now = Math.floor(Date.now() / 1000);
206
207 db.run(
208 "INSERT INTO meeting_times (id, class_id, label, created_at) VALUES (?, ?, ?, ?)",
209 [id, classId, label, now],
210 );
211
212 return {
213 id,
214 class_id: classId,
215 label,
216 created_at: now,
217 };
218}
219
220/**
221 * Get all meeting times for a class
222 */
223export function getMeetingTimesForClass(classId: string): MeetingTime[] {
224 return db
225 .query<MeetingTime, [string]>(
226 "SELECT * FROM meeting_times WHERE class_id = ? ORDER BY created_at ASC",
227 )
228 .all(classId);
229}
230
231/**
232 * Update a meeting time label
233 */
234export function updateMeetingTime(meetingId: string, label: string): void {
235 db.run("UPDATE meeting_times SET label = ? WHERE id = ?", [label, meetingId]);
236}
237
238/**
239 * Delete a meeting time
240 */
241export function deleteMeetingTime(meetingId: string): void {
242 db.run("DELETE FROM meeting_times WHERE id = ?", [meetingId]);
243}
244
245/**
246 * Get transcriptions for a class
247 */
248export function getTranscriptionsForClass(classId: string) {
249 return db
250 .query<
251 {
252 id: string;
253 user_id: number;
254 meeting_time_id: string | null;
255 filename: string;
256 original_filename: string;
257 status: string;
258 progress: number;
259 error_message: string | null;
260 created_at: number;
261 updated_at: number;
262 },
263 [string]
264 >(
265 `SELECT id, user_id, meeting_time_id, filename, original_filename, status, progress, error_message, created_at, updated_at
266 FROM transcriptions
267 WHERE class_id = ?
268 ORDER BY created_at DESC`,
269 )
270 .all(classId);
271}
272
273/**
274 * Search for classes by course code
275 */
276export function searchClassesByCourseCode(courseCode: string): Class[] {
277 return db
278 .query<Class, [string]>(
279 `SELECT * FROM classes
280 WHERE UPPER(course_code) LIKE UPPER(?)
281 AND archived = 0
282 ORDER BY year DESC, semester DESC, professor ASC`,
283 )
284 .all(`%${courseCode}%`);
285}
286
287/**
288 * Join a class by class ID
289 */
290export function joinClass(
291 classId: string,
292 userId: number,
293): { success: boolean; error?: string } {
294 // Find class by ID
295 const cls = db
296 .query<Class, [string]>("SELECT * FROM classes WHERE id = ?")
297 .get(classId);
298
299 if (!cls) {
300 return { success: false, error: "Class not found" };
301 }
302
303 if (cls.archived) {
304 return { success: false, error: "This class is archived" };
305 }
306
307 // Check if already enrolled
308 const existing = db
309 .query<ClassMember, [string, number]>(
310 "SELECT * FROM class_members WHERE class_id = ? AND user_id = ?",
311 )
312 .get(cls.id, userId);
313
314 if (existing) {
315 return { success: false, error: "Already enrolled in this class" };
316 }
317
318 // Enroll user
319 db.query(
320 "INSERT INTO class_members (class_id, user_id, enrolled_at) VALUES (?, ?, ?)",
321 ).run(cls.id, userId, Math.floor(Date.now() / 1000));
322
323 return { success: true };
324}
325
326/**
327 * Waitlist entry interface
328 */
329export interface WaitlistEntry {
330 id: string;
331 user_id: number;
332 course_code: string;
333 course_name: string;
334 professor: string;
335 semester: string;
336 year: number;
337 additional_info: string | null;
338 meeting_times: string | null;
339 created_at: number;
340}
341
342/**
343 * Add a class to the waitlist
344 */
345export function addToWaitlist(
346 userId: number,
347 courseCode: string,
348 courseName: string,
349 professor: string,
350 semester: string,
351 year: number,
352 additionalInfo: string | null,
353 meetingTimes: string[] | null,
354): string {
355 const id = nanoid();
356 const meetingTimesJson = meetingTimes ? JSON.stringify(meetingTimes) : null;
357
358 db.query(
359 `INSERT INTO class_waitlist
360 (id, user_id, course_code, course_name, professor, semester, year, additional_info, meeting_times, created_at)
361 VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
362 ).run(
363 id,
364 userId,
365 courseCode,
366 courseName,
367 professor,
368 semester,
369 year,
370 additionalInfo,
371 meetingTimesJson,
372 Math.floor(Date.now() / 1000),
373 );
374 return id;
375}
376
377/**
378 * Get all waitlist entries
379 */
380export function getAllWaitlistEntries(): WaitlistEntry[] {
381 return db
382 .query<WaitlistEntry, []>(
383 "SELECT * FROM class_waitlist ORDER BY created_at DESC",
384 )
385 .all();
386}
387
388/**
389 * Delete a waitlist entry
390 */
391export function deleteWaitlistEntry(id: string): void {
392 db.query("DELETE FROM class_waitlist WHERE id = ?").run(id);
393}