Page Stubs and Click-Through Flow

This commit is contained in:
2026-02-03 10:14:00 +01:00
parent 3684d9ef6b
commit e8be239c32
27 changed files with 595 additions and 31 deletions

View File

@@ -20,7 +20,11 @@
"Bash(npm run build:*)",
"Bash(ls:*)",
"Bash(xargs:*)",
"mcp__playwright__browser_take_screenshot"
"mcp__playwright__browser_take_screenshot",
"Write",
"Bash",
"mcp__playwright__browser_console_messages",
"mcp__playwright__browser_navigate_back"
]
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use Inertia\Inertia;
use Inertia\Response;
final class LandingController extends Controller
{
/**
* Display the landing page.
*/
public function index(): Response
{
return Inertia::render('Landing');
}
}

View File

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Category;
use App\Models\Screening;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
final class ScreeningController extends Controller
{
/**
* Create a new screening session for the authenticated user.
*/
public function store(Request $request): RedirectResponse
{
$screening = Screening::create([
'user_id' => auth()->id(),
]);
return redirect()->route('screening.show', $screening);
}
/**
* Display the screening questionnaire.
*/
public function show(Screening $screening): Response
{
return Inertia::render('Screening/Show', [
'screening' => $screening,
]);
}
/**
* Save screening answers and redirect to result.
*/
public function update(Request $request, Screening $screening): RedirectResponse
{
return redirect()->route('screening.result', $screening);
}
/**
* Display the screening result with available categories.
*/
public function result(Screening $screening): Response
{
return Inertia::render('Screening/Result', [
'screening' => $screening,
'categories' => Category::orderBy('sort_order')->get(['id', 'name']),
]);
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace App\Http\Controllers;
use App\Models\Session;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
use Inertia\Inertia;
use Inertia\Response;
final class SessionController extends Controller
{
/**
* Create a new session for the selected category.
*/
public function store(Request $request): RedirectResponse
{
$session = Session::create([
'user_id' => auth()->id(),
'category_id' => $request->input('category_id'),
'screening_id' => $request->input('screening_id'),
'status' => 'in_progress',
]);
return redirect()->route('sessions.show', $session);
}
/**
* Display the session questionnaire with category.
*/
public function show(Session $session): Response
{
$session->load('category');
return Inertia::render('Session/Show', [
'session' => $session,
]);
}
/**
* Save session answers and redirect to result.
*/
public function update(Request $request, Session $session): RedirectResponse
{
return redirect()->route('sessions.result', $session);
}
/**
* Display the final session result.
*/
public function result(Session $session): Response
{
return Inertia::render('Session/Result', [
'session' => $session,
]);
}
}

View File

@@ -16,6 +16,10 @@
$middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class,
]);
$middleware->alias([
'encrypt.history' => \Inertia\Middleware\EncryptHistory::class,
]);
})
->withExceptions(function (Exceptions $exceptions): void {
//

View File

@@ -10,7 +10,7 @@ # Implementation Plan: Go No Go
## Step 1: Install Dependencies and Configure Build
[ ] **Install and configure the frontend toolchain and auth packages.**
[x] **Install and configure the frontend toolchain and auth packages.**
Install Vue 3, Inertia.js v2 (server-side adapter + client-side `@inertiajs/vue3`), Tailwind CSS 4 with design tokens from `docs/theming-templating-vue.md`, Heroicons, and Laravel Socialite. Configure `vite.config.js` for Vue + Inertia. Configure `tailwind.config.js` with the project color tokens (primary `#d1ec51`, secondary `#00b7b3`, surface `#2b303a`). Register the `HandleInertiaRequests` middleware. Create the Inertia root Blade template (`app.blade.php`).
@@ -36,7 +36,7 @@ ## Step 1: Install Dependencies and Configure Build
## Step 2: Database -- Migrations and Models
[ ] **Create all database tables and Eloquent models.**
[x] **Create all database tables and Eloquent models.**
Create migrations for: `categories`, `question_groups`, `questions`, `screenings`, `screening_answers`, `sessions`, `answers`, `logs`. Create corresponding Eloquent models with relationships, `$fillable`, `$casts`, and any unique constraints (e.g., `answers` has a unique composite on `session_id + question_id`, `screening_answers` has a unique composite on `screening_id + question_number`). The `logs` table has no `updated_at` column (append-only). The `sessions` table has a nullable `screening_id` FK linking back to the screening that authorized it. See `docs/technical-requirements.md` section 5 for exact column definitions.
@@ -70,7 +70,7 @@ ## Step 2: Database -- Migrations and Models
## Step 3: Authentication
[ ] **Set up Socialite for frontend SSO.**
[x] **Set up Socialite for frontend SSO.**
Nova already handles its own admin authentication at `/cp` (built-in). Configure Socialite with the Azure AD driver for frontend SSO. Create `SocialiteController` with `redirect`, `callback`, and `logout` methods. The callback creates or matches users by email. Add the `/login-jonathan` dev auto-login route (local/testing environments only).
@@ -94,7 +94,7 @@ ## Step 3: Authentication
## Step 4: Frontend Layout and Shared Components
[ ] **Build the persistent layout and all shared Vue components.**
[x] **Build the persistent layout and all shared Vue components.**
Create `AppLayout.vue` as a persistent Inertia layout with `PageHeader` containing the Piccadilly logo and page title. Create shared components: `AppLogo`, `PageHeader`, `AppButton` (with variant/size/href/disabled/loading props per `docs/theming-templating-vue.md`), `ScoreIndicator` (color-coded by threshold). Wire up `HandleInertiaRequests` shared data (authenticated user, flash messages). Add the `EncryptHistory` middleware for sensitive session data.
@@ -121,7 +121,7 @@ ## Step 4: Frontend Layout and Shared Components
## Step 5: Page Stubs and Click-Through Flow
[ ] **Create stub pages and controllers so the full two-stage navigation flow is clickable end to end.**
[x] **Create stub pages and controllers so the full two-stage navigation flow is clickable end to end.**
Create stub Vue page components for the entire flow: `Pages/Landing.vue` (intro text + Continue button), `Pages/Screening/Show.vue` (placeholder for pre-screening questions), `Pages/Screening/Result.vue` (placeholder pass/fail + category picker), `Pages/Session/Show.vue` (placeholder for basic info + questionnaire), `Pages/Session/Result.vue` (placeholder final result + Again button). Create stub controllers: `LandingController@index`, `ScreeningController` with `store`, `show`, `update`, `result` methods, `SessionController` with `store`, `show`, `update`, `result` methods. Wire up all routes in `web.php` matching the route table in `docs/technical-requirements.md` section 6.
@@ -280,25 +280,25 @@ ## Step 9: Questionnaire Flow -- Question Rendering and Answer Saving
[ ] **Build the full questionnaire UI with all 6 question patterns and answer persistence.**
Create the `QuestionCard` component that renders questions based on their field configuration (see the 6 patterns in `docs/technical-requirements.md` section 5). Render question groups as steps within `Session/Show`. Each step shows its questions, scoring instructions (if present), and a "Next" / "Previous" navigation. Save answers via `PUT /sessions/{session}` using Inertia `useForm` with partial reloads (only reload answers/score, not the full question set). Handle `details` textarea visibility: show when `details` is `required` or `optional`; show conditionally for `req_on_yes` / `req_on_no` based on the selected value. Include the Additional Comments textarea as the final step.
Create the `QuestionCard` component that renders questions based on their field configuration (see the 6 patterns in `docs/technical-requirements.md` section 5). Render all questions on a single scrollable page within `Session/Show` (not paginated per group). In phase 2, questions will be visually grouped by their question group with group headers and scoring instructions. Save answers via `PUT /sessions/{session}` using Inertia `useForm` with partial reloads (only reload answers/score, not the full question set). Handle `details` textarea visibility: show when `details` is `required` or `optional`; show conditionally for `req_on_yes` / `req_on_no` based on the selected value. Include the Additional Comments textarea at the bottom of the page.
**Creates:**
- `resources/js/Components/QuestionCard.vue`
- Updated `resources/js/Pages/Session/Show.vue` -- step navigation, question group rendering
- Updated `resources/js/Pages/Session/Show.vue` -- single-page question rendering
- Updated `app/Http/Controllers/SessionController.php` -- answer saving logic in `update`
- Answer validation rules
**Validates:** Navigate through all question groups for a category. Each question renders the correct UI pattern (radio buttons, text-only, details visibility). Answers save without page reload. Navigating away and back retains saved answers. All 6 question patterns render correctly.
**Validates:** All questions for the category render on a single scrollable page. Each question renders the correct UI pattern (radio buttons, text-only, details visibility). Answers save without page reload. Refreshing the page retains saved answers. All 6 question patterns render correctly. Additional Comments textarea appears at the bottom.
**Browser Verification (Playwright MCP):**
1. `browser_navigate` to `http://go-no-go.test/login-jonathan`
2. Complete pre-screening (5+ Yes), select a category known to have diverse question types (e.g., Audit), fill basic info
*Test question group navigation:*
3. `browser_snapshot` -- confirm first question group renders with questions and Next button
4. `browser_take_screenshot` -- save as `step-09-question-group-1.png`
*Test single-page question rendering:*
3. `browser_snapshot` -- confirm all questions render on a single page
4. `browser_take_screenshot` -- save as `step-09-questions-full-page.png`
*Test all 6 question patterns (navigate through groups to find each):*
*Test all 6 question patterns (scroll through to find each):*
5. For each question pattern encountered:
- `browser_snapshot` -- verify correct UI elements (radio buttons for Yes/No/NA, details textarea visibility)
- `browser_take_screenshot` -- save as `step-09-pattern-{N}.png` for each distinct pattern
@@ -308,17 +308,16 @@ ## Step 9: Questionnaire Flow -- Question Rendering and Answer Saving
- `browser_take_screenshot` -- save as `step-09-conditional-details.png`
*Test answer saving:*
7. `browser_fill_form` -- answer several questions in the current group
8. `browser_click` the Next button to go to the next group
9. `browser_click` the Previous button to return
10. `browser_snapshot` -- confirm previously saved answers are still selected
11. `browser_take_screenshot` -- save as `step-09-answers-retained.png`
7. `browser_fill_form` -- answer several questions on the page
8. Refresh the page
9. `browser_snapshot` -- confirm previously saved answers are still selected
10. `browser_take_screenshot` -- save as `step-09-answers-retained.png`
*Test step navigation:*
12. `browser_click` Next through all question groups to reach the Additional Comments step
13. `browser_snapshot` -- confirm Additional Comments textarea renders
14. `browser_take_screenshot` -- save as `step-09-additional-comments.png`
15. `browser_console_messages` with level `error` -- verify no errors throughout navigation
*Test Additional Comments section:*
11. Scroll to the bottom of the page
12. `browser_snapshot` -- confirm Additional Comments textarea renders at the bottom
13. `browser_take_screenshot` -- save as `step-09-additional-comments.png`
14. `browser_console_messages` with level `error` -- verify no errors
---

View File

@@ -278,14 +278,14 @@ ## 7. Questionnaire Flow
- Score >= 5 points → pass. Page shows category picker to continue.
6. User selects a category → creates a session, redirected to Session/Show
7. Basic Information form (client name, client contact, lead firm name, lead firm contact)
8. Step through question groups in order (each group is a page/step)
8. All questions are displayed on a single page (not paginated per question or per group). In phase 2, questions will be visually grouped by their question group.
9. Each question renders based on its field configuration:
- If `has_yes`/`has_no`/`has_na` are all false → render as open text (textarea only)
- If any are true → render radio buttons for the enabled options
- If `details` is set → render a details textarea with the appropriate requirement behavior (`required`, `optional`, `req_on_yes`, `req_on_no`)
10. Running score displayed (for scored questions only)
11. Color-coded result indicator updates live (green/yellow/red)
12. Final step: Additional comments free text
12. Additional comments free text section at the bottom of the page
13. Submit and view result (GO / NO GO / Consult Leadership)
14. Session saved with score and result
15. All result pages have an "Again" button that returns to `/`

View File

@@ -0,0 +1,109 @@
<script setup>
import { computed } from 'vue'
import { Link } from '@inertiajs/vue3'
import { ArrowPathIcon } from '@heroicons/vue/20/solid'
const props = defineProps({
variant: {
type: String,
default: 'primary',
validator: (value) => ['primary', 'danger', 'ghost'].includes(value),
},
size: {
type: String,
default: 'md',
validator: (value) => ['sm', 'md', 'lg'].includes(value),
},
href: {
type: String,
default: undefined,
},
disabled: {
type: Boolean,
default: false,
},
loading: {
type: Boolean,
default: false,
},
})
const emit = defineEmits(['click'])
const isDisabled = computed(() => props.disabled || props.loading)
const component = computed(() => props.href ? Link : 'button')
const buttonClasses = computed(() => {
const classes = [
'inline-flex items-center justify-center gap-2',
'font-semibold rounded-lg',
'transition-all duration-200',
'focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-offset-surface',
]
// Size classes
if (props.size === 'sm') {
classes.push('px-3 py-1.5 text-sm')
} else if (props.size === 'md') {
classes.push('px-5 py-2.5 text-base')
} else if (props.size === 'lg') {
classes.push('px-7 py-3 text-lg')
}
// Variant classes
if (isDisabled.value) {
classes.push('opacity-50 cursor-not-allowed')
if (props.variant === 'primary') {
classes.push('bg-primary text-gray-900')
} else if (props.variant === 'danger') {
classes.push('bg-red-500 text-white')
} else if (props.variant === 'ghost') {
classes.push('bg-transparent text-gray-400')
}
} else {
if (props.variant === 'primary') {
classes.push(
'bg-primary text-gray-900',
'hover:bg-secondary hover:text-white',
'focus:ring-primary'
)
} else if (props.variant === 'danger') {
classes.push(
'bg-red-500 text-white',
'hover:bg-red-600',
'focus:ring-red-500'
)
} else if (props.variant === 'ghost') {
classes.push(
'bg-transparent text-gray-400',
'hover:text-white hover:bg-white/10',
'focus:ring-gray-400'
)
}
}
return classes.join(' ')
})
const handleClick = (event) => {
if (!isDisabled.value) {
emit('click', event)
}
}
</script>
<template>
<component
:is="component"
:href="href"
:class="buttonClasses"
:disabled="isDisabled"
:type="href ? undefined : 'button'"
v-bind="$attrs"
@click="handleClick"
>
<ArrowPathIcon v-if="loading" class="h-5 w-5 animate-spin" />
<slot />
</component>
</template>

View File

@@ -0,0 +1,14 @@
<script setup>
defineProps({
class: {
type: String,
default: '',
},
})
</script>
<template>
<div :class="['font-bold text-primary', $props.class]">
Piccadilly
</div>
</template>

View File

@@ -0,0 +1,21 @@
<script setup>
import AppLogo from '@/Components/AppLogo.vue'
defineProps({
title: {
type: String,
default: '',
},
})
</script>
<template>
<header class="sticky top-0 z-50 bg-surface border-b border-gray-700">
<div class="px-6 py-4 flex items-center gap-6">
<AppLogo class="text-2xl" />
<h1 v-if="title" class="text-xl font-semibold text-white">
{{ title }}
</h1>
</div>
</header>
</template>

View File

@@ -0,0 +1,65 @@
<script setup>
import { computed } from 'vue'
const props = defineProps({
score: {
type: Number,
default: 0,
},
visible: {
type: Boolean,
default: true,
},
})
const scoreData = computed(() => {
if (props.score >= 10) {
return {
color: 'green',
bgClass: 'bg-green-500',
textClass: 'text-green-500',
label: 'GO',
}
} else if (props.score >= 5) {
return {
color: 'amber',
bgClass: 'bg-amber-500',
textClass: 'text-amber-500',
label: 'Consult Leadership',
}
} else if (props.score >= 1) {
return {
color: 'red',
bgClass: 'bg-red-500',
textClass: 'text-red-500',
label: 'NO GO',
}
} else {
return {
color: 'gray',
bgClass: 'bg-gray-500',
textClass: 'text-gray-400',
label: 'No Score',
}
}
})
</script>
<template>
<div v-if="visible" class="inline-flex items-center gap-3">
<div class="flex items-baseline gap-2">
<span class="text-4xl font-bold" :class="scoreData.textClass">
{{ score }}
</span>
<span class="text-sm text-gray-400">points</span>
</div>
<div
:class="[
scoreData.bgClass,
'px-4 py-2 rounded-lg text-white font-semibold text-sm',
]"
>
{{ scoreData.label }}
</div>
</div>
</template>

View File

@@ -0,0 +1,20 @@
<script setup>
import { computed } from 'vue'
import { usePage } from '@inertiajs/vue3'
import PageHeader from '@/Components/PageHeader.vue'
const page = usePage()
const pageTitle = computed(() => {
return page.props?.title || ''
})
</script>
<template>
<div class="min-h-screen flex flex-col">
<PageHeader :title="pageTitle" />
<main class="flex-1">
<slot />
</main>
</div>
</template>

View File

@@ -1,14 +1,25 @@
<script setup>
import { Head } from '@inertiajs/vue3';
import { Head, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import AppButton from '@/Components/AppButton.vue'
defineOptions({ layout: AppLayout })
const handleContinue = () => {
router.post('/screening')
}
</script>
<template>
<Head title="Welcome" />
<div class="min-h-screen flex items-center justify-center bg-surface">
<div class="text-center">
<div class="flex items-center justify-center py-16">
<div class="text-center max-w-2xl mx-auto px-4">
<h1 class="text-4xl font-bold text-white mb-4">Go / No Go</h1>
<p class="text-gray-400">Baker Tilly International Questionnaire Application</p>
<p class="text-gray-400 mb-8">Baker Tilly International Questionnaire Application</p>
<AppButton size="lg" @click="handleContinue">
Continue
</AppButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,51 @@
<script setup>
import { Head, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import AppButton from '@/Components/AppButton.vue'
defineOptions({ layout: AppLayout })
const props = defineProps({
screening: {
type: Object,
required: true,
},
categories: {
type: Array,
required: true,
},
})
const handleStartCategory = (categoryId) => {
router.post('/sessions', {
category_id: categoryId,
screening_id: props.screening.id,
})
}
</script>
<template>
<Head title="Screening Result" />
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-white mb-6">Screening Result</h1>
<div class="bg-surface/50 rounded-lg p-6 mb-8">
<p class="text-gray-400 text-center mb-4">Your screening result: Passed</p>
</div>
<div class="space-y-4">
<h2 class="text-2xl font-semibold text-white mb-4">Select a Category</h2>
<div
v-for="category in categories"
:key="category.id"
class="bg-surface/50 rounded-lg p-4 flex items-center justify-between hover:bg-surface/70 transition-colors"
>
<span class="text-white font-medium">{{ category.name }}</span>
<AppButton size="md" @click="handleStartCategory(category.id)">
Start
</AppButton>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,36 @@
<script setup>
import { Head, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import AppButton from '@/Components/AppButton.vue'
defineOptions({ layout: AppLayout })
const props = defineProps({
screening: {
type: Object,
required: true,
},
})
const handleSubmit = () => {
router.put(`/screening/${props.screening.id}`)
}
</script>
<template>
<Head title="Pre-Screening Questions" />
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-white mb-6">Pre-Screening Questions</h1>
<div class="bg-surface/50 rounded-lg p-6 mb-8">
<p class="text-gray-400 text-center">10 Yes/No questions will appear here</p>
</div>
<div class="flex justify-end">
<AppButton size="lg" @click="handleSubmit">
Submit
</AppButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,32 @@
<script setup>
import { Head } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import AppButton from '@/Components/AppButton.vue'
defineOptions({ layout: AppLayout })
const props = defineProps({
session: {
type: Object,
required: true,
},
})
</script>
<template>
<Head title="Session Result" />
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-white mb-6">Session Result</h1>
<div class="bg-surface/50 rounded-lg p-6 mb-8">
<p class="text-gray-400 text-center">Your result will appear here</p>
</div>
<div class="flex justify-center">
<AppButton size="lg" href="/">
Again
</AppButton>
</div>
</div>
</template>

View File

@@ -0,0 +1,44 @@
<script setup>
import { Head, router } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import AppButton from '@/Components/AppButton.vue'
defineOptions({ layout: AppLayout })
const props = defineProps({
session: {
type: Object,
required: true,
},
})
const handleComplete = () => {
router.put(`/sessions/${props.session.id}`)
}
</script>
<template>
<Head :title="`${session.category.name} Questionnaire`" />
<div class="max-w-4xl mx-auto px-4 py-8">
<h1 class="text-3xl font-bold text-white mb-6">{{ session.category.name }} Questionnaire</h1>
<div class="space-y-6">
<div class="bg-surface/50 rounded-lg p-6">
<h2 class="text-xl font-semibold text-white mb-4">Basic Info Form</h2>
<p class="text-gray-400">Client and lead firm information will appear here</p>
</div>
<div class="bg-surface/50 rounded-lg p-6">
<h2 class="text-xl font-semibold text-white mb-4">Questions</h2>
<p class="text-gray-400">Category questions will appear here</p>
</div>
</div>
<div class="flex justify-end mt-8">
<AppButton size="lg" @click="handleComplete">
Complete
</AppButton>
</div>
</div>
</template>

View File

@@ -3,16 +3,36 @@
declare(strict_types=1);
use App\Http\Controllers\Auth\SocialiteController;
use App\Http\Controllers\LandingController;
use App\Http\Controllers\ScreeningController;
use App\Http\Controllers\SessionController;
use Illuminate\Support\Facades\Route;
use Inertia\Inertia;
Route::get('/', fn () => Inertia::render('Landing'))->name('landing');
// Landing page (public)
Route::get('/', [LandingController::class, 'index'])->name('landing');
// Authentication routes
Route::get('/login', [SocialiteController::class, 'redirect'])->name('login');
Route::get('/auth/callback', [SocialiteController::class, 'callback']);
Route::post('/logout', [SocialiteController::class, 'logout'])->name('logout')->middleware('auth');
// Questionnaire routes (authenticated)
Route::middleware('auth')->group(function () {
// Screening routes
Route::post('/screening', [ScreeningController::class, 'store'])->name('screening.store');
Route::get('/screening/{screening}', [ScreeningController::class, 'show'])->name('screening.show');
Route::put('/screening/{screening}', [ScreeningController::class, 'update'])->name('screening.update');
Route::get('/screening/{screening}/result', [ScreeningController::class, 'result'])->name('screening.result');
// Session routes (with history encryption)
Route::middleware('encrypt.history')->group(function () {
Route::post('/sessions', [SessionController::class, 'store'])->name('sessions.store');
Route::get('/sessions/{session}', [SessionController::class, 'show'])->name('sessions.show');
Route::put('/sessions/{session}', [SessionController::class, 'update'])->name('sessions.update');
Route::get('/sessions/{session}/result', [SessionController::class, 'result'])->name('sessions.result');
});
});
// Dev auto-login route
if (app()->environment('local', 'testing')) {
Route::get('/login-jonathan', function () {