Add Question Logic
April 9, 2025 · AI Conversations
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)
{{– Customize layout sections –}}
@section('subtitle', __('Edit – Survey'))
{{– Content body: main page content –}}
@section('content_body')
{{ __('Edit Survey') }}
@if($errors->any())
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
@endif
@if(session()->has('message.level'))
{!! session('message.content') !!}
@endif
Loading…
{{ __('Loading survey creation…') }}
1. Survey
2. Pillers
3. Questions
{{ __('Create Survey') }}
Name
@{{ surveyError }}
{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}
{{ __('Save and Next') }}
{{ __('Edit Pillers') }}
{{ __('Piller') }} @{{ index + 1 }}
{{ __('Title') }} (@{{ lang.code.toUpperCase() }})
{{ __('Weight') }}
{{ __('Position') }}
{{ __('Add More Pillers') }}
{{ __('Save and Next') }}
{{ __('Add Questions') }}
{{ __('Select Piller') }}
{{ __('– Select Piller –') }}
@{{ pill.titles.en }}
@{{ getPillerTitle(selectedPillerId) }}
{{ __('Title') }} (@{{ lang.code.toUpperCase() }})
{{ __('Position') }}
{{ __('Add More Question') }}
{{ __('Submit All Questions') }}
@stop
{{– Push extra scripts –}}
@push('js')
<script>
window.surveyData = @json($survey);
console.log(window.surveyData);
window.pillarsData = @json($pillars);
console.log(window.pillarsData);
window.questionsData = @json($questions);
console.log(window.questionsData);
window.availableLanguages = @json(getLanguages());
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
</script>
<script>
import { createApp } from 'https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.prod.js'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp({
data() {
return {
message: 'Hello Vue!',
loading: false,
step: 1,
survey: {
name: window.surveyData.name || '',
options: []
},
surveyError: '',
survey_id: window.surveyData.id || null,
availableLanguages: window.availableLanguages || [],
pillers: window.pillarsData || [{
titles: {},
weight: '',
position: ''
}],
selectedPillerId: '',
questions: window.questionsData || {},
}
},
mounted() {
// Initialize options from booleans
if (window.surveyData?.allow_comments_survey) {
this.survey.options.push('allow_comments');
}
if (window.surveyData?.allow_comments_pillars) {
this.survey.options.push('allow_comments_on_pillers');
}
if (window.surveyData?.external) {
this.survey.options.push('mark_as_external');
}
// Init first piller titles
if (this.pillers.length > 0) {
this.pillers.forEach(piller => {
this.availableLanguages.forEach(lang => {
if (!piller.titles[lang.code]) {
piller.titles[lang.code] = '';
}
});
});
}
},
methods: {
onPillerChange() {
if (this.selectedPillerId && !this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
// If there are no questions yet for this pillar, initialize with one
if (!this.questions[this.selectedPillerId] || this.questions[this.selectedPillerId].length === 0) {
const newQuestion = {
id: null, // in case you support editing
titles: {},
position: ''
};
this.availableLanguages.forEach(lang => {
newQuestion.titles[lang.code] = '';
});
this.questions[this.selectedPillerId] = [newQuestion];
}
},
getPillerTitle(id) {
const piller = this.pillers.find(p => p.id == id);
return piller ? piller.titles.en : '';
},
addQuestion() {
if (!this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
if (this.questions[this.selectedPillerId].length >= 10) return;
const newQuestion = {
titles: {},
position: ''
};
this.availableLanguages.forEach(lang => {
newQuestion.titles[lang.code] = '';
});
this.questions[this.selectedPillerId].push(newQuestion);
},
removeQuestion(index) {
this.questions[this.selectedPillerId].splice(index, 1);
},
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.every(p => !p.id)) return;
this.step = step;
if (step === 3 && !this.selectedPillerId && this.pillers.length > 0) {
this.selectedPillerId = this.pillers[0].id;
// Initialize questions array for selected pillar if not already present
if (!this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
}
},
addPiller() {
if (this.pillers.length >= 5) return;
const newPiller = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPiller.titles[lang.code] = '';
});
this.pillers.push(newPiller);
},
removePiller(index) {
this.pillers.splice(index, 1);
},
async submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
this.survey.name = window.surveyData?.name || '';
return;
}
const data = {
name: this.survey.name,
allow_comments: this.survey.options.includes('allow_comments') ? 1 : 0,
allow_comments_on_pillers: this.survey.options.includes('allow_comments_on_pillers') ? 1 : 0,
mark_as_external: this.survey.options.includes('mark_as_external') ? 1 : 0,
};
this.loading = true;
try {
fetch(`{{ route('admin.surveys.update', ['survey' => ':id']) }}`.replace(':id', this.survey_id), {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken
},
body: JSON.stringify(data)
})
.then(res => res.json().then(json => ({ status: res.status, body: json })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
this.survey_id = body.survey_id;
Swal.fire({
icon: 'success',
title: "{{ __('Survey saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 2;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Validation Error') }}",
text: body.message || "{{ __('Please check your input.') }}"
});
}
})
.catch(err => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the survey.') }}"
});
});
} catch (error) {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the survey.') }}"
});
} finally {
this.loading = false;
}
},
submitPillers() {
if (this.pillers.length !== 5) {
Swal.fire('Validation Error', 'You must add exactly 5 Pillers to continue.', 'warning');
return;
}
// Validate each piller
for (let [i, piller] of this.pillers.entries()) {
for (let lang of this.availableLanguages) {
if (!piller.titles[lang.code]) {
Swal.fire('Validation Error', `Piller ${i + 1}: Title required for ${lang.title}`, 'error');
return;
}
}
if (!piller.weight || !piller.position) {
Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
const data = {
pillars: this.pillers
};
this.loading = true;
try {
fetch(`{{ route('admin.pillars.updateAll', ['survey' => ':id']) }}`.replace(':id', this.survey_id), {
method: 'PUT',
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken
},
body: JSON.stringify(data)
})
.then(res => res.json().then(json => ({ status: res.status, body: json })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
// Save returned piller_ids if needed
this.pillers = body.pillers;
this.selectedPillerId = this.pillers[0].id;
Swal.fire({
icon: 'success',
title: "{{ __('Pillers saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 3;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Validation Error') }}",
text: body.message || "{{ __('Please check your input.') }}"
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
});
} catch (error) {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
} finally {
this.loading = false;
}
},
submitQuestions() {
const payload = {
questions: []
};
for (let piller of this.pillers) {
const list = this.questions[piller.id] || [];
if (list.length < 1) {
Swal.fire('Validation Error', `Please add at least 1 question for "${piller.titles.en}"`, 'warning');
return;
}
for (let q of list) {
for (let lang of this.availableLanguages) {
if (!q.titles[lang.code]) {
Swal.fire('Validation Error', `All languages required for a question in "${piller.titles.en}"`, 'error');
return;
}
}
if (!q.position) {
Swal.fire('Validation Error', `Position is required for a question in "${piller.titles.en}"`, 'error');
return;
}
payload.questions.push({
id: q.id || null,
piller_id: piller.id,
titles: q.titles,
position: q.position
});
}
}
fetch("{{ route('admin.questions.updateAll', ['survey' => ':id']) }}".replace(':id', this.survey_id), {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken
},
body: JSON.stringify(payload)
})
.then(res => res.json().then(json => ({ status: res.status, body: json })))
.then(({ status, body }) => {
if (status === 200 && body.success) {
Swal.fire({
icon: 'success',
title: "{{ __('Questions saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
window.location.href = "{{ route('admin.surveys.index') }}";
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Validation Error') }}",
text: body.message || "{{ __('Please check your input.') }}"
});
}
})
.catch(() => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the questions.') }}"
});
});
},
deletePiller(id, index) {
if (!id) {
// Not saved to backend yet, just remove from local list
this.pillers.splice(index, 1);
return;
}
Swal.fire({
title: `{{ __('Are you sure?') }}`,
text: `{{ __('This will permanently delete the pillar and its questions!') }}`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
confirmButtonText: `{{ __('Delete') }}`,
cancelButtonText: `{{ __('Cancel') }}`
}).then((result) => {
if (result.isConfirmed) {
fetch(`{{ route('admin.pillars.destroy', ['pillar' => ':id']) }}`.replace(':id', id), {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
this.pillers.splice(index, 1);
delete this.questions[id]; // clean up related questions
Swal.fire({
icon: 'success',
title: `{{ __('Deleted!') }}`,
text: data.message,
showConfirmButton: false,
timer: 1500
});
} else {
Swal.fire({
icon: 'error',
title: `{{ __('Error') }}`,
text: data.message || 'Failed to delete pillar.',
showConfirmButton: false,
timer: 1500
});
}
})
.catch(() => {
Swal.fire('Error', 'Something went wrong.', 'error');
});
}
});
},
deleteQuestion(id, index) {
if (!id) {
// Not saved to backend yet, just remove from local list
this.questions[this.selectedPillerId].splice(index, 1);
return;
}
Swal.fire({
title: `{{ __('Are you sure?') }}`,
text: `{{ __('This will permanently delete the question!') }}`,
icon: 'warning',
showCancelButton: true,
confirmButtonColor: '#d33',
confirmButtonText: `{{ __('Delete') }}`,
cancelButtonText: `{{ __('Cancel') }}`
}).then((result) => {
if (result.isConfirmed) {
fetch(`{{ route('admin.questions.destroy', ['question' => ':id']) }}`.replace(':id', id), {
method: 'DELETE',
headers: {
'X-CSRF-TOKEN': csrfToken
}
})
.then(res => res.json())
.then(data => {
if (data.success) {
this.questions[this.selectedPillerId].splice(index, 1);
Swal.fire({
icon: 'success',
title: `{{ __('Deleted!') }}`,
text: data.message,
showConfirmButton: false,
timer: 1500
});
} else {
Swal.fire({
icon: 'error',
title: `{{ __('Error') }}`,
text: data.message || `{{ __('Failed to delete question.') }}`,
showConfirmButton: false,
timer: 1500
});
}
})
.catch(() => {
Swal.fire('Error', `{{ __('Something went wrong.') }}`, 'error');
});
}
});
},
}
});
app.mount('#surveyApp');
// Hide loading spinner after mount
const loadingDiv = document.getElementById('vue-loading');
if (loadingDiv) {
loadingDiv.classList.add('fade-out');
setTimeout(() => loadingDiv.remove(), 500); // fully remove from DOM after fade
}
});
</script>
@endpush
explain then addQuestion logic
The `addQuestion` method is responsible for **adding a new question** to the selected piller in **Step 3** of the survey editing form.
Here's the breakdown of the logic:
addQuestion() {
if (!this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
if (this.questions[this.selectedPillerId].length >= 10) return;
const newQuestion = {
titles: {},
position: ''
};
this.availableLanguages.forEach(lang => {
newQuestion.titles[lang.code] = '';
});
this.questions[this.selectedPillerId].push(newQuestion);
}
—
### Step-by-step Logic:
1. **Check if Questions Exist for Selected Piller**:
if (!this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
– If the current selected piller (`selectedPillerId`) has no questions array yet, it initializes an empty array for it.
2. **Limit to 10 Questions**:
if (this.questions[this.selectedPillerId].length >= 10) return;
– If 10 or more questions already exist for the selected piller, the method exits to enforce a maximum of 10 questions per piller.
3. **Initialize New Question Object**:
const newQuestion = {
titles: {},
position: ''
};
– A new question object is created. It contains:
– `titles`: an empty object to be filled with language-specific question titles.
– `position`: a field to define the order or position of the question.
4. **Add Title Field for Each Language**:
this.availableLanguages.forEach(lang => {
newQuestion.titles[lang.code] = '';
});
– For each language available (like `en`, `de`, etc.), a blank title is initialized inside `titles`.
5. **Push the New Question into the Array**:
this.questions[this.selectedPillerId].push(newQuestion);
– The fully prepared question object is added to the selected piller's question list.
—
### Purpose:
This method helps in dynamically managing multiple localized questions under a specific piller. It ensures:
– Multilingual support.
– Position management.
– Enforced limit to prevent UI overload or unexpected behavior.
—
Let me know if you want this to auto-initialize a default title or position, or bind it to a form submit!
The `addQuestion` method doesn’t **directly create input boxes**, but it **adds new data objects** to the `questions` array for a specific `piller_id`. Then Vue automatically re-renders the DOM and **generates input fields using `v-for`** in the template.
Let’s break down exactly how the input boxes are created:
—
### 🔁 This `v-for` in your template handles rendering:
<div v-for="(question, index) in questions[selectedPillerId]" :key="index" class="form-row align-items-end mb-2">
<div v-for="lang in availableLanguages" :key="lang.code" class="col-md-4">
<label>{{ __('Title') }} (@{{ lang.code.toUpperCase() }})</label>
<input
type="text"
v-model="question.titles[lang.code]"
class="form-control"
:placeholder="`Enter title in ${lang.title}`">
</div>
<div class="col-md-2">
<label>{{ __('Position') }}</label>
<input type="number" v-model="question.position" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm mt-4" @click="question.id ? deleteQuestion(question.id, index) : removeQuestion(index)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
—
### 🔧 How it works:
1. `v-for="(question, index) in questions[selectedPillerId]"`
– Loops over all questions for the selected piller ID.
2. `v-for="lang in availableLanguages"` inside that
– For each question, it creates **one input field per language** (like English, German, etc.) bound to `question.titles[lang.code]`.
3. A numeric input for position:
<input type="number" v-model="question.position" class="form-control" placeholder="e.g. 1">
4. A remove button to delete that specific question.
—
### ⚡ Vue Reactivity:
As soon as `addQuestion()` pushes a new question object into `questions[selectedPillerId]`, Vue detects the change and updates the DOM, which renders the new set of input boxes for that new question.
—
Let me know if you want:
– A default position number auto-filled.
– Input validation per field.
– A button to clone an existing question.
{
$survey = Survey::with(['pillars'])->where('id', $clientId)->first();
$competenceId = Pillar::where('survey_id',$survey->id)->pluck('id')->toArray();
$questionsIds = SurveyQuestion::where('survey_id', $survey->id)->whereIn('competence_id',$competenceId)->pluck('question_id')->toArray();
$survey->survey_competences = $survey->survey_competences->unique('competence_id');
$survey->survey_competences->map(function ($sc) use ($questionsIds) {
$sc->questions = Question::whereIn('id', $questionsIds)
->where('competence_id', $sc->competence_id)
->get();
});
return $survey;
}
pillar has survey_id
question has piller id
$survey->survey_competences = $survey->survey_competences->unique('competence_id');
now pillars
private function surveyDataFromClientId($clientId)
{
$survey = Survey::with(['pillars'])->where('id', $clientId)->first();
$competenceId = Pillar::where('survey_id',$survey->id)->pluck('id')->toArray();
$questionsIds = SurveyQuestion::where('survey_id', $survey->id)->whereIn('competence_id',$competenceId)->pluck('question_id')->toArray();
$survey->survey_competences = $survey->survey_competences->unique('competence_id');
$survey->survey_competences->map(function ($sc) use ($questionsIds) {
$sc->questions = Question::whereIn('id', $questionsIds)
->where('competence_id', $sc->competence_id)
->get();
});
return $survey;
}
now adjust above
Here's a revised version of your method where:
– `Pillar` is the primary relation with `survey_id`.
– `Question` is directly related to `pillar_id` (instead of `competence_id`).
– You're collecting `pillar_id`s and associating related `questions` with them.
– We cleanly attach questions to each `pillar`.
—
### ✅ Updated Method (pillar-based version):
private function surveyDataFromClientId($clientId)
{
// Load survey with its pillars
$survey = Survey::with(['pillars'])->where('id', $clientId)->firstOrFail();
// Get pillar IDs from the survey
$pillarIds = $survey->pillars->pluck('id')->toArray();
// Get all related questions grouped by pillar_id
$questions = Question::whereIn('pillar_id', $pillarIds)->get()->groupBy('pillar_id');
// Map questions to each pillar
$survey->pillars->map(function ($pillar) use ($questions) {
$pillar->questions = $questions[$pillar->id] ?? collect();
});
return $survey;
}
—
### ✅ Explanation:
– `Survey::with(['pillars'])`: eager loads pillars.
– `$pillarIds`: IDs of all pillars related to the survey.
– `Question::whereIn('pillar_id', $pillarIds)`: fetches all related questions.
– `.groupBy('pillar_id')`: organizes questions into buckets per pillar.
– The `map()` attaches the grouped questions to each pillar using a new property `$pillar->questions`.
—
Let me know if:
– You want to preserve the original `survey_competences` structure.
– Questions need to be localized or include answers/responses.
– You want to format it for API output (e.g., using resources or arrays).
$questionsIds = SurveyQuestion::where('survey_id', $survey->id)->whereIn('competence_id',$competenceId)->pluck('question_id')->toArray();
Here’s the adjusted version:
$questionIds = Question::whereIn('pillar_id', $pillarIds)->pluck('id')->toArray();
### Explanation:
– `pillar_id` is the new foreign key reference in the `questions` table.
– `$pillarIds` comes from:
$pillarIds = Pillar::where('survey_id', $survey->id)->pluck('id')->toArray();
So the full update context looks like:
$pillarIds = Pillar::where('survey_id', $survey->id)->pluck('id')->toArray();
$questionIds = Question::whereIn('pillar_id', $pillarIds)->pluck('id')->toArray();
Let me know if you want to include additional filtering (e.g., only active questions or localized data).