#2 - reset user ratings
This commit is contained in:
		
							parent
							
								
									dfa45436d4
								
							
						
					
					
						commit
						209e05f959
					
				
					 8 changed files with 142 additions and 25 deletions
				
			
		
							
								
								
									
										7
									
								
								src/db/answers.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								src/db/answers.ts
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,7 @@
 | 
				
			||||||
 | 
					import { eq } from "drizzle-orm";
 | 
				
			||||||
 | 
					import { db } from ".";
 | 
				
			||||||
 | 
					import { surveyAnswersTable } from "./schema";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export async function deleteAnswers(participandId: number) {
 | 
				
			||||||
 | 
					    await db.delete(surveyAnswersTable).where(eq(surveyAnswersTable.participantId, participandId));
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										33
									
								
								src/lib/components/WarningDialog.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								src/lib/components/WarningDialog.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,33 @@
 | 
				
			||||||
 | 
					<script lang="ts">
 | 
				
			||||||
 | 
						import type { Snippet } from 'svelte';
 | 
				
			||||||
 | 
						import type { HTMLDialogAttributes } from 'svelte/elements';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						type Props = {
 | 
				
			||||||
 | 
							title: string;
 | 
				
			||||||
 | 
							dialogRef: HTMLDialogElement | null;
 | 
				
			||||||
 | 
							onAccept: () => void;
 | 
				
			||||||
 | 
							buttons?: Snippet<[]>;
 | 
				
			||||||
 | 
						} & HTMLDialogAttributes;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let { title, dialogRef = $bindable(), onAccept, children, buttons }: Props = $props();
 | 
				
			||||||
 | 
					</script>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					<dialog
 | 
				
			||||||
 | 
						bind:this={dialogRef}
 | 
				
			||||||
 | 
						class="border-2 border-red-300 p-4 backdrop:bg-black/40 backdrop:backdrop-blur-sm"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						<h2 class="mb-4 text-2xl">{title}</h2>
 | 
				
			||||||
 | 
						<div>
 | 
				
			||||||
 | 
							{@render children?.()}
 | 
				
			||||||
 | 
						</div>
 | 
				
			||||||
 | 
						{#if buttons}
 | 
				
			||||||
 | 
							{@render buttons()}
 | 
				
			||||||
 | 
						{:else}
 | 
				
			||||||
 | 
							<div class="mt-4 grid grid-cols-2 gap-2">
 | 
				
			||||||
 | 
								<button onclick={() => dialogRef?.close()} class="w-40 justify-self-end bg-slate-400"
 | 
				
			||||||
 | 
									>Cancel</button
 | 
				
			||||||
 | 
								>
 | 
				
			||||||
 | 
								<button class="w-40 bg-red-500" onclick={onAccept}>Delete</button>
 | 
				
			||||||
 | 
							</div>
 | 
				
			||||||
 | 
						{/if}
 | 
				
			||||||
 | 
					</dialog>
 | 
				
			||||||
							
								
								
									
										14
									
								
								src/lib/components/icons/DeleteIcon.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/components/icons/DeleteIcon.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					<svg
 | 
				
			||||||
 | 
						xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
						fill="none"
 | 
				
			||||||
 | 
						viewBox="0 0 24 24"
 | 
				
			||||||
 | 
						stroke-width="1.5"
 | 
				
			||||||
 | 
						stroke="currentColor"
 | 
				
			||||||
 | 
						class="size-5"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						<path
 | 
				
			||||||
 | 
							stroke-linecap="round"
 | 
				
			||||||
 | 
							stroke-linejoin="round"
 | 
				
			||||||
 | 
							d="M12 9.75 14.25 12m0 0 2.25 2.25M14.25 12l2.25-2.25M14.25 12 12 14.25m-2.58 4.92-6.374-6.375a1.125 1.125 0 0 1 0-1.59L9.42 4.83c.21-.211.497-.33.795-.33H19.5a2.25 2.25 0 0 1 2.25 2.25v10.5a2.25 2.25 0 0 1-2.25 2.25h-9.284c-.298 0-.585-.119-.795-.33Z"
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 460 B  | 
| 
						 | 
					@ -1,4 +1,4 @@
 | 
				
			||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-4">
 | 
					<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor" class="size-5">
 | 
				
			||||||
	<path
 | 
						<path
 | 
				
			||||||
		fill-rule="evenodd"
 | 
							fill-rule="evenodd"
 | 
				
			||||||
		d="M19.902 4.098a3.75 3.75 0 0 0-5.304 0l-4.5 4.5a3.75 3.75 0 0 0 1.035 6.037.75.75 0 0 1-.646 1.353 5.25 5.25 0 0 1-1.449-8.45l4.5-4.5a5.25 5.25 0 1 1 7.424 7.424l-1.757 1.757a.75.75 0 1 1-1.06-1.06l1.757-1.757a3.75 3.75 0 0 0 0-5.304Zm-7.389 4.267a.75.75 0 0 1 1-.353 5.25 5.25 0 0 1 1.449 8.45l-4.5 4.5a5.25 5.25 0 1 1-7.424-7.424l1.757-1.757a.75.75 0 1 1 1.06 1.06l-1.757 1.757a3.75 3.75 0 1 0 5.304 5.304l4.5-4.5a3.75 3.75 0 0 0-1.035-6.037.75.75 0 0 1-.354-1Z"
 | 
							d="M19.902 4.098a3.75 3.75 0 0 0-5.304 0l-4.5 4.5a3.75 3.75 0 0 0 1.035 6.037.75.75 0 0 1-.646 1.353 5.25 5.25 0 0 1-1.449-8.45l4.5-4.5a5.25 5.25 0 1 1 7.424 7.424l-1.757 1.757a.75.75 0 1 1-1.06-1.06l1.757-1.757a3.75 3.75 0 0 0 0-5.304Zm-7.389 4.267a.75.75 0 0 1 1-.353 5.25 5.25 0 0 1 1.449 8.45l-4.5 4.5a5.25 5.25 0 1 1-7.424-7.424l1.757-1.757a.75.75 0 1 1 1.06 1.06l-1.757 1.757a3.75 3.75 0 1 0 5.304 5.304l4.5-4.5a3.75 3.75 0 0 0-1.035-6.037.75.75 0 0 1-.354-1Z"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
		 Before Width: | Height: | Size: 627 B After Width: | Height: | Size: 627 B  | 
							
								
								
									
										14
									
								
								src/lib/components/icons/MenuIcon.svelte
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								src/lib/components/icons/MenuIcon.svelte
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,14 @@
 | 
				
			||||||
 | 
					<svg
 | 
				
			||||||
 | 
						xmlns="http://www.w3.org/2000/svg"
 | 
				
			||||||
 | 
						fill="none"
 | 
				
			||||||
 | 
						viewBox="0 0 24 24"
 | 
				
			||||||
 | 
						stroke-width="1.5"
 | 
				
			||||||
 | 
						stroke="currentColor"
 | 
				
			||||||
 | 
						class="size-6"
 | 
				
			||||||
 | 
					>
 | 
				
			||||||
 | 
						<path
 | 
				
			||||||
 | 
							stroke-linecap="round"
 | 
				
			||||||
 | 
							stroke-linejoin="round"
 | 
				
			||||||
 | 
							d="M12 6.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 12.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5ZM12 18.75a.75.75 0 1 1 0-1.5.75.75 0 0 1 0 1.5Z"
 | 
				
			||||||
 | 
						/>
 | 
				
			||||||
 | 
					</svg>
 | 
				
			||||||
| 
		 After Width: | Height: | Size: 352 B  | 
| 
						 | 
					@ -1,12 +1,13 @@
 | 
				
			||||||
import { error } from '@sveltejs/kit';
 | 
					import { error, redirect } from '@sveltejs/kit';
 | 
				
			||||||
import type { PageServerLoad } from './$types';
 | 
					import type { PageServerLoad, RouteParams } from './$types';
 | 
				
			||||||
import { loadSurvey } from '$lib/queries';
 | 
					import { loadSurvey } from '$lib/queries';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import debug from 'debug';
 | 
					import debug from 'debug';
 | 
				
			||||||
 | 
					import { deleteAnswers } from '../../../../db/answers';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const log = debug('survey:admin');
 | 
					const log = debug('survey:admin');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const load: PageServerLoad = async ({ params, locals }) => {
 | 
					async function loadSurveyData(params: RouteParams, locals: App.Locals) {
 | 
				
			||||||
    const surveyId = parseInt(params.surveyId);
 | 
					    const surveyId = parseInt(params.surveyId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (isNaN(surveyId)) {
 | 
					    if (isNaN(surveyId)) {
 | 
				
			||||||
| 
						 | 
					@ -25,3 +26,25 @@ export const load: PageServerLoad = async ({ params, locals }) => {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return surveyData;
 | 
					    return surveyData;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const load: PageServerLoad = async ({ params, locals }) => {
 | 
				
			||||||
 | 
					    return await loadSurveyData(params, locals);
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const actions = {
 | 
				
			||||||
 | 
					    deleteAnswers: async ({ params, locals, request }) => {
 | 
				
			||||||
 | 
					        const survey = await loadSurveyData(params, locals);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let formData = await request.formData();
 | 
				
			||||||
 | 
					        const participantId = parseInt(formData.get('participantId')?.toString() ?? '');
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (isNaN(participantId)) {
 | 
				
			||||||
 | 
					            log('Invalid participant ID when trying to delete answers: %s', formData.get('participandId')?.toString());
 | 
				
			||||||
 | 
					            error(400, 'Invalid participant ID');
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await deleteAnswers(participantId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        redirect(303, survey.id.toString());
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -11,6 +11,9 @@
 | 
				
			||||||
	import { goto } from '$app/navigation';
 | 
						import { goto } from '$app/navigation';
 | 
				
			||||||
	import { success } from '$lib/toast';
 | 
						import { success } from '$lib/toast';
 | 
				
			||||||
	import MarkdownBlock from '$lib/components/MarkdownBlock.svelte';
 | 
						import MarkdownBlock from '$lib/components/MarkdownBlock.svelte';
 | 
				
			||||||
 | 
						import MenuIcon from '$lib/components/icons/MenuIcon.svelte';
 | 
				
			||||||
 | 
						import DeleteIcon from '$lib/components/icons/DeleteIcon.svelte';
 | 
				
			||||||
 | 
						import WarningDialog from '$lib/components/WarningDialog.svelte';
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let { data }: { data: PageData } = $props();
 | 
						let { data }: { data: PageData } = $props();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,7 +29,10 @@
 | 
				
			||||||
		navigator.clipboard.writeText(link);
 | 
							navigator.clipboard.writeText(link);
 | 
				
			||||||
	}
 | 
						}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	let dialogRef: HTMLDialogElement | null = null;
 | 
						let deleteSurveyDialogRef: HTMLDialogElement | null = $state(null);
 | 
				
			||||||
 | 
						let deleteAnswersDialogRef: HTMLDialogElement | null = $state(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
						let participantAnswersDeletionCandidateId = $state<number | null>(null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
	async function deleteSurvey() {
 | 
						async function deleteSurvey() {
 | 
				
			||||||
		await fetch('', {
 | 
							await fetch('', {
 | 
				
			||||||
| 
						 | 
					@ -50,7 +56,7 @@
 | 
				
			||||||
		</a>
 | 
							</a>
 | 
				
			||||||
		<button
 | 
							<button
 | 
				
			||||||
			onclick={() => {
 | 
								onclick={() => {
 | 
				
			||||||
				dialogRef?.showModal();
 | 
									deleteSurveyDialogRef?.showModal();
 | 
				
			||||||
			}}
 | 
								}}
 | 
				
			||||||
			class="ml-2 inline-block"
 | 
								class="ml-2 inline-block"
 | 
				
			||||||
			aria-label="Delete"
 | 
								aria-label="Delete"
 | 
				
			||||||
| 
						 | 
					@ -84,6 +90,19 @@
 | 
				
			||||||
			>
 | 
								>
 | 
				
			||||||
				<LinkIcon />
 | 
									<LinkIcon />
 | 
				
			||||||
			</button>
 | 
								</button>
 | 
				
			||||||
 | 
								{#if participant.answers.length > 0}
 | 
				
			||||||
 | 
									<button
 | 
				
			||||||
 | 
										aria-label="Reset ratings"
 | 
				
			||||||
 | 
										title="Clear answers"
 | 
				
			||||||
 | 
										class="text-red-500 hover:bg-slate-200"
 | 
				
			||||||
 | 
										onclick={() => {
 | 
				
			||||||
 | 
											participantAnswersDeletionCandidateId = participant.id;
 | 
				
			||||||
 | 
											deleteAnswersDialogRef?.showModal();
 | 
				
			||||||
 | 
										}}
 | 
				
			||||||
 | 
									>
 | 
				
			||||||
 | 
										<DeleteIcon />
 | 
				
			||||||
 | 
									</button>
 | 
				
			||||||
 | 
								{/if}
 | 
				
			||||||
		</li>
 | 
							</li>
 | 
				
			||||||
	{/each}
 | 
						{/each}
 | 
				
			||||||
</ul>
 | 
					</ul>
 | 
				
			||||||
| 
						 | 
					@ -92,20 +111,27 @@
 | 
				
			||||||
	<Diagram data={diagramData} />
 | 
						<Diagram data={diagramData} />
 | 
				
			||||||
</div>
 | 
					</div>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
<dialog
 | 
					<WarningDialog title="Delete survey" bind:dialogRef={deleteSurveyDialogRef} onAccept={deleteSurvey}>
 | 
				
			||||||
	bind:this={dialogRef}
 | 
						<p>Are you sure you want to delete the survey.</p>
 | 
				
			||||||
	class="border-2 border-red-300 p-4 backdrop:bg-black/40 backdrop:backdrop-blur-sm"
 | 
						<p class="font-bold text-red-400">This action cannot be undone.</p>
 | 
				
			||||||
>
 | 
					</WarningDialog>
 | 
				
			||||||
	<h2 class="mb-4 text-2xl">Delete survey</h2>
 | 
					
 | 
				
			||||||
 | 
					<WarningDialog title="Delete answers" onAccept={() => {}} bind:dialogRef={deleteAnswersDialogRef}>
 | 
				
			||||||
 | 
						<form id="delete-answers" method="POST" action="?/deleteAnswers">
 | 
				
			||||||
 | 
							<input type="hidden" name="participantId" value={participantAnswersDeletionCandidateId} />
 | 
				
			||||||
		<p>
 | 
							<p>
 | 
				
			||||||
		Are you sure you want to delete the survey. <span class="font-bold text-red-400"
 | 
								Are you sure you want to remove this user's ratings? This will delete whatever answers the
 | 
				
			||||||
			>This action cannot be undone.</span
 | 
								user has given and allow them to submit new ratings instead.
 | 
				
			||||||
		>
 | 
					 | 
				
			||||||
		</p>
 | 
							</p>
 | 
				
			||||||
 | 
							<p class="font-bold text-red-400">This action cannot be undone.</p>
 | 
				
			||||||
 | 
						</form>
 | 
				
			||||||
 | 
						{#snippet buttons()}
 | 
				
			||||||
		<div class="mt-4 grid grid-cols-2 gap-2">
 | 
							<div class="mt-4 grid grid-cols-2 gap-2">
 | 
				
			||||||
		<button onclick={() => dialogRef?.close()} class="w-40 justify-self-end bg-slate-400"
 | 
								<button
 | 
				
			||||||
			>Cancel</button
 | 
									onclick={() => deleteAnswersDialogRef?.close()}
 | 
				
			||||||
 | 
									class="w-40 justify-self-end bg-slate-400">Cancel</button
 | 
				
			||||||
			>
 | 
								>
 | 
				
			||||||
		<button class="w-40 bg-red-500" onclick={deleteSurvey}>Delete</button>
 | 
								<button class="w-40 bg-red-500" form="delete-answers">Delete</button>
 | 
				
			||||||
		</div>
 | 
							</div>
 | 
				
			||||||
</dialog>
 | 
						{/snippet}
 | 
				
			||||||
 | 
					</WarningDialog>
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@ export const load: PageServerLoad = async ({ params, url }) => {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
 | 
					    if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
 | 
				
			||||||
        log_load('Answers already submitted: %s', params.accessToken);
 | 
					        log_load('Answers already submitted: %s', params.accessToken);
 | 
				
			||||||
        error(400, 'Answers already submitted');
 | 
					        error(400, 'You have already submitted your answers. If you feel that this is wrong, please contact the survey creator to reset your answers.');
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const survey = results[0].surveys_table;
 | 
					    const survey = results[0].surveys_table;
 | 
				
			||||||
| 
						 | 
					@ -57,7 +57,7 @@ export const actions = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
 | 
					        if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
 | 
				
			||||||
            log_store('Answers already submitted: %s', params.accessToken);
 | 
					            log_store('Answers already submitted: %s', params.accessToken);
 | 
				
			||||||
            error(400, 'Answers already submitted');
 | 
					            error(400, 'You have already submitted your answers. If you feel that this is wrong, please contact the survey creator to reset your answers.');
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const survey = results[0].surveys_table;
 | 
					        const survey = results[0].surveys_table;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
	Add table
		
		Reference in a new issue