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(npm run build:*)",
"Bash(ls:*)", "Bash(ls:*)",
"Bash(xargs:*)", "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: [ $middleware->web(append: [
\App\Http\Middleware\HandleInertiaRequests::class, \App\Http\Middleware\HandleInertiaRequests::class,
]); ]);
$middleware->alias([
'encrypt.history' => \Inertia\Middleware\EncryptHistory::class,
]);
}) })
->withExceptions(function (Exceptions $exceptions): void { ->withExceptions(function (Exceptions $exceptions): void {
// //

View File

@@ -10,7 +10,7 @@ # Implementation Plan: Go No Go
## Step 1: Install Dependencies and Configure Build ## 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`). 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 ## 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. 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 ## 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). 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 ## 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. 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 ## 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. 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.** [ ] **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:** **Creates:**
- `resources/js/Components/QuestionCard.vue` - `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` - Updated `app/Http/Controllers/SessionController.php` -- answer saving logic in `update`
- Answer validation rules - 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):** **Browser Verification (Playwright MCP):**
1. `browser_navigate` to `http://go-no-go.test/login-jonathan` 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 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:* *Test single-page question rendering:*
3. `browser_snapshot` -- confirm first question group renders with questions and Next button 3. `browser_snapshot` -- confirm all questions render on a single page
4. `browser_take_screenshot` -- save as `step-09-question-group-1.png` 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: 5. For each question pattern encountered:
- `browser_snapshot` -- verify correct UI elements (radio buttons for Yes/No/NA, details textarea visibility) - `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 - `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` - `browser_take_screenshot` -- save as `step-09-conditional-details.png`
*Test answer saving:* *Test answer saving:*
7. `browser_fill_form` -- answer several questions in the current group 7. `browser_fill_form` -- answer several questions on the page
8. `browser_click` the Next button to go to the next group 8. Refresh the page
9. `browser_click` the Previous button to return 9. `browser_snapshot` -- confirm previously saved answers are still selected
10. `browser_snapshot` -- confirm previously saved answers are still selected 10. `browser_take_screenshot` -- save as `step-09-answers-retained.png`
11. `browser_take_screenshot` -- save as `step-09-answers-retained.png`
*Test step navigation:* *Test Additional Comments section:*
12. `browser_click` Next through all question groups to reach the Additional Comments step 11. Scroll to the bottom of the page
13. `browser_snapshot` -- confirm Additional Comments textarea renders 12. `browser_snapshot` -- confirm Additional Comments textarea renders at the bottom
14. `browser_take_screenshot` -- save as `step-09-additional-comments.png` 13. `browser_take_screenshot` -- save as `step-09-additional-comments.png`
15. `browser_console_messages` with level `error` -- verify no errors throughout navigation 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. - Score >= 5 points → pass. Page shows category picker to continue.
6. User selects a category → creates a session, redirected to Session/Show 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) 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: 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 `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 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`) - 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) 10. Running score displayed (for scored questions only)
11. Color-coded result indicator updates live (green/yellow/red) 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) 13. Submit and view result (GO / NO GO / Consult Leadership)
14. Session saved with score and result 14. Session saved with score and result
15. All result pages have an "Again" button that returns to `/` 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> <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> </script>
<template> <template>
<Head title="Welcome" /> <Head title="Welcome" />
<div class="min-h-screen flex items-center justify-center bg-surface"> <div class="flex items-center justify-center py-16">
<div class="text-center"> <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> <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>
</div> </div>
</template> </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); declare(strict_types=1);
use App\Http\Controllers\Auth\SocialiteController; 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 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 // Authentication routes
Route::get('/login', [SocialiteController::class, 'redirect'])->name('login'); Route::get('/login', [SocialiteController::class, 'redirect'])->name('login');
Route::get('/auth/callback', [SocialiteController::class, 'callback']); Route::get('/auth/callback', [SocialiteController::class, 'callback']);
Route::post('/logout', [SocialiteController::class, 'logout'])->name('logout')->middleware('auth'); 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 // Dev auto-login route
if (app()->environment('local', 'testing')) { if (app()->environment('local', 'testing')) {
Route::get('/login-jonathan', function () { Route::get('/login-jonathan', function () {