feat: Fragen-Export (JSON) und -Import im Admin-Bereich
This commit is contained in:
@@ -5,6 +5,7 @@ use App\Models\Question;
|
|||||||
use App\Models\Subject;
|
use App\Models\Subject;
|
||||||
use App\Models\AnswerOption;
|
use App\Models\AnswerOption;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
class QuestionController extends Controller {
|
class QuestionController extends Controller {
|
||||||
public function index(Request $r) {
|
public function index(Request $r) {
|
||||||
$subjects = Subject::all();
|
$subjects = Subject::all();
|
||||||
@@ -97,4 +98,64 @@ class QuestionController extends Controller {
|
|||||||
$question->delete();
|
$question->delete();
|
||||||
return redirect()->route('admin.questions.index')->with('success','Frage gelöscht.');
|
return redirect()->route('admin.questions.index')->with('success','Frage gelöscht.');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function export(Request $r) {
|
||||||
|
$query = Question::with(['subject','answerOptions'])->latest();
|
||||||
|
if ($r->filled('subject')) $query->where('subject_id', $r->subject);
|
||||||
|
$data = $query->get()->map(fn($q) => [
|
||||||
|
'subject' => $q->subject->slug,
|
||||||
|
'question_text' => $q->question_text,
|
||||||
|
'type' => $q->type,
|
||||||
|
'difficulty' => $q->difficulty,
|
||||||
|
'points_value' => $q->points_value,
|
||||||
|
'active' => (bool)$q->active,
|
||||||
|
'options' => $q->answerOptions->map(fn($o) => [
|
||||||
|
'text' => $o->text,
|
||||||
|
'is_correct' => (bool)$o->is_correct,
|
||||||
|
])->values(),
|
||||||
|
]);
|
||||||
|
$filename = 'fragen-' . now()->format('Y-m-d') . '.json';
|
||||||
|
return response()->streamDownload(
|
||||||
|
fn() => print(json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE)),
|
||||||
|
$filename,
|
||||||
|
['Content-Type' => 'application/json']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import(Request $r) {
|
||||||
|
$r->validate(['file' => 'required|file|mimes:json|max:2048']);
|
||||||
|
$raw = json_decode(file_get_contents($r->file('file')->getRealPath()), true);
|
||||||
|
if (!is_array($raw)) {
|
||||||
|
return back()->with('error', 'Ungültiges JSON-Format.');
|
||||||
|
}
|
||||||
|
$subjects = Subject::pluck('id','slug');
|
||||||
|
$imported = 0;
|
||||||
|
$skipped = 0;
|
||||||
|
DB::transaction(function() use ($raw, $subjects, &$imported, &$skipped) {
|
||||||
|
foreach ($raw as $item) {
|
||||||
|
if (empty($item['subject']) || !isset($subjects[$item['subject']])) { $skipped++; continue; }
|
||||||
|
if (empty($item['question_text'])) { $skipped++; continue; }
|
||||||
|
$q = Question::create([
|
||||||
|
'subject_id' => $subjects[$item['subject']],
|
||||||
|
'question_text' => $item['question_text'],
|
||||||
|
'type' => $item['type'] ?? 'multiple_choice',
|
||||||
|
'difficulty' => $item['difficulty'] ?? 1,
|
||||||
|
'points_value' => $item['points_value'] ?? 5,
|
||||||
|
'active' => $item['active'] ?? true,
|
||||||
|
]);
|
||||||
|
foreach (($item['options'] ?? []) as $i => $opt) {
|
||||||
|
AnswerOption::create([
|
||||||
|
'question_id' => $q->id,
|
||||||
|
'text' => $opt['text'],
|
||||||
|
'is_correct' => $opt['is_correct'] ?? false,
|
||||||
|
'sort_order' => $i,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
$imported++;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$msg = "{$imported} Fragen importiert.";
|
||||||
|
if ($skipped) $msg .= " {$skipped} übersprungen (unbekanntes Fach oder fehlender Text).";
|
||||||
|
return back()->with('success', $msg);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
@extends('layouts.admin')
|
@extends('layouts.admin')
|
||||||
@section('title','Fragen')
|
@section('title','Fragen')
|
||||||
@section('content')
|
@section('content')
|
||||||
<div class="flex flex-wrap gap-3 items-center justify-between mb-6">
|
<div class="flex flex-wrap gap-3 items-center justify-between mb-6" x-data="{showImport:false}">
|
||||||
<form method="GET" class="flex gap-2">
|
<form method="GET" class="flex gap-2">
|
||||||
<select name="subject" class="border border-slate-300 rounded-lg px-3 py-2 text-sm">
|
<select name="subject" class="border border-slate-300 rounded-lg px-3 py-2 text-sm">
|
||||||
<option value="">Alle Fächer</option>
|
<option value="">Alle Fächer</option>
|
||||||
@@ -9,8 +9,36 @@
|
|||||||
</select>
|
</select>
|
||||||
<button class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm">Filtern</button>
|
<button class="bg-slate-700 text-white px-4 py-2 rounded-lg text-sm">Filtern</button>
|
||||||
</form>
|
</form>
|
||||||
|
<div class="flex gap-2 flex-wrap">
|
||||||
|
{{-- Export --}}
|
||||||
|
<a href="{{ route('admin.questions.export', request()->only('subject')) }}"
|
||||||
|
class="flex items-center gap-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg text-sm font-medium">
|
||||||
|
⬇ Export JSON
|
||||||
|
</a>
|
||||||
|
{{-- Import trigger --}}
|
||||||
|
<button @click="showImport=!showImport"
|
||||||
|
class="flex items-center gap-1.5 bg-slate-100 hover:bg-slate-200 text-slate-700 px-4 py-2 rounded-lg text-sm font-medium">
|
||||||
|
⬆ Import JSON
|
||||||
|
</button>
|
||||||
<a href="{{ route('admin.questions.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neue Frage</a>
|
<a href="{{ route('admin.questions.create') }}" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium">+ Neue Frage</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{{-- Import panel --}}
|
||||||
|
<div x-show="showImport" x-cloak class="w-full mt-1">
|
||||||
|
<div class="bg-amber-50 border border-amber-200 rounded-xl p-5">
|
||||||
|
<h3 class="font-semibold text-slate-800 mb-1">Fragen importieren</h3>
|
||||||
|
<p class="text-xs text-slate-500 mb-3">JSON-Datei im gleichen Format wie der Export. Bestehende Fragen bleiben erhalten.</p>
|
||||||
|
<form method="POST" action="{{ route('admin.questions.import') }}" enctype="multipart/form-data" class="flex items-center gap-3">
|
||||||
|
@csrf
|
||||||
|
<input type="file" name="file" accept=".json" required
|
||||||
|
class="flex-1 text-sm text-slate-600 file:mr-3 file:py-1.5 file:px-3 file:rounded-lg file:border-0 file:text-sm file:font-medium file:bg-violet-100 file:text-violet-700 hover:file:bg-violet-200">
|
||||||
|
<button type="submit" class="bg-violet-600 hover:bg-violet-700 text-white px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap">
|
||||||
|
Importieren
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
<div class="bg-white rounded-xl shadow-sm border border-slate-200 overflow-hidden">
|
||||||
<table class="w-full text-sm">
|
<table class="w-full text-sm">
|
||||||
<thead class="bg-slate-50 border-b border-slate-200">
|
<thead class="bg-slate-50 border-b border-slate-200">
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ Route::middleware(['auth','admin'])->prefix('admin')->name('admin.')->group(func
|
|||||||
Route::get('/', [Admin\DashboardController::class, 'index'])->name('dashboard');
|
Route::get('/', [Admin\DashboardController::class, 'index'])->name('dashboard');
|
||||||
Route::resource('users', Admin\UserController::class);
|
Route::resource('users', Admin\UserController::class);
|
||||||
Route::post('users/{user}/reset', [Admin\UserController::class, 'reset'])->name('users.reset');
|
Route::post('users/{user}/reset', [Admin\UserController::class, 'reset'])->name('users.reset');
|
||||||
|
Route::get ('questions/export', [Admin\QuestionController::class,'export'])->name('questions.export');
|
||||||
|
Route::post('questions/import', [Admin\QuestionController::class,'import'])->name('questions.import');
|
||||||
Route::resource('questions',Admin\QuestionController::class);
|
Route::resource('questions',Admin\QuestionController::class);
|
||||||
Route::resource('rewards', Admin\RewardController::class)->except('show');
|
Route::resource('rewards', Admin\RewardController::class)->except('show');
|
||||||
Route::get ('redemptions', [Admin\RedemptionController::class,'index']) ->name('redemptions.index');
|
Route::get ('redemptions', [Admin\RedemptionController::class,'index']) ->name('redemptions.index');
|
||||||
|
|||||||
Reference in New Issue
Block a user