···
+
import { css, html, LitElement } from "lit";
+
import { customElement, state } from "lit/decorators.js";
+
interface PendingRecording {
+
original_filename: string;
+
user_name: string | null;
+
meeting_time_id: string | null;
+
meeting_label: string | null;
+
@customElement("admin-pending-recordings")
+
export class AdminPendingRecordings extends LitElement {
+
@state() recordings: PendingRecording[] = [];
+
@state() isLoading = true;
+
@state() error: string | null = null;
+
static override styles = css`
+
color: var(--paynes-gray);
+
background: color-mix(in srgb, red 10%, transparent);
+
border-collapse: collapse;
+
background: var(--background);
+
border: 2px solid var(--secondary);
+
background: var(--primary);
+
border-top: 1px solid var(--secondary);
+
background: color-mix(in srgb, var(--primary) 5%, transparent);
+
flex-direction: column;
+
background: color-mix(in srgb, var(--primary) 10%, transparent);
+
padding: 0.25rem 0.5rem;
+
color: var(--paynes-gray);
+
background: var(--accent);
+
transition: opacity 0.2s;
+
.approve-btn:hover:not(:disabled) {
+
.approve-btn:disabled {
+
background: transparent;
+
border: 2px solid #dc2626;
+
.delete-btn:hover:not(:disabled) {
+
override async connectedCallback() {
+
super.connectedCallback();
+
await this.loadRecordings();
+
private async loadRecordings() {
+
// Get all classes with their transcriptions
+
const response = await fetch("/api/classes");
+
throw new Error("Failed to load classes");
+
const data = await response.json();
+
const classesGrouped = data.classes || {};
+
const allClasses: any[] = [];
+
for (const classes of Object.values(classesGrouped)) {
+
allClasses.push(...(classes as any[]));
+
// Fetch transcriptions for each class
+
const pendingRecordings: PendingRecording[] = [];
+
allClasses.map(async (cls) => {
+
const classResponse = await fetch(`/api/classes/${cls.id}`);
+
if (!classResponse.ok) return;
+
const classData = await classResponse.json();
+
const pendingTranscriptions = (classData.transcriptions || []).filter(
+
(t: any) => t.status === "pending",
+
for (const transcription of pendingTranscriptions) {
+
const userResponse = await fetch(
+
`/api/admin/transcriptions/${transcription.id}/details`,
+
if (!userResponse.ok) continue;
+
const transcriptionDetails = await userResponse.json();
+
const meetingTime = classData.meetingTimes.find(
+
(m: any) => m.id === transcription.meeting_time_id,
+
pendingRecordings.push({
+
original_filename: transcription.original_filename,
+
user_id: transcriptionDetails.user_id,
+
user_name: transcriptionDetails.user_name,
+
user_email: transcriptionDetails.user_email,
+
course_code: cls.course_code,
+
meeting_time_id: transcription.meeting_time_id,
+
meeting_label: meetingTime?.label || null,
+
created_at: transcription.created_at,
+
status: transcription.status,
+
console.error(`Failed to load class ${cls.id}:`, error);
+
// Sort by created_at descending
+
pendingRecordings.sort((a, b) => b.created_at - a.created_at);
+
this.recordings = pendingRecordings;
+
console.error("Failed to load pending recordings:", error);
+
this.error = "Failed to load pending recordings. Please try again.";
+
this.isLoading = false;
+
private async handleApprove(recordingId: string) {
+
const response = await fetch(`/api/transcripts/${recordingId}/select`, {
+
throw new Error("Failed to approve recording");
+
await this.loadRecordings();
+
console.error("Failed to approve recording:", error);
+
alert("Failed to approve recording. Please try again.");
+
private async handleDelete(recordingId: string) {
+
"Are you sure you want to delete this recording? This cannot be undone.",
+
const response = await fetch(`/api/admin/transcriptions/${recordingId}`, {
+
throw new Error("Failed to delete recording");
+
await this.loadRecordings();
+
console.error("Failed to delete recording:", error);
+
alert("Failed to delete recording. Please try again.");
+
private formatTimestamp(timestamp: number): string {
+
const date = new Date(timestamp * 1000);
+
return date.toLocaleString();
+
return html`<div class="loading">Loading pending recordings...</div>`;
+
<div class="error">${this.error}</div>
+
<button @click=${this.loadRecordings}>Retry</button>
+
if (this.recordings.length === 0) {
+
<div class="empty-state">
+
<p>No pending recordings</p>
+
<td>${recording.original_filename}</td>
+
<div class="class-info">
+
<span class="course-code">${recording.course_code}</span>
+
<span class="class-name">${recording.class_name}</span>
+
recording.meeting_label
+
? html`<span class="meeting-label">${recording.meeting_label}</span>`
+
: html`<span style="color: var(--paynes-gray); font-style: italic;">Not specified</span>`
+
<div class="user-info">
+
src="https://hostedboringavatars.vercel.app/api/marble?size=32&name=${recording.user_id}&colors=2d3142ff,4f5d75ff,bfc0c0ff,ef8354ff"
+
<span>${recording.user_name || recording.user_email}</span>
+
<td class="timestamp">${this.formatTimestamp(recording.created_at)}</td>
+
<button class="approve-btn" @click=${() => this.handleApprove(recording.id)}>
+
<button class="delete-btn" @click=${() => this.handleDelete(recording.id)}>