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.
+
diff --git a/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.server.ts b/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.server.ts
index c4ecf91..3b67728 100644
--- a/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.server.ts
+++ b/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.server.ts
@@ -2,9 +2,10 @@ import { fail, redirect, error } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { escapeGame, step } from '$lib/server/db/schema';
-import { and, eq, max } from 'drizzle-orm';
+import { and, eq, max, not } from 'drizzle-orm';
import { ensureUploadDir, generateUniqueFilename, getUploadPath } from '$lib/server/uploads';
import { writeFile } from 'fs/promises';
+import { countActiveIncompleteSessions } from '$lib/server/gameValidation';
const stepTypes = ['question', 'text', 'puzzle', 'location'] as const;
type StepType = (typeof stepTypes)[number];
@@ -42,10 +43,13 @@ export const load: PageServerLoad = async ({ params }) => {
.where(eq(step.escapeGameId, gameId))
.then((result) => result[0]?.order ?? 0);
+ const activeSessionCount = await countActiveIncompleteSessions(gameId);
+
return {
game,
step: gameStep,
- totalSteps: lastStep
+ totalSteps: lastStep,
+ activeSessionCount
};
};
@@ -58,6 +62,13 @@ export const actions: Actions = {
return fail(400, { error: 'Invalid ID' });
}
+ const hasActiveSessions = await countActiveIncompleteSessions(gameId);
+ if (hasActiveSessions > 0) {
+ return fail(400, {
+ error: `Cannot modify step: ${hasActiveSessions} active session(s) in progress. Wait until they are completed or mark them as inactive.`
+ });
+ }
+
const formData = await request.formData();
const title = formData.get('title')?.toString().trim() ?? '';
const description = formData.get('description')?.toString().trim() ?? '';
@@ -237,6 +248,48 @@ export const actions: Actions = {
});
}
+ // Get the current step to check if order changed
+ const currentStep = await db.query.step.findFirst({
+ where: and(eq(step.id, stepId), eq(step.escapeGameId, gameId))
+ });
+
+ if (!currentStep) {
+ return fail(404, {
+ title,
+ description,
+ type,
+ content,
+ answer,
+ hint,
+ order,
+ error: 'Etape introuvable'
+ });
+ }
+
+ // Check if order changed and if there's a conflict
+ if (currentStep.order !== order) {
+ const conflictingStep = await db.query.step.findFirst({
+ where: and(
+ eq(step.escapeGameId, gameId),
+ eq(step.order, order),
+ not(eq(step.id, stepId))
+ )
+ });
+
+ if (conflictingStep) {
+ return fail(400, {
+ title,
+ description,
+ type,
+ content,
+ answer,
+ hint,
+ order,
+ error: `Une autre etape utilise deja l'ordre ${order}. Veuillez reordonner les etapes via drag-and-drop.`
+ });
+ }
+ }
+
try {
const updated = await db
.update(step)
diff --git a/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.svelte b/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.svelte
index 7f4f323..65feac9 100644
--- a/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.svelte
+++ b/src/routes/(admin)/admin/games/[id]/steps/[stepId]/+page.svelte
@@ -35,6 +35,28 @@
}));
+{#if data.activeSessionCount > 0}
+
+
+
+
+
+
🚫 Cannot edit step
+
+ This game has {data.activeSessionCount} active session{data.activeSessionCount > 1 ? 's' : ''} in progress.
+ Editing steps would break the progression for active players.
+
+
+ Wait for all sessions to complete or mark them as inactive before modifying this step.
+
+
+
+
+
+{/if}
+
0}
/>
diff --git a/src/routes/(admin)/admin/games/[id]/steps/new/+page.server.ts b/src/routes/(admin)/admin/games/[id]/steps/new/+page.server.ts
index 6c88f62..6ab4595 100644
--- a/src/routes/(admin)/admin/games/[id]/steps/new/+page.server.ts
+++ b/src/routes/(admin)/admin/games/[id]/steps/new/+page.server.ts
@@ -2,9 +2,10 @@ import { fail, redirect, error } from '@sveltejs/kit';
import type { PageServerLoad, Actions } from './$types';
import { db } from '$lib/server/db';
import { escapeGame, step } from '$lib/server/db/schema';
-import { eq, max } from 'drizzle-orm';
+import { and, eq, gte, max } from 'drizzle-orm';
import { ensureUploadDir, generateUniqueFilename, getUploadPath } from '$lib/server/uploads';
import { writeFile } from 'fs/promises';
+import { countActiveIncompleteSessions } from '$lib/server/gameValidation';
const stepTypes = ['question', 'text', 'puzzle', 'location'] as const;
type StepType = (typeof stepTypes)[number];
@@ -36,10 +37,13 @@ export const load: PageServerLoad = async ({ params }) => {
const nextStepOrder = lastStep + 1;
+ const activeSessionCount = await countActiveIncompleteSessions(gameId);
+
return {
game,
nextStepOrder,
- totalSteps: lastStep
+ totalSteps: lastStep,
+ activeSessionCount
};
};
@@ -51,6 +55,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 add step: ${hasActiveSessions} active session(s) in progress. Wait until they are completed or mark them as inactive.`
+ });
+ }
+
const formData = await request.formData();
const title = formData.get('title')?.toString().trim() ?? '';
const description = formData.get('description')?.toString().trim() ?? '';
@@ -224,6 +235,22 @@ export const actions: Actions = {
}
try {
+ // If inserting in the middle, shift existing steps
+ if (order <= lastStep) {
+ const stepsToShift = await db
+ .select({ id: step.id, order: step.order })
+ .from(step)
+ .where(and(eq(step.escapeGameId, gameId), gte(step.order, order)));
+
+ // Shift all steps >= order by 1
+ for (const s of stepsToShift) {
+ await db
+ .update(step)
+ .set({ order: s.order + 1 })
+ .where(eq(step.id, s.id));
+ }
+ }
+
await db.insert(step).values({
escapeGameId: gameId,
title,
diff --git a/src/routes/(admin)/admin/games/[id]/steps/new/+page.svelte b/src/routes/(admin)/admin/games/[id]/steps/new/+page.svelte
index a1f453b..2591e82 100644
--- a/src/routes/(admin)/admin/games/[id]/steps/new/+page.svelte
+++ b/src/routes/(admin)/admin/games/[id]/steps/new/+page.svelte
@@ -32,6 +32,28 @@
}));
+{#if data.activeSessionCount > 0}
+
+
+
+
+
+
🚫 Cannot add step
+
+ This game has {data.activeSessionCount} active session{data.activeSessionCount > 1 ? 's' : ''} in progress.
+ Adding steps would break the progression for active players.
+
+
+ Wait for all sessions to complete or mark them as inactive before adding new steps.
+