···
1
+
import { css, html, LitElement } from "lit";
2
+
import { customElement, state } from "lit/decorators.js";
4
+
interface PendingRecording {
6
+
original_filename: string;
8
+
user_name: string | null;
12
+
course_code: string;
13
+
meeting_time_id: string | null;
14
+
meeting_label: string | null;
19
+
@customElement("admin-pending-recordings")
20
+
export class AdminPendingRecordings extends LitElement {
21
+
@state() recordings: PendingRecording[] = [];
22
+
@state() isLoading = true;
23
+
@state() error: string | null = null;
25
+
static override styles = css`
34
+
color: var(--paynes-gray);
38
+
background: color-mix(in srgb, red 10%, transparent);
39
+
border: 1px solid red;
43
+
margin-bottom: 1rem;
48
+
border-collapse: collapse;
49
+
background: var(--background);
50
+
border: 2px solid var(--secondary);
56
+
background: var(--primary);
57
+
color: var(--white);
68
+
border-top: 1px solid var(--secondary);
73
+
background: color-mix(in srgb, var(--primary) 5%, transparent);
78
+
flex-direction: column;
84
+
color: var(--accent);
85
+
font-size: 0.875rem;
89
+
font-size: 0.875rem;
94
+
display: inline-block;
95
+
background: color-mix(in srgb, var(--primary) 10%, transparent);
96
+
color: var(--primary);
97
+
padding: 0.25rem 0.5rem;
105
+
align-items: center;
112
+
border-radius: 50%;
116
+
color: var(--paynes-gray);
117
+
font-size: 0.875rem;
121
+
background: var(--accent);
122
+
color: var(--white);
124
+
padding: 0.5rem 1rem;
125
+
border-radius: 4px;
127
+
font-size: 0.875rem;
129
+
transition: opacity 0.2s;
132
+
.approve-btn:hover:not(:disabled) {
136
+
.approve-btn:disabled {
138
+
cursor: not-allowed;
147
+
background: transparent;
148
+
border: 2px solid #dc2626;
150
+
padding: 0.5rem 1rem;
151
+
border-radius: 4px;
153
+
font-size: 0.875rem;
155
+
transition: all 0.2s;
158
+
.delete-btn:hover:not(:disabled) {
159
+
background: #dc2626;
160
+
color: var(--white);
163
+
.delete-btn:disabled {
165
+
cursor: not-allowed;
169
+
override async connectedCallback() {
170
+
super.connectedCallback();
171
+
await this.loadRecordings();
174
+
private async loadRecordings() {
175
+
this.isLoading = true;
179
+
// Get all classes with their transcriptions
180
+
const response = await fetch("/api/classes");
181
+
if (!response.ok) {
182
+
throw new Error("Failed to load classes");
185
+
const data = await response.json();
186
+
const classesGrouped = data.classes || {};
188
+
// Flatten all classes
189
+
const allClasses: any[] = [];
190
+
for (const classes of Object.values(classesGrouped)) {
191
+
allClasses.push(...(classes as any[]));
194
+
// Fetch transcriptions for each class
195
+
const pendingRecordings: PendingRecording[] = [];
198
+
allClasses.map(async (cls) => {
200
+
const classResponse = await fetch(`/api/classes/${cls.id}`);
201
+
if (!classResponse.ok) return;
203
+
const classData = await classResponse.json();
204
+
const pendingTranscriptions = (classData.transcriptions || []).filter(
205
+
(t: any) => t.status === "pending",
208
+
for (const transcription of pendingTranscriptions) {
210
+
const userResponse = await fetch(
211
+
`/api/admin/transcriptions/${transcription.id}/details`,
213
+
if (!userResponse.ok) continue;
215
+
const transcriptionDetails = await userResponse.json();
217
+
// Find meeting label
218
+
const meetingTime = classData.meetingTimes.find(
219
+
(m: any) => m.id === transcription.meeting_time_id,
222
+
pendingRecordings.push({
223
+
id: transcription.id,
224
+
original_filename: transcription.original_filename,
225
+
user_id: transcriptionDetails.user_id,
226
+
user_name: transcriptionDetails.user_name,
227
+
user_email: transcriptionDetails.user_email,
229
+
class_name: cls.name,
230
+
course_code: cls.course_code,
231
+
meeting_time_id: transcription.meeting_time_id,
232
+
meeting_label: meetingTime?.label || null,
233
+
created_at: transcription.created_at,
234
+
status: transcription.status,
238
+
console.error(`Failed to load class ${cls.id}:`, error);
243
+
// Sort by created_at descending
244
+
pendingRecordings.sort((a, b) => b.created_at - a.created_at);
246
+
this.recordings = pendingRecordings;
248
+
console.error("Failed to load pending recordings:", error);
249
+
this.error = "Failed to load pending recordings. Please try again.";
251
+
this.isLoading = false;
255
+
private async handleApprove(recordingId: string) {
257
+
const response = await fetch(`/api/transcripts/${recordingId}/select`, {
261
+
if (!response.ok) {
262
+
throw new Error("Failed to approve recording");
265
+
// Reload recordings
266
+
await this.loadRecordings();
268
+
console.error("Failed to approve recording:", error);
269
+
alert("Failed to approve recording. Please try again.");
273
+
private async handleDelete(recordingId: string) {
276
+
"Are you sure you want to delete this recording? This cannot be undone.",
283
+
const response = await fetch(`/api/admin/transcriptions/${recordingId}`, {
287
+
if (!response.ok) {
288
+
throw new Error("Failed to delete recording");
291
+
// Reload recordings
292
+
await this.loadRecordings();
294
+
console.error("Failed to delete recording:", error);
295
+
alert("Failed to delete recording. Please try again.");
299
+
private formatTimestamp(timestamp: number): string {
300
+
const date = new Date(timestamp * 1000);
301
+
return date.toLocaleString();
304
+
override render() {
305
+
if (this.isLoading) {
306
+
return html`<div class="loading">Loading pending recordings...</div>`;
311
+
<div class="error">${this.error}</div>
312
+
<button @click=${this.loadRecordings}>Retry</button>
316
+
if (this.recordings.length === 0) {
318
+
<div class="empty-state">
319
+
<p>No pending recordings</p>
330
+
<th>Meeting Time</th>
331
+
<th>Uploaded By</th>
332
+
<th>Uploaded At</th>
337
+
${this.recordings.map(
338
+
(recording) => html`
340
+
<td>${recording.original_filename}</td>
342
+
<div class="class-info">
343
+
<span class="course-code">${recording.course_code}</span>
344
+
<span class="class-name">${recording.class_name}</span>
349
+
recording.meeting_label
350
+
? html`<span class="meeting-label">${recording.meeting_label}</span>`
351
+
: html`<span style="color: var(--paynes-gray); font-style: italic;">Not specified</span>`
355
+
<div class="user-info">
357
+
src="https://hostedboringavatars.vercel.app/api/marble?size=32&name=${recording.user_id}&colors=2d3142ff,4f5d75ff,bfc0c0ff,ef8354ff"
359
+
class="user-avatar"
361
+
<span>${recording.user_name || recording.user_email}</span>
364
+
<td class="timestamp">${this.formatTimestamp(recording.created_at)}</td>
366
+
<div class="actions">
367
+
<button class="approve-btn" @click=${() => this.handleApprove(recording.id)}>
368
+
✓ Approve & Transcribe
370
+
<button class="delete-btn" @click=${() => this.handleDelete(recording.id)}>