303 lines
8.2 KiB
JavaScript
303 lines
8.2 KiB
JavaScript
import { redirect, error } from '@sveltejs/kit';
|
|
import { db } from '$lib/server/db/index.js';
|
|
import { audioFile, rating, participantProgress, participant, overallRating } from '$lib/server/db/schema.js';
|
|
import { eq, and, isNull } from 'drizzle-orm';
|
|
|
|
export async function load({ params, url, cookies }) {
|
|
const audioId = params.id;
|
|
const token = url.searchParams.get('token');
|
|
|
|
if (!token) {
|
|
throw error(400, 'Invalid or missing invite token');
|
|
}
|
|
|
|
const participantId = cookies.get(`participant-${token}`);
|
|
if (!participantId) {
|
|
redirect(302, `/participate?token=${token}`);
|
|
}
|
|
|
|
// Verify participant exists and isn't soft-deleted
|
|
const participants = await db
|
|
.select()
|
|
.from(participant)
|
|
.where(
|
|
and(
|
|
eq(participant.id, participantId),
|
|
isNull(participant.deletedAt)
|
|
)
|
|
);
|
|
|
|
if (participants.length === 0) {
|
|
redirect(302, `/participate?token=${token}`);
|
|
}
|
|
|
|
const audioFiles = await db.select({
|
|
id: audioFile.id,
|
|
filename: audioFile.filename,
|
|
contentType: audioFile.contentType,
|
|
duration: audioFile.duration,
|
|
fileSize: audioFile.fileSize,
|
|
createdAt: audioFile.createdAt
|
|
})
|
|
.from(audioFile)
|
|
.where(and(
|
|
eq(audioFile.id, audioId),
|
|
isNull(audioFile.deletedAt) // Only show active audio files
|
|
));
|
|
|
|
if (audioFiles.length === 0) {
|
|
throw error(404, 'Audio file not found');
|
|
}
|
|
|
|
const progressData = await db
|
|
.select({
|
|
id: participantProgress.id,
|
|
isCompleted: participantProgress.isCompleted,
|
|
lastPosition: participantProgress.lastPosition,
|
|
maxReachedTime: participantProgress.maxReachedTime,
|
|
updatedAt: participantProgress.updatedAt
|
|
})
|
|
.from(participantProgress)
|
|
.where(
|
|
and(
|
|
eq(participantProgress.participantId, participantId),
|
|
eq(participantProgress.audioFileId, audioId)
|
|
)
|
|
);
|
|
|
|
const progress = progressData.length > 0 ? progressData[0] : null;
|
|
|
|
const existingRatings = await db
|
|
.select()
|
|
.from(rating)
|
|
.where(and(
|
|
eq(rating.participantId, participantId),
|
|
eq(rating.audioFileId, audioId),
|
|
isNull(rating.deletedAt)
|
|
));
|
|
|
|
return {
|
|
audioFile: audioFiles[0],
|
|
participantId,
|
|
token,
|
|
progress,
|
|
existingRatings
|
|
};
|
|
}
|
|
|
|
export const actions = {
|
|
saveRating: async ({ request }) => {
|
|
const data = await request.formData();
|
|
const participantId = data.get('participantId');
|
|
const audioFileId = data.get('audioFileId');
|
|
const ratingHistoryStr = data.get('ratingHistory');
|
|
const finalValue = parseFloat(data.get('finalValue'));
|
|
const maxReachedTime = parseFloat(data.get('maxReachedTime')) || 0;
|
|
const currentPosition = parseFloat(data.get('currentPosition')) || 0;
|
|
|
|
if (!participantId || !audioFileId || !ratingHistoryStr || isNaN(finalValue)) {
|
|
return { error: 'Invalid rating data' };
|
|
}
|
|
|
|
try {
|
|
const ratingHistory = JSON.parse(ratingHistoryStr);
|
|
console.log('Rating history length:', ratingHistory.length);
|
|
console.log('Rating history:', ratingHistory);
|
|
|
|
// Use a transaction to ensure atomicity
|
|
await db.transaction(async (tx) => {
|
|
// Soft delete any existing ratings for this participant and audio file (for redo functionality)
|
|
await tx.update(rating)
|
|
.set({ deletedAt: new Date() })
|
|
.where(
|
|
and(
|
|
eq(rating.participantId, participantId),
|
|
eq(rating.audioFileId, audioFileId),
|
|
isNull(rating.deletedAt)
|
|
)
|
|
);
|
|
|
|
// Save single completed rating record with entire timeseries as JSON
|
|
const ratingId = `${participantId}-${audioFileId}-${Date.now()}`;
|
|
const finalTimestamp = ratingHistory[ratingHistory.length - 1]?.timestamp || 0;
|
|
|
|
await tx.insert(rating).values({
|
|
id: ratingId,
|
|
participantId,
|
|
audioFileId,
|
|
timestamp: finalTimestamp,
|
|
value: finalValue,
|
|
isCompleted: true,
|
|
timeseriesData: JSON.stringify(ratingHistory), // Store entire timeseries as JSON
|
|
createdAt: new Date(),
|
|
deletedAt: null // Active rating
|
|
});
|
|
});
|
|
|
|
// Save listening progress (only when rating is saved)
|
|
const progressId = `${participantId}-${audioFileId}`;
|
|
const existingProgress = await db
|
|
.select()
|
|
.from(participantProgress)
|
|
.where(
|
|
and(
|
|
eq(participantProgress.participantId, participantId),
|
|
eq(participantProgress.audioFileId, audioFileId)
|
|
)
|
|
);
|
|
|
|
if (existingProgress.length > 0) {
|
|
await db
|
|
.update(participantProgress)
|
|
.set({
|
|
lastPosition: currentPosition,
|
|
maxReachedTime: maxReachedTime,
|
|
isCompleted: true, // Mark as completed when rating is saved
|
|
updatedAt: new Date()
|
|
})
|
|
.where(eq(participantProgress.id, existingProgress[0].id));
|
|
} else {
|
|
await db.insert(participantProgress).values({
|
|
id: progressId,
|
|
participantId,
|
|
audioFileId,
|
|
lastPosition: currentPosition,
|
|
maxReachedTime: maxReachedTime,
|
|
isCompleted: true,
|
|
updatedAt: new Date()
|
|
});
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Error saving rating:', error);
|
|
return { error: 'Failed to save rating' };
|
|
}
|
|
},
|
|
|
|
deleteRating: async ({ request }) => {
|
|
const data = await request.formData();
|
|
const participantId = data.get('participantId');
|
|
const audioFileId = data.get('audioFileId');
|
|
|
|
if (!participantId || !audioFileId) {
|
|
return { error: 'Invalid request data' };
|
|
}
|
|
|
|
try {
|
|
// Soft delete the active rating for this participant and audio file
|
|
await db.update(rating)
|
|
.set({ deletedAt: new Date() })
|
|
.where(
|
|
and(
|
|
eq(rating.participantId, participantId),
|
|
eq(rating.audioFileId, audioFileId),
|
|
isNull(rating.deletedAt)
|
|
)
|
|
);
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Error deleting rating:', error);
|
|
return { error: 'Failed to delete rating' };
|
|
}
|
|
},
|
|
|
|
updateProgress: async ({ request }) => {
|
|
const data = await request.formData();
|
|
const participantId = data.get('participantId');
|
|
const audioFileId = data.get('audioFileId');
|
|
const lastPosition = parseFloat(data.get('lastPosition'));
|
|
const isCompleted = data.get('isCompleted') === 'true';
|
|
|
|
if (!participantId || !audioFileId || isNaN(lastPosition)) {
|
|
return { error: 'Invalid progress data' };
|
|
}
|
|
|
|
try {
|
|
const progressId = `${participantId}-${audioFileId}`;
|
|
|
|
const existingProgress = await db
|
|
.select()
|
|
.from(participantProgress)
|
|
.where(
|
|
and(
|
|
eq(participantProgress.participantId, participantId),
|
|
eq(participantProgress.audioFileId, audioFileId)
|
|
)
|
|
);
|
|
|
|
if (existingProgress.length > 0) {
|
|
await db
|
|
.update(participantProgress)
|
|
.set({
|
|
lastPosition,
|
|
isCompleted,
|
|
updatedAt: new Date()
|
|
})
|
|
.where(eq(participantProgress.id, existingProgress[0].id));
|
|
} else {
|
|
await db.insert(participantProgress).values({
|
|
id: progressId,
|
|
participantId,
|
|
audioFileId,
|
|
isCompleted,
|
|
lastPosition,
|
|
updatedAt: new Date()
|
|
});
|
|
}
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Error updating progress:', error);
|
|
return { error: 'Failed to update progress' };
|
|
}
|
|
},
|
|
|
|
saveOverallRating: async ({ request }) => {
|
|
const data = await request.formData();
|
|
const participantId = data.get('participantId');
|
|
const audioFileId = data.get('audioFileId');
|
|
const overallRatingValue = parseFloat(data.get('overallRating'));
|
|
|
|
if (!participantId || !audioFileId || isNaN(overallRatingValue)) {
|
|
return { error: 'Invalid overall rating data' };
|
|
}
|
|
|
|
try {
|
|
const overallRatingId = `${participantId}-${audioFileId}-overall-${Date.now()}`;
|
|
const now = new Date();
|
|
|
|
// Use a transaction to ensure atomicity (same pattern as saveRating)
|
|
await db.transaction(async (tx) => {
|
|
// Soft delete any existing overall ratings for this participant and audio file (for redo functionality)
|
|
await tx.update(overallRating)
|
|
.set({ deletedAt: now })
|
|
.where(
|
|
and(
|
|
eq(overallRating.participantId, participantId),
|
|
eq(overallRating.audioFileId, audioFileId),
|
|
isNull(overallRating.deletedAt)
|
|
)
|
|
);
|
|
|
|
// Always insert new overall rating record
|
|
await tx.insert(overallRating).values({
|
|
id: overallRatingId,
|
|
participantId,
|
|
audioFileId,
|
|
value: overallRatingValue,
|
|
createdAt: now,
|
|
updatedAt: now,
|
|
deletedAt: null
|
|
});
|
|
});
|
|
|
|
return { success: true };
|
|
} catch (error) {
|
|
console.error('Error saving overall rating:', error);
|
|
return { error: 'Failed to save overall rating' };
|
|
}
|
|
}
|
|
};
|