diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 69596fd..69df8df 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -13,7 +13,8 @@ "mcp__playwright__browser_click", "mcp__playwright__browser_handle_dialog", "mcp__playwright__browser_snapshot", - "mcp__playwright__browser_close" + "mcp__playwright__browser_close", + "WebFetch(domain:docs.laravel-excel.com)" ] } } diff --git a/CLAUDE.md b/CLAUDE.md index 1d844b8..a35c494 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -2,11 +2,12 @@ # CLAUDE.md – Go No Go ## Project Overview -Laravel 12 application with Laravel Nova 5 administration panel. The admin panel is served at `/cp`. Authentication includes two-factor authentication (2FA) and email verification. +Laravel 12 application with Laravel Nova 5 administration panel. The admin panel is served at `/cp`. Frontend users authenticate via Azure AD SSO (Laravel Socialite). Nova administrators use Fortify with 2FA and email verification. - **Framework:** Laravel 12 - **Admin Panel:** Laravel Nova 5 at `/cp` -- **Auth:** Fortify with 2FA + email verification +- **Auth (frontend):** Laravel Socialite with Azure AD SSO +- **Auth (Nova):** Fortify with 2FA + email verification - **Database:** MySQL (`go-no-go`) - **Local URL:** http://go-no-go.test (Laravel Herd) diff --git a/docs/index.md b/docs/index.md index dbe0498..4403bda 100644 --- a/docs/index.md +++ b/docs/index.md @@ -7,6 +7,7 @@ ## Documentation Files ### Root Level - `docs/index.md` - This file; master index of all project documentation +- `docs/technical-requirements.md` - Technical requirements for the questionnaire system ### Agents diff --git a/docs/technical-requirements.md b/docs/technical-requirements.md new file mode 100644 index 0000000..05e3828 --- /dev/null +++ b/docs/technical-requirements.md @@ -0,0 +1,305 @@ +# Technical Requirements: Go No Go + +## 1. Overview + +Baker Tilly International "Go/No Go" checklist application. Users complete scored questionnaires across categories to decide whether to pursue business opportunities. Each questionnaire session covers one category, with questions organized into groups. Scored questions produce a cumulative score that determines a Go / No Go / Consult Leadership result. + +**Category note:** The original plan specifies 6 categories (Audit, Outsource, Solution, Digital Solutions, Legal, Tax). The question source documents contain 5 files. "Solution" may be separate from "Outsource Solutions" or merged -- seed all 6 per the plan and reconcile during question seeding. + +## 2. Tech Stack + +- **Backend:** Laravel 12 +- **Frontend:** Vue 3 with Inertia.js v2 +- **Admin Panel:** Laravel Nova 5 at `/cp` +- **Database:** MySQL (`go-no-go`) +- **Auth (frontend):** Laravel Socialite with Azure AD SSO +- **Auth (Nova):** Laravel Fortify (email/password + 2FA) +- **Excel Export:** `maatwebsite/laravel-nova-excel` + +## 3. Two Separate Interfaces + +The application has two distinct interfaces: + +- **Frontend (questionnaire):** Custom Vue 3 + Inertia.js v2 application at `/`. All questionnaire flow, session management, and user-facing pages. Custom routes, controllers, and Vue page components. +- **Admin panel (Nova):** Laravel Nova 5 at `/cp`. Administrators log in here to manage questions and view sessions/answers. No questionnaire logic — strictly data management. + +These are fully separate. The frontend has its own routes, controllers, and Vue pages. Nova handles administration only. + +### Authentication + +Each interface has its own authentication method: + +- **Frontend:** Azure AD Single Sign-On via Laravel Socialite. Users visit `/login`, get redirected to Azure AD, and are returned to `/auth/callback`. Socialite handles the OAuth flow. Users are created or matched by email on first login. No password stored for frontend users. +- **Nova:** Standard email/password login via Laravel Fortify with 2FA and email verification at `/cp/login`. Only administrators access Nova. + +## 4. Frontend Architecture (Vue + Inertia.js v2) + +The entire user-facing frontend is built with Vue 3 and Inertia.js v2. No Blade templates for user pages. + +### Inertia Features and Their Application + +| Feature | Usage | Why | +|---------|-------|-----| +| **useForm helper** | All questionnaire form submissions | Provides form state, error handling, processing indicators, dirty tracking. Each questionnaire step uses useForm to submit answers. | +| **Form component** | Login, basic info entry | Declarative form handling for simpler forms that don't need granular control. | +| **Link component** | Navigation between steps, dashboard, session history | SPA navigation. Use `only`/`except` props for partial reloads. | +| **Shared data** | Via `HandleInertiaRequests` middleware | Authenticated user, current session progress, flash messages, scoring thresholds -- available on every page without explicit passing. | +| **Persistent layouts** | Main app layout | Layout persists across questionnaire steps, keeping sidebar/progress indicator state without remounting. | +| **Deferred props** | `Inertia::defer()` for session history, dashboard statistics | Load questionnaire data immediately; defer non-critical analytics data. | +| **WhenVisible** | Long questionnaire pages displayed as single scroll | Load question group data only when user scrolls to that section. | +| **Progress indicator** | Built-in NProgress bar | Visual feedback during page transitions between questionnaire steps. | +| **Head component** | Dynamic page titles | Per-step titles like "Step 3: Risk Assessment - Audit". | +| **Partial reloads** | Saving individual answers | When saving a single answer, only reload answers/score data, not the entire question set. | +| **History encryption** | `EncryptHistory` middleware | Questionnaire data may contain sensitive client information -- encrypt browser history state. | +| **Prefetching** | Next questionnaire step | Prefetch on hover/focus for instant navigation to the next step. | +| **Polling** | Optional/future | Not needed for core flow. Could poll for session lock status if multi-user editing is added later. | +| **Scroll management** | Save inline vs. navigate | Preserve scroll position when saving answers inline; scroll to top when navigating to a new question group. | + +## 5. Database Tables + +### categories + +The 6 fixed assessment categories. Seeded, not user-managed. + +| Column | Type | Constraints | +|--------|------|-------------| +| id | bigint unsigned | PK, auto-increment | +| name | varchar(255) | required, unique | +| sort_order | unsigned integer | required, default 0 | +| created_at | timestamp | nullable | +| updated_at | timestamp | nullable | + +**Seed data (in order):** + +1. Audit +2. Outsource +3. Solution +4. Digital Solutions +5. Legal +6. Tax + +### question_groups + +Groups of related questions, scoped to a single category. Each group becomes one step in the questionnaire flow. + +| Column | Type | Constraints | +|--------|------|-------------| +| id | bigint unsigned | PK, auto-increment | +| category_id | bigint unsigned | FK to categories.id, required | +| name | varchar(255) | required | +| sort_order | unsigned integer | required, default 0 | +| description | text | nullable | +| scoring_instructions | text | nullable, for display text like "If you answer yes, you will score 1 point..." | +| created_at | timestamp | nullable | +| updated_at | timestamp | nullable | + +**Groups per category (from question docs):** + +- **Audit** (7 groups): Opportunity Details, Client Background and History, Financial Information, Regulatory Compliance, Risk Assessment, Resource Allocation, Reporting Requirements +- **Outsource Solutions** (6 groups): Opportunity Details, Client Background and History, Regulatory Compliance, Risk Assessment, Resource Allocation +- **Digital Solutions** (7 groups): Opportunity Details, Client Background and History, Regulatory Compliance, Risk Assessment, Resource Allocation, Technology & Innovation Fit +- **Legal** (8 groups): Opportunity Details, Client Background and History, Financial Information, Regulatory Compliance, Risk Assessment, Resource Allocation, Stakeholder Engagement, Fee Quote +- **Tax** (7 groups): Opportunity Details, Client Background and History, Financial Information, Regulatory Compliance, Risk Assessment, Resource Allocation, Stakeholder Engagement + +All categories also have an "Additional Comments" section handled as the `additional_comments` field on the session, not as a separate question group. + +### questions + +Individual questions within a group. Each question is self-contained with boolean flags controlling which answer options are available and how details are handled. + +| Column | Type | Constraints | +|--------|------|-------------| +| id | bigint unsigned | PK, auto-increment | +| question_group_id | bigint unsigned | FK to question_groups.id, required | +| text | text | required | +| has_yes | boolean | required, default false | +| has_no | boolean | required, default false | +| has_na | boolean | required, default false | +| details | varchar(50) | nullable, enum: `req_on_no`, `req_on_yes`, `optional`, `required` | +| sort_order | unsigned integer | required, default 0 | +| is_scored | boolean | required, default false | +| created_at | timestamp | nullable | +| updated_at | timestamp | nullable | + +#### Question Configuration + +Each question is configured through boolean flags and a details mode. No cross-question dependencies exist — every question is self-contained. + +**Six configuration patterns cover all question types:** + +| Pattern | has_yes | has_no | has_na | details | Example | +|---------|---------|--------|--------|---------|---------| +| Pure open text | false | false | false | `required` | "What sort of audit opportunity is it?" | +| Scored, no details | true | true | true | null | "Does the sector come with a reputation we are comfortable with?" | +| Scored + details on no | true | true | true | `req_on_no` | "There have been no significant changes in the client's business?" | +| Scored + details on yes | true | true | true | `req_on_yes` | "Are there any previous audit reports or findings that need to be considered?" | +| Scored + details always | true | true | true | `required` | "Have you completed a conflict check?" | +| Scored + details optional | true | true | true | `optional` | "Has the client provided financial statements?" | + +- When `has_yes`, `has_no`, and `has_na` are all false → the question renders as open text only (textarea) +- When any are true → radio buttons render for the enabled options +- The `details` column controls when a details textarea appears and whether it is required + +### sessions + +One entry per user per questionnaire run. A session covers one category (not all categories). + +| Column | Type | Constraints | +|--------|------|-------------| +| id | bigint unsigned | PK, auto-increment | +| user_id | bigint unsigned | FK to users.id, required | +| category_id | bigint unsigned | FK to categories.id, required | +| status | varchar(50) | required, enum: `in_progress`, `completed`, `abandoned` | +| score | integer | nullable, calculated from scored answers | +| result | varchar(50) | nullable, enum: `go`, `no_go`, `consult_leadership` | +| basic_info | json | stores: client_name, client_contact, lead_firm_name, lead_firm_contact | +| additional_comments | text | nullable, free text from final section | +| completed_at | timestamp | nullable | +| created_at | timestamp | nullable | +| updated_at | timestamp | nullable | + +#### Basic Info JSON Structure + +Every category starts with these fields, stored in `basic_info`: + +```json +{ + "client_name": "Acme Corp", + "client_contact": "John Doe", + "lead_firm_name": "Baker Tilly NL", + "lead_firm_contact": "Jane Smith" +} +``` + +### answers + +Stores all user responses. One answer per question per session. + +| Column | Type | Constraints | +|--------|------|-------------| +| id | bigint unsigned | PK, auto-increment | +| session_id | bigint unsigned | FK to sessions.id, required | +| question_id | bigint unsigned | FK to questions.id, required | +| value | varchar(255) | nullable, stores `"yes"` / `"no"` / `"not_applicable"` for scored questions | +| text_value | text | nullable, stores free text for open text questions and details fields | +| created_at | timestamp | nullable | +| updated_at | timestamp | nullable | + +**Unique constraint:** `(session_id, question_id)` -- one answer per question per session. + +### logs + +Comprehensive activity log for usage analytics. Every meaningful user action is recorded to enable statistics on: who is using the application, which categories are used most, usage frequency, whether certain questions are always answered the same way, and whether specific firms or groups are more active than others. + +| Column | Type | Constraints | +|--------|------|-------------| +| id | bigint unsigned | PK, auto-increment | +| user_id | bigint unsigned | FK to users.id, nullable (for unauthenticated events like login attempts) | +| session_id | bigint unsigned | FK to sessions.id, nullable | +| category_id | bigint unsigned | FK to categories.id, nullable | +| action | varchar(100) | required | +| metadata | json | nullable, additional context per action | +| created_at | timestamp | nullable | + +The `logs` table is append-only — rows are never updated or deleted. + +#### Logged Actions + +| Action | When | Metadata | +|--------|------|----------| +| `login` | User completes SSO | `{email, firm_name}` | +| `logout` | User logs out | — | +| `session_started` | New session created | `{category_id}` | +| `session_completed` | Session submitted | `{category_id, score, result}` | +| `session_abandoned` | Session marked abandoned | `{category_id}` | +| `answer_saved` | User saves an answer | `{question_id, question_group_id, value}` | +| `step_viewed` | User navigates to a question group | `{question_group_id}` | + +## 6. Frontend Routes & Controllers + +All questionnaire routes are custom (defined in `routes/web.php`), handled by custom controllers, and render Vue page components via Inertia. + +| Method | Route | Controller | Vue Page | Description | +|--------|-------|------------|----------|-------------| +| GET | `/` | DashboardController@index | Dashboard | Start page. Start new session or view past sessions. | +| GET | `/login` | SocialiteController@redirect | — | Redirects to Azure AD for SSO | +| GET | `/auth/callback` | SocialiteController@callback | — | Azure AD callback, creates/finds user, logs in | +| POST | `/logout` | SocialiteController@logout | — | Logout | +| POST | `/sessions` | SessionController@store | — | Create a new session for a selected category | +| GET | `/sessions/{session}` | SessionController@show | Session/Show | Basic info form + questionnaire flow | +| PUT | `/sessions/{session}` | SessionController@update | — | Save answers, update basic info, submit session | +| GET | `/sessions/{session}/result` | SessionController@result | Session/Result | View final result after submission | + +Nova is served separately at `/cp` with its own authentication. No custom routes needed for Nova. + +## 7. Questionnaire Flow + +1. User logs in via Azure AD SSO +2. `/` is the start page — shows: start new session, view past sessions +3. User selects a category to start a new session +4. Basic Information form (client name, client contact, lead firm name, lead firm contact) +5. Step through question groups in order (each group is a page/step) +6. 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`) +7. Running score displayed (for scored questions only) +8. Color-coded result indicator updates live (green/yellow/red) +9. Final step: Additional comments free text +10. Submit and view result (GO / NO GO / Consult Leadership) +11. Session saved with score and result + +## 8. Scoring Logic + +- Only questions with `is_scored = true` are scored +- **Yes** = 1 point +- **No** = 0 points +- **Not Applicable** = excluded from scoring (does not count toward total) +- Score calculated server-side and returned to frontend via Inertia props + +### Thresholds (same across all categories) + +| Score | Result | Color | Meaning | +|-------|--------|-------|---------| +| 10+ points | GO | Green | Pursue the opportunity | +| 5-9 points | Consult Leadership | Yellow | Speak to SL or SSL leadership | +| 1-4 points | NO GO | Red | Do not pursue | + +## 9. Nova Resources & Policies + +All Nova resources have an associated Laravel Policy that controls authorization. + +| Resource | Model | Policy | Menu Item | Notes | +|----------|-------|--------|-----------|-------| +| CategoryResource | Category | CategoryPolicy | No | Lookup data only — no dedicated resource page. Exists as a Nova resource class so it can be referenced in relational fields (BelongsTo, HasMany). No CRUD operations exposed. | +| QuestionGroupResource | QuestionGroup | QuestionGroupPolicy | No | Read-only. Filterable by category. No create, update, or delete. Displayed inline via HasMany on other resources only. | +| QuestionResource | Question | QuestionPolicy | Yes | Only the `text` field is editable by administrators. All other fields (has_yes, has_no, has_na, details, is_scored, sort_order) are read-only after seeding. No create or delete. | +| SessionResource | Session | SessionPolicy | Yes | Filterable by user, category, and status. View scores and results. No create, update, or delete from Nova. | +| AnswerResource | Answer | AnswerPolicy | No | Strictly read-only — no create, update, or delete by anyone (administrators included). Viewed within session context only. | +| LogResource | Log | LogPolicy | Yes | Read-only. Filterable by user, category, action, and date range. For usage analytics and statistics. No create, update, or delete. | + +### Policy Summary + +| Model | View | Create | Update | Delete | +|-------|------|--------|--------|--------| +| Category | Yes | No | No | No | +| QuestionGroup | Yes | No | No | No | +| Question | Yes | No | Yes (text only) | No | +| Session | Yes | No | No | No | +| Answer | Yes | No | No | No | +| Log | Yes | No | No | No | + +### Nova Field Behavior (Global Rule) + +Applies to **all** Nova resources without exception: + +- **Filterable:** Every field that can be filtered, must be filterable. +- **Sortable:** Every field that can be sorted, must be sortable. +- **Copyable:** Every field that can be copied, must be copyable. + +## 10. Nova Excel Export + +- **Package:** `maatwebsite/laravel-nova-excel` +- Every Nova resource index gets a `DownloadExcel` action +- Exports all visible columns to `.xlsx` +- Added to every resource's `actions()` method diff --git a/docs/theming-templating-vue.md b/docs/theming-templating-vue.md new file mode 100644 index 0000000..e69de29