Fixed bugs in edit form, Added softDelete, Added form_snapshot to save the older responses even after editing the form

This commit is contained in:
Yash 2024-07-25 01:11:19 +05:30
parent 93916d6cde
commit 87e0486962
12 changed files with 255 additions and 169 deletions

View File

@ -1,7 +1,7 @@
<?php <?php
namespace App\Http\Controllers; namespace App\Http\Controllers;
use Illuminate\Support\Facades\DB;
use Illuminate\Http\Request; use Illuminate\Http\Request;
use App\Models\Form; use App\Models\Form;
use App\Models\User; use App\Models\User;
@ -156,15 +156,22 @@ Contact us at (123) 456-7890 or no_reply@example.com
public function update(Request $request, Form $form) public function update(Request $request, Form $form)
{ {
if ($request->has('publish')) { try {
$form->is_published = !$form->is_published; // Normalize the 'required' field to boolean
$form->save(); if ($request->has('questions')) {
$questions = $request->input('questions');
return redirect()->route('forms.show', $form); foreach ($questions as $index => $question) {
if (isset($question['required']) && $question['required'] === 'on') {
$questions[$index]['required'] = true;
} else {
$questions[$index]['required'] = false;
}
}
$request->merge(['questions' => $questions]);
} }
Log::info('Incoming request data: ', $request->all());
// Validate the request
$validatedData = $request->validate([ $validatedData = $request->validate([
'title' => 'required|string|max:255', 'title' => 'required|string|max:255',
'description' => 'nullable|string|max:255', 'description' => 'nullable|string|max:255',
@ -174,50 +181,38 @@ Contact us at (123) 456-7890 or no_reply@example.com
'questions.*.text' => 'required|string|max:255', 'questions.*.text' => 'required|string|max:255',
'questions.*.options' => 'nullable|array', 'questions.*.options' => 'nullable|array',
'questions.*.options.*' => 'nullable|string|max:255', 'questions.*.options.*' => 'nullable|string|max:255',
'questions.*.required' => 'boolean',
]); ]);
Log::info('Validated data: ', $validatedData); // Update form
$form->update([ $form->update([
'title' => $validatedData['title'], 'title' => $validatedData['title'],
'description' => $validatedData['description'], 'description' => $validatedData['description'],
]); ]);
$existingQuestionIds = []; // Clear existing questions
$form->questions()->delete();
// Create or update questions
foreach ($validatedData['questions'] as $questionData) { foreach ($validatedData['questions'] as $questionData) {
if (isset($questionData['id'])) { $question = new Question([
$question = Question::find($questionData['id']); 'form_id' => $form->id,
} else { 'type' => $questionData['type'],
$question = new Question(); 'question_text' => $questionData['text'],
$question->form_id = $form->id; 'options' => json_encode($questionData['options'] ?? []),
} 'required' => $questionData['required'],
]);
$question->type = $questionData['type'];
$question->question_text = $questionData['text'];
$question->options = isset($questionData['options']) ? json_encode($questionData['options']) : json_encode([]);
$question->save(); $question->save();
Log::info('Saved question: ', $question->toArray());
$existingQuestionIds[] = $question->id;
} }
$form->questions()->whereNotIn('id', $existingQuestionIds)->delete(); DB::commit();
Log::info('Remaining questions: ', $form->questions()->get()->toArray());
return redirect()->route('forms.show', $form)->with('success', 'Form updated successfully.'); return redirect()->route('forms.show', $form)->with('success', 'Form updated successfully.');
} catch (\Exception $e) {
DB::rollBack();
Log::error('Error updating form: ' . $e->getMessage());
return back()->withErrors(['error' => 'An error occurred while updating the form. Please try again.'])->withInput();
} }
}

View File

