Page Stubs and Click-Through Flow
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
BIN
.playwright-mcp/step-01-homepage.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/step-03-login-jonathan.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
BIN
.playwright-mcp/step-03-nova-login.png
Normal file
|
After Width: | Height: | Size: 101 KiB |
BIN
.playwright-mcp/step-04-layout.png
Normal file
|
After Width: | Height: | Size: 17 KiB |
BIN
.playwright-mcp/step-05-landing.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
.playwright-mcp/step-05-screening-result.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
.playwright-mcp/step-05-screening.png
Normal file
|
After Width: | Height: | Size: 22 KiB |
BIN
.playwright-mcp/step-05-session-result.png
Normal file
|
After Width: | Height: | Size: 18 KiB |
BIN
.playwright-mcp/step-05-session-show.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
19
app/Http/Controllers/LandingController.php
Normal 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');
|
||||
}
|
||||
}
|
||||
56
app/Http/Controllers/ScreeningController.php
Normal 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']),
|
||||
]);
|
||||
}
|
||||
}
|
||||
59
app/Http/Controllers/SessionController.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
//
|
||||
|
||||
@@ -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
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 `/`
|
||||
|
||||
109
resources/js/Components/AppButton.vue
Normal 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>
|
||||
14
resources/js/Components/AppLogo.vue
Normal 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>
|
||||
21
resources/js/Components/PageHeader.vue
Normal 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>
|
||||
65
resources/js/Components/ScoreIndicator.vue
Normal 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>
|
||||
20
resources/js/Layouts/AppLayout.vue
Normal 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>
|
||||
@@ -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>
|
||||
|
||||
51
resources/js/Pages/Screening/Result.vue
Normal 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>
|
||||
36
resources/js/Pages/Screening/Show.vue
Normal 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>
|
||||
32
resources/js/Pages/Session/Result.vue
Normal 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>
|
||||
44
resources/js/Pages/Session/Show.vue
Normal 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>
|
||||
@@ -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 () {
|
||||
|
||||