plan implementation 6, 7, 8, 9, 10
This commit is contained in:
@@ -24,7 +24,9 @@
|
|||||||
"Write",
|
"Write",
|
||||||
"Bash",
|
"Bash",
|
||||||
"mcp__playwright__browser_console_messages",
|
"mcp__playwright__browser_console_messages",
|
||||||
"mcp__playwright__browser_navigate_back"
|
"mcp__playwright__browser_navigate_back",
|
||||||
|
"mcp__playwright__browser_run_code",
|
||||||
|
"mcp__playwright__browser_wait_for"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\Screening\UpdateScreeningRequest;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
use App\Models\Screening;
|
use App\Models\Screening;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
@@ -32,14 +33,20 @@ public function show(Screening $screening): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('Screening/Show', [
|
return Inertia::render('Screening/Show', [
|
||||||
'screening' => $screening,
|
'screening' => $screening,
|
||||||
|
'questions' => array_values(config('screening.questions')),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save screening answers and redirect to result.
|
* Save screening answers and redirect to result.
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, Screening $screening): RedirectResponse
|
public function update(UpdateScreeningRequest $request, Screening $screening): RedirectResponse
|
||||||
{
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
$this->saveAnswers($screening, $validated['answers']);
|
||||||
|
$this->calculateAndUpdateScore($screening, $validated['answers']);
|
||||||
|
|
||||||
return redirect()->route('screening.result', $screening);
|
return redirect()->route('screening.result', $screening);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -50,7 +57,58 @@ public function result(Screening $screening): Response
|
|||||||
{
|
{
|
||||||
return Inertia::render('Screening/Result', [
|
return Inertia::render('Screening/Result', [
|
||||||
'screening' => $screening,
|
'screening' => $screening,
|
||||||
'categories' => Category::orderBy('sort_order')->get(['id', 'name']),
|
'passed' => $screening->passed,
|
||||||
|
'score' => $screening->score,
|
||||||
|
'totalQuestions' => count(config('screening.questions')),
|
||||||
|
'categories' => $screening->passed ? Category::orderBy('sort_order')->get(['id', 'name']) : [],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save screening answers to the database using upsert pattern.
|
||||||
|
*/
|
||||||
|
private function saveAnswers(Screening $screening, array $answers): void
|
||||||
|
{
|
||||||
|
foreach ($answers as $questionNumber => $value) {
|
||||||
|
$screening->answers()->updateOrCreate(
|
||||||
|
[
|
||||||
|
'screening_id' => $screening->id,
|
||||||
|
'question_number' => (int) $questionNumber,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'value' => $value,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the score and update the screening record.
|
||||||
|
*/
|
||||||
|
private function calculateAndUpdateScore(Screening $screening, array $answers): void
|
||||||
|
{
|
||||||
|
$score = $this->calculateScore($answers);
|
||||||
|
$passed = $score >= config('screening.passing_score', 5);
|
||||||
|
|
||||||
|
$screening->update([
|
||||||
|
'score' => $score,
|
||||||
|
'passed' => $passed,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calculate the total score from the answers.
|
||||||
|
*/
|
||||||
|
private function calculateScore(array $answers): int
|
||||||
|
{
|
||||||
|
$score = 0;
|
||||||
|
|
||||||
|
foreach ($answers as $value) {
|
||||||
|
if ($value === 'yes') {
|
||||||
|
$score++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $score;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers;
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\Session\UpdateSessionRequest;
|
||||||
use App\Models\Session;
|
use App\Models\Session;
|
||||||
|
use App\Services\ScoringService;
|
||||||
use Illuminate\Http\RedirectResponse;
|
use Illuminate\Http\RedirectResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use Inertia\Inertia;
|
use Inertia\Inertia;
|
||||||
@@ -28,22 +30,89 @@ public function store(Request $request): RedirectResponse
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Display the session questionnaire with category.
|
* Display the session questionnaire with category, question groups, questions, and existing answers.
|
||||||
*/
|
*/
|
||||||
public function show(Session $session): Response
|
public function show(Session $session): Response
|
||||||
{
|
{
|
||||||
$session->load('category');
|
$session->load('category');
|
||||||
|
|
||||||
|
$questionGroups = $session->category
|
||||||
|
->questionGroups()
|
||||||
|
->with(['questions' => fn ($q) => $q->orderBy('sort_order')])
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$answers = $session->answers()->get()->keyBy('question_id');
|
||||||
|
|
||||||
|
$scoringService = new ScoringService;
|
||||||
|
$score = $scoringService->calculateScore($session);
|
||||||
|
|
||||||
return Inertia::render('Session/Show', [
|
return Inertia::render('Session/Show', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
|
'questionGroups' => $questionGroups,
|
||||||
|
'answers' => $answers,
|
||||||
|
'score' => $score,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save session answers and redirect to result.
|
* Save session basic info, answers, and additional comments.
|
||||||
*/
|
*/
|
||||||
public function update(Request $request, Session $session): RedirectResponse
|
public function update(UpdateSessionRequest $request, Session $session): RedirectResponse
|
||||||
{
|
{
|
||||||
|
$validated = $request->validated();
|
||||||
|
|
||||||
|
if (isset($validated['basic_info'])) {
|
||||||
|
$session->update(['basic_info' => $validated['basic_info']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($validated['answers'])) {
|
||||||
|
$this->saveAnswers($session, $validated['answers']);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($validated['additional_comments'])) {
|
||||||
|
$session->update(['additional_comments' => $validated['additional_comments']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->boolean('complete')) {
|
||||||
|
return $this->completeSession($session);
|
||||||
|
}
|
||||||
|
|
||||||
|
return back();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save or update answers for the session using composite key upsert.
|
||||||
|
*/
|
||||||
|
private function saveAnswers(Session $session, array $answers): void
|
||||||
|
{
|
||||||
|
foreach ($answers as $questionId => $answer) {
|
||||||
|
$session->answers()->updateOrCreate(
|
||||||
|
['question_id' => (int) $questionId],
|
||||||
|
[
|
||||||
|
'value' => $answer['value'] ?? null,
|
||||||
|
'text_value' => $answer['text_value'] ?? null,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Complete the session by calculating final score and result.
|
||||||
|
*/
|
||||||
|
private function completeSession(Session $session): RedirectResponse
|
||||||
|
{
|
||||||
|
$scoringService = new ScoringService;
|
||||||
|
$score = $scoringService->calculateScore($session);
|
||||||
|
$result = $scoringService->determineResult($score);
|
||||||
|
|
||||||
|
$session->update([
|
||||||
|
'score' => $score,
|
||||||
|
'result' => $result,
|
||||||
|
'status' => 'completed',
|
||||||
|
'completed_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
return redirect()->route('sessions.result', $session);
|
return redirect()->route('sessions.result', $session);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,8 +121,13 @@ public function update(Request $request, Session $session): RedirectResponse
|
|||||||
*/
|
*/
|
||||||
public function result(Session $session): Response
|
public function result(Session $session): Response
|
||||||
{
|
{
|
||||||
|
$session->load('category');
|
||||||
|
|
||||||
return Inertia::render('Session/Result', [
|
return Inertia::render('Session/Result', [
|
||||||
'session' => $session,
|
'session' => $session,
|
||||||
|
'score' => $session->score,
|
||||||
|
'result' => $session->result,
|
||||||
|
'categoryName' => $session->category->name,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
app/Http/Requests/Screening/UpdateScreeningRequest.php
Normal file
46
app/Http/Requests/Screening/UpdateScreeningRequest.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Screening;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class UpdateScreeningRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'answers' => ['required', 'array', 'size:10'],
|
||||||
|
'answers.*' => ['required', 'string', 'in:yes,no'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation messages.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'answers.required' => 'All screening questions must be answered.',
|
||||||
|
'answers.array' => 'Answers must be provided as an array.',
|
||||||
|
'answers.size' => 'All 10 screening questions must be answered.',
|
||||||
|
'answers.*.required' => 'Each screening question must have an answer.',
|
||||||
|
'answers.*.string' => 'Each answer must be a valid text value.',
|
||||||
|
'answers.*.in' => 'Each answer must be either "yes" or "no".',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
67
app/Http/Requests/Session/UpdateSessionRequest.php
Normal file
67
app/Http/Requests/Session/UpdateSessionRequest.php
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Session;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class UpdateSessionRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'basic_info' => ['sometimes', 'required', 'array'],
|
||||||
|
'basic_info.client_name' => ['required_with:basic_info', 'string', 'max:255'],
|
||||||
|
'basic_info.client_contact' => ['required_with:basic_info', 'string', 'max:255'],
|
||||||
|
'basic_info.lead_firm_name' => ['required_with:basic_info', 'string', 'max:255'],
|
||||||
|
'basic_info.lead_firm_contact' => ['required_with:basic_info', 'string', 'max:255'],
|
||||||
|
'answers' => ['sometimes', 'array'],
|
||||||
|
'answers.*.value' => ['nullable', 'string', 'in:yes,no,not_applicable'],
|
||||||
|
'answers.*.text_value' => ['nullable', 'string', 'max:10000'],
|
||||||
|
'additional_comments' => ['sometimes', 'nullable', 'string', 'max:10000'],
|
||||||
|
'complete' => ['sometimes', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom validation messages.
|
||||||
|
*/
|
||||||
|
public function messages(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'basic_info.required' => 'Basic information is required.',
|
||||||
|
'basic_info.array' => 'Basic information must be a valid data structure.',
|
||||||
|
'basic_info.client_name.required_with' => 'The client name is required.',
|
||||||
|
'basic_info.client_name.string' => 'The client name must be text.',
|
||||||
|
'basic_info.client_name.max' => 'The client name cannot exceed 255 characters.',
|
||||||
|
'basic_info.client_contact.required_with' => 'The client contact is required.',
|
||||||
|
'basic_info.client_contact.string' => 'The client contact must be text.',
|
||||||
|
'basic_info.client_contact.max' => 'The client contact cannot exceed 255 characters.',
|
||||||
|
'basic_info.lead_firm_name.required_with' => 'The lead firm name is required.',
|
||||||
|
'basic_info.lead_firm_name.string' => 'The lead firm name must be text.',
|
||||||
|
'basic_info.lead_firm_name.max' => 'The lead firm name cannot exceed 255 characters.',
|
||||||
|
'basic_info.lead_firm_contact.required_with' => 'The lead firm contact is required.',
|
||||||
|
'basic_info.lead_firm_contact.string' => 'The lead firm contact must be text.',
|
||||||
|
'basic_info.lead_firm_contact.max' => 'The lead firm contact cannot exceed 255 characters.',
|
||||||
|
'answers.array' => 'Answers must be a valid data structure.',
|
||||||
|
'answers.*.value.in' => 'Answer value must be yes, no, or not_applicable.',
|
||||||
|
'answers.*.text_value.string' => 'Answer text must be text.',
|
||||||
|
'answers.*.text_value.max' => 'Answer text cannot exceed 10000 characters.',
|
||||||
|
'additional_comments.string' => 'Additional comments must be text.',
|
||||||
|
'additional_comments.max' => 'Additional comments cannot exceed 10000 characters.',
|
||||||
|
'complete.boolean' => 'The complete flag must be true or false.',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
37
app/Services/ScoringService.php
Normal file
37
app/Services/ScoringService.php
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Services;
|
||||||
|
|
||||||
|
use App\Models\Session;
|
||||||
|
|
||||||
|
final class ScoringService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Calculate the score for a session based on scored answers.
|
||||||
|
*/
|
||||||
|
public function calculateScore(Session $session): int
|
||||||
|
{
|
||||||
|
return $session->answers()
|
||||||
|
->whereHas('question', fn ($q) => $q->where('is_scored', true))
|
||||||
|
->where('value', 'yes')
|
||||||
|
->count();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine the result based on the score.
|
||||||
|
*/
|
||||||
|
public function determineResult(int $score): string
|
||||||
|
{
|
||||||
|
if ($score >= 10) {
|
||||||
|
return 'go';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($score >= 5) {
|
||||||
|
return 'consult_leadership';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'no_go';
|
||||||
|
}
|
||||||
|
}
|
||||||
42
config/screening.php
Normal file
42
config/screening.php
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Pre-Screening Questions
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These 10 Yes/No questions are presented before category selection.
|
||||||
|
| Each "Yes" answer scores 1 point. A score of 5 or more is required
|
||||||
|
| to proceed to the category questionnaire.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'questions' => [
|
||||||
|
1 => 'Is the opportunity aligned with our strategic goals?',
|
||||||
|
2 => 'Do we have the necessary expertise to deliver?',
|
||||||
|
3 => 'Is the client financially stable?',
|
||||||
|
4 => 'Are there no significant conflicts of interest?',
|
||||||
|
5 => 'Is the timeline realistic?',
|
||||||
|
6 => 'Do we have available resources?',
|
||||||
|
7 => 'Is the expected fee reasonable for the scope?',
|
||||||
|
8 => 'Are the client\'s expectations manageable?',
|
||||||
|
9 => 'Have we successfully completed similar engagements?',
|
||||||
|
10 => 'Is the risk level acceptable?',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Passing Score Threshold
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Minimum score required to proceed to category selection.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'passing_score' => 5,
|
||||||
|
|
||||||
|
];
|
||||||
35
database/seeders/CategorySeeder.php
Normal file
35
database/seeders/CategorySeeder.php
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class CategorySeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Seed the 6 fixed assessment categories.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
$categories = [
|
||||||
|
['name' => 'Audit', 'sort_order' => 1],
|
||||||
|
['name' => 'Outsource', 'sort_order' => 2],
|
||||||
|
['name' => 'Solution', 'sort_order' => 3],
|
||||||
|
['name' => 'Digital Solutions', 'sort_order' => 4],
|
||||||
|
['name' => 'Legal', 'sort_order' => 5],
|
||||||
|
['name' => 'Tax', 'sort_order' => 6],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($categories as $category) {
|
||||||
|
DB::table('categories')->insert([
|
||||||
|
'name' => $category['name'],
|
||||||
|
'sort_order' => $category['sort_order'],
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
namespace Database\Seeders;
|
namespace Database\Seeders;
|
||||||
|
|
||||||
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
|
||||||
use Illuminate\Database\Seeder;
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
class DatabaseSeeder extends Seeder
|
final class DatabaseSeeder extends Seeder
|
||||||
{
|
{
|
||||||
use WithoutModelEvents;
|
use WithoutModelEvents;
|
||||||
|
|
||||||
@@ -14,6 +16,10 @@ class DatabaseSeeder extends Seeder
|
|||||||
*/
|
*/
|
||||||
public function run(): void
|
public function run(): void
|
||||||
{
|
{
|
||||||
$this->call(JonathanSeeder::class);
|
$this->call([
|
||||||
|
JonathanSeeder::class,
|
||||||
|
CategorySeeder::class,
|
||||||
|
QuestionSeeder::class,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1761
database/seeders/QuestionSeeder.php
Normal file
1761
database/seeders/QuestionSeeder.php
Normal file
File diff suppressed because it is too large
Load Diff
119
resources/js/Components/QuestionCard.vue
Normal file
119
resources/js/Components/QuestionCard.vue
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
question: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
modelValue: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({ value: null, text_value: '' }),
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:modelValue'])
|
||||||
|
|
||||||
|
const hasRadioButtons = computed(() => {
|
||||||
|
return props.question.has_yes || props.question.has_no || props.question.has_na
|
||||||
|
})
|
||||||
|
|
||||||
|
const showDetails = computed(() => {
|
||||||
|
const d = props.question.details
|
||||||
|
if (!d) return false
|
||||||
|
if (d === 'required' || d === 'optional') return true
|
||||||
|
if (d === 'req_on_yes' && props.modelValue.value === 'yes') return true
|
||||||
|
if (d === 'req_on_no' && props.modelValue.value === 'no') return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const detailsRequired = computed(() => {
|
||||||
|
const d = props.question.details
|
||||||
|
if (d === 'required') return true
|
||||||
|
if (d === 'req_on_yes' && props.modelValue.value === 'yes') return true
|
||||||
|
if (d === 'req_on_no' && props.modelValue.value === 'no') return true
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
|
||||||
|
const isTextOnly = computed(() => {
|
||||||
|
return !hasRadioButtons.value && props.question.details
|
||||||
|
})
|
||||||
|
|
||||||
|
const updateValue = (value) => {
|
||||||
|
emit('update:modelValue', { ...props.modelValue, value })
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateTextValue = (event) => {
|
||||||
|
emit('update:modelValue', { ...props.modelValue, text_value: event.target.value })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="py-4">
|
||||||
|
<p class="text-white mb-3">{{ question.text }}</p>
|
||||||
|
|
||||||
|
<!-- Text-only question (no radio buttons) -->
|
||||||
|
<div v-if="isTextOnly">
|
||||||
|
<textarea
|
||||||
|
:value="modelValue.text_value"
|
||||||
|
@input="updateTextValue"
|
||||||
|
rows="3"
|
||||||
|
class="w-full rounded-lg border border-gray-600 bg-surface px-3 py-2 text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
|
placeholder="Enter your response..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Radio button question -->
|
||||||
|
<div v-if="hasRadioButtons">
|
||||||
|
<div class="flex flex-wrap gap-4 mb-3">
|
||||||
|
<label v-if="question.has_yes" class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:name="`question-${question.id}`"
|
||||||
|
value="yes"
|
||||||
|
:checked="modelValue.value === 'yes'"
|
||||||
|
@change="updateValue('yes')"
|
||||||
|
class="w-4 h-4 text-primary bg-surface border-gray-600 focus:ring-primary focus:ring-offset-surface"
|
||||||
|
/>
|
||||||
|
<span class="text-white">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label v-if="question.has_no" class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:name="`question-${question.id}`"
|
||||||
|
value="no"
|
||||||
|
:checked="modelValue.value === 'no'"
|
||||||
|
@change="updateValue('no')"
|
||||||
|
class="w-4 h-4 text-primary bg-surface border-gray-600 focus:ring-primary focus:ring-offset-surface"
|
||||||
|
/>
|
||||||
|
<span class="text-white">No</span>
|
||||||
|
</label>
|
||||||
|
<label v-if="question.has_na" class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:name="`question-${question.id}`"
|
||||||
|
value="not_applicable"
|
||||||
|
:checked="modelValue.value === 'not_applicable'"
|
||||||
|
@change="updateValue('not_applicable')"
|
||||||
|
class="w-4 h-4 text-primary bg-surface border-gray-600 focus:ring-primary focus:ring-offset-surface"
|
||||||
|
/>
|
||||||
|
<span class="text-white">N/A</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Details textarea (conditional) -->
|
||||||
|
<div v-if="showDetails" class="mt-2">
|
||||||
|
<label class="block text-sm text-gray-400 mb-1">
|
||||||
|
Details{{ detailsRequired ? ' (required)' : ' (optional)' }}
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
:value="modelValue.text_value"
|
||||||
|
@input="updateTextValue"
|
||||||
|
rows="2"
|
||||||
|
class="w-full rounded-lg border border-gray-600 bg-surface px-3 py-2 text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary text-sm"
|
||||||
|
placeholder="Enter details..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -16,7 +16,14 @@ const handleContinue = () => {
|
|||||||
<div class="flex items-center justify-center py-16">
|
<div class="flex items-center justify-center py-16">
|
||||||
<div class="text-center max-w-2xl mx-auto px-4">
|
<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 mb-8">Baker Tilly International Questionnaire Application</p>
|
<p class="text-gray-400 mb-4 text-lg">
|
||||||
|
Baker Tilly International Go/No Go Checklist
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400 mb-8">
|
||||||
|
This tool helps you evaluate business opportunities through a structured assessment process.
|
||||||
|
You will first complete a short pre-screening questionnaire, followed by a detailed category-specific checklist
|
||||||
|
to determine whether to pursue (Go), decline (No Go), or escalate (Consult Leadership) an opportunity.
|
||||||
|
</p>
|
||||||
<AppButton size="lg" @click="handleContinue">
|
<AppButton size="lg" @click="handleContinue">
|
||||||
Continue
|
Continue
|
||||||
</AppButton>
|
</AppButton>
|
||||||
|
|||||||
@@ -10,6 +10,18 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
passed: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
totalQuestions: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
categories: {
|
categories: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
@@ -28,14 +40,34 @@ const handleStartCategory = (categoryId) => {
|
|||||||
<Head title="Screening Result" />
|
<Head title="Screening Result" />
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||||
<h1 class="text-3xl font-bold text-white mb-6">Screening Result</h1>
|
<h1 class="text-3xl font-bold text-white mb-6">Pre-Screening Result</h1>
|
||||||
|
|
||||||
<div class="bg-surface/50 rounded-lg p-6 mb-8">
|
<!-- Score Display -->
|
||||||
<p class="text-gray-400 text-center mb-4">Your screening result: Passed</p>
|
<div class="rounded-lg p-6 mb-8" :class="passed ? 'bg-green-500/10 border border-green-500/30' : 'bg-red-500/10 border border-red-500/30'">
|
||||||
|
<div class="text-center">
|
||||||
|
<p class="text-5xl font-bold mb-2" :class="passed ? 'text-green-500' : 'text-red-500'">
|
||||||
|
{{ score }} / {{ totalQuestions }}
|
||||||
|
</p>
|
||||||
|
<p class="text-xl font-semibold" :class="passed ? 'text-green-400' : 'text-red-400'">
|
||||||
|
{{ passed ? 'Passed' : 'No Go' }}
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400 mt-2">
|
||||||
|
{{ passed ? 'You may proceed to select a category for detailed assessment.' : 'The pre-screening score is below the required threshold. You cannot proceed at this time.' }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="space-y-4">
|
<!-- Failed: Show Again button -->
|
||||||
|
<div v-if="!passed" class="flex justify-center">
|
||||||
|
<AppButton size="lg" href="/">
|
||||||
|
Again
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Passed: Show category picker -->
|
||||||
|
<div v-else>
|
||||||
<h2 class="text-2xl font-semibold text-white mb-4">Select a Category</h2>
|
<h2 class="text-2xl font-semibold text-white mb-4">Select a Category</h2>
|
||||||
|
<div class="space-y-3">
|
||||||
<div
|
<div
|
||||||
v-for="category in categories"
|
v-for="category in categories"
|
||||||
:key="category.id"
|
:key="category.id"
|
||||||
@@ -48,4 +80,5 @@ const handleStartCategory = (categoryId) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Head, router } from '@inertiajs/vue3'
|
import { computed } from 'vue'
|
||||||
|
import { Head, useForm } from '@inertiajs/vue3'
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||||
import AppButton from '@/Components/AppButton.vue'
|
import AppButton from '@/Components/AppButton.vue'
|
||||||
|
|
||||||
@@ -10,25 +11,80 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
questions: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Initialize form with empty answers for all questions
|
||||||
|
const initialAnswers = {}
|
||||||
|
props.questions.forEach((_, index) => {
|
||||||
|
initialAnswers[index + 1] = null
|
||||||
|
})
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
answers: initialAnswers,
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
router.put(`/screening/${props.screening.id}`)
|
form.put(`/screening/${props.screening.id}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const allAnswered = computed(() => {
|
||||||
|
return Object.values(form.answers).every(v => v !== null)
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Pre-Screening Questions" />
|
<Head title="Pre-Screening Questions" />
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
<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>
|
<h1 class="text-3xl font-bold text-white mb-2">Pre-Screening Questions</h1>
|
||||||
|
<p class="text-gray-400 mb-8">Answer all 10 questions to proceed. Each "Yes" answer scores 1 point. You need at least 5 points to pass.</p>
|
||||||
|
|
||||||
<div class="bg-surface/50 rounded-lg p-6 mb-8">
|
<div class="space-y-4 mb-8">
|
||||||
<p class="text-gray-400 text-center">10 Yes/No questions will appear here</p>
|
<div
|
||||||
|
v-for="(question, index) in questions"
|
||||||
|
:key="index"
|
||||||
|
class="bg-surface/50 rounded-lg p-5"
|
||||||
|
>
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
<span class="text-gray-400 font-mono text-sm mt-1 shrink-0">{{ index + 1 }}.</span>
|
||||||
|
<div class="flex-1">
|
||||||
|
<p class="text-white mb-3">{{ question }}</p>
|
||||||
|
<div class="flex gap-4">
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:name="`question-${index + 1}`"
|
||||||
|
value="yes"
|
||||||
|
v-model="form.answers[index + 1]"
|
||||||
|
class="w-4 h-4 text-primary bg-surface border-gray-600 focus:ring-primary focus:ring-offset-surface"
|
||||||
|
/>
|
||||||
|
<span class="text-white">Yes</span>
|
||||||
|
</label>
|
||||||
|
<label class="flex items-center gap-2 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
:name="`question-${index + 1}`"
|
||||||
|
value="no"
|
||||||
|
v-model="form.answers[index + 1]"
|
||||||
|
class="w-4 h-4 text-primary bg-surface border-gray-600 focus:ring-primary focus:ring-offset-surface"
|
||||||
|
/>
|
||||||
|
<span class="text-white">No</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p v-if="form.errors[`answers.${index + 1}`]" class="text-red-500 text-sm mt-1">
|
||||||
|
{{ form.errors[`answers.${index + 1}`] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex justify-end">
|
<div class="flex justify-end">
|
||||||
<AppButton size="lg" @click="handleSubmit">
|
<AppButton size="lg" @click="handleSubmit" :loading="form.processing" :disabled="!allAnswered || form.processing">
|
||||||
Submit
|
Submit
|
||||||
</AppButton>
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
|
import { computed } from 'vue'
|
||||||
import { Head } from '@inertiajs/vue3'
|
import { Head } from '@inertiajs/vue3'
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||||
import AppButton from '@/Components/AppButton.vue'
|
import AppButton from '@/Components/AppButton.vue'
|
||||||
@@ -10,19 +11,101 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
score: {
|
||||||
|
type: Number,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
result: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
categoryName: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const resultDisplay = computed(() => {
|
||||||
|
switch (props.result) {
|
||||||
|
case 'go':
|
||||||
|
return {
|
||||||
|
label: 'GO',
|
||||||
|
description: 'Pursue this opportunity.',
|
||||||
|
bgClass: 'bg-green-500/10 border-green-500/30',
|
||||||
|
textClass: 'text-green-500',
|
||||||
|
badgeClass: 'bg-green-500',
|
||||||
|
}
|
||||||
|
case 'consult_leadership':
|
||||||
|
return {
|
||||||
|
label: 'Consult Leadership',
|
||||||
|
description: 'Speak to SL or SSL leadership before proceeding.',
|
||||||
|
bgClass: 'bg-amber-500/10 border-amber-500/30',
|
||||||
|
textClass: 'text-amber-500',
|
||||||
|
badgeClass: 'bg-amber-500',
|
||||||
|
}
|
||||||
|
case 'no_go':
|
||||||
|
default:
|
||||||
|
return {
|
||||||
|
label: 'NO GO',
|
||||||
|
description: 'Do not pursue this opportunity.',
|
||||||
|
bgClass: 'bg-red-500/10 border-red-500/30',
|
||||||
|
textClass: 'text-red-500',
|
||||||
|
badgeClass: 'bg-red-500',
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head title="Session Result" />
|
<Head :title="`${categoryName} - Result`" />
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
<div class="max-w-4xl mx-auto px-4 py-8">
|
||||||
<h1 class="text-3xl font-bold text-white mb-6">Session Result</h1>
|
<h1 class="text-3xl font-bold text-white mb-6">{{ categoryName }} — Result</h1>
|
||||||
|
|
||||||
<div class="bg-surface/50 rounded-lg p-6 mb-8">
|
<!-- Result Card -->
|
||||||
<p class="text-gray-400 text-center">Your result will appear here</p>
|
<div class="rounded-lg p-8 mb-8 border" :class="resultDisplay.bgClass">
|
||||||
|
<div class="text-center">
|
||||||
|
<div class="mb-4">
|
||||||
|
<span
|
||||||
|
class="inline-block px-6 py-3 rounded-lg text-white text-2xl font-bold"
|
||||||
|
:class="resultDisplay.badgeClass"
|
||||||
|
>
|
||||||
|
{{ resultDisplay.label }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p class="text-5xl font-bold mb-2" :class="resultDisplay.textClass">
|
||||||
|
{{ score }} points
|
||||||
|
</p>
|
||||||
|
<p class="text-gray-400 text-lg mt-4">
|
||||||
|
{{ resultDisplay.description }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Session Details -->
|
||||||
|
<div class="bg-surface/50 rounded-lg p-6 mb-8">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Session Details</h2>
|
||||||
|
<dl class="grid grid-cols-2 gap-4 text-sm">
|
||||||
|
<div>
|
||||||
|
<dt class="text-gray-400">Category</dt>
|
||||||
|
<dd class="text-white font-medium">{{ categoryName }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-gray-400">Client</dt>
|
||||||
|
<dd class="text-white font-medium">{{ session.basic_info?.client_name ?? 'N/A' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-gray-400">Lead Firm</dt>
|
||||||
|
<dd class="text-white font-medium">{{ session.basic_info?.lead_firm_name ?? 'N/A' }}</dd>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<dt class="text-gray-400">Completed</dt>
|
||||||
|
<dd class="text-white font-medium">{{ new Date(session.completed_at).toLocaleDateString() }}</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Again button -->
|
||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<AppButton size="lg" href="/">
|
<AppButton size="lg" href="/">
|
||||||
Again
|
Again
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { Head, router } from '@inertiajs/vue3'
|
import { computed, reactive } from 'vue'
|
||||||
|
import { Head, useForm, router } from '@inertiajs/vue3'
|
||||||
import AppLayout from '@/Layouts/AppLayout.vue'
|
import AppLayout from '@/Layouts/AppLayout.vue'
|
||||||
import AppButton from '@/Components/AppButton.vue'
|
import AppButton from '@/Components/AppButton.vue'
|
||||||
|
import QuestionCard from '@/Components/QuestionCard.vue'
|
||||||
|
import ScoreIndicator from '@/Components/ScoreIndicator.vue'
|
||||||
|
|
||||||
defineOptions({ layout: AppLayout })
|
defineOptions({ layout: AppLayout })
|
||||||
|
|
||||||
@@ -10,33 +13,225 @@ const props = defineProps({
|
|||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
questionGroups: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
answers: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
score: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleComplete = () => {
|
// Basic info form (unchanged from Step 8)
|
||||||
router.put(`/sessions/${props.session.id}`)
|
const basicInfoForm = useForm({
|
||||||
|
basic_info: {
|
||||||
|
client_name: props.session.basic_info?.client_name ?? '',
|
||||||
|
client_contact: props.session.basic_info?.client_contact ?? '',
|
||||||
|
lead_firm_name: props.session.basic_info?.lead_firm_name ?? '',
|
||||||
|
lead_firm_contact: props.session.basic_info?.lead_firm_contact ?? '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
const saveBasicInfo = () => {
|
||||||
|
basicInfoForm.put(`/sessions/${props.session.id}`, {
|
||||||
|
preserveScroll: true,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Answer management
|
||||||
|
const answerData = reactive({})
|
||||||
|
|
||||||
|
// Initialize answers from existing data
|
||||||
|
const initializeAnswers = () => {
|
||||||
|
props.questionGroups.forEach(group => {
|
||||||
|
group.questions.forEach(question => {
|
||||||
|
const existing = props.answers[question.id]
|
||||||
|
answerData[question.id] = {
|
||||||
|
value: existing?.value ?? null,
|
||||||
|
text_value: existing?.text_value ?? '',
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
initializeAnswers()
|
||||||
|
|
||||||
|
// Save a single answer with partial reload including score
|
||||||
|
let saveTimeout = null
|
||||||
|
const saveAnswer = (questionId) => {
|
||||||
|
clearTimeout(saveTimeout)
|
||||||
|
saveTimeout = setTimeout(() => {
|
||||||
|
router.put(`/sessions/${props.session.id}`, {
|
||||||
|
answers: {
|
||||||
|
[questionId]: answerData[questionId],
|
||||||
|
},
|
||||||
|
}, {
|
||||||
|
preserveScroll: true,
|
||||||
|
preserveState: true,
|
||||||
|
only: ['answers', 'score'],
|
||||||
|
})
|
||||||
|
}, 500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const updateAnswer = (questionId, newValue) => {
|
||||||
|
answerData[questionId] = newValue
|
||||||
|
saveAnswer(questionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Additional comments
|
||||||
|
const additionalComments = useForm({
|
||||||
|
additional_comments: props.session.additional_comments ?? '',
|
||||||
|
})
|
||||||
|
|
||||||
|
let commentsTimeout = null
|
||||||
|
const saveComments = () => {
|
||||||
|
clearTimeout(commentsTimeout)
|
||||||
|
commentsTimeout = setTimeout(() => {
|
||||||
|
additionalComments.put(`/sessions/${props.session.id}`, {
|
||||||
|
preserveScroll: true,
|
||||||
|
preserveState: true,
|
||||||
|
})
|
||||||
|
}, 1000)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Session completion
|
||||||
|
let completing = false
|
||||||
|
const completeSession = () => {
|
||||||
|
completing = true
|
||||||
|
router.put(`/sessions/${props.session.id}`, {
|
||||||
|
complete: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if any scored answers have been given
|
||||||
|
const hasScoredAnswers = computed(() => {
|
||||||
|
return props.score > 0
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Head :title="`${session.category.name} Questionnaire`" />
|
<Head :title="`${session.category.name} Questionnaire`" />
|
||||||
|
|
||||||
<div class="max-w-4xl mx-auto px-4 py-8">
|
<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="flex items-center justify-between mb-6">
|
||||||
|
<h1 class="text-3xl font-bold text-white">{{ session.category.name }} Questionnaire</h1>
|
||||||
<div class="space-y-6">
|
<ScoreIndicator :score="score" :visible="hasScoredAnswers" />
|
||||||
<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>
|
||||||
|
|
||||||
<div class="bg-surface/50 rounded-lg p-6">
|
<!-- Basic Info Section (unchanged from Step 8) -->
|
||||||
<h2 class="text-xl font-semibold text-white mb-4">Questions</h2>
|
<div class="bg-surface/50 rounded-lg p-6 mb-6">
|
||||||
<p class="text-gray-400">Category questions will appear here</p>
|
<h2 class="text-xl font-semibold text-white mb-4">Basic Information</h2>
|
||||||
|
<p class="text-gray-400 text-sm mb-6">All fields are required before you can proceed to the questionnaire.</p>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<div>
|
||||||
|
<label for="client_name" class="block text-sm font-medium text-gray-400 mb-1">Client Name</label>
|
||||||
|
<input
|
||||||
|
id="client_name"
|
||||||
|
v-model="basicInfoForm.basic_info.client_name"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-600 bg-surface px-3 py-2 text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
|
placeholder="Enter client name"
|
||||||
|
/>
|
||||||
|
<p v-if="basicInfoForm.errors['basic_info.client_name']" class="text-red-500 text-sm mt-1">
|
||||||
|
{{ basicInfoForm.errors['basic_info.client_name'] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="client_contact" class="block text-sm font-medium text-gray-400 mb-1">Client Contact</label>
|
||||||
|
<input
|
||||||
|
id="client_contact"
|
||||||
|
v-model="basicInfoForm.basic_info.client_contact"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-600 bg-surface px-3 py-2 text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
|
placeholder="Enter client contact"
|
||||||
|
/>
|
||||||
|
<p v-if="basicInfoForm.errors['basic_info.client_contact']" class="text-red-500 text-sm mt-1">
|
||||||
|
{{ basicInfoForm.errors['basic_info.client_contact'] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="lead_firm_name" class="block text-sm font-medium text-gray-400 mb-1">Lead Firm Name</label>
|
||||||
|
<input
|
||||||
|
id="lead_firm_name"
|
||||||
|
v-model="basicInfoForm.basic_info.lead_firm_name"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-600 bg-surface px-3 py-2 text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
|
placeholder="Enter lead firm name"
|
||||||
|
/>
|
||||||
|
<p v-if="basicInfoForm.errors['basic_info.lead_firm_name']" class="text-red-500 text-sm mt-1">
|
||||||
|
{{ basicInfoForm.errors['basic_info.lead_firm_name'] }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<label for="lead_firm_contact" class="block text-sm font-medium text-gray-400 mb-1">Lead Firm Contact</label>
|
||||||
|
<input
|
||||||
|
id="lead_firm_contact"
|
||||||
|
v-model="basicInfoForm.basic_info.lead_firm_contact"
|
||||||
|
type="text"
|
||||||
|
class="w-full rounded-lg border border-gray-600 bg-surface px-3 py-2 text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
|
placeholder="Enter lead firm contact"
|
||||||
|
/>
|
||||||
|
<p v-if="basicInfoForm.errors['basic_info.lead_firm_contact']" class="text-red-500 text-sm mt-1">
|
||||||
|
{{ basicInfoForm.errors['basic_info.lead_firm_contact'] }}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="flex justify-end mt-6">
|
||||||
|
<AppButton
|
||||||
|
@click="saveBasicInfo"
|
||||||
|
:loading="basicInfoForm.processing"
|
||||||
|
:disabled="basicInfoForm.processing"
|
||||||
|
>
|
||||||
|
Save Basic Info
|
||||||
|
</AppButton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Question Groups -->
|
||||||
|
<div
|
||||||
|
v-for="group in questionGroups"
|
||||||
|
:key="group.id"
|
||||||
|
class="bg-surface/50 rounded-lg p-6 mb-6"
|
||||||
|
>
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-1">{{ group.name }}</h2>
|
||||||
|
<p v-if="group.description" class="text-gray-400 text-sm mb-2">{{ group.description }}</p>
|
||||||
|
<p v-if="group.scoring_instructions" class="text-amber-400 text-sm italic mb-4">{{ group.scoring_instructions }}</p>
|
||||||
|
|
||||||
|
<div class="divide-y divide-gray-700">
|
||||||
|
<QuestionCard
|
||||||
|
v-for="question in group.questions"
|
||||||
|
:key="question.id"
|
||||||
|
:question="question"
|
||||||
|
:modelValue="answerData[question.id]"
|
||||||
|
@update:modelValue="updateAnswer(question.id, $event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Comments -->
|
||||||
|
<div class="bg-surface/50 rounded-lg p-6 mb-6">
|
||||||
|
<h2 class="text-xl font-semibold text-white mb-4">Additional Comments</h2>
|
||||||
|
<textarea
|
||||||
|
v-model="additionalComments.additional_comments"
|
||||||
|
@input="saveComments"
|
||||||
|
rows="4"
|
||||||
|
class="w-full rounded-lg border border-gray-600 bg-surface px-3 py-2 text-white placeholder-gray-500 focus:border-primary focus:ring-1 focus:ring-primary"
|
||||||
|
placeholder="Enter any additional comments to support your decision..."
|
||||||
|
></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Complete button - now enabled -->
|
||||||
<div class="flex justify-end mt-8">
|
<div class="flex justify-end mt-8">
|
||||||
<AppButton size="lg" @click="handleComplete">
|
<AppButton size="lg" @click="completeSession">
|
||||||
Complete
|
Complete
|
||||||
</AppButton>
|
</AppButton>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user