@ -31,33 +31,27 @@ class ResponseController extends Controller
public function viewResponse(Form $form, $responseId) public function viewResponse(Form $form, $responseId)
{ {
$responses = Response::where('response_id', $responseId) $responses = Response::where('response_id', $responseId)
->where('form_id', $form->id) ->where('form_id', $form->id)
->get(); ->get();
if ($responses->isEmpty()) {
$questions = Question::where('form_id', $form->id)->get()->keyBy('id'); abort(404, 'Response not found');
$statistics = [];
foreach ($questions as $question) {
$statistics[$question->id] = [
'question_text' => $question->question_text,
'type' => $question->type,
'options' => json_decode($question->options),
'responses' => []
];
foreach ($responses as $response) {
$decodedAnswers = json_decode($response->answers, true);
if (isset($decodedAnswers[$question->id])) {
$statistics[$question->id]['responses'][] = $decodedAnswers[$question->id];
}
}
} }
return view('responses.viewResponse', compact('form', 'responses', 'questions', 'statistics')); $formSnapshot = json_decode($responses->first()->form_snapshot, true);
if (is_null($formSnapshot) || !isset($formSnapshot['questions'])) {
Log::error('Form snapshot is null or does not contain questions', [
'response_id' => $responseId,
'form_snapshot' => $responses->first()->form_snapshot
]);
abort(500, 'Form snapshot is invalid');
}
$questions = collect($formSnapshot['questions'])->keyBy('id');
return view('responses.viewResponse', compact('form', 'responses', 'questions'));
} }
@ -108,34 +102,39 @@ class ResponseController extends Controller
public function submitForm(Request $request, Form $form) public function submitForm(Request $request, Form $form)
{ {
Log::info($request->all()); Log::info('Form submission started', $request->all());
$questions = $form->questions; $questions = $form->questions;
$requiredQuestionIds = $questions->where('required', true)->pluck('id')->toArray(); $requiredQuestionIds = $questions->where('required', true)->pluck('id')->toArray();
$validatedData = $request->validate([ $validatedData = $request->validate([
'answers' => 'array', 'answers' => 'array',
'answers.*' => '', 'answers.*' => '',
]); ]);
foreach ($requiredQuestionIds as $requiredQuestionId) { foreach ($requiredQuestionIds as $requiredQuestionId) {
if (!isset($validatedData['answers'][$requiredQuestionId]) || empty($validatedData['answers'][$requiredQuestionId])) { if (!isset($validatedData['answers'][$requiredQuestionId]) || empty($validatedData['answers'][$requiredQuestionId])) {
return redirect()->back() return response()->json(['success' => false, 'message' => 'Please answer all required questions.']);
->withErrors(['errors' => 'Please answer all required questions.'])
->withInput();
} }
} }
Log::info($validatedData); Log::info('Validation passed', $validatedData);
$responseId = Uuid::uuid4()->toString(); $responseId = Uuid::uuid4()->toString();
$formSnapshot = [
'title' => $form->title,
'description' => $form->description,
'questions' => $questions->map(function ($question) {
return [
'id' => $question->id,
'question_text' => $question->question_text,
'type' => $question->type,
'options' => $question->options,
];
})->toArray(),
];
foreach ($validatedData['answers'] as $questionId => $answer) { foreach ($validatedData['answers'] as $questionId => $answer) {
$response = new Response(); $response = new Response();
@ -145,10 +144,12 @@ class ResponseController extends Controller
$response->user_id = auth()->id(); $response->user_id = auth()->id();
$response->answers = json_encode($answer); $response->answers = json_encode($answer);
$response->submitted_at = now(); $response->submitted_at = now();
$response->form_snapshot = json_encode($formSnapshot);
$response->save(); $response->save();
Log::info('Response saved', $response->toArray());
} }
return redirect()->route('responses.showForm', $form) return response()->json(['success' => true, 'message' => 'Response submitted successfully.']);
->with('success', 'Response submitted successfully.');
} }
} }

View File

