3 Step Form Laravel
April 6, 2025 · AI Conversations
survey
Pillers
questions
survey step will have
name, 3 checkbox
piller (depen on survey)
en titile,de title,wieght,position
question
give piller dropdown
en title,de title, position,piller id
user cannot got piller if survey not filled and created in db
Give laravel code 3 step form
– Step 1: Survey creation (saved immediately to DB).
– Step 2: Pillars (based on created survey).
– Step 3: Questions (based on selected pillar).
—
### ✅ **Controller: SurveyController.php**
use AppModelsSurvey;
use AppModelsPiller;
use AppModelsQuestion;
use IlluminateHttpRequest;
public function create()
{
return view('surveys.create');
}
public function storeSurvey(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
'options' => 'nullable|array',
]);
$survey = Survey::create([
'name' => $request->name,
]);
return response()->json(['survey_id' => $survey->id]);
}
public function storePiller(Request $request)
{
$request->validate([
'survey_id' => 'required|exists:surveys,id',
'title_en' => 'required|string',
'title_de' => 'required|string',
'weight' => 'required|numeric',
'position' => 'required|integer',
]);
$piller = Piller::create([
'survey_id' => $request->survey_id,
'title_en' => $request->title_en,
'title_de' => $request->title_de,
'weight' => $request->weight,
'position' => $request->position,
]);
return response()->json(['piller_id' => $piller->id]);
}
public function storeQuestion(Request $request)
{
$request->validate([
'piller_id' => 'required|exists:pillars,id',
'title_en' => 'required|string',
'title_de' => 'required|string',
'position' => 'required|integer',
]);
$question = Question::create([
'piller_id' => $request->piller_id,
'title_en' => $request->title_en,
'title_de' => $request->title_de,
'position' => $request->position,
]);
return response()->json(['success' => true]);
}
—
### ✅ **Blade Template: `resources/views/surveys/create.blade.php`**
@extends('layouts.app')
@section('content')
<div id="multiStepForm">
<!-- Step 1: Survey -->
<div v-if="step === 1">
<h3>Create Survey</h3>
<input v-model="survey.name" placeholder="Survey Name">
<label><input type="checkbox" v-model="survey.options" value="option1"> Option 1</label>
<label><input type="checkbox" v-model="survey.options" value="option2"> Option 2</label>
<label><input type="checkbox" v-model="survey.options" value="option3"> Option 3</label>
<button @click="submitSurvey">Next</button>
</div>
<!-- Step 2: Pillers -->
<div v-if="step === 2">
<h3>Add Piller</h3>
<input v-model="piller.title_en" placeholder="Piller Title (EN)">
<input v-model="piller.title_de" placeholder="Piller Title (DE)">
<input v-model="piller.weight" placeholder="Weight">
<input v-model="piller.position" placeholder="Position">
<button @click="submitPiller">Next</button>
</div>
<!-- Step 3: Questions -->
<div v-if="step === 3">
<h3>Add Question</h3>
<select v-model="question.piller_id">
<option v-for="pill in pillers" :value="pill.id">@{{ pill.title_en }}</option>
</select>
<input v-model="question.title_en" placeholder="Question Title (EN)">
<input v-model="question.title_de" placeholder="Question Title (DE)">
<input v-model="question.position" placeholder="Position">
<button @click="submitQuestion">Submit</button>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
new Vue({
el: '#multiStepForm',
data: {
step: 1,
survey: {
name: '',
options: [],
},
survey_id: null,
piller: {
title_en: '',
title_de: '',
weight: '',
position: '',
},
piller_id: null,
question: {
piller_id: '',
title_en: '',
title_de: '',
position: '',
},
pillers: [],
},
methods: {
submitSurvey() {
fetch("{{ route('surveys.storeSurvey') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(this.survey)
})
.then(res => res.json())
.then(data => {
this.survey_id = data.survey_id;
this.step = 2;
});
},
submitPiller() {
fetch("{{ route('surveys.storePiller') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({ ...this.piller, survey_id: this.survey_id })
})
.then(res => res.json())
.then(data => {
this.piller_id = data.piller_id;
this.pillers.push({
id: data.piller_id,
title_en: this.piller.title_en
});
this.step = 3;
});
},
submitQuestion() {
fetch("{{ route('surveys.storeQuestion') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(this.question)
})
.then(res => res.json())
.then(data => {
alert("Question created successfully!");
location.reload();
});
}
}
});
</script>
@endpush
—
### ✅ **Web Routes (routes/web.php)**
use AppHttpControllersSurveyController;
Route::get('/surveys/create', [SurveyController::class, 'create'])->name('surveys.create');
Route::post('/surveys/store-survey', [SurveyController::class, 'storeSurvey'])->name('surveys.storeSurvey');
Route::post('/surveys/store-piller', [SurveyController::class, 'storePiller'])->name('surveys.storePiller');
Route::post('/surveys/store-question', [SurveyController::class, 'storeQuestion'])->name('surveys.storeQuestion');
—
### ✅ **Migration Notes (language support)**
For `pillars` and `questions` tables, make sure fields are like:
$table->string('title_en');
$table->string('title_de');
Let me know if you want to dynamically switch fields based on current locale (e.g. `app()->getLocale()`).
Want to enhance it with step indicators or validation messages?
– You **can't go to Pillers** unless the Survey is saved.
– You **can't go to Questions** unless a Piller is saved.
—
### ✅ **Blade View: `resources/views/surveys/create.blade.php`**
@extends('layouts.app')
@section('content')
<div id="surveyApp" class="container mt-4">
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ active: step === 1 }" href="#" @click.prevent="goToStep(1)">1. Survey</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ active: step === 2, disabled: !survey_id }" href="#" @click.prevent="goToStep(2)">2. Pillers</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ active: step === 3, disabled: !pillers.length }" href="#" @click.prevent="goToStep(3)">3. Questions</a>
</li>
</ul>
<div class="tab-content p-4 border border-top-0">
<!-- Step 1 -->
<div v-if="step === 1">
<h4>Create Survey</h4>
<div class="form-group">
<label>Name</label>
<input v-model="survey.name" class="form-control" placeholder="Survey Name">
</div>
<label><input type="checkbox" v-model="survey.options" value="option1"> Option 1</label><br>
<label><input type="checkbox" v-model="survey.options" value="option2"> Option 2</label><br>
<label><input type="checkbox" v-model="survey.options" value="option3"> Option 3</label><br>
<button class="btn btn-primary mt-3" @click="submitSurvey">Save and Next</button>
</div>
<!-- Step 2 -->
<div v-if="step === 2">
<h4>Add Piller</h4>
<div class="form-group">
<label>Title (EN)</label>
<input v-model="piller.title_en" class="form-control">
</div>
<div class="form-group">
<label>Title (DE)</label>
<input v-model="piller.title_de" class="form-control">
</div>
<div class="form-group">
<label>Weight</label>
<input v-model="piller.weight" class="form-control" type="number">
</div>
<div class="form-group">
<label>Position</label>
<input v-model="piller.position" class="form-control" type="number">
</div>
<button class="btn btn-primary" @click="submitPiller">Save and Next</button>
</div>
<!-- Step 3 -->
<div v-if="step === 3">
<h4>Add Question</h4>
<div class="form-group">
<label>Piller</label>
<select v-model="question.piller_id" class="form-control">
<option disabled value="">-- Select Piller --</option>
<option v-for="pill in pillers" :value="pill.id">@{{ pill.title_en }}</option>
</select>
</div>
<div class="form-group">
<label>Title (EN)</label>
<input v-model="question.title_en" class="form-control">
</div>
<div class="form-group">
<label>Title (DE)</label>
<input v-model="question.title_de" class="form-control">
</div>
<div class="form-group">
<label>Position</label>
<input v-model="question.position" class="form-control" type="number">
</div>
<button class="btn btn-success" @click="submitQuestion">Submit</button>
</div>
</div>
</div>
@endsection
@push('scripts')
<script src="https://cdn.jsdelivr.net/npm/vue@2"></script>
<script>
new Vue({
el: '#surveyApp',
data: {
step: 1,
survey: {
name: '',
options: [],
},
survey_id: null,
piller: {
title_en: '',
title_de: '',
weight: '',
position: '',
},
piller_id: null,
question: {
piller_id: '',
title_en: '',
title_de: '',
position: '',
},
pillers: [],
},
methods: {
goToStep(stepNumber) {
if (stepNumber === 2 && !this.survey_id) return;
if (stepNumber === 3 && this.pillers.length === 0) return;
this.step = stepNumber;
},
submitSurvey() {
fetch("{{ route('surveys.storeSurvey') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(this.survey)
})
.then(res => res.json())
.then(data => {
this.survey_id = data.survey_id;
this.step = 2;
});
},
submitPiller() {
fetch("{{ route('surveys.storePiller') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({ ...this.piller, survey_id: this.survey_id })
})
.then(res => res.json())
.then(data => {
this.piller_id = data.piller_id;
this.pillers.push({
id: data.piller_id,
title_en: this.piller.title_en
});
this.step = 3;
});
},
submitQuestion() {
fetch("{{ route('surveys.storeQuestion') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(this.question)
})
.then(res => res.json())
.then(data => {
alert("Question added!");
location.reload();
});
}
}
});
</script>
@endpush
—
### ✅ Tip for Bootstrap 4 Tabs
Bootstrap 4 marks disabled tabs using `.disabled` class only visually — users can still click them. To enforce logic, Vue controls tab access via `goToStep()`.
—
### ✅ Routes Example
Route::get('/surveys/create', [SurveyController::class, 'create'])->name('surveys.create');
Route::post('/surveys/store-survey', [SurveyController::class, 'storeSurvey'])->name('surveys.storeSurvey');
Route::post('/surveys/store-piller', [SurveyController::class, 'storePiller'])->name('surveys.storePiller');
Route::post('/surveys/store-question', [SurveyController::class, 'storeQuestion'])->name('surveys.storeQuestion');
Let me know if you also want to:
– Add step validation with error display
– Allow adding **multiple** pillers/questions in one session
– Prefill dropdowns via AJAX
—
### ✅ 1. **Add Validation State in Vue**
Update your Vue data to include a validation error:
data: {
step: 1,
survey: {
name: '',
options: [],
},
surveyError: '',
// ... rest
}
—
### ✅ 2. **Update Survey Form HTML**
Update the form group for name input to show error state:
<div class="form-group">
<label>Name</label>
<input v-model="survey.name" class="form-control" :class="{ 'is-invalid': surveyError }" placeholder="Survey Name">
<div class="invalid-feedback" v-if="surveyError">
@{{ surveyError }}
</div>
</div>
—
### ✅ 3. **Update submitSurvey() to Include Validation**
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
return;
}
fetch("{{ route('surveys.storeSurvey') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(this.survey)
})
.then(res => {
if (!res.ok) throw res;
return res.json();
})
.then(data => {
this.survey_id = data.survey_id;
this.step = 2;
})
.catch(async (err) => {
if (err.status === 422) {
const errorData = await err.json();
this.surveyError = errorData.errors.name ? errorData.errors.name[0] : 'Invalid input';
} else {
this.surveyError = 'An unexpected error occurred.';
}
});
}
—
### ✅ 4. **Controller Validation (Already Added)**
Make sure this exists in your controller:
$request->validate([
'name' => 'required|string|max:255',
]);
—
Now if the name field is empty, it’ll:
– Show a red border with Bootstrap 4's `.is-invalid`
– Display the error message below the input
Let me know if you want to show alerts or toast messages instead!
—
### ✅ 1. **Include SweetAlert2 CDN**
Add this to your `create.blade.php` (within `@push('scripts')` or directly before “):
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
—
### ✅ 2. **Update `submitSurvey()` Method**
Replace your existing `submitSurvey()` with this version that shows a SweetAlert success message:
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
return;
}
fetch("{{ route('surveys.storeSurvey') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(this.survey)
})
.then(res => {
if (!res.ok) throw res;
return res.json();
})
.then(data => {
this.survey_id = data.survey_id;
// Show SweetAlert and move to step 2 on confirmation
Swal.fire({
title: 'Survey Created!',
text: 'Do you want to continue to Pillers?',
icon: 'success',
showCancelButton: true,
confirmButtonText: 'Yes, proceed',
cancelButtonText: 'No'
}).then((result) => {
if (result.isConfirmed) {
this.step = 2;
}
});
})
.catch(async (err) => {
if (err.status === 422) {
const errorData = await err.json();
this.surveyError = errorData.errors.name ? errorData.errors.name[0] : 'Invalid input';
} else {
this.surveyError = 'An unexpected error occurred.';
}
});
}
—
### ✅ Result:
– Survey will be saved via AJAX.
– If successful, a SweetAlert2 modal will pop up.
– On "Yes, proceed", the user is taken to **Step 2** (Pillers).
Let me know if you want a similar confirmation before **Step 3** too!
—
### ✅ Use SweetAlert for Info Only
Update your `submitSurvey()` like this:
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
return;
}
fetch("{{ route('surveys.storeSurvey') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(this.survey)
})
.then(res => {
if (!res.ok) throw res;
return res.json();
})
.then(data => {
this.survey_id = data.survey_id;
// Show success alert and move directly to step 2
Swal.fire({
icon: 'success',
title: 'Survey saved successfully!',
showConfirmButton: false,
timer: 1500
});
this.step = 2;
})
.catch(async (err) => {
if (err.status === 422) {
const errorData = await err.json();
this.surveyError = errorData.errors.name ? errorData.errors.name[0] : 'Invalid input';
} else {
this.surveyError = 'An unexpected error occurred.';
}
});
}
—
### ✅ Result:
– If the survey saves successfully:
– A SweetAlert success message pops up briefly.
– It auto-hides after 1.5 seconds.
– User is taken to **Step 2** automatically.
Let me know if you want to do the same for Pillers or Questions too!
{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}
and have 3 colunmns in survey table
—
### ✅ 1. **Database Migration**
Make sure your `surveys` table migration includes these boolean fields:
$table->boolean('allow_comments')->default(false);
$table->boolean('allow_comments_on_pillers')->default(false);
$table->boolean('mark_as_external')->default(false);
—
### ✅ 2. **Update Blade Checkbox Code (No change needed)**
You're already using this:
<label><input type="checkbox" v-model="survey.options" value="allow_comments">{{ __('Allow comments on Survey') }}</label><br>
<label><input type="checkbox" v-model="survey.options" value="allow_comments_on_pillers">{{ __('Allow comments on Pillers') }}</label><br>
<label><input type="checkbox" v-model="survey.options" value="mark_as_external">{{ __('Mark as External') }}</label><br>
This binds `survey.options` as an array like:
['allow_comments', 'mark_as_external'] // based on what’s checked
—
### ✅ 3. **Vue: Before Submit, Map to Flags**
Update your `submitSurvey()` method to convert the options array into individual boolean fields:
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
return;
}
// Convert options to individual fields
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,
};
fetch("{{ route('surveys.storeSurvey') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(data)
})
.then(res => {
if (!res.ok) throw res;
return res.json();
})
.then(data => {
this.survey_id = data.survey_id;
this.step = 2;
})
.catch(async (err) => {
if (err.status === 422) {
const errorData = await err.json();
this.surveyError = errorData.errors.name ? errorData.errors.name[0] : 'Invalid input';
} else {
this.surveyError = 'An unexpected error occurred.';
}
});
}
—
### ✅ 4. **SurveyController Update**
Update the `storeSurvey` method to accept these fields:
public function storeSurvey(Request $request)
{
$request->validate([
'name' => 'required|string|max:255',
]);
$survey = Survey::create([
'name' => $request->name,
'allow_comments' => $request->allow_comments ?? 0,
'allow_comments_on_pillers' => $request->allow_comments_on_pillers ?? 0,
'mark_as_external' => $request->mark_as_external ?? 0,
]);
return response()->json(['survey_id' => $survey->id]);
}
—
Let me know if you also want to **edit** this form later with pre-checked boxes based on the saved values!
'name' => 'required|string|max:255|unique:Surveys,name',
]);
$survey = Survey::create([
'name' => $request->name,
'allow_comments_survey' => $request->allow_comments ?? 0,
'allow_comments_on_pillers' => $request->allow_comments_on_pillers ?? 0,
'external' => $request->mark_as_external ?? 0,
]);
return response()->json(['survey_id' => $survey->id]);
use try catch and unique name chekck if validation fails then show proper message in sweet alert
—
### ✅ **Controller Method with Try-Catch & JSON Error Response**
use IlluminateValidationValidationException;
use IlluminateDatabaseQueryException;
public function storeSurvey(Request $request)
{
try {
$request->validate([
'name' => 'required|string|max:255|unique:surveys,name',
]);
$survey = Survey::create([
'name' => $request->name,
'allow_comments_survey' => $request->allow_comments ?? 0,
'allow_comments_on_pillers' => $request->allow_comments_on_pillers ?? 0,
'external' => $request->mark_as_external ?? 0,
]);
return response()->json([
'success' => true,
'survey_id' => $survey->id
]);
} catch (ValidationException $e) {
return response()->json([
'success' => false,
'message' => $e->validator->errors()->first('name') // or use ->all() for all messages
], 422);
} catch (QueryException $e) {
return response()->json([
'success' => false,
'message' => 'A database error occurred. Please try again.'
], 500);
} catch (Exception $e) {
return response()->json([
'success' => false,
'message' => 'Unexpected error: ' . $e->getMessage()
], 500);
}
}
—
### ✅ **Vue: SweetAlert Display on Error**
Use SweetAlert2 (include it via CDN or already in your project):
<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script>
Update your `submitSurvey()` method in Vue:
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
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,
};
fetch("{{ route('surveys.storeSurvey') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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;
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.'
});
});
}
—
### ✅ Result
– If the name is not unique, the user sees a SweetAlert with `Validation Error`.
– Any unexpected issue will trigger a general error alert.
– Backend is safe with fallback exception handling.
Let me know if you want to also handle form-wide validation errors (not just `name`) or support inline feedback as well.
we have surveys sin small letter
SQLSTATE[42S02]: Base table or view not found: 1146 Table 'db.Surveys' doesn't exist
…is happening because in your validation rule, you’ve written:
'unique:Surveys,name'
This incorrectly uses a capital **`S`** in `Surveys`, but your actual table name is lowercase: `surveys`.
—
### ✅ ✅ **Fix it like this:**
Update the validation rule to use lowercase:
$request->validate([
'name' => 'required|string|max:255|unique:surveys,name',
]);
—
### ✅ Why this matters:
Table names in **MySQL/MariaDB on Linux** are **case-sensitive by default**, unlike Windows.
So:
– ✅ `surveys` → correct
– ❌ `Surveys` → will not be found
—
After fixing the case, the validation should work without throwing a SQL error.
Let me know if you want to also enforce lowercase model table names globally via `$table` property on your model.
Title (EN)
Title (DE)
Weight
Position
Save and Next
little bit change in second step
we need multiple piller with add more piller
also we have langauge table in db and want to create title boxes based in that table data and validate before submit
1. Add **multiple Pillers** using an "Add More" button.
2. Dynamically generate `title` fields **per language** from your `languages` table.
3. Validate each Piller before submission.
—
### ✅ 1. **Pass Language List from Laravel to Blade**
In your `SurveyController@create()`:
use AppModelsLanguage;
public function create()
{
$languages = Language::all(); // ['id', 'code', 'name']
return view('surveys.create', compact('languages'));
}
In Blade:
<script>
window.availableLanguages = @json($languages);
</script>
—
### ✅ 2. **Vue Setup for Multiple Pillers**
Update your Vue `data`:
data: {
step: 2,
survey_id: null,
pillers: [
{
titles: {}, // {'en': '', 'de': '', ...}
weight: '',
position: ''
}
],
availableLanguages: window.availableLanguages,
}
—
### ✅ 3. **Updated Step 2 Markup**
<div v-if="step === 2">
<h4>Add Pillers</h4>
<div v-for="(piller, index) in pillers" :key="index" class="border p-3 mb-3">
<h5>Piller @{{ index + 1 }}</h5>
<div v-for="lang in availableLanguages" :key="lang.code" class="form-group">
<label>Title (@{{ lang.name }})</label>
<input type="text" v-model="piller.titles[lang.code]" class="form-control" :placeholder="`Title in ${lang.name}`">
</div>
<div class="form-group">
<label>Weight</label>
<input type="number" v-model="piller.weight" class="form-control">
</div>
<div class="form-group">
<label>Position</label>
<input type="number" v-model="piller.position" class="form-control">
</div>
<button class="btn btn-danger btn-sm" @click="removePiller(index)" v-if="pillers.length > 1">Remove</button>
</div>
<button class="btn btn-secondary" @click="addPiller">Add More Pillers</button>
<button class="btn btn-primary ml-2" @click="submitPillers">Save and Next</button>
</div>
—
### ✅ 4. **Vue Methods**
methods: {
addPiller() {
const newPiller = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPiller.titles[lang.code] = '';
});
this.pillers.push(newPiller);
},
removePiller(index) {
this.pillers.splice(index, 1);
},
submitPillers() {
// Validate
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.name}`, 'error');
return;
}
}
if (!piller.weight || !piller.position) {
Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
// Submit each piller
const requests = this.pillers.map(piller => {
return fetch("{{ route('surveys.storePiller') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
survey_id: this.survey_id,
titles: piller.titles,
weight: piller.weight,
position: piller.position
})
}).then(res => res.json());
});
Promise.all(requests).then(results => {
this.piller_ids = results.map(r => r.piller_id);
this.step = 3;
}).catch(() => {
Swal.fire('Error', 'Failed to save one or more Pillers.', 'error');
});
}
}
—
### ✅ 5. **Backend Controller for `storePiller`**
Update to accept `titles` as an array:
public function storePiller(Request $request)
{
$request->validate([
'survey_id' => 'required|exists:surveys,id',
'titles' => 'required|array',
'titles.en' => 'required|string',
'titles.de' => 'required|string',
'weight' => 'required|numeric',
'position' => 'required|integer',
]);
$piller = Piller::create([
'survey_id' => $request->survey_id,
'weight' => $request->weight,
'position' => $request->position,
]);
foreach ($request->titles as $lang => $title) {
$piller->translations()->create([
'language_code' => $lang,
'title' => $title,
]);
}
return response()->json(['piller_id' => $piller->id]);
}
—
Let me know if you're using a `piller_translations` table or storing multilingual fields in the same row — I’ll adjust accordingly.
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)
{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))
{{– Content body: main page content –}}
@section('content_body')
{{ __('Create Surveys') }}
@if($errors->any())
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
@endif
@if(session()->has('message.level'))
{!! session('message.content') !!}
@endif
@{{ message }}
1. Survey
2. Pillers
3. Questions
Create Survey
Name
@{{ surveyError }}
{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}
Save and Next
Add Pillers
Piller @{{ index + 1 }}
Title (@{{ lang.name }})
Weight
Position
Remove
Add More Pillers
Save and Next
Add Question
Piller
— Select Piller —
@{{ pill.title_en }}
Title (EN)
Title (DE)
Position
Submit
@stop
{{– Push extra scripts –}}
@push('js')
<script>
window.availableLanguages = @json(getLanguages());
</script>
<script>
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp({
data() {
return {
message: 'Hello Vue!',
step: 1,
survey: {
name: '',
options: []
},
surveyError: '',
survey_id: null,
pillers: [
{
titles: {}, // {'en': '', 'de': '', ...}
weight: '',
position: ''
}
],
availableLanguages: window.availableLanguages,
question: {
piller_id: '',
title_en: '',
title_de: '',
position: ''
}
}
},
methods: {
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.length === 0) return;
this.step = step;
},
addPiller() {
const newPiller = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPiller.titles[lang.code] = '';
});
this.pillers.push(newPiller);
},
removePiller(index) {
this.pillers.splice(index, 1);
},
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
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,
};
fetch("{{ route('admin.surveys.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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.') }}"
});
});
},
submitPiller() {
const newPiller = { ...this.piller, id: this.pillers.length + 1 };
this.pillers.push(newPiller);
this.message = 'Piller added';
this.piller = { title_en: '', title_de: '', weight: '', position: '' };
this.step = 3;
},
submitQuestion() {
this.message = 'Question added!';
this.question = { piller_id: '', title_en: '', title_de: '', position: '' };
}
}
});
app.mount('#surveyApp');
});
</script>
@endpush
above has some erro after above changes step1 selected and we can go to step3
—
### ✅ **Fixes to Apply:**
#### 1. ✅ **Prevent Step 3 Access Until Pillers Are Saved**
Update the step navigation condition in the `goToStep` method:
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.every(p => !p.id)) return; // prevent if no saved piller has ID
this.step = step;
}
—
#### 2. ✅ **Fix `submitPiller()` to:**
– Validate titles, weight, position.
– Send each piller to Laravel backend.
– Save and store returned `piller_id`.
– Move to step 3 only if successful.
#### 🛠 Updated `submitPillers()` Method:
submitPillers() {
// Validate
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.name}`, 'error');
return;
}
}
if (!piller.weight || !piller.position) {
Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
// Submit all pillers
const requests = this.pillers.map(piller => {
return fetch("{{ route('admin.pillers.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
survey_id: this.survey_id,
titles: piller.titles,
weight: piller.weight,
position: piller.position
})
})
.then(res => res.json());
});
Promise.all(requests).then(results => {
// Update pillers with IDs
results.forEach((result, index) => {
if (result.piller_id) {
this.pillers[index].id = result.piller_id;
}
});
this.step = 3;
}).catch(() => {
Swal.fire('Error', 'One or more pillers failed to save.', 'error');
});
}
—
#### 3. ✅ **Update Step 2 Buttons**
Replace `submitPiller()` call in your Blade template with:
<button class="btn btn-primary ml-2" @click="submitPillers">Save and Next</button>
This ensures you trigger the correct logic to save all pillers and assign their IDs.
—
#### 4. ✅ **Backend Route Example**
Make sure you have this route and controller method:
// routes/web.php
Route::post('/admin/pillers/store', [AdminPillerController::class, 'store'])->name('admin.pillers.store');
// AdminPillerController.php
public function store(Request $request)
{
$request->validate([
'survey_id' => 'required|exists:surveys,id',
'titles' => 'required|array',
'titles.en' => 'required|string',
'titles.de' => 'required|string',
'weight' => 'required|numeric',
'position' => 'required|integer',
]);
$piller = Piller::create([
'survey_id' => $request->survey_id,
'weight' => $request->weight,
'position' => $request->position,
]);
foreach ($request->titles as $lang => $title) {
$piller->translations()->create([
'language_code' => $lang,
'title' => $title,
]);
}
return response()->json(['piller_id' => $piller->id]);
}
—
Let me know if you want to:
– Add title preview in the dropdown on step 3.
– Support editing pillers after saving.
– Store `title_en`, `title_de` in the main table instead of translations.
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)
{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))
{{– Content body: main page content –}}
@section('content_body')
{{ __('Create Surveys') }}
@if($errors->any())
@foreach ($errors->all() as $error)
{{ $error }}
@endforeach
@endif
@if(session()->has('message.level'))
{!! session('message.content') !!}
@endif
@{{ message }}
1. Survey
2. Pillers
3. Questions
Create Survey
Name
@{{ surveyError }}
{{ __('Allow comments on Survey') }}
{{ __('Allow comments on Pillers') }}
{{ __('Mark as External') }}
Save and Next
Add Piller
Title (EN)
Title (DE)
Weight
Position
Save and Next
Add Question
Piller
— Select Piller —
@{{ pill.title_en }}
Title (EN)
Title (DE)
Position
Submit
@stop
{{– Push extra scripts –}}
@push('js')
<script>
window.availableLanguages = @json(getLanguages());
</script>
<script>
import { createApp } from 'https://unpkg.com/vue@3/dist/vue.esm-browser.js'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp({
data() {
return {
message: 'Hello Vue!',
step: 1,
survey: {
name: '',
options: []
},
surveyError: '',
survey_id: null,
availableLanguages: window.availableLanguages,
piller: {
title_en: '',
title_de: '',
weight: '',
position: ''
},
pillers: [],
question: {
piller_id: '',
title_en: '',
title_de: '',
position: ''
}
}
},
methods: {
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.length === 0) return;
this.step = step;
},
submitSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
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,
};
fetch("{{ route('admin.surveys.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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.') }}"
});
});
},
submitPiller() {
const newPiller = { ...this.piller, id: this.pillers.length + 1 };
this.pillers.push(newPiller);
this.message = 'Piller added';
this.piller = { title_en: '', title_de: '', weight: '', position: '' };
this.step = 3;
},
submitQuestion() {
this.message = 'Question added!';
this.question = { piller_id: '', title_en: '', title_de: '', position: '' };
}
}
});
app.mount('#surveyApp');
});
</script>
@endpush
add more piller option and based on dynamic laguange from databse titles
– ✅ Adding **multiple Pillers** dynamically using “Add More”
– ✅ Creating **title fields dynamically** based on languages from DB (like EN, DE, etc.)
Here’s the updated implementation with clean changes to your Blade + Vue code:
—
### ✅ Step-by-Step Updates
—
### ✅ 1. **Pass Languages to Blade**
In your controller:
public function create()
{
$languages = Language::select('code', 'name')->get(); // ['en' => 'English', 'de' => 'Deutsch']
return view('admin.surveys.create', compact('languages'));
}
In Blade:
<script>
window.availableLanguages = @json($languages);
</script>
—
### ✅ 2. **Blade – Step 2 UI Update**
Replace your Step 2 HTML block with this:
<!-- Step 2 -->
<div v-if="step === 2">
<h4>Add Pillers</h4>
<div v-for="(piller, index) in pillers" :key="index" class="border p-3 mb-3">
<h5>Piller @{{ index + 1 }}</h5>
<div v-for="lang in availableLanguages" :key="lang.code" class="form-group">
<label>Title (@{{ lang.name }})</label>
<input type="text" v-model="piller.titles[lang.code]" class="form-control" :placeholder="`Enter title in ${lang.name}`">
</div>
<div class="form-group">
<label>Weight</label>
<input type="number" v-model="piller.weight" class="form-control">
</div>
<div class="form-group">
<label>Position</label>
<input type="number" v-model="piller.position" class="form-control">
</div>
<button class="btn btn-danger btn-sm" @click="removePiller(index)" v-if="pillers.length > 1">Remove</button>
</div>
<button class="btn btn-secondary" @click="addPiller">Add More Pillers</button>
<button class="btn btn-primary ml-2" @click="submitPillers">Save and Next</button>
</div>
—
### ✅ 3. **Vue Data + Methods Update**
Update your `data()` and `methods` in Vue:
data() {
return {
step: 1,
message: '',
survey: {
name: '',
options: []
},
surveyError: '',
survey_id: null,
availableLanguages: window.availableLanguages || [],
pillers: [
{
titles: {}, // dynamically assigned
weight: '',
position: ''
}
],
question: {
piller_id: '',
title_en: '',
title_de: '',
position: ''
}
}
},
mounted() {
// Init first piller titles
this.availableLanguages.forEach(lang => {
this.pillers[0].titles[lang.code] = '';
});
},
methods: {
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.every(p => !p.id)) return;
this.step = step;
},
addPiller() {
const newPiller = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPiller.titles[lang.code] = '';
});
this.pillers.push(newPiller);
},
removePiller(index) {
this.pillers.splice(index, 1);
},
submitPillers() {
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.name}`, 'error');
return;
}
}
if (!piller.weight || !piller.position) {
Swal.fire('Validation Error', `Piller ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
const requests = this.pillers.map(piller => {
return fetch("{{ route('admin.pillers.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
survey_id: this.survey_id,
titles: piller.titles,
weight: piller.weight,
position: piller.position
})
}).then(res => res.json());
});
Promise.all(requests).then(results => {
results.forEach((result, index) => {
if (result.piller_id) {
this.pillers[index].id = result.piller_id;
}
});
this.step = 3;
}).catch(() => {
Swal.fire('Error', 'One or more Pillers failed to save.', 'error');
});
},
submitSurvey() {
// Already good – no change here
},
submitQuestion() {
this.message = 'Question added!';
this.question = { piller_id: '', title_en: '', title_de: '', position: '' };
}
}
—
### ✅ 4. **Backend Controller Example**
In `AdminPillerController@store`:
public function store(Request $request)
{
$request->validate([
'survey_id' => 'required|exists:surveys,id',
'titles' => 'required|array',
'titles.en' => 'required|string',
'titles.de' => 'required|string',
'weight' => 'required|numeric',
'position' => 'required|integer',
]);
$piller = Piller::create([
'survey_id' => $request->survey_id,
'weight' => $request->weight,
'position' => $request->position,
]);
foreach ($request->titles as $lang => $title) {
$piller->translations()->create([
'language_code' => $lang,
'title' => $title,
]);
}
return response()->json(['piller_id' => $piller->id]);
}
—
Let me know if your project:
– Stores title translations in the same table (one row per piller), or
– Uses a `piller_translations` table
I’ll adapt the backend structure accordingly.
this is empty even availableLanguages has values lang.name is empty also lang.code
Let’s debug and fix this:
—
### ✅ 1. **Check Data in Controller**
In your controller, ensure you're sending the correct structure:
$languages = AppModelsLanguage::select('code', 'name')->get(); // code = 'en', name = 'English'
return view('admin.surveys.create', compact('languages'));
—
### ✅ 2. **Pass to Blade as JSON**
Ensure it's assigned correctly in the Blade view:
<script>
window.availableLanguages = @json($languages);
</script>
—
### ✅ 3. **Inspect in Browser DevTools**
In your browser console, run:
console.log(window.availableLanguages);
✅ It should log something like:
[
{ "code": "en", "name": "English" },
{ "code": "de", "name": "German" }
]
❌ If it logs just a list of strings like `["en", "de"]`, your `select()` or format is wrong.
—
### ✅ 4. **Fix Vue Template (If Still Broken)**
Make sure you're using this exact `v-for`:
<div v-for="lang in availableLanguages" :key="lang.code" class="form-group">
<label>Title (@{{ lang.name }})</label>
<input type="text" v-model="piller.titles[lang.code]" class="form-control" :placeholder="`Enter title in ${lang.name}`">
</div>
—
### ✅ 5. **Alternative Debug Tip**
If still not working, temporarily try:
<pre>@{{ availableLanguages }}</pre>
To visually inspect the data in your view.
—
### ✅ Summary
| ✅ Check | Description |
|———|————-|
| Controller | `select('code', 'name')` not just `pluck('name', 'code')` |
| Blade | Use `@json()` and assign to `window.availableLanguages` |
| Vue | Make sure `availableLanguages` is assigned in `data()` |
| Template | Use `lang.code` and `lang.name` |
—
Let me know what `console.log(window.availableLanguages)` shows — I’ll adjust based on the real structure.
—
### ✅ Quick Fix in Vue Template
Update your `v-for` block like this:
<div v-for="lang in availableLanguages" :key="lang.code" class="form-group">
<label>Title (@{{ lang.title }})</label>
<input type="text" v-model="piller.titles[lang.code]" class="form-control" :placeholder="`Enter title in ${lang.title}`">
</div>
—
### 🧠 Optional: If you prefer `name` in Vue for consistency, you could also map it in your controller:
$languages = AppModelsLanguage::select('code', 'title as name')->get();
Then your original Vue template using `lang.name` will work without changes.
—
Let me know which approach you prefer, and I’ll help you align everything accordingly.
[
{ "id": 1, "title": "English", "code": "en" },
{ "id": 2, "title": "German", "code": "de" }
]
So instead of `lang.name`, you should use `lang.title`.
—
### ✅ Update your Vue template loop like this:
<div v-for="lang in availableLanguages" :key="lang.code" class="form-group">
<label>Title (@{{ lang.title }})</label>
<input type="text" v-model="piller.titles[lang.code]" class="form-control" :placeholder="`Enter title in ${lang.title}`">
</div>
—
### ✅ Summary
| Property | Description |
|———-|————-|
| `lang.code` | Language code (`en`, `de`, etc.) |
| `lang.title` | Language display name (`English`, `German`, etc.) |
—
You're all set! Let me know if you want to auto-fill one language from another or pre-translate.
—
### ✅ Updated Code:
<li class="nav-item">
<a class="nav-link"
:class="{ active: step === 3, disabled: !pillers.some(p => p.id) }"
href="#"
@click.prevent="goToStep(3)">
3. Questions
</a>
</li>
—
### ✅ Explanation:
– `pillers.some(p => p.id)` ensures the tab only activates if **at least one Piller has been successfully saved** (and thus has an `id`).
– Prevents users from jumping to Step 3 if only frontend Piller data exists but wasn’t submitted.
—
Let me know if you want to visually **disable the link** or show a tooltip like "Please add Pillers first".
—
### ✅ 1. **Limit "Add More Pillers" to max 5**
Update the button and method:
<button class="btn btn-secondary"
@click="addPiller"
:disabled="pillers.length >= 5">
Add More Pillers
</button>
<small v-if="pillers.length >= 5" class="text-muted">
Maximum 5 Pillers allowed.
</small>
addPiller() {
if (this.pillers.length >= 5) return;
const newPiller = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPiller.titles[lang.code] = '';
});
this.pillers.push(newPiller);
}
—
### ✅ 2. **Enforce Minimum of 1 Piller**
Update your `removePiller()` method to prevent going below 1:
removePiller(index) {
if (this.pillers.length <= 1) {
Swal.fire('Minimum Required', 'At least one Piller is required.', 'warning');
return;
}
this.pillers.splice(index, 1);
}
—
### ✅ Optional UI Feedback
You can show a counter or visual guide:
<p class="text-muted">
Total Pillers: @{{ pillers.length }} / 5
</p>
—
Let me know if you want to:
– Enforce the limit **on the backend as well**
– Disable submit if pillers count is out of bounds
{{__('Piller')}} @{{ index + 1 }}
{{__('Title')}} @{{ lang.code }}
{{__('Weight')}}
{{__('Position')}}
{{__('Add More Pillers')}}
{{__('Save and Next')}}
add bootsrap 4 for styling make input fields for each pillor in one line and also buttons in one line
Here’s the updated Step 2 block with:
– Inputs (`title`, `weight`, `position`) arranged **in one row per piller**
– Buttons (`Add More`, `Save and Next`) in one line
– Proper Bootstrap 4 structure
—
### ✅ Updated `step === 2` HTML with Bootstrap 4 Styling
<div v-if="step === 2">
<h4>{{ __('Add Pillers') }}</h4>
<div v-for="(piller, index) in pillers" :key="index" class="border p-3 mb-3">
<div class="form-row align-items-end">
<div class="col-12">
<h5>{{ __('Piller') }} @{{ index + 1 }}</h5>
</div>
<div v-for="lang in availableLanguages" :key="lang.code" class="col-md-4 mb-3">
<label>{{ __('Title') }} (@{{ lang.code.toUpperCase() }})</label>
<input type="text" v-model="piller.titles[lang.code]" class="form-control" :placeholder="`Enter title in ${lang.code}`">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Weight') }}</label>
<input type="number" v-model="piller.weight" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Position') }}</label>
<input type="number" v-model="piller.position" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-auto mb-3" v-if="pillers.length > 1">
<label> </label><br>
<button class="btn btn-danger btn-sm" @click="removePiller(index)" title="Remove Piller">
<i class="fa fa-trash" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
<!-- Button Row -->
<div class="form-row mt-3">
<div class="col-auto">
<button class="btn btn-secondary" @click="addPiller" :disabled="pillers.length >= 5">
{{ __('Add More Pillers') }}
</button>
</div>
<div class="col-auto">
<button class="btn btn-primary" @click="submitPillers">
{{ __('Save and Next') }}
</button>
</div>
</div>
</div>
—
### 🧠 Notes:
– `form-row` is Bootstrap 4’s way of aligning form controls horizontally.
– `col-md-*` determines column width per field.
– `col-auto` ensures buttons shrink to content size.
– You can adjust column widths (`col-md-4`, `col-md-2`, etc.) based on how many languages you expect.
Let me know if you'd like to include **inline validation messages** or collapse each Piller in an accordion layout for larger forms.
– Add it as a `col` inside the same `.form-row`
– Use vertical alignment with `align-self-end` to match the input bottoms
—
### ✅ Updated `form-row` Layout with Delete Button in Same Line
<div v-for="(piller, index) in pillers" :key="index" class="border p-3 mb-3">
<div class="form-row align-items-end">
<div class="col-12">
<h5>{{ __('Piller') }} @{{ index + 1 }}</h5>
</div>
<div v-for="lang in availableLanguages" :key="lang.code" class="col-md-4 mb-3">
<label>{{ __('Title') }} (@{{ lang.code.toUpperCase() }})</label>
<input type="text" v-model="piller.titles[lang.code]" class="form-control" :placeholder="`Enter title in ${lang.code}`">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Weight') }}</label>
<input type="number" v-model="piller.weight" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Position') }}</label>
<input type="number" v-model="piller.position" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-md-1 mb-3 d-flex align-items-end" v-if="pillers.length > 1">
<button class="btn btn-danger btn-sm" @click="removePiller(index)" title="Remove Piller">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
—
### 🧩 Visual Result:
All inputs + the delete button will appear **in a single row**, neatly aligned at the bottom.
Let me know if you want to hide the delete button for the **first Piller only** or add confirmation before deleting.
return fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify({
survey_id: this.survey_id,
titles: piller.titles,
weight: piller.weight,
position: piller.position
})
}).then(res => res.json());
});
Promise.all(requests).then(results => {
results.forEach((result, index) => {
if (result.piller_id) {
this.pillers[index].id = result.piller_id;
}
});
this.step = 3;
}).catch(() => {
Swal.fire('Error', 'One or more Pillers failed to save.', 'error');
});
}
change above to
fetch("{{ route('admin.surveys.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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.') }}"
});
});
—
### ✅ Updated `submitPillers()` with structured `fetch` and SweetAlert logic
submitPillers() {
// Validation
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 requests = this.pillers.map(piller => {
const data = {
survey_id: this.survey_id,
titles: piller.titles,
weight: piller.weight,
position: piller.position
};
return fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(data)
}).then(res => res.json().then(json => ({ status: res.status, body: json })));
});
Promise.all(requests).then(results => {
let allSuccess = true;
results.forEach(({ status, body }, index) => {
if (status === 200 && body.piller_id) {
this.pillers[index].id = body.piller_id;
} else {
allSuccess = false;
}
});
if (allSuccess) {
Swal.fire({
icon: 'success',
title: "{{ __('Pillers saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 3;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Some Pillers failed to save') }}",
text: "{{ __('Please try again or contact admin.') }}"
});
}
}).catch(() => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
});
}
—
### ✅ Improvements made:
– Matches your `submitSurvey()` structure
– Adds better `status` checking per response
– Shows SweetAlert based on success/failure of individual requests
– Keeps `this.step = 3` only if all Pillers save successfully
Let me know if you want to **retry failed piller saves** or display per-piller error in the UI.
—
### ✅ Final Version of `submitPillers()` with:
– 🟢 Structured `fetch` with `status` check
– 🟢 SweetAlert feedback
– 🛑 Validation: min 1, max 5 pillers
– 🛑 Field-level checks for `titles`, `weight`, and `position`
—
submitPillers() {
// Check min/max piller count
if (this.pillers.length < 1) {
Swal.fire('Validation Error', 'At least 1 Piller is required.', 'warning');
return;
}
if (this.pillers.length > 5) {
Swal.fire('Validation Error', 'You can add a maximum of 5 Pillers only.', 'warning');
return;
}
// Field-level validation
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 requests = this.pillers.map(piller => {
const data = {
survey_id: this.survey_id,
titles: piller.titles,
weight: piller.weight,
position: piller.position
};
return fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(data)
}).then(res => res.json().then(json => ({ status: res.status, body: json })));
});
Promise.all(requests).then(results => {
let allSuccess = true;
results.forEach(({ status, body }, index) => {
if (status === 200 && body.piller_id) {
this.pillers[index].id = body.piller_id;
} else {
allSuccess = false;
}
});
if (allSuccess) {
Swal.fire({
icon: 'success',
title: "{{ __('Pillers saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 3;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Some Pillers failed to save') }}",
text: "{{ __('Please try again or contact admin.') }}"
});
}
}).catch(() => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
});
}
—
### ✅ Tip:
You can also **disable the submit button** when `pillers.length` is out of bounds:
<button class="btn btn-primary ml-2"
@click="submitPillers"
:disabled="pillers.length < 1 || pillers.length > 5">
{{ __('Save and Next') }}
</button>
Let me know if you also want to auto-focus on the first invalid field or scroll to it.
—
### ✅ Updated `submitPillers()` with **exactly 5 Pillers required**:
submitPillers() {
// Require exactly 5 pillers
if (this.pillers.length !== 5) {
Swal.fire('Validation Error', 'You must add exactly 5 Pillers to continue.', 'warning');
return;
}
// Field-level validation
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 requests = this.pillers.map(piller => {
const data = {
survey_id: this.survey_id,
titles: piller.titles,
weight: piller.weight,
position: piller.position
};
return fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
body: JSON.stringify(data)
}).then(res => res.json().then(json => ({ status: res.status, body: json })));
});
Promise.all(requests).then(results => {
let allSuccess = true;
results.forEach(({ status, body }, index) => {
if (status === 200 && body.piller_id) {
this.pillers[index].id = body.piller_id;
} else {
allSuccess = false;
}
});
if (allSuccess) {
Swal.fire({
icon: 'success',
title: "{{ __('Pillers saved successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.step = 3;
} else {
Swal.fire({
icon: 'error',
title: "{{ __('Some Pillers failed to save') }}",
text: "{{ __('Please try again or contact admin.') }}"
});
}
}).catch(() => {
Swal.fire({
icon: 'error',
title: "{{ __('Unexpected Error') }}",
text: "{{ __('Something went wrong while saving the Pillers.') }}"
});
});
}
—
### ✅ Optional: Disable Submit Until 5 Pillers
<button class="btn btn-primary ml-2"
@click="submitPillers"
:disabled="pillers.length !== 5">
{{ __('Save and Next') }}
</button>
—
Let me know if you want to enforce the same rule server-side for extra security, or visually mark how many Pillers are remaining.
—
### ✅ 1. **Vue `submitPillers()` – Single Payload for All Pillers**
Update your method like this:
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 = {
survey_id: this.survey_id,
pillers: this.pillers
};
fetch("{{ route('admin.pillars.storeAll') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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;
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.') }}"
});
});
}
—
### ✅ 2. **Laravel Route**
Route::post('/admin/pillars/store-all', [AdminPillerController::class, 'storeAll'])->name('admin.pillars.storeAll');
—
### ✅ 3. **Backend: `storeAll()` Method**
public function storeAll(Request $request)
{
$request->validate([
'survey_id' => 'required|exists:surveys,id',
'pillers' => 'required|array|size:5',
'pillers.*.titles' => 'required|array',
'pillers.*.titles.en' => 'required|string',
'pillers.*.titles.de' => 'required|string',
'pillers.*.weight' => 'required|numeric',
'pillers.*.position' => 'required|integer',
]);
$savedPillers = [];
foreach ($request->pillers as $pillerData) {
$piller = AppModelsPiller::create([
'survey_id' => $request->survey_id,
'weight' => $pillerData['weight'],
'position' => $pillerData['position'],
]);
foreach ($pillerData['titles'] as $lang => $title) {
$piller->translations()->create([
'language_code' => $lang,
'title' => $title,
]);
}
$savedPillers[] = [
'id' => $piller->id,
'titles' => $pillerData['titles'],
'weight' => $piller->weight,
'position' => $piller->position
];
}
return response()->json(['success' => true, 'pillers' => $savedPillers]);
}
—
### ✅ Summary
– ✅ Sends all 5 Pillers in one request
– ✅ Validates input fully in frontend and backend
– ✅ Responds with created Pillers (including IDs if needed for next step)
Let me know if your `translations()` relation uses a different structure or if you want to batch-insert for performance.
Piller
— Select Piller —
@{{ pill.title_en }}
Title (EN)
Title (DE)
Position
Submit
now we have return pillers from second step so
now display pillers dropdown with id and en title in 3rd step
and each pillar will have have min 1 and max 10 question
✅ Show the Piller dropdown using returned `pillers` with `id` and `titles.en`
✅ Allow adding questions dynamically
✅ Enforce **min 1** and **max 10 questions per Piller**
✅ Display questions **grouped by Piller**
—
### ✅ 1. **Update Step 3 Template**
<div v-if="step === 3">
<h4>{{ __('Add Questions') }}</h4>
<div v-for="piller in pillers" :key="piller.id" class="border p-3 mb-4">
<div class="d-flex justify-content-between align-items-center mb-2">
<h5>{{ __('Piller') }}: @{{ piller.titles.en }}</h5>
<button class="btn btn-sm btn-secondary"
@click="addQuestion(piller.id)"
:disabled="questions[piller.id]?.length >= 10">
+ {{ __('Add Question') }}
</button>
</div>
<div v-if="questions[piller.id]?.length === 0" class="text-muted">
{{ __('No questions added yet.') }}
</div>
<div v-for="(question, qIndex) in questions[piller.id]" :key="qIndex" class="form-row align-items-end mb-2">
<div class="col-md-4">
<label>{{ __('Title (EN)') }}</label>
<input type="text" v-model="question.title_en" class="form-control" />
</div>
<div class="col-md-4">
<label>{{ __('Title (DE)') }}</label>
<input type="text" v-model="question.title_de" class="form-control" />
</div>
<div class="col-md-2">
<label>{{ __('Position') }}</label>
<input type="number" v-model="question.position" class="form-control" />
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm mt-4"
@click="removeQuestion(piller.id, qIndex)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
<button class="btn btn-success mt-3" @click="submitQuestions">
{{ __('Submit All Questions') }}
</button>
</div>
—
### ✅ 2. **Vue Data Structure**
Update your `data()`:
data() {
return {
...
questions: {}, // { pillerId: [ { title_en, title_de, position } ] }
}
}
—
### ✅ 3. **Vue Methods**
addQuestion(pillerId) {
if (!this.questions[pillerId]) {
this.questions[pillerId] = [];
}
if (this.questions[pillerId].length >= 10) return;
this.questions[pillerId].push({
title_en: '',
title_de: '',
position: ''
});
},
removeQuestion(pillerId, index) {
this.questions[pillerId].splice(index, 1);
},
submitQuestions() {
// Validate: at least 1 question per piller
for (let piller of this.pillers) {
const list = this.questions[piller.id] || [];
if (list.length < 1) {
Swal.fire('Validation Error', `Piller "${piller.titles.en}" must have at least 1 question.`, 'warning');
return;
}
}
const payload = {
questions: []
};
for (let pillerId in this.questions) {
this.questions[pillerId].forEach(q => {
payload.questions.push({
piller_id: pillerId,
title_en: q.title_en,
title_de: q.title_de,
position: q.position
});
});
}
fetch("{{ route('admin.questions.storeAll') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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
});
// Optionally reset
} 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.') }}"
});
});
}
—
### ✅ 4. **Backend Route & Controller**
**Route:**
Route::post('/admin/questions/store-all', [AdminQuestionController::class, 'storeAll'])->name('admin.questions.storeAll');
**Controller:**
public function storeAll(Request $request)
{
$request->validate([
'questions' => 'required|array',
'questions.*.piller_id' => 'required|exists:pillers,id',
'questions.*.title_en' => 'required|string',
'questions.*.title_de' => 'required|string',
'questions.*.position' => 'required|integer',
]);
foreach ($request->questions as $question) {
AppModelsQuestion::create([
'piller_id' => $question['piller_id'],
'title_en' => $question['title_en'],
'title_de' => $question['title_de'],
'position' => $question['position'],
]);
}
return response()->json(['success' => true]);
}
—
Let me know if you want to:
– Pre-fill 1 empty question per Piller automatically
– Allow sorting questions within each Piller
– Add question-level error display per field
✅ Show a **dropdown** of all Pillers (with their `titles.en`)
✅ When a Piller is selected, display a form to **add/edit up to 10 questions**
✅ Allow dynamically adding/removing questions for that selected Piller
✅ Enforce min 1, max 10 questions per selected Piller
—
### ✅ 1. **Vue Data Update**
data() {
return {
...
selectedPillerId: '', // the currently selected piller for question input
questions: {}, // e.g. { '1': [ {...}, {...} ] }
};
}
—
### ✅ 2. **HTML Template for Step 3**
<div v-if="step === 3">
<h4>{{ __('Add Questions') }}</h4>
<!-- Piller Selection -->
<div class="form-group">
<label>{{ __('Select Piller') }}</label>
<select v-model="selectedPillerId" class="form-control">
<option disabled value="">{{ __('-- Select Piller --') }}</option>
<option v-for="pill in pillers" :key="pill.id" :value="pill.id">
@{{ pill.titles.en }}
</option>
</select>
</div>
<!-- Questions for Selected Piller -->
<div v-if="selectedPillerId">
<div v-if="!questions[selectedPillerId] || questions[selectedPillerId].length === 0" class="text-muted mb-3">
{{ __('No questions yet. Please add at least one.') }}
</div>
<div v-for="(question, qIndex) in questions[selectedPillerId]" :key="qIndex" class="form-row align-items-end mb-3">
<div class="col-md-4">
<label>{{ __('Title (EN)') }}</label>
<input v-model="question.title_en" class="form-control" placeholder="Title in English">
</div>
<div class="col-md-4">
<label>{{ __('Title (DE)') }}</label>
<input v-model="question.title_de" class="form-control" placeholder="Title in German">
</div>
<div class="col-md-2">
<label>{{ __('Position') }}</label>
<input v-model="question.position" type="number" class="form-control" placeholder="1">
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm mt-4" @click="removeQuestion(selectedPillerId, qIndex)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
<!-- Add More Button -->
<button class="btn btn-secondary"
@click="addQuestion(selectedPillerId)"
:disabled="questions[selectedPillerId]?.length >= 10">
+ {{ __('Add More Question') }}
</button>
</div>
<hr>
<!-- Submit All Questions -->
<button class="btn btn-success mt-3" @click="submitQuestions">
{{ __('Submit All Questions') }}
</button>
</div>
—
### ✅ 3. **Vue Methods**
addQuestion(pillerId) {
if (!this.questions[pillerId]) {
this.questions[pillerId] = [];
}
if (this.questions[pillerId].length >= 10) return;
this.questions[pillerId].push({
title_en: '',
title_de: '',
position: ''
});
},
removeQuestion(pillerId, index) {
this.questions[pillerId].splice(index, 1);
},
submitQuestions() {
const payload = {
questions: []
};
// Validate: at least 1 question per piller
for (let piller of this.pillers) {
const list = this.questions[piller.id] || [];
if (list.length < 1) {
Swal.fire('Validation Error', `Piller "${piller.titles.en}" must have at least 1 question.`, 'warning');
return;
}
list.forEach(q => {
if (!q.title_en || !q.title_de || !q.position) {
Swal.fire('Validation Error', `All fields are required for questions in "${piller.titles.en}".`, 'error');
return;
}
payload.questions.push({
piller_id: piller.id,
title_en: q.title_en,
title_de: q.title_de,
position: q.position
});
});
}
fetch("{{ route('admin.questions.storeAll') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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
});
} 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.') }}"
});
});
}
—
Let me know if you want:
– A default question auto-added when selecting a piller
– Backend validation to enforce 1–10 questions per piller
– Inline error markers on specific question fields
✅ **Select one Piller at a time** from the dropdown
✅ Add up to **10 questions** for that selected Piller
✅ Switch to another Piller and **add questions** for it too
✅ Submit **all questions** for all Pillers in one go
—
### ✅ How it works:
– You **select a Piller** → see input fields for that one only
– You can **add/edit questions** specific to the selected Piller
– Switch to another Piller from dropdown → input fields update
– All question data is stored in `questions[piller_id]` behind the scenes
—
### ✅ Quick Example Flow:
1. Select **Piller A** → add 3 questions
2. Select **Piller B** → add 2 questions
3. Select **Piller C** → nothing yet
4. Click **Submit All Questions** → backend gets structured payload with all questions grouped by Piller IDs.
—
### ✅ Optional Improvements:
If you want to **visually preview which Pillers have questions** added, you can enhance the dropdown:
<option
v-for="pill in pillers"
:key="pill.id"
:value="pill.id">
@{{ pill.titles.en }}
(@{{ questions[pill.id]?.length || 0 }} {{ __('question(s)') }})
</option>
This will show:
Piller A (3 question(s))
Piller B (2 question(s))
Piller C (0 question(s))
—
Let me know if you'd like to:
– Add a **"Preview All Questions"** button
– Show all Pillers + their questions at once (instead of dropdown)
– Add per-question validation indicators (like red border if empty)
✅ Each question will have titles for all languages (e.g. `en`, `de`, etc.)
✅ These fields will be generated **dynamically** based on the `availableLanguages` array
✅ You'll manage questions per Piller (`questions[pillerId] = […]`) with language-based titles
—
### ✅ 1. Update Vue Data Structure for Questions
Instead of:
{
title_en: '',
title_de: '',
position: ''
}
Use:
{
titles: { en: '', de: '' },
position: ''
}
—
### ✅ 2. Updated `addQuestion()` Method
addQuestion(pillerId) {
if (!this.questions[pillerId]) {
this.questions[pillerId] = [];
}
if (this.questions[pillerId].length >= 10) return;
const titles = {};
this.availableLanguages.forEach(lang => {
titles[lang.code] = '';
});
this.questions[pillerId].push({
titles,
position: ''
});
}
—
### ✅ 3. Updated HTML for Questions (Step 3)
<div v-for="(question, qIndex) in questions[selectedPillerId]" :key="qIndex" class="form-row align-items-end mb-3">
<template v-for="lang in availableLanguages" :key="lang.code">
<div class="col-md-4 mb-2">
<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>
</template>
<div class="col-md-2 mb-2">
<label>{{ __('Position') }}</label>
<input type="number" v-model="question.position" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-md-1 mb-2">
<button class="btn btn-danger btn-sm mt-4" @click="removeQuestion(selectedPillerId, qIndex)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
—
### ✅ 4. Update `submitQuestions()` Logic
Send all question titles per language to the backend:
submitQuestions() {
const payload = {
questions: []
};
for (let piller of this.pillers) {
const list = this.questions[piller.id] || [];
if (list.length < 1) {
Swal.fire('Validation Error', `Piller "${piller.titles.en}" must have at least 1 question.`, 'warning');
return;
}
list.forEach(q => {
for (let lang of this.availableLanguages) {
if (!q.titles[lang.code]) {
Swal.fire('Validation Error', `Title (${lang.title}) is required for all questions in "${piller.titles.en}"`, 'error');
return;
}
}
payload.questions.push({
piller_id: piller.id,
titles: q.titles,
position: q.position
});
});
}
fetch("{{ route('admin.questions.storeAll') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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
});
} 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.') }}"
});
});
}
—
### ✅ 5. Backend Validation & Save
In your controller:
$request->validate([
'questions' => 'required|array',
'questions.*.piller_id' => 'required|exists:pillers,id',
'questions.*.titles' => 'required|array',
'questions.*.titles.en' => 'required|string', // repeat as needed or use a custom loop
'questions.*.position' => 'required|integer',
]);
foreach ($request->questions as $question) {
$q = AppModelsQuestion::create([
'piller_id' => $question['piller_id'],
'position' => $question['position'],
]);
foreach ($question['titles'] as $lang => $title) {
$q->translations()->create([
'language_code' => $lang,
'title' => $title
]);
}
}
—
Let me know if:
– You store language titles in the same `questions` table or in a separate `question_translations` table
– You want to **auto-assign position values** or allow sorting via drag & drop
✅ You select a Piller from a dropdown
✅ Add multiple questions for that Piller (up to 10 max, at least 1 required)
✅ Each question includes a dynamic `titles` object (based on available languages)
✅ All questions for all Pillers are submitted together in one request
—
### ✅ Final Working Structure (Just like Pillers)
—
### 🔁 Step 3 Blade Template
Replace your Step 3 block with this:
<div v-if="step === 3">
<h4>{{ __('Add Questions') }}</h4>
<!-- Piller Selector -->
<div class="form-group">
<label>{{ __('Select Piller') }}</label>
<select v-model="selectedPillerId" class="form-control">
<option disabled value="">{{ __('-- Select Piller --') }}</option>
<option v-for="pill in pillers" :key="pill.id" :value="pill.id">
@{{ pill.titles.en }}
</option>
</select>
</div>
<!-- Question Entry for Selected Piller -->
<div v-if="selectedPillerId" class="border p-3 mb-3">
<h5>@{{ getPillerTitle(selectedPillerId) }}</h5>
<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="1">
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm mt-4" @click="removeQuestion(index)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
<button class="btn btn-secondary" @click="addQuestion" :disabled="questions[selectedPillerId].length >= 10">
{{ __('Add More Question') }}
</button>
</div>
<div class="text-right mt-4">
<button class="btn btn-success" @click="submitQuestions">
{{ __('Submit All Questions') }}
</button>
</div>
</div>
—
### ✅ Vue `data()`
data() {
return {
...
selectedPillerId: '',
questions: {}, // questions[piller_id] = [ { titles: {en:'',de:''}, position: '' }, ... ]
availableLanguages: window.availableLanguages || []
};
}
—
### ✅ Vue `methods`
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);
},
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({
piller_id: piller.id,
titles: q.titles,
position: q.position
});
}
}
fetch("{{ route('admin.questions.storeAll') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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
});
} 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.') }}"
});
});
}
—
### ✅ Backend Controller Example
public function storeAll(Request $request)
{
$request->validate([
'questions' => 'required|array',
'questions.*.piller_id' => 'required|exists:pillers,id',
'questions.*.titles' => 'required|array',
'questions.*.titles.en' => 'required|string',
'questions.*.titles.de' => 'required|string',
'questions.*.position' => 'required|integer',
]);
foreach ($request->questions as $q) {
$question = AppModelsQuestion::create([
'piller_id' => $q['piller_id'],
'position' => $q['position']
]);
foreach ($q['titles'] as $lang => $title) {
$question->translations()->create([
'language_code' => $lang,
'title' => $title,
]);
}
}
return response()->json(['success' => true]);
}
—
This setup now mirrors the **Pillers step**:
– Dynamic language-based fields
– Grouped by selected item
– Clean, extendable structure
Let me know if you'd like to:
– Add question preview per piller
– Show number of questions beside each piller in dropdown
– Enable drag & drop sorting of questions
—
### ✅ 1. **Open DevTools → Console Tab**
In Chrome or Firefox, press `F12` or right-click → Inspect → go to the **Console** tab.
Look for errors like:
– `Cannot read properties of undefined (reading 'length')`
– `Cannot read property 'titles' of undefined`
– `this.questions[selectedPillerId] is undefined`
—
### ✅ 2. **Common Fixes**
#### 🔁 **Initialize questions on selection**
Make sure this code runs when selecting a piller:
watch: {
selectedPillerId(newVal) {
if (newVal && !this.questions[newVal]) {
// initialize empty question list with 1 question
const titles = {};
this.availableLanguages.forEach(lang => {
titles[lang.code] = '';
});
this.questions[newVal] = [{
titles,
position: ''
}];
}
}
},
If you're not using `watch`, you can also do this inside your `addQuestion()`:
addQuestion() {
if (!this.selectedPillerId) return;
if (!this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
if (this.questions[this.selectedPillerId].length >= 10) return;
const titles = {};
this.availableLanguages.forEach(lang => {
titles[lang.code] = '';
});
this.questions[this.selectedPillerId].push({
titles,
position: ''
});
}
—
### ✅ 3. **Safe Template Access (avoid undefined)**
In Blade template:
<div v-if="selectedPillerId && questions[selectedPillerId]">
<div v-for="(question, index) in questions[selectedPillerId]" :key="index" class="form-row 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" />
</div>
<div class="col-md-2">
<label>{{ __('Position') }}</label>
<input type="number" v-model="question.position" class="form-control" />
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm mt-4" @click="removeQuestion(index)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
—
### ✅ 4. **Confirm piller `titles.en` exists**
If your piller structure is like:
{
id: 3,
titles: { en: 'Performance', de: 'Leistung' }
}
Make sure:
getPillerTitle(id) {
const piller = this.pillers.find(p => p.id == id);
return piller?.titles?.en || '';
}
—
### ✅ Final Tip
If all else fails, add this debug line in your mounted lifecycle:
mounted() {
console.log('Pillers:', this.pillers);
console.log('Languages:', this.availableLanguages);
}
This confirms the data Vue is working with.
—
Let me know the exact error from console if you're still stuck — I’ll give a precise fix based on that.
when select piller in 3rd step then it shows balnk and above error
@extends('layouts.admin')
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)
{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))
{{– Content body: main page content –}}
@section('content_body')
{{ __('Create Surveys') }}
@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') }}
{{ __('Add 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.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: '',
options: []
},
surveyError: '',
survey_id: null,
availableLanguages: window.availableLanguages || [],
pillers: [
{
titles: {}, // dynamically assigned
weight: '',
position: ''
}
],
selectedPillerId: '',
questions: {},
}
},
mounted() {
// Init first piller titles
this.availableLanguages.forEach(lang => {
this.pillers[0].titles[lang.code] = '';
});
},
methods: {
onPillerChange() {
if (this.selectedPillerId && !this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
},
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;
},
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.';
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.store') }}", {
method: "POST",
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 = {
survey_id: this.survey_id,
pillers: this.pillers
};
this.loading = true;
try {
fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
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;
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({
piller_id: piller.id,
titles: q.titles,
position: q.position
});
}
}
fetch("{{ route('admin.questions.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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.') }}"
});
});
}
}
});
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
this
– **$survey** – the survey model (with properties like name and options)
– **$pillars** – a collection of pillar records with their translations (each pillar has an `id` and a `titles` array containing language keys such as “en” and “de”)
– **$questions** – the questions data grouped by pillar ID (an associative array where keys are pillar IDs and values are arrays of question objects; each question contains a `titles` array and a `position`)
– **$languages** – the available languages (each record with at least `code` and `title`)
Adjust your controller accordingly before using this Blade template.
—
### Blade Template: `resources/views/admin/surveys/edit.blade.php`
@extends('layouts.admin')
@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')
<!-- Content Header (Page header) -->
<section class="content-header">
<div class="row mb-2">
<div class="col-sm-12">
<h1>{{ __('Edit Survey') }}</h1>
</div>
</div>
</section>
<!-- Main content -->
<section class="">
@if($errors->any())
<div class="alert alert-danger">
<ul class="list-unstyled">
@foreach ($errors->all() as $error)
<li>{{ $error }}</li>
@endforeach
</ul>
</div>
@endif
@if(session()->has('message.level'))
<div class="alert alert-{{ session('message.level') }}">
{!! session('message.content') !!}
</div>
@endif
<!-- Loading Overlay -->
<div id="vue-loading"
style="position: fixed; top: 0; left: 0; width: 100vw; height: 100vh; background: white; z-index: 9999;"
class="d-flex justify-content-center align-items-center">
<div class="text-center">
<div class="spinner-border text-primary" role="status" style="width: 3rem; height: 3rem;">
<span class="sr-only">Loading...</span>
</div>
<div class="mt-2">{{ __('Loading survey data...') }}</div>
</div>
</div>
<div class="row">
<div class="col-md-12">
<div class="card card-primary">
<div id="surveyApp" class="container-fluid mt-4">
<!-- Vue will display any message here -->
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ active: step === 1 }" href="#" @click.prevent="goToStep(1)">1. Survey</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ active: step === 2, disabled: !survey_id }" href="#" @click.prevent="goToStep(2)">2. Pillars</a>
</li>
<li class="nav-item">
<a class="nav-link" :class="{ active: step === 3, disabled: !pillars.some(p => p.id) }" href="#" @click.prevent="goToStep(3)">3. Questions</a>
</li>
</ul>
<div class="tab-content p-4 border border-top-0">
<!-- Step 1: Edit Survey -->
<div v-if="step === 1">
<h4>{{ __('Edit Survey') }}</h4>
<div class="form-group">
<label>{{ __('Name') }}</label>
<input v-model="survey.name" :class="{ 'is-invalid': surveyError }" class="form-control" placeholder="Survey Name">
<div class="invalid-feedback" v-if="surveyError">
@{{ surveyError }}
</div>
</div>
<label>
<input type="checkbox" v-model="survey.options" value="allow_comments">
{{ __('Allow comments on Survey') }}
</label><br>
<label>
<input type="checkbox" v-model="survey.options" value="allow_comments_on_pillars">
{{ __('Allow comments on Pillars') }}
</label><br>
<label>
<input type="checkbox" v-model="survey.options" value="mark_as_external">
{{ __('Mark as External') }}
</label><br>
<button class="btn btn-primary mt-3" @click="updateSurvey">
<span v-if="loading" class="spinner-border spinner-border-sm"></span>
{{ __('Update Survey and Next') }}
</button>
</div>
<!-- Step 2: Edit Pillars -->
<div v-if="step === 2">
<h4>{{ __('Edit Pillars') }}</h4>
<div v-for="(pillar, index) in pillars" :key="index" class="p-3 mb-3">
<div class="form-row align-items-end">
<div class="col-12">
<h5>{{ __('Pillar') }} @{{ index + 1 }}</h5>
</div>
<div v-for="lang in availableLanguages" :key="lang.code" class="col-md-4 mb-3">
<label>{{ __('Title') }} (@{{ lang.code.toUpperCase() }})</label>
<input type="text" v-model="pillar.titles[lang.code]" class="form-control" :placeholder="`Enter title in ${lang.code}`">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Weight') }}</label>
<input type="number" v-model="pillar.weight" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Position') }}</label>
<input type="number" v-model="pillar.position" class="form-control" placeholder="e.g. 1">
</div>
<div class="col-md-1 mb-3 d-flex align-items-end" v-if="pillars.length > 1">
<button class="btn btn-danger btn-sm" @click="removePillar(index)" title="Remove Pillar">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
<!-- Button Row for Pillars -->
<div class="form-row mt-3">
<div class="col-auto">
<button class="btn btn-primary" @click="addPillar" :disabled="pillars.length >= 5">
{{ __('Add More Pillars') }}
</button>
</div>
<div class="col-auto">
<button class="btn btn-primary" @click="updatePillars">
<span v-if="loading" class="spinner-border spinner-border-sm"></span>
{{ __('Update Pillars and Next') }}
</button>
</div>
</div>
</div>
<!-- Step 3: Edit Questions -->
<div v-if="step === 3">
<h4>{{ __('Edit Questions') }}</h4>
<!-- Piller Selector -->
<div class="form-group">
<label>{{ __('Select Pillar') }}</label>
<select v-model="selectedPillarId" class="form-control" @change="onPillarChange">
<option disabled value="">{{ __('-- Select Pillar --') }}</option>
<option v-for="pill in pillars" :key="pill.id" :value="pill.id">
@{{ pill.titles.en }}
</option>
</select>
</div>
<!-- Question Entry for Selected Pillar -->
<div v-if="selectedPillarId && questions[selectedPillarId]" class="border p-3 mb-3">
<h5>@{{ getPillarTitle(selectedPillarId) }}</h5>
<div v-for="(question, index) in questions[selectedPillarId]" :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="1">
</div>
<div class="col-md-1">
<button class="btn btn-danger btn-sm mt-4" @click="removeQuestion(index)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
<button class="btn btn-primary" @click="addQuestion" :disabled="questions[selectedPillarId].length >= 10">
{{ __('Add More Question') }}
</button>
</div>
<div class="text-right mt-4">
<button class="btn btn-success" @click="updateQuestions">
{{ __('Submit All Questions') }}
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
@stop
{{-- Push extra scripts --}}
@push('js')
<script>
// Pass backend data to Vue
window.surveyData = @json($survey);
window.pillarsData = @json($pillars);
window.questionsData = @json($questions); // Assumed to be grouped by pillar id
window.availableLanguages = @json($languages);
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
</script>
<script type="module">
import { createApp } from 'https://unpkg.com/vue@3.5.13/dist/vue.esm-browser.prod.js'
document.addEventListener('DOMContentLoaded', () => {
const app = createApp({
data() {
return {
loading: false,
step: 1,
survey: window.surveyData || { name: '', options: [] },
surveyError: '',
survey_id: window.surveyData ? window.surveyData.id : null,
availableLanguages: window.availableLanguages || [],
// Pillars (using "pillars" naming for edit)
pillars: window.pillarsData || [
{
titles: {},
weight: '',
position: ''
}
],
// Selected pillar for editing questions
selectedPillarId: '',
// Questions grouped by pillar id
questions: window.questionsData || {}
}
},
mounted() {
// Initialize titles for first pillar if not set
if(this.pillars.length > 0) {
this.availableLanguages.forEach(lang => {
if(!this.pillars[0].titles[lang.code]) {
this.pillars[0].titles[lang.code] = '';
}
});
}
},
methods: {
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillars.every(p => !p.id)) return;
this.step = step;
},
getPillarTitle(id) {
const pillar = this.pillars.find(p => p.id == id);
return pillar ? pillar.titles.en : '';
},
onPillarChange() {
if (this.selectedPillarId && !this.questions[this.selectedPillarId]) {
// Initialize with one default question for the selected pillar
const titles = {};
this.availableLanguages.forEach(lang => {
titles[lang.code] = '';
});
this.questions[this.selectedPillarId] = [{
titles,
position: ''
}];
}
},
addQuestion() {
if (!this.selectedPillarId) return;
if (!this.questions[this.selectedPillarId]) {
this.questions[this.selectedPillarId] = [];
}
if (this.questions[this.selectedPillarId].length >= 10) return;
const newQuestion = {
titles: {},
position: ''
};
this.availableLanguages.forEach(lang => {
newQuestion.titles[lang.code] = '';
});
this.questions[this.selectedPillarId].push(newQuestion);
},
removeQuestion(index) {
this.questions[this.selectedPillarId].splice(index, 1);
},
addPillar() {
if (this.pillars.length >= 5) return;
const newPillar = {
titles: {},
weight: '',
position: ''
};
this.availableLanguages.forEach(lang => {
newPillar.titles[lang.code] = '';
});
this.pillars.push(newPillar);
},
removePillar(index) {
this.pillars.splice(index, 1);
},
async updateSurvey() {
this.surveyError = '';
if (!this.survey.name) {
this.surveyError = 'Survey name is required.';
return;
}
const data = {
name: this.survey.name,
options: this.survey.options,
};
this.loading = true;
try {
fetch("{{ route('admin.surveys.update', ['survey' => window.surveyData.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) {
Swal.fire({
icon: 'success',
title: "{{ __('Survey updated successfully!') }}",
showConfirmButton: false,
timer: 1500
});
this.survey_id = body.survey.id;
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 updating the survey.') }}"
});
});
} finally {
this.loading = false;
}
},
updatePillars() {
// Validate pillars first
if (this.pillars.length !== 5) {
Swal.fire('Validation Error', 'You must have exactly 5 Pillars.', 'warning');
return;
}
for (let [i, pillar] of this.pillars.entries()) {
for (let lang of this.availableLanguages) {
if (!pillar.titles[lang.code]) {
Swal.fire('Validation Error', `Pillar ${i + 1}: Title required for ${lang.title}`, 'error');
return;
}
}
if (!pillar.weight || !pillar.position) {
Swal.fire('Validation Error', `Pillar ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
const data = {
survey_id: this.survey_id,
pillars: this.pillars
};
this.loading = true;
try {
fetch("{{ route('admin.pillars.updateAll', ['survey' => window.surveyData.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.pillars = body.pillars;
Swal.fire({
icon: 'success',
title: "{{ __('Pillars updated 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 updating the pillars.') }}"
});
});
} finally {
this.loading = false;
}
},
updateQuestions() {
const payload = { questions: [] };
for (let pillar of this.pillars) {
const list = this.questions[pillar.id] || [];
if (list.length < 1) {
Swal.fire('Validation Error', `Please add at least 1 question for "${pillar.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 "${pillar.titles.en}"`, 'error');
return;
}
}
if (!q.position) {
Swal.fire('Validation Error', `Position is required for a question in "${pillar.titles.en}"`, 'error');
return;
}
payload.questions.push({
piller_id: pillar.id,
titles: q.titles,
position: q.position
});
}
}
fetch("{{ route('admin.questions.updateAll', ['survey' => window.surveyData.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 updated 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 updating the questions.') }}"
});
});
}
}
});
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);
}
});
</script>
@endpush
—
### Explanation
1. **Preloading Data:**
– The backend passes existing survey data, pillars, questions, and available languages to the view.
– These are assigned to global JavaScript variables (`window.surveyData`, etc.) so that the Vue instance can initialize with pre-populated values.
2. **Vue Instance Data:**
– The `survey`, `pillars`, and `questions` objects are initialized from the global variables.
– The selected pillar for editing questions is stored in `selectedPillarId`.
3. **Step Functions:**
– **Step 1:** Allows editing the survey’s name and options.
– **Step 2:** Displays existing pillars (or a default pillar if none exists) and lets you add, remove, and update them.
– **Step 3:** Lets you select a pillar from a dropdown, add or remove questions for that pillar, and update all questions.
4. **Submission:**
– Each update method uses `fetch` with the appropriate HTTP method (`PUT` in this case) and displays feedback with SweetAlert.
– After successful updates, you are redirected (for example, back to the survey index after updating questions).
5. **Loading Overlay:**
– A loading overlay is displayed until Vue is mounted and then removed.
—
Adjust the routes and controller method names (`admin.surveys.update`, `admin.pillars.updateAll`, and `admin.questions.updateAll`) to match your backend implementation. This complete edit page should allow you to update your survey and its related data in three steps using Vue and Bootstrap 4.
Let me know if you need further customizations or encounter any issues!
—
### ✅ What the Edit Page Should Do:
1. **Load existing Survey**, Pillers, and Questions data from the backend
2. Populate Vue data (`survey`, `pillers`, `questions`) on mount
3. Support editing all dynamic language-based fields
4. Submit updates to respective update routes
—
### ✅ Step-by-Step Plan:
#### 1. **Controller for Edit**
In your controller:
public function edit($id)
{
$survey = Survey::with(['pillars.translations', 'questions.translations'])->findOrFail($id);
$languages = Language::select('code', 'title')->get();
// transform for frontend
$pillars = $survey->pillars->map(function ($pillar) {
return [
'id' => $pillar->id,
'titles' => $pillar->translations->pluck('title', 'language_code'),
'weight' => $pillar->weight,
'position' => $pillar->position
];
});
$questions = [];
foreach ($survey->questions as $q) {
$questions[$q->pillar_id][] = [
'titles' => $q->translations->pluck('title', 'language_code'),
'position' => $q->position
];
}
return view('admin.surveys.edit', [
'survey' => $survey,
'languages' => $languages,
'pillars' => $pillars,
'questions' => $questions,
]);
}
—
#### 2. **Blade View Changes**
Pass the backend data into your Vue app:
<script>
window.editSurveyData = {
survey: @json($survey),
languages: @json($languages),
pillers: @json($pillars),
questions: @json($questions)
};
</script>
—
#### 3. **Update Vue Initialization (in `data()` and `mounted()`)**
In `data()`:
data() {
return {
step: 1,
loading: false,
survey_id: window.editSurveyData.survey.id,
survey: {
name: window.editSurveyData.survey.name,
options: []
},
availableLanguages: window.editSurveyData.languages,
pillers: window.editSurveyData.pillers,
questions: window.editSurveyData.questions,
selectedPillerId: '',
surveyError: ''
}
}
In `mounted()`:
mounted() {
// Convert boolean options to array
const opts = [];
if (window.editSurveyData.survey.allow_comments) opts.push('allow_comments');
if (window.editSurveyData.survey.allow_comments_on_pillers) opts.push('allow_comments_on_pillers');
if (window.editSurveyData.survey.mark_as_external) opts.push('mark_as_external');
this.survey.options = opts;
}
—
#### 4. **Modify Submit Methods**
Use `PUT` or `PATCH` methods and include the survey ID for updating:
fetch(`/admin/surveys/${this.survey_id}`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": csrfToken
},
body: JSON.stringify(updatedData)
})
Repeat similar adjustments for `pillars/update` and `questions/update`.
—
Let me know if you want:
– A full working `edit.blade.php` boilerplate
– Separate update routes/controllers for Survey, Pillars, and Questions
– The ability to **add/remove** Pillers/Questions on the Edit page just like Create
Ready to send the full editable version?
pillars has many questions
—
### ✅ Laravel Eloquent Relationships
#### 📋 1. **Survey Model**
public function pillars()
{
return $this->hasMany(Pillar::class);
}
#### 📋 2. **Pillar Model**
public function survey()
{
return $this->belongsTo(Survey::class);
}
public function questions()
{
return $this->hasMany(Question::class);
}
#### 📋 3. **Question Model**
public function pillar()
{
return $this->belongsTo(Pillar::class);
}
—
### ✅ Eager Loading in Controller (Edit Page)
When you load the survey for editing, make sure to eager-load everything:
$survey = Survey::with(['pillars.questions'])->findOrFail($id);
// Structure for Vue frontend
$pillars = $survey->pillars->map(function ($pillar) {
return [
'id' => $pillar->id,
'titles' => $pillar->translations->pluck('title', 'language_code'),
'weight' => $pillar->weight,
'position' => $pillar->position,
];
});
$questions = [];
foreach ($survey->pillars as $pillar) {
$questions[$pillar->id] = $pillar->questions->map(function ($question) {
return [
'id' => $question->id,
'titles' => $question->translations->pluck('title', 'language_code'),
'position' => $question->position,
];
});
}
$languages = Language::select('code', 'title')->get();
return view('admin.surveys.edit', compact('survey', 'pillars', 'questions', 'languages'));
> Adjust `translations` logic based on your actual schema. If you're not using a translations table, simplify accordingly.
—
### ✅ Your Relationships Summary
| Model | Relationship | Description |
|———|—————————-|——————————————|
| Survey | `hasMany(Pillar::class)` | One survey has many pillars |
| Pillar | `belongsTo(Survey::class)“hasMany(Question::class)` | Each pillar belongs to a survey and has many questions |
| Question| `belongsTo(Pillar::class)` | Each question belongs to a pillar |
—
Let me know if you:
– Need help with syncing/attaching data while saving from Vue
– Want to validate nested relations (`questions.*.titles.*`, etc.)
– Use translations in a separate table (`question_translations`, etc.) and want examples for those too
@section('plugins.Summernote', true)
@section('plugins.icheckBootstrap', true)
@section('plugins.TempusDominusBs4', true)
{{– Customize layout sections –}}
@section('subtitle', __('Create – Survey'))
{{– Content body: main page content –}}
@section('content_body')
{{ __('Create Surveys') }}
@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') }}
{{ __('Add 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.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: '',
options: []
},
surveyError: '',
survey_id: null,
availableLanguages: window.availableLanguages || [],
pillers: [
{
titles: {}, // dynamically assigned
weight: '',
position: ''
}
],
selectedPillerId: '',
questions: {},
}
},
mounted() {
// Init first piller titles
this.availableLanguages.forEach(lang => {
this.pillers[0].titles[lang.code] = '';
});
},
methods: {
onPillerChange() {
if (this.selectedPillerId && !this.questions[this.selectedPillerId]) {
this.questions[this.selectedPillerId] = [];
}
},
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;
},
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.';
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.store') }}", {
method: "POST",
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 = {
survey_id: this.survey_id,
pillers: this.pillers
};
this.loading = true;
try {
fetch("{{ route('admin.pillars.store') }}", {
method: "POST",
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;
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({
piller_id: piller.id,
titles: q.titles,
position: q.position
});
}
}
fetch("{{ route('admin.questions.store') }}", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-CSRF-TOKEN": "{{ csrf_token() }}"
},
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.') }}"
});
});
}
}
});
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
now adjust above as per edit code
1. **Preload existing survey, pillar, and question data from the controller**
2. **Initialize that data into your Vue `data()` block**
3. Adjust submission routes to use `PUT` methods (`update` routes)
—
### ✅ CONTROLLER: Example Data for Edit Page
Ensure your controller sends:
$survey = Survey::with('pillars.questions')->findOrFail($id);
$pillars = $survey->pillars->map(function ($pillar) {
return [
'id' => $pillar->id,
'titles' => $pillar->translations->pluck('title', 'language_code'),
'weight' => $pillar->weight,
'position' => $pillar->position
];
});
$questions = [];
foreach ($survey->pillars as $pillar) {
$questions[$pillar->id] = $pillar->questions->map(function ($question) {
return [
'id' => $question->id,
'titles' => $question->translations->pluck('title', 'language_code'),
'position' => $question->position
];
})->toArray();
}
$languages = Language::select('code', 'title')->get();
return view('admin.surveys.edit', compact('survey', 'pillars', 'questions', 'languages'));
—
### ✅ IN YOUR BLADE TEMPLATE
Add at the top of your Blade:
<script>
window.surveyData = @json($survey);
window.pillarsData = @json($pillars);
window.questionsData = @json($questions);
window.availableLanguages = @json($languages);
const csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute('content');
</script>
—
### ✅ MODIFY YOUR `Vue` INIT DATA
Update `data()`:
data() {
return {
message: '',
loading: false,
step: 1,
survey: {
name: window.surveyData?.name || '',
options: window.surveyData?.options || []
},
survey_id: window.surveyData?.id || null,
surveyError: '',
availableLanguages: window.availableLanguages || [],
pillers: window.pillarsData || [],
questions: window.questionsData || {},
selectedPillerId: '',
}
},
—
### ✅ UPDATE ROUTES IN YOUR FETCH CALLS
#### In `submitSurvey()`:
fetch(`{{ route('admin.surveys.update', ['survey' => ':id']) }}`.replace(':id', this.survey_id), {
method: 'PUT',
...
})
#### In `submitPillers()`:
fetch(`{{ route('admin.pillars.updateAll', ['survey' => ':id']) }}`.replace(':id', this.survey_id), {
method: 'PUT',
...
})
#### In `submitQuestions()`:
fetch(`{{ route('admin.questions.updateAll', ['survey' => ':id']) }}`.replace(':id', this.survey_id), {
method: 'PUT',
...
})
—
### ✅ TWEAK `mounted()` FOR TITLES INIT (only if empty)
mounted() {
if (this.pillers.length > 0) {
this.pillers.forEach(piller => {
this.availableLanguages.forEach(lang => {
if (!piller.titles[lang.code]) {
piller.titles[lang.code] = '';
}
});
});
}
},
—
### ✅ FINAL REMARKS
– You don’t need to change the Blade HTML markup — just the backend and Vue logic to populate existing data.
– Be sure the `id` is preserved in `pillars` and `questions` so updates can target them.
Would you like me to generate the exact modified `submitSurvey`, `submitPillers`, and `submitQuestions` for edit mode now?
it has
allow_comments_pillars
allow_comments_surveys
external
Since your `survey` model stores individual boolean columns instead of an `options` array, you'll want to **map those booleans into the Vue `survey.options` array** on load — and convert them back when submitting.
—
### ✅ 1. **Initialize Vue `survey.options` on Edit Load**
In your `data()` section, use:
survey: {
name: window.surveyData?.name || '',
options: []
},
Then in `mounted()`, map the booleans to `options`:
mounted() {
// Initialize titles for pillars
this.pillers.forEach(piller => {
this.availableLanguages.forEach(lang => {
if (!piller.titles[lang.code]) {
piller.titles[lang.code] = '';
}
});
});
// Initialize options from booleans
if (window.surveyData?.allow_comments_surveys) {
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');
}
}
—
### ✅ 2. **Update `submitSurvey()` for PUT Request**
Replace your POST call with a `PUT` call and map `options` back to boolean fields:
const data = {
name: this.survey.name,
allow_comments_surveys: this.survey.options.includes('allow_comments') ? 1 : 0,
allow_comments_pillars: this.survey.options.includes('allow_comments_on_pillers') ? 1 : 0,
external: this.survey.options.includes('mark_as_external') ? 1 : 0,
};
And the fetch call:
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)
})
—
This will keep your frontend UI unchanged (checkbox-based), while still syncing with your existing database schema (`allow_comments_surveys`, `allow_comments_pillars`, `external`).
Let me know if you want me to also refactor the Pillers/Questions update logic accordingly!
—
### ✅ Update your `goToStep()` method:
goToStep(step) {
if (step === 2 && !this.survey_id) return;
if (step === 3 && this.pillers.every(p => !p.id)) return;
this.step = step;
// Auto-select first pillar when going to Step 3
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] = [];
}
}
}
—
### 🔁 Optional: Also trigger `onPillerChange()` if needed:
If your `onPillerChange()` does something extra (like init structure), you can call it:
this.onPillerChange();
Add it right after setting `selectedPillerId`.
—
✅ This ensures that when the user reaches Step 3, the first pillar is already selected and ready to have questions added immediately.
Let me know if you'd like to also **auto-inject a blank question row** for that first pillar on entry.
'name' => 'required|string|max:255|unique:surveys,name',
]);
no compatre name unique with current id
—
### ✅ Updated Validation Rule (with current ID)
In your controller’s `update` method:
use IlluminateValidationRule;
$request->validate([
'name' => [
'required',
'string',
'max:255',
Rule::unique('surveys', 'name')->ignore($survey->id),
],
]);
– `surveys` = table name
– `'name'` = column name
– `$survey->id` = current record ID to ignore
> ✅ This ensures the name is unique **among other surveys**, but allows it to remain unchanged when editing.
—
Let me know if you're also validating nested relations like Pillers or Questions!
—
### ✅ Route
Route::put('/admin/surveys/{survey}/pillars/update-all', [AdminPillarController::class, 'updateAll'])
->name('admin.pillars.updateAll');
—
### ✅ Controller Method
use IlluminateHttpRequest;
use AppModelsPillar;
use AppModelsPillarTranslation;
public function updateAll(Request $request, $surveyId)
{
$request->validate([
'pillars' => 'required|array|size:5',
'pillars.*.id' => 'nullable|exists:pillars,id',
'pillars.*.titles' => 'required|array',
'pillars.*.weight' => 'required|numeric',
'pillars.*.position' => 'required|integer',
]);
$updatedPillars = [];
foreach ($request->pillars as $pillarData) {
if (!empty($pillarData['id'])) {
// Update existing pillar
$pillar = Pillar::where('survey_id', $surveyId)->findOrFail($pillarData['id']);
$pillar->update([
'weight' => $pillarData['weight'],
'position' => $pillarData['position'],
]);
} else {
// Create new pillar
$pillar = Pillar::create([
'survey_id' => $surveyId,
'weight' => $pillarData['weight'],
'position' => $pillarData['position'],
]);
}
// Handle translations
foreach ($pillarData['titles'] as $langCode => $title) {
PillarTranslation::updateOrCreate(
['pillar_id' => $pillar->id, 'language_code' => $langCode],
['title' => $title]
);
}
// Return structured data for frontend
$updatedPillars[] = [
'id' => $pillar->id,
'titles' => $pillarData['titles'],
'weight' => $pillar->weight,
'position' => $pillar->position,
];
}
return response()->json([
'success' => true,
'pillars' => $updatedPillars,
]);
}
—
### ✅ Notes
– Assumes you have a `pillars` table and a `pillar_translations` table.
– Validates exactly **5 pillars**.
– Handles both **new** and **existing** pillars.
– Returns the updated list (with IDs) to Vue so it can map future steps (e.g., questions).
—
Let me know if:
– You’re not using a separate `pillar_translations` table
– You need the Eloquent models structure for Pillar and PillarTranslation
– You want to soft-delete removed pillars during update
1. Accept a **batch of pillar data** from the frontend
2. Iterate and update/create them in the `update()` method
3. Use the `survey_id` as the route model or parameter to link pillars
—
### ✅ Route (still using `update`, but structured for bulk)
Route::put('/admin/surveys/{survey}/pillars', [AdminPillarController::class, 'update'])
->name('admin.pillars.update');
> This avoids creating a separate method like `updateAll`.
—
### ✅ Controller: `update()` method in `AdminPillarController`
public function update(Request $request, $surveyId)
{
$request->validate([
'pillars' => 'required|array|size:5',
'pillars.*.id' => 'nullable|exists:pillars,id',
'pillars.*.titles' => 'required|array',
'pillars.*.weight' => 'required|numeric',
'pillars.*.position' => 'required|integer',
]);
$updated = [];
foreach ($request->pillars as $pillarData) {
if (!empty($pillarData['id'])) {
// Update existing
$pillar = AppModelsPillar::where('survey_id', $surveyId)->findOrFail($pillarData['id']);
$pillar->update([
'weight' => $pillarData['weight'],
'position' => $pillarData['position'],
]);
} else {
// Create new
$pillar = AppModelsPillar::create([
'survey_id' => $surveyId,
'weight' => $pillarData['weight'],
'position' => $pillarData['position'],
]);
}
// Translations (if applicable)
foreach ($pillarData['titles'] as $lang => $title) {
AppModelsPillarTranslation::updateOrCreate(
['pillar_id' => $pillar->id, 'language_code' => $lang],
['title' => $title]
);
}
$updated[] = [
'id' => $pillar->id,
'titles' => $pillarData['titles'],
'weight' => $pillar->weight,
'position' => $pillar->position,
];
}
return response()->json([
'success' => true,
'pillars' => $updated,
]);
}
—
### ✅ Update Vue fetch call
In your `submitPillers()` method (Vue):
fetch(`{{ route('admin.pillars.update', ['survey' => ':id']) }}`.replace(':id', this.survey_id), {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-CSRF-TOKEN': csrfToken
},
body: JSON.stringify({
pillars: this.pillers
})
})
—
### ✅ Summary
| Feature | Value |
|——————–|——————————-|
| Single controller | `AdminPillarController@update` |
| Route name | `admin.pillars.update` |
| Bulk support | ✅ (handles multiple pillars) |
| Translation ready | ✅ (via `pillar_translations`) |
—
Let me know if you're not using a separate `pillar_translations` table — I can give a version with translations stored in the main table (`title_en`, `title_de`, etc.).
—
### ✅ Updated Step 2 Blade Template
Replace your current **Step 2** block with the following:
<!-- Step 2 -->
<div v-if="step === 2">
<h4>{{ __('Edit Pillars') }}</h4>
<div v-for="(piller, index) in pillers" :key="index" class="p-3 mb-3 border rounded">
<div class="form-row align-items-end">
<div class="col-12 mb-2">
<h5>{{ __('Pillar') }} @{{ index + 1 }}</h5>
</div>
<div v-for="lang in availableLanguages" :key="lang.code" class="col-md-4 mb-3">
<label>{{ __('Title') }} (@{{ lang.code.toUpperCase() }})</label>
<input type="text"
v-model="piller.titles[lang.code]"
class="form-control"
:placeholder="`Enter title in ${lang.code}`">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Weight') }}</label>
<input type="number"
v-model="piller.weight"
class="form-control"
placeholder="e.g. 1">
</div>
<div class="col-md-2 mb-3">
<label>{{ __('Position') }}</label>
<input type="number"
v-model="piller.position"
class="form-control"
placeholder="e.g. 1">
</div>
<div class="col-md-1 mb-3 d-flex align-items-end" v-if="pillers.length > 1">
<button class="btn btn-danger btn-sm" @click="removePiller(index)">
<i class="fa fa-trash"></i>
</button>
</div>
</div>
</div>
<div class="form-row mt-3">
<div class="col-auto">
<button class="btn btn-primary" @click="addPiller" :disabled="pillers.length >= 5">
{{ __('Add More Pillars') }}
</button>
</div>
<div class="col-auto">
<button class="btn btn-success" @click="submitPillers">
<span v-if="loading" class="spinner-border spinner-border-sm"></span>
{{ __('Save and Next') }}
</button>
</div>
</div>
</div>
—
### ✅ Updated `submitPillers()` Method (Vue)
submitPillers() {
if (this.pillers.length !== 5) {
Swal.fire('Validation Error', 'You must add exactly 5 Pillars to continue.', 'warning');
return;
}
// Validate each pillar
for (let [i, piller] of this.pillers.entries()) {
for (let lang of this.availableLanguages) {
if (!piller.titles[lang.code]) {
Swal.fire('Validation Error', `Pillar ${i + 1}: Title required for ${lang.title}`, 'error');
return;
}
}
if (!piller.weight || !piller.position) {
Swal.fire('Validation Error', `Pillar ${i + 1}: Weight and Position are required`, 'error');
return;
}
}
const data = {
pillars: this.pillers
};
this.loading = true;
fetch(`{{ route('admin.pillars.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.pillers = body.pillars; // ensure latest IDs returned
Swal.fire({
icon: 'success',
title: "{{ __('Pillars updated 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 Pillars.') }}"
});
})
.finally(() => {
this.loading = false;
});
}
—
This updated Step 2:
– Limits to exactly **5 Pillars**
– Initializes `titles` per language
– Submits to the `admin.pillars.update` route via `PUT`
– Works whether IDs exist (update) or not (create)
Let me know if you’d like to **visually reorder Pillars via drag & drop** or enforce **position uniqueness**!
—
### ✅ `update()` Method – `AdminPillarController.php`
use IlluminateHttpRequest;
use AppModelsPillar;
use AppModelsPillarTranslation;
use IlluminateSupportFacadesDB;
public function update(Request $request, $surveyId)
{
$request->validate([
'pillars' => 'required|array|size:5',
'pillars.*.id' => 'nullable|exists:pillars,id',
'pillars.*.titles' => 'required|array',
'pillars.*.weight' => 'required|numeric',
'pillars.*.position' => 'required|integer',
]);
$updated = [];
DB::beginTransaction();
try {
foreach ($request->pillars as $pillarData) {
// Create or update Pillar
$pillar = isset($pillarData['id'])
? Pillar::where('survey_id', $surveyId)->findOrFail($pillarData['id'])
: new Pillar(['survey_id' => $surveyId]);
$pillar->weight = $pillarData['weight'];
$pillar->position = $pillarData['position'];
$pillar->survey_id = $surveyId;
$pillar->save();
// Sync translations (or save directly if using JSON column)
foreach ($pillarData['titles'] as $lang => $title) {
PillarTranslation::updateOrCreate(
[
'pillar_id' => $pillar->id,
'language_code' => $lang,
],
[
'title' => $title,
]
);
}
$updated[] = [
'id' => $pillar->id,
'titles' => $pillarData['titles'],
'weight' => $pillar->weight,
'position' => $pillar->position,
];
}
DB::commit();
return response()->json([
'success' => true,
'pillars' => $updated,
]);
} catch (Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to update pillars: ' . $e->getMessage(),
], 500);
}
}
—
### ✅ Notes:
– Assumes you're using a separate `pillar_translations` table.
– If you're storing titles as JSON in the `pillars` table, let me know — I’ll adjust the translation part.
– You can also add logic to **delete old unused pillars** (not in the incoming array) if needed.
—
Let me know if you want to:
– Include `created_by`/`updated_by` metadata
– Soft delete missing pillars
– Validate unique `position` per pillar or per survey
> **Missing required parameter for [Route: admin.pillars.update] [URI: admin/pillars/{pillar}] [Missing parameter: pillar]**
…means you're trying to use the **default `update()` route**, which expects a **single pillar ID** (like `admin/pillars/5`), but you're actually submitting **multiple pillars** in one go.
—
### ✅ The Fix
You **should not use** the default `update()` resource route (`admin/pillars/{pillar}`) for batch updates.
Instead, define a **custom route** that looks like this:
—
### ✅ In `web.php`
Route::put('/admin/surveys/{survey}/pillars', [AdminPillarController::class, 'updateAll'])
->name('admin.pillars.updateAll');
—
### ✅ In Vue (Update `submitPillers()`)
Update your fetch URL:
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({ pillars: this.pillers })
})
—
### ✅ In Controller
Rename the method:
public function updateAll(Request $request, $surveyId)
👉 If you still want to **keep it named `update()`**, you must:
– Change your route URI to something like `admin/surveys/{survey}/pillars`
– And set a **custom name** (`admin.pillars.update`), overriding the default Laravel resource route
—
### ❌ Don't Use This
route('admin.pillars.update')
That route expects a single `pillar` parameter:
/admin/pillars/{pillar}
Which won’t work for bulk updates.
—
Let me know if you want to **rename the method back to `update()`** or keep it **as `updateAll()`** for clarity.
—
## ✅ 1. Add Delete Route in Laravel
In `routes/web.php`:
Route::delete('/admin/pillars/{pillar}', [AdminPillarController::class, 'destroy'])->name('admin.pillars.destroy');
—
## ✅ 2. Add `destroy()` Method in `AdminPillarController`
public function destroy($id)
{
$pillar = Pillar::findOrFail($id);
// Optionally delete related translations and questions
$pillar->translations()->delete(); // if you use a translations table
$pillar->questions()->delete(); // if pillar has many questions
$pillar->delete();
return response()->json(['success' => true, 'message' => 'Pillar deleted successfully.']);
}
—
## ✅ 3. Update Vue Template (Add Delete Button in Step 2)
Inside your loop over `pillers` in Step 2:
<div class="col-md-1 mb-3 d-flex align-items-end" v-if="pillers.length > 1">
<button class="btn btn-danger btn-sm" @click="deletePiller(piller.id, index)">
<i class="fa fa-trash"></i>
</button>
</div>
—
## ✅ 4. Add `deletePiller()` Method in Vue
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: 'Yes, delete it!'
}).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('Deleted!', data.message, 'success');
} else {
Swal.fire('Error', data.message || 'Failed to delete pillar.', 'error');
}
})
.catch(() => {
Swal.fire('Error', 'Something went wrong.', 'error');
});
}
});
}
—
## ✅ Optional: Soft Delete Support
If you're using `SoftDeletes`, replace `->delete()` with `->forceDelete()` if needed.
—
Let me know if you also want to:
– Prevent deleting if questions exist
– Allow undo/delete-restore behavior
– Update positions after deletion
– **Soft removal** of new (unsaved) pillars using `removePiller(index)`
– **Permanent deletion** of saved pillars using `deletePiller(id, index)`
…you can conditionally choose the right method inside your delete button.
—
### ✅ Replace your “ with:
<div class="col-md-1 mb-3 d-flex align-items-end">
<button
class="btn btn-danger btn-sm"
:title="piller.id ? 'Delete from DB' : 'Remove unsaved Pillar'"
@click="piller.id ? deletePiller(piller.id, index) : removePiller(index)">
<i class="fa fa-trash"></i>
</button>
</div>
—
### ✅ Explanation
| Case | Action |
|——|——–|
| `piller.id` exists | The item is saved in DB → call `deletePiller(id, index)` |
| No `piller.id` yet | It's a new unsaved row → call `removePiller(index)` |
This approach keeps the UI consistent, showing the delete icon for **all** items — but applies the correct logic based on whether the item exists in the database.
Let me know if you’d also like to confirm deletion only for DB items (with SweetAlert), but skip confirmation for unsaved rows.
– Multiple **questions per pillar**
– **Multilingual titles**
– Replacing or syncing existing questions
—
## ✅ Step 3: Laravel Route
In `routes/web.php`:
Route::put('/admin/surveys/{survey}/questions', [AdminQuestionController::class, 'updateAll'])
->name('admin.questions.updateAll');
—
## ✅ Controller Method – `AdminQuestionController`
use IlluminateHttpRequest;
use AppModelsQuestion;
use AppModelsQuestionTranslation;
use IlluminateSupportFacadesDB;
public function updateAll(Request $request, $surveyId)
{
$request->validate([
'questions' => 'required|array|min:1',
'questions.*.piller_id' => 'required|exists:pillars,id',
'questions.*.titles' => 'required|array',
'questions.*.position' => 'required|integer',
]);
DB::beginTransaction();
try {
$questionIds = [];
foreach ($request->questions as $data) {
// Create or update question (you can use ID if provided)
$question = Question::create([
'piller_id' => $data['piller_id'],
'position' => $data['position'],
]);
// Translations
foreach ($data['titles'] as $lang => $title) {
QuestionTranslation::updateOrCreate(
[
'question_id' => $question->id,
'language_code' => $lang,
],
[
'title' => $title,
]
);
}
$questionIds[] = $question->id;
}
// Optionally, delete old questions not in this batch for the same survey
// Question::whereIn('piller_id', Pillar::where('survey_id', $surveyId)->pluck('id'))->whereNotIn('id', $questionIds)->delete();
DB::commit();
return response()->json(['success' => true]);
} catch (Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to save questions: ' . $e->getMessage()
], 500);
}
}
—
## ✅ Expected Frontend Payload
The Vue app should send:
{
"questions": [
{
"piller_id": 1,
"titles": {
"en": "Question in English",
"de": "Frage auf Deutsch"
},
"position": 1
},
...
]
}
—
## ✅ Notes
– You can optionally **clear old questions** not included in the update.
– Adjust for soft deletes if you're using them.
– If you're editing existing questions, you can accept `id` and call `update()` instead of always `create()`.
Let me know if you want to:
– Enable full **sync** (delete removed questions)
– Handle **editing existing questions** with `id`
– Validate uniqueness of position per pillar
– Multiple questions per pillar
– Multilingual titles
– Validation
– Submission to a Laravel `PUT` endpoint like `admin.questions.updateAll`
—
### ✅ `submitQuestions()` – Vue
submitQuestions() {
const payload = {
questions: []
};
// Loop through each pillar and its 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) {
// Validate titles
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;
}
}
// Validate position
if (!q.position) {
Swal.fire('Validation Error', `Position is required for a question in "${piller.titles.en}"`, 'error');
return;
}
payload.questions.push({
piller_id: piller.id,
titles: q.titles,
position: q.position
});
}
}
this.loading = true;
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.') }}"
});
})
.finally(() => {
this.loading = false;
});
}
—
### 🧠 Bonus Tips
– 🔁 You can enhance this by tracking existing question IDs for updates instead of always inserting new ones.
– 📦 If the backend returns updated question IDs, map them back to Vue's `this.questions` for further editing.
Let me know if you want to support **editing individual questions later**, or need the backend to support **update-or-create** logic!
—
### ✅ Step-by-step plan to support `id` per question
—
### 🟢 1. Update your Vue questions structure to **store `id`** (if present)
When loading existing questions (Step 3), ensure they include `id`:
questions: {
1: [ // piller_id
{
id: 101,
titles: { en: 'Title EN', de: 'Titel DE' },
position: 1
}
]
}
—
### 🟢 2. Include `id` in the payload when submitting:
Update this in `submitQuestions()`:
payload.questions.push({
id: q.id || null, // include id if present
piller_id: piller.id,
titles: q.titles,
position: q.position
});
—
### 🟢 3. Update Laravel Controller to handle create or update
In your `AdminQuestionController@updateAll`:
foreach ($request->questions as $data) {
if (!empty($data['id'])) {
// Update existing
$question = Question::findOrFail($data['id']);
$question->update([
'position' => $data['position'],
]);
} else {
// New question
$question = Question::create([
'piller_id' => $data['piller_id'],
'position' => $data['position'],
]);
}
// Sync translations
foreach ($data['titles'] as $lang => $title) {
QuestionTranslation::updateOrCreate(
['question_id' => $question->id, 'language_code' => $lang],
['title' => $title]
);
}
$updatedIds[] = $question->id;
}
—
### 🟢 4. Optional: Send updated question IDs back to frontend
So Vue can update its state with newly created IDs:
return response()->json([
'success' => true,
'updated_question_ids' => $updatedIds
]);
—
### ✅ Summary
| Frontend | Backend |
|———-|———|
| Send `id` for each question if exists | Use `findOrFail($id)` for update |
| Use `q.id || null` when preparing payload | Call `create()` when `id` is empty |
—
Would you like help modifying the question creation logic in Vue to store the returned IDs after submission?
✅ Existing **question updates**
✅ New **question creation**
✅ **Multilingual titles**
✅ Clean structure to handle bulk updates
✅ Returns updated question IDs if needed
—
### ✅ `AdminQuestionController@updateAll`
use IlluminateHttpRequest;
use AppModelsQuestion;
use AppModelsQuestionTranslation;
use IlluminateSupportFacadesDB;
public function updateAll(Request $request, $surveyId)
{
$request->validate([
'questions' => 'required|array|min:1',
'questions.*.piller_id' => 'required|exists:pillars,id',
'questions.*.titles' => 'required|array',
'questions.*.position' => 'required|integer',
'questions.*.id' => 'nullable|integer|exists:questions,id'
]);
DB::beginTransaction();
try {
$updatedQuestions = [];
foreach ($request->questions as $data) {
// Update or create question
if (!empty($data['id'])) {
$question = Question::findOrFail($data['id']);
$question->update([
'position' => $data['position'],
]);
} else {
$question = Question::create([
'piller_id' => $data['piller_id'],
'position' => $data['position'],
]);
}
// Update or create translations
foreach ($data['titles'] as $lang => $title) {
QuestionTranslation::updateOrCreate(
[
'question_id' => $question->id,
'language_code' => $lang
],
[
'title' => $title
]
);
}
$updatedQuestions[] = [
'id' => $question->id,
'piller_id' => $question->piller_id,
'position' => $question->position,
'titles' => $data['titles']
];
}
DB::commit();
return response()->json([
'success' => true,
'questions' => $updatedQuestions
]);
} catch (Exception $e) {
DB::rollBack();
return response()->json([
'success' => false,
'message' => 'Failed to update questions: ' . $e->getMessage()
], 500);
}
}
—
### ✅ Notes:
– This handles both **create** and **update** of questions in a single request.
– Translations are updated via a related `question_translations` table (adjust if you store JSON titles instead).
– If needed, you can later implement logic to **delete missing questions** that are no longer present in the request (sync style).
—
Let me know if:
– You need to **sync** (delete removed questions)
– You store multilingual titles in the `questions` table itself (e.g., `title_en`, `title_de`)
– You want to track who updated/created each question (`created_by`, `updated_by`)
—
### ✅ Modify Your Tab Navigation HTML (Step 1, 2, 3)
Update your tabs to this:
<ul class="nav nav-tabs">
<li class="nav-item">
<a class="nav-link" :class="{ active: step === 1 }" href="#" @click.prevent="goToStep(1)">
1. Survey
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{ active: step === 2, disabled: !survey.name.trim() }"
href="#"
@click.prevent="survey.name.trim() && goToStep(2)">
2. Pillars
</a>
</li>
<li class="nav-item">
<a class="nav-link"
:class="{ active: step === 3, disabled: !survey.name.trim() || pillers.length === 0 }"
href="#"
@click.prevent="survey.name.trim() && pillers.length && goToStep(3)">
3. Questions
</a>
</li>
</ul>
—
### ✅ Explanation
– `survey.name.trim()` ensures the name isn’t just spaces
– Tab 2 is disabled unless survey name is filled
– Tab 3 is disabled unless survey name is filled **and** at least one pillar exists
—
Let me know if you want the tabs **visually greyed out** with Bootstrap styling or tooltips saying "Please fill out the survey name first"!
—
### ✅ Updated `onPillerChange()` Method (Vue)
onPillerChange() {
if (!this.selectedPillerId) return;
// 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];
}
}
—
### ✅ When It Triggers
This will fire when the user selects a pillar in Step 3, ensuring the UI is **never blank**, and gives the user an immediate input form to add their first question.
You already have this in your dropdown:
<select v-model="selectedPillerId" class="form-control" @change="onPillerChange">
So nothing else needs to change.
—
✅ Now when a user picks a pillar in Step 3 and there are no questions yet, they'll always see a blank input row for:
– all languages (based on `availableLanguages`)
– position field
Let me know if you'd like to auto-fill the `position` (e.g., incrementally) for convenience!
Here's how you can do it:
—
### ✅ 1. Update `mounted()` in Vue to auto-trigger question init
In your Vue app, inside the `mounted()` hook:
mounted() {
// Initialize piller titles
this.pillers.forEach(piller => {
this.availableLanguages.forEach(lang => {
if (!piller.titles[lang.code]) {
piller.titles[lang.code] = '';
}
});
});
// Automatically select the first piller if exists
if (this.pillers.length > 0) {
this.selectedPillerId = this.pillers[0].id;
this.onPillerChange();
}
}
—
### ✅ 2. Updated `onPillerChange()` method
onPillerChange() {
if (!this.selectedPillerId) return;
// If no questions exist yet, add one default
if (!this.questions[this.selectedPillerId] || this.questions[this.selectedPillerId].length === 0) {
const newQuestion = {
id: null,
titles: {},
position: ''
};
this.availableLanguages.forEach(lang => {
newQuestion.titles[lang.code] = '';
});
this.questions[this.selectedPillerId] = [newQuestion];
}
}
—
### ✅ What This Does:
– When the form loads, if Step 3 is reached or the first piller is auto-selected:
– It **automatically shows one blank question row**
– This gives users a consistent UI experience even when creating a new survey from scratch.
—
Let me know if you'd also like to:
– Auto-assign default position numbers (like 1, 2, 3…)
– Prevent empty submissions with at least 1 question per pillar validation on backend