diff --git a/src/lib/components/StepForm.svelte b/src/lib/components/StepForm.svelte index ee8cb11..1aaf676 100644 --- a/src/lib/components/StepForm.svelte +++ b/src/lib/components/StepForm.svelte @@ -25,7 +25,8 @@ submitLabel, errorMessage, initialValues, - maxOrder + maxOrder, + disabled = false }: { gameId: number; gameTitle: string; @@ -35,6 +36,7 @@ errorMessage?: string; initialValues: StepFormValues; maxOrder: number; + disabled?: boolean; } = $props(); const stepTypes = ['question', 'text', 'puzzle', 'location']; @@ -439,7 +441,15 @@
diff --git a/src/lib/server/gameValidation.ts b/src/lib/server/gameValidation.ts new file mode 100644 index 0000000..97e9034 --- /dev/null +++ b/src/lib/server/gameValidation.ts @@ -0,0 +1,40 @@ +import { db } from '$lib/server/db'; +import { gameSession } from '$lib/server/db/schema'; +import { and, eq, isNull } from 'drizzle-orm'; + +/** + * Check if a game has any active sessions that are not yet completed + * @param escapeGameId The ID of the escape game to check + * @returns true if there are active incomplete sessions, false otherwise + */ +export async function hasActiveIncompleteSessions(escapeGameId: number): Promise { + const activeSessions = await db.query.gameSession.findFirst({ + where: and( + eq(gameSession.escapeGameId, escapeGameId), + eq(gameSession.isActive, 1), + isNull(gameSession.completedAt) + ) + }); + + return activeSessions !== undefined; +} + +/** + * Get count of active incomplete sessions for a game + * @param escapeGameId The ID of the escape game to check + * @returns Number of active incomplete sessions + */ +export async function countActiveIncompleteSessions(escapeGameId: number): Promise { + const sessions = await db + .select() + .from(gameSession) + .where( + and( + eq(gameSession.escapeGameId, escapeGameId), + eq(gameSession.isActive, 1), + isNull(gameSession.completedAt) + ) + ); + + return sessions.length; +} diff --git a/src/routes/(admin)/admin/games/[id]/+page.server.ts b/src/routes/(admin)/admin/games/[id]/+page.server.ts index 99be553..f400b94 100644 --- a/src/routes/(admin)/admin/games/[id]/+page.server.ts +++ b/src/routes/(admin)/admin/games/[id]/+page.server.ts @@ -3,6 +3,7 @@ import type { PageServerLoad, Actions } from './$types'; import { db } from '$lib/server/db'; import { escapeGame, step } from '$lib/server/db/schema'; import { and, eq } from 'drizzle-orm'; +import { countActiveIncompleteSessions } from '$lib/server/gameValidation'; export const load: PageServerLoad = async ({ params }) => { const gameId = parseInt(params.id, 10); @@ -15,7 +16,7 @@ export const load: PageServerLoad = async ({ params }) => { where: eq(escapeGame.id, gameId), with: { steps: { - orderBy: (steps) => steps.order + orderBy: (steps, { asc }) => [asc(steps.order), asc(steps.id)] } } }); @@ -24,8 +25,11 @@ export const load: PageServerLoad = async ({ params }) => { error(404, 'Game not found'); } + const activeSessionCount = await countActiveIncompleteSessions(gameId); + return { - game + game, + activeSessionCount }; }; @@ -36,6 +40,13 @@ export const actions: Actions = { return fail(400, { error: 'Invalid game ID' }); } + const hasActiveSessions = await countActiveIncompleteSessions(gameId); + if (hasActiveSessions > 0) { + return fail(400, { + error: `Cannot reorder steps: ${hasActiveSessions} active session(s) in progress. Wait until they are completed or mark them as inactive.` + }); + } + const formData = await request.formData(); const orderRaw = formData.get('order')?.toString() ?? ''; @@ -100,6 +111,13 @@ export const actions: Actions = { return fail(400, { error: 'Invalid game ID' }); } + const hasActiveSessions = await countActiveIncompleteSessions(gameId); + if (hasActiveSessions > 0) { + return fail(400, { + error: `Cannot delete step: ${hasActiveSessions} active session(s) in progress. Wait until they are completed or mark them as inactive.` + }); + } + const formData = await request.formData(); const stepId = Number.parseInt(formData.get('stepId')?.toString() ?? '', 10); diff --git a/src/routes/(admin)/admin/games/[id]/+page.svelte b/src/routes/(admin)/admin/games/[id]/+page.svelte index ee4856f..51b7bc2 100644 --- a/src/routes/(admin)/admin/games/[id]/+page.svelte +++ b/src/routes/(admin)/admin/games/[id]/+page.svelte @@ -6,12 +6,17 @@ let { data }: { data: PageData } = $props(); let game = $derived.by(() => data.game); - let steps = $derived([...(game.steps ?? [])]); + let steps = $state([]); let draggedStepId = $state(null); let reorderPayload = $state(''); let reorderForm = $state(undefined); let stepToDelete = $state<{ id: number; title: string; order: number } | null>(null); + // Update steps when game data changes + $effect(() => { + steps = [...(game.steps ?? [])]; + }); + function openDeleteModal(step: { id: number; title: string; order: number }) { stepToDelete = step; } @@ -81,37 +86,81 @@
+ + {#if data.activeSessionCount > 0} +
+
+
+ + + +
+

⚠️ Editing disabled

+

+ This game has {data.activeSessionCount} active session{data.activeSessionCount > 1 ? 's' : ''} in progress. + You cannot add, edit, reorder, or delete steps while sessions are active. +

+

+ Modifying steps would break the progression for active players. Wait for all sessions to complete or mark them as inactive. +

+
+
+
+
+ {/if} +

Game Steps

-

Glissez-deposez pour reordonner

- - + Add Step - + {#if data.activeSessionCount === 0} +

Glissez-deposez pour reordonner

+ {/if} + {#if data.activeSessionCount > 0} + + {:else} + + + Add Step + + {/if}
-
- -
+
{ + return async ({ update }) => { + await update({ reset: false }); + }; + }} + bind:this={reorderForm} + > + {#if steps.length > 0}
{#each steps as step (step.id)}
onDragStart(step.id)} + ondragstart={() => data.activeSessionCount === 0 && onDragStart(step.id)} ondragover={onDragOver} - ondrop={() => onDrop(step.id)} + ondrop={() => data.activeSessionCount === 0 && onDrop(step.id)} >
@@ -128,6 +177,30 @@ {/if}
+ {#if data.activeSessionCount > 0} + + + {:else} Delete -
+ {/if} +
{/each} @@ -171,10 +245,9 @@
{/if} - + - {#if stepToDelete}