@ -1,25 +1,21 @@
<?php <?php
namespace App\Models; namespace App\Models;
use Illuminate\Database\Eloquent\SoftDeletes;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
class Question extends Model class Question extends Model
{ {
use SoftDeletes;
use HasFactory; use HasFactory;
protected $fillable = ['form_id', 'user_id', 'submitted_at', 'answers']; protected $fillable = ['form_id', 'type', 'question_text', 'options', 'required'];
protected $casts = [ protected $casts = [
'answers' => 'array',
'options' => 'array', 'options' => 'array',
'required' => 'boolean',
]; ];
public function getOptionsAttribute($value)
{
return json_decode($value, true);
}
public function form() public function form()
{ {
return $this->belongsTo(Form::class); return $this->belongsTo(Form::class);

View File

@ -8,7 +8,7 @@ use Illuminate\Database\Eloquent\Model;
class Response extends Model class Response extends Model
{ {
use HasFactory; use HasFactory;
protected $fillable = ['form_id', 'user_id', 'answers']; protected $fillable = ['form_id', 'user_id', 'response_id', 'question_id', 'answers', 'submitted_at', 'form_snapshot'];
// Define relationships // Define relationships
public function form() public function form()

View File

@ -0,0 +1,22 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up()
{
Schema::table('questions', function (Blueprint $table) {
$table->softDeletes();
});
}
public function down()
{
Schema::table('questions', function (Blueprint $table) {
$table->dropSoftDeletes();
});
}
};

View File

@ -0,0 +1,28 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('responses', function (Blueprint $table) {
$table->json('form_snapshot')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('responses', function (Blueprint $table) {
$table->dropColumn('form_snapshot');
});
}
};

View File

@ -0,0 +1,30 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::table('responses', function (Blueprint $table) {
$table->json('form_snapshot')->nullable();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::table('responses', function (Blueprint $table) {
if (Schema::hasColumn('responses', 'form_snapshot')) {
$table->dropColumn('form_snapshot');
}
});
}
};

View File

@ -59,6 +59,7 @@
<div id="questions-section"> <div id="questions-section">
@foreach ($questions as $index => $question) @foreach ($questions as $index => $question)
<div class="question mb-4 p-3 border rounded bg-light" data-index="{{ $index }}"> <div class="question mb-4 p-3 border rounded bg-light" data-index="{{ $index }}">
<input type="hidden" name="questions[{{ $index }}][id]" value="{{ $question->id }}">
<div class="form-group"> <div class="form-group">
<select class="form-control question-type" id="question-type-{{ $index }}" name="questions[{{ $index }}][type]"> <select class="form-control question-type" id="question-type-{{ $index }}" name="questions[{{ $index }}][type]">
<option value="multiple_choice" {{ $question->type === 'multiple_choice' ? 'selected' : '' }}>Multiple Choice</option> <option value="multiple_choice" {{ $question->type === 'multiple_choice' ? 'selected' : '' }}>Multiple Choice</option>
@ -137,6 +138,7 @@
const questionHtml = ` const questionHtml = `
<div class="question mb-4 p-3 border rounded bg-light" data-index="${questionIndex}"> <div class="question mb-4 p-3 border rounded bg-light" data-index="${questionIndex}">
<input type="hidden" name="questions[${questionIndex}][id]" value="">
<div class="form-group"> <div class="form-group">
<select class="form-control question-type" id="question-type-${questionIndex}" name="questions[${questionIndex}][type]" onchange="handleQuestionTypeChange(this)"> <select class="form-control question-type" id="question-type-${questionIndex}" name="questions[${questionIndex}][type]" onchange="handleQuestionTypeChange(this)">
<option value="multiple_choice">Multiple Choice</option> <option value="multiple_choice">Multiple Choice</option>
@ -149,10 +151,10 @@
<input type="text" id="question-text-${questionIndex}" name="questions[${questionIndex}][text]" class="form-control question-input" placeholder="Type your question here" required> <input type="text" id="question-text-${questionIndex}" name="questions[${questionIndex}][text]" class="form-control question-input" placeholder="Type your question here" required>
</div> </div>
<div class="form-group form-check"> <div class="form-group form-check">
<input type="checkbox" id="question-required-{{ $index }}" <input type="checkbox" id="question-required-${questionIndex}"
name="questions[{{ $index }}][required]" class="form-check-input" name="questions[${questionIndex}][required]" class="form-check-input"
{{ $question->required ? 'checked' : '' }}> {{ $question->required ? 'checked' : '' }}>
<label for="question-required-{{ $index }}" class="form-check-label">Required</label> <label for="question-required-${questionIndex}" class="form-check-label">Required</label>
</div> </div>
<div class="form-group options-container"> <div class="form-group options-container">
<label>Options</label> <label>Options</label>
@ -239,16 +241,14 @@
}); });
updateAddButtonPosition(); updateAddButtonPosition();
$('#edit-form').on('submit', function(e) {
e.preventDefault();
updateQuestionIndices();
this.submit();
});
}); });
</script> </script>
</body> </body>
</html> </html>

View File

