#2 - reset user ratings

This commit is contained in:
Markus Brueckner 2024-12-29 09:13:42 +01:00
parent dfa45436d4
commit 209e05f959
8 changed files with 142 additions and 25 deletions

7
src/db/answers.ts Normal file
View 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));
}

View 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>

View 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

View file

@ -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
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"

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 627 B

View 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

View file

@ -1,12 +1,13 @@
import { error } from '@sveltejs/kit';
import type { PageServerLoad } from './$types';
import { error, redirect } from '@sveltejs/kit';
import type { PageServerLoad, RouteParams } from './$types';
import { loadSurvey } from '$lib/queries';
import debug from 'debug';
import { deleteAnswers } from '../../../../db/answers';
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);
if (isNaN(surveyId)) {
@ -25,3 +26,25 @@ export const load: PageServerLoad = async ({ params, locals }) => {
}
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());
}
}

View file

@ -11,6 +11,9 @@
import { goto } from '$app/navigation';
import { success } from '$lib/toast';
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();
@ -26,7 +29,10 @@
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() {
await fetch('', {
@ -50,7 +56,7 @@
</a>
<button
onclick={() => {
dialogRef?.showModal();
deleteSurveyDialogRef?.showModal();
}}
class="ml-2 inline-block"
aria-label="Delete"
@ -84,6 +90,19 @@
>
<LinkIcon />
</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>
{/each}
</ul>
@ -92,20 +111,27 @@
<Diagram data={diagramData} />
</div>
<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">Delete survey</h2>
<WarningDialog title="Delete survey" bind:dialogRef={deleteSurveyDialogRef} onAccept={deleteSurvey}>
<p>Are you sure you want to delete the survey.</p>
<p class="font-bold text-red-400">This action cannot be undone.</p>
</WarningDialog>
<WarningDialog title="Delete answers" onAccept={() => {}} bind:dialogRef={deleteAnswersDialogRef}>
<form id="delete-answers" method="POST" action="?/deleteAnswers">
<input type="hidden" name="participantId" value={participantAnswersDeletionCandidateId} />
<p>
Are you sure you want to delete the survey. <span class="font-bold text-red-400"
>This action cannot be undone.</span
>
Are you sure you want to remove this user's ratings? This will delete whatever answers the
user has given and allow them to submit new ratings instead.
</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">
<button onclick={() => dialogRef?.close()} class="w-40 justify-self-end bg-slate-400"
>Cancel</button
<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>
</dialog>
{/snippet}
</WarningDialog>

View file

@ -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) {
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;
@ -57,7 +57,7 @@ export const actions = {
if (await db.$count(surveyAnswersTable, eq(surveyAnswersTable.participantId, results[0].survey_access_table.id)) > 0) {
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;