diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ba4f4f1..f14931e 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -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" ] } } diff --git a/.playwright-mcp/step-01-homepage.png b/.playwright-mcp/step-01-homepage.png new file mode 100644 index 0000000..c0d2dde Binary files /dev/null and b/.playwright-mcp/step-01-homepage.png differ diff --git a/.playwright-mcp/step-03-login-jonathan.png b/.playwright-mcp/step-03-login-jonathan.png new file mode 100644 index 0000000..c0d2dde Binary files /dev/null and b/.playwright-mcp/step-03-login-jonathan.png differ diff --git a/.playwright-mcp/step-03-nova-login.png b/.playwright-mcp/step-03-nova-login.png new file mode 100644 index 0000000..1d14c75 Binary files /dev/null and b/.playwright-mcp/step-03-nova-login.png differ diff --git a/.playwright-mcp/step-04-layout.png b/.playwright-mcp/step-04-layout.png new file mode 100644 index 0000000..c348767 Binary files /dev/null and b/.playwright-mcp/step-04-layout.png differ diff --git a/.playwright-mcp/step-05-landing.png b/.playwright-mcp/step-05-landing.png new file mode 100644 index 0000000..d2455ce Binary files /dev/null and b/.playwright-mcp/step-05-landing.png differ diff --git a/.playwright-mcp/step-05-screening-result.png b/.playwright-mcp/step-05-screening-result.png new file mode 100644 index 0000000..06f4050 Binary files /dev/null and b/.playwright-mcp/step-05-screening-result.png differ diff --git a/.playwright-mcp/step-05-screening.png b/.playwright-mcp/step-05-screening.png new file mode 100644 index 0000000..0c95453 Binary files /dev/null and b/.playwright-mcp/step-05-screening.png differ diff --git a/.playwright-mcp/step-05-session-result.png b/.playwright-mcp/step-05-session-result.png new file mode 100644 index 0000000..a6222b8 Binary files /dev/null and b/.playwright-mcp/step-05-session-result.png differ diff --git a/.playwright-mcp/step-05-session-show.png b/.playwright-mcp/step-05-session-show.png new file mode 100644 index 0000000..d01748e Binary files /dev/null and b/.playwright-mcp/step-05-session-show.png differ diff --git a/app/Http/Controllers/LandingController.php b/app/Http/Controllers/LandingController.php new file mode 100644 index 0000000..b28a135 --- /dev/null +++ b/app/Http/Controllers/LandingController.php @@ -0,0 +1,19 @@ + 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']), + ]); + } +} diff --git a/app/Http/Controllers/SessionController.php b/app/Http/Controllers/SessionController.php new file mode 100644 index 0000000..37d5191 --- /dev/null +++ b/app/Http/Controllers/SessionController.php @@ -0,0 +1,59 @@ + 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, + ]); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 3844a23..021bca9 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -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 { // diff --git a/docs/implementation-plan.md b/docs/implementation-plan.md index 846d38e..349cefb 100644 --- a/docs/implementation-plan.md +++ b/docs/implementation-plan.md @@ -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 --- diff --git a/docs/technical-requirements.md b/docs/technical-requirements.md index 7f7ba61..d3c19d3 100644 --- a/docs/technical-requirements.md +++ b/docs/technical-requirements.md @@ -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 `/` diff --git a/resources/js/Components/AppButton.vue b/resources/js/Components/AppButton.vue new file mode 100644 index 0000000..1e8d7e0 --- /dev/null +++ b/resources/js/Components/AppButton.vue @@ -0,0 +1,109 @@ + + + diff --git a/resources/js/Components/AppLogo.vue b/resources/js/Components/AppLogo.vue new file mode 100644 index 0000000..1fc79ee --- /dev/null +++ b/resources/js/Components/AppLogo.vue @@ -0,0 +1,14 @@ + + + diff --git a/resources/js/Components/PageHeader.vue b/resources/js/Components/PageHeader.vue new file mode 100644 index 0000000..87a2fdd --- /dev/null +++ b/resources/js/Components/PageHeader.vue @@ -0,0 +1,21 @@ + + + diff --git a/resources/js/Components/ScoreIndicator.vue b/resources/js/Components/ScoreIndicator.vue new file mode 100644 index 0000000..e3f8d57 --- /dev/null +++ b/resources/js/Components/ScoreIndicator.vue @@ -0,0 +1,65 @@ + + + diff --git a/resources/js/Layouts/AppLayout.vue b/resources/js/Layouts/AppLayout.vue new file mode 100644 index 0000000..6c98141 --- /dev/null +++ b/resources/js/Layouts/AppLayout.vue @@ -0,0 +1,20 @@ + + + diff --git a/resources/js/Pages/Landing.vue b/resources/js/Pages/Landing.vue index ed5304e..f0574a8 100644 --- a/resources/js/Pages/Landing.vue +++ b/resources/js/Pages/Landing.vue @@ -1,14 +1,25 @@ diff --git a/resources/js/Pages/Screening/Result.vue b/resources/js/Pages/Screening/Result.vue new file mode 100644 index 0000000..4a823f7 --- /dev/null +++ b/resources/js/Pages/Screening/Result.vue @@ -0,0 +1,51 @@ + + + diff --git a/resources/js/Pages/Screening/Show.vue b/resources/js/Pages/Screening/Show.vue new file mode 100644 index 0000000..1e094cc --- /dev/null +++ b/resources/js/Pages/Screening/Show.vue @@ -0,0 +1,36 @@ + + + diff --git a/resources/js/Pages/Session/Result.vue b/resources/js/Pages/Session/Result.vue new file mode 100644 index 0000000..b72fbe9 --- /dev/null +++ b/resources/js/Pages/Session/Result.vue @@ -0,0 +1,32 @@ + + + diff --git a/resources/js/Pages/Session/Show.vue b/resources/js/Pages/Session/Show.vue new file mode 100644 index 0000000..ecd5837 --- /dev/null +++ b/resources/js/Pages/Session/Show.vue @@ -0,0 +1,44 @@ + + + diff --git a/routes/web.php b/routes/web.php index 12cbb3f..d8dd4c0 100644 --- a/routes/web.php +++ b/routes/web.php @@ -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 () {