@ -12,7 +12,12 @@
@csrf @csrf
@foreach ($questions as $question) @foreach ($questions as $question)
<div class="mt-6"> <div class="mt-6">
<label class="block font-medium text-base text-gray-800 mb-2">{{ $question->question_text }}</label> <label class="block font-medium text-base text-gray-800 mb-2">
{{ $question->question_text }}
@if ($question->required)
<span class="text-red-600">*</span>
@endif
</label>
@if ($question->type == 'multiple_choice') @if ($question->type == 'multiple_choice')
@foreach (json_decode($question->options) as $option) @foreach (json_decode($question->options) as $option)
<label class="flex items-center mt-2"> <label class="flex items-center mt-2">
@ -52,7 +57,24 @@
const form = event.target; const form = event.target;
const formData = new FormData(form); const formData = new FormData(form);
let valid = true;
@foreach ($questions as $question)
@if ($question->required)
if (!formData.has('answers[{{ $question->id }}]') || !formData.get('answers[{{ $question->id }}]').trim()) {
valid = false;
Swal.fire({
title: 'Error!',
text: 'Please answer all required questions.',
icon: 'error',
confirmButtonText: 'OK'
});
break;
}
@endif
@endforeach
if (valid) {
fetch(form.action, { fetch(form.action, {
method: form.method, method: form.method,
headers: { headers: {
@ -89,16 +111,13 @@
.catch(error => { .catch(error => {
console.error('Error:', error); console.error('Error:', error);
Swal.fire({ Swal.fire({
title: 'Success!', title: 'Error!',
text: 'Form submitted successfully.', text: 'There was an error submitting the form.',
icon: 'success', icon: 'error',
confirmButtonText: 'OK' confirmButtonText: 'OK'
}).then((result) => { });
if (result.isConfirmed) { });
window.location.href = '{{ route('responses.success', $form) }}';
} }
}); });
});
});
</script> </script>
@endsection @endsection

View File

@ -22,39 +22,41 @@
</div> </div>
</header> </header>
<div mt-4">
<div class="container mt-4">
<!-- Responses Section --> <!-- Responses Section -->
<div class="question_form bg-light p-4 rounded shadow-md" id="responses_section"> <div class="question_form bg-light p-4 rounded shadow-md" id="responses_section">
<div class="section"> <div class="section">
<div class="question_title_section mb-4"> <div class="question_title_section mb-4">
<div class="question_form_top"> <div class="question_form_top">
<input type="text" id="form-title" name="title" class="form-control form-control-lg mb-2" style="color: black" placeholder="Untitled Form" value="{{ $form->title }}" readonly /> <input type="text" id="form-title" name="title" class="form-control form-control-lg mb-2" style="color: black" placeholder="Untitled Form" value="{{ $form->title }}" readonly />
<input type="text" name="description" id="form-description" class="form-control form-control-sm" style="color: black" value="{{$form->description}}" readonly/> <input type="text" name="description" id="form-description" class="form-control form-control-sm" style="color: black" value="{{ $form->description }}" readonly/>
</div> </div>
</div> </div>
</div> </div>
<div class="section shadow-md" id="questions_section"> <div class="section shadow-md" id="questions_section">
@foreach ($responses as $response) @foreach ($responses as $response)
@php @php
$question = $questions[$response->question_id] ?? null; $question = $questions[$response->question_id];
$decodedAnswers = json_decode($response->answers, true); $decodedAnswers = json_decode($response->answers, true);
@endphp @endphp
@if ($question)
<div class="question mb-4 p-3 border rounded bg-white shadow-md"> <div class="question mb-4 p-3 border rounded bg-white shadow-md">
<h3 class="text-lg font-medium mb-2">{{ $question->question_text }}</h3> <h3 class="text-lg font-medium mb-2">
@if ($question->type == 'dropdown') {{ $question['question_text'] }}
</h3>
@if ($question['type'] == 'dropdown')
<select disabled class="form-control"> <select disabled class="form-control">
@foreach (json_decode($question->options) as $option) @foreach (json_decode($question['options'] ?? '[]') as $option)
<option {{ ($option == $decodedAnswers) ? 'selected' : '' }}> <option {{ ($option == $decodedAnswers) ? 'selected' : '' }}>
{{ $option }} {{ $option }}
</option> </option>
@endforeach @endforeach
</select> </select>
@elseif (in_array($question->type, ['multiple_choice', 'checkbox'])) @elseif (in_array($question['type'], ['multiple_choice', 'checkbox']))
<div class="options-container mb-3"> <div class="options-container mb-3">
@foreach (json_decode($question->options) as $option) @foreach (json_decode($question['options'] ?? '[]') as $option)
<div class="option d-flex align-items-center mb-2"> <div class="option d-flex align-items-center mb-2">
<input type="{{ $question->type == 'checkbox' ? 'checkbox' : 'radio' }}" disabled {{ in_array($option, (array)$decodedAnswers) ? 'checked' : '' }} class="mr-2"> <input type="{{ $question['type'] == 'checkbox' ? 'checkbox' : 'radio' }}" disabled {{ in_array($option, (array)$decodedAnswers) ? 'checked' : '' }} class="mr-2">
{{ $option }} {{ $option }}
</div> </div>
@endforeach @endforeach
@ -63,9 +65,6 @@
<p class="mt-2 p-3 bg-light rounded">{{ is_array($decodedAnswers) ? implode(', ', $decodedAnswers) : $decodedAnswers }}</p> <p class="mt-2 p-3 bg-light rounded">{{ is_array($decodedAnswers) ? implode(', ', $decodedAnswers) : $decodedAnswers }}</p>
@endif @endif
</div> </div>
@else
<p class="text-danger">Question not found for ID: {{ $response->question_id }}</p>
@endif
@endforeach @endforeach
</div> </div>
</div> </div>

View File

@ -13,6 +13,7 @@
<body class="font-roboto text-gray-800 bg-white"> <body class="font-roboto text-gray-800 bg-white">
<!-- Header --> <!-- Header -->
<div class="bg-white shadow-md px-6 py-4 flex justify-between items-center"> <div class="bg-white shadow-md px-6 py-4 flex justify-between items-center">
<div class="flex items-center"> <div class="flex items-center">

View File

@ -11,9 +11,4 @@ export default defineConfig({
refresh: true, refresh: true,
}), }),
], ],
server: {
// host: '192.168.29.229',
host: '192.168.2.179',
port: 5173
},
}); });