plan implementation 6, 7, 8, 9, 10

This commit is contained in:
2026-02-03 10:50:56 +01:00
parent e8be239c32
commit 0b6c6736ef
16 changed files with 2665 additions and 44 deletions

View 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>

View File

@@ -16,7 +16,14 @@ const handleContinue = () => {
<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 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">
Continue
</AppButton>

View File

@@ -10,6 +10,18 @@ const props = defineProps({
type: Object,
required: true,
},
passed: {
type: Boolean,
required: true,
},
score: {
type: Number,
required: true,
},
totalQuestions: {
type: Number,
required: true,
},
categories: {
type: Array,
required: true,
@@ -28,23 +40,44 @@ const handleStartCategory = (categoryId) => {
<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>
<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">
<p class="text-gray-400 text-center mb-4">Your screening result: Passed</p>
<!-- Score Display -->
<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 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>
<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 class="space-y-3">
<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>
</div>

View File

@@ -1,5 +1,6 @@
<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 AppButton from '@/Components/AppButton.vue'
@@ -10,25 +11,80 @@ const props = defineProps({
type: Object,
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 = () => {
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>
<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>
<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">
<p class="text-gray-400 text-center">10 Yes/No questions will appear here</p>
<div class="space-y-4 mb-8">
<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 class="flex justify-end">
<AppButton size="lg" @click="handleSubmit">
<AppButton size="lg" @click="handleSubmit" :loading="form.processing" :disabled="!allAnswered || form.processing">
Submit
</AppButton>
</div>

View File

@@ -1,4 +1,5 @@
<script setup>
import { computed } from 'vue'
import { Head } from '@inertiajs/vue3'
import AppLayout from '@/Layouts/AppLayout.vue'
import AppButton from '@/Components/AppButton.vue'
@@ -10,19 +11,101 @@ const props = defineProps({
type: Object,
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>
<template>
<Head title="Session Result" />
<Head :title="`${categoryName} - 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>
<h1 class="text-3xl font-bold text-white mb-6">{{ categoryName }} 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>
<!-- Result Card -->
<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>
<!-- 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">
<AppButton size="lg" href="/">
Again

View File

@@ -1,7 +1,10 @@
<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 AppButton from '@/Components/AppButton.vue'
import QuestionCard from '@/Components/QuestionCard.vue'
import ScoreIndicator from '@/Components/ScoreIndicator.vue'
defineOptions({ layout: AppLayout })
@@ -10,33 +13,225 @@ const props = defineProps({
type: Object,
required: true,
},
questionGroups: {
type: Array,
default: () => [],
},
answers: {
type: Object,
default: () => ({}),
},
score: {
type: Number,
default: 0,
},
})
const handleComplete = () => {
router.put(`/sessions/${props.session.id}`)
// Basic info form (unchanged from Step 8)
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>
<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="flex items-center justify-between mb-6">
<h1 class="text-3xl font-bold text-white">{{ session.category.name }} Questionnaire</h1>
<ScoreIndicator :score="score" :visible="hasScoredAnswers" />
</div>
<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>
<!-- Basic Info Section (unchanged from Step 8) -->
<div class="bg-surface/50 rounded-lg p-6 mb-6">
<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 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 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">
<AppButton size="lg" @click="handleComplete">
<AppButton size="lg" @click="completeSession">
Complete
</AppButton>
</div>