feat: Lernapp mit Mathe/Deutsch/Englisch, Münzsystem und Belohnungen

This commit is contained in:
root
2026-05-05 14:41:09 +00:00
parent 21e40cd2da
commit bd1640994c
45 changed files with 1522 additions and 58 deletions
@@ -0,0 +1,15 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::table('users', function (Blueprint $table) {
$table->enum('role', ['admin', 'child'])->default('child')->after('email');
$table->unsignedInteger('points')->default(0)->after('role');
});
}
public function down(): void {
Schema::table('users', fn(Blueprint $t) => $t->dropColumn(['role','points']));
}
};
@@ -0,0 +1,17 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('subjects', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('slug')->unique();
$table->string('icon')->default('📚');
$table->string('color')->default('blue');
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('subjects'); }
};
@@ -0,0 +1,19 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('questions', function (Blueprint $table) {
$table->id();
$table->foreignId('subject_id')->constrained()->onDelete('cascade');
$table->text('question_text');
$table->enum('type', ['multiple_choice', 'number_input'])->default('multiple_choice');
$table->unsignedTinyInteger('difficulty')->default(1);
$table->unsignedTinyInteger('points_value')->default(5);
$table->boolean('active')->default(true);
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('questions'); }
};
@@ -0,0 +1,16 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('answer_options', function (Blueprint $table) {
$table->id();
$table->foreignId('question_id')->constrained()->onDelete('cascade');
$table->string('text');
$table->boolean('is_correct')->default(false);
$table->unsignedTinyInteger('sort_order')->default(0);
});
}
public function down(): void { Schema::dropIfExists('answer_options'); }
};
@@ -0,0 +1,17 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('question_attempts', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('question_id')->constrained()->onDelete('cascade');
$table->boolean('is_correct');
$table->unsignedTinyInteger('points_earned')->default(0);
$table->timestamp('created_at')->useCurrent();
});
}
public function down(): void { Schema::dropIfExists('question_attempts'); }
};
@@ -0,0 +1,19 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('rewards', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->string('description')->nullable();
$table->string('icon')->default('🎁');
$table->unsignedInteger('points_cost');
$table->unsignedInteger('minutes')->nullable();
$table->boolean('active')->default(true);
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('rewards'); }
};
@@ -0,0 +1,20 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration {
public function up(): void {
Schema::create('reward_redemptions', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained()->onDelete('cascade');
$table->foreignId('reward_id')->constrained()->onDelete('cascade');
$table->enum('status', ['pending','approved','rejected'])->default('pending');
$table->unsignedInteger('points_spent');
$table->string('note')->nullable();
$table->timestamp('resolved_at')->nullable();
$table->foreignId('resolved_by')->nullable()->constrained('users')->nullOnDelete();
$table->timestamps();
});
}
public function down(): void { Schema::dropIfExists('reward_redemptions'); }
};
+109 -17
View File
@@ -1,25 +1,117 @@
<?php
namespace Database\Seeders;
use App\Models\User;
use Illuminate\Database\Console\Seeds\WithoutModelEvents;
use App\Models\Subject;
use App\Models\Question;
use App\Models\AnswerOption;
use App\Models\Reward;
use Illuminate\Database\Seeder;
use Illuminate\Support\Facades\Hash;
class DatabaseSeeder extends Seeder
{
use WithoutModelEvents;
/**
* Seed the application's database.
*/
public function run(): void
{
// User::factory(10)->create();
User::factory()->create([
'name' => 'Test User',
'email' => 'test@example.com',
class DatabaseSeeder extends Seeder {
public function run(): void {
User::create([
'name' => 'Admin',
'email' => 'admin@lernapp.de',
'password' => Hash::make('Admin1234!'),
'role' => 'admin',
'points' => 0,
]);
$subjects = [
['name'=>'Mathe', 'slug'=>'mathe', 'icon'=>'🔢', 'color'=>'green'],
['name'=>'Deutsch', 'slug'=>'deutsch', 'icon'=>'📖', 'color'=>'blue'],
['name'=>'Englisch','slug'=>'englisch', 'icon'=>'🌍', 'color'=>'orange'],
];
foreach ($subjects as $s) {
Subject::create($s);
}
$questions = [
// Mathe
['subject'=>'mathe','text'=>'7 × 8 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['56',true],['48',false],['54',false],['64',false]]],
['subject'=>'mathe','text'=>'144 ÷ 12 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['12',true],['10',false],['11',false],['14',false]]],
['subject'=>'mathe','text'=>'234 + 567 = ?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['801',true],['791',false],['800',false],['811',false]]],
['subject'=>'mathe','text'=>'1000 387 = ?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['613',true],['623',false],['713',false],['587',false]]],
['subject'=>'mathe','text'=>'Eine Schachtel hat 24 Pralinen. Wie viele Schachteln brauchst du für 96 Pralinen?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['4',true],['3',false],['5',false],['6',false]]],
['subject'=>'mathe','text'=>'Was ist die Hälfte von 136?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['68',true],['64',false],['70',false],['72',false]]],
['subject'=>'mathe','text'=>'Ein Rechteck ist 9 cm lang und 6 cm breit. Wie groß ist sein Flächeninhalt?','type'=>'multiple_choice','diff'=>3,'pts'=>20,
'options'=>[['54 cm²',true],['30 cm²',false],['45 cm²',false],['60 cm²',false]]],
['subject'=>'mathe','text'=>'3 × 4 + 2 × 5 = ?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['22',true],['34',false],['26',false],['20',false]]],
// Deutsch
['subject'=>'deutsch','text'=>'Welches Wort ist ein Nomen?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['Hund',true],['laufen',false],['schnell',false],['grün',false]]],
['subject'=>'deutsch','text'=>'Was ist die Einzahl von „Häuser"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['Haus',true],['Häusen',false],['Häuse',false],['Hausung',false]]],
['subject'=>'deutsch','text'=>'Welche Zeitform ist: „Er spielte Fußball."?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['Präteritum',true],['Präsens',false],['Futur',false],['Perfekt',false]]],
['subject'=>'deutsch','text'=>'Welche Satzart ist das: „Wie schön der Tag ist!"?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['Ausrufesatz',true],['Aussagesatz',false],['Fragesatz',false],['Aufforderungssatz',false]]],
['subject'=>'deutsch','text'=>'Was ist das Gegenteil von „fröhlich"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['traurig',true],['lustig',false],['mutig',false],['fleißig',false]]],
['subject'=>'deutsch','text'=>'Welches Wort ist ein Verb?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['rennen',true],['Tisch',false],['blau',false],['schnell',false]]],
['subject'=>'deutsch','text'=>'Wie heißt der Plural von „das Kind"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['die Kinder',true],['die Kinde',false],['die Kinds',false],['die Kindern',false]]],
['subject'=>'deutsch','text'=>'Welches Komma ist richtig? „Ich esse ___ Brot, Butter und Käse."','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['kein Komma nötig',true],['nach „esse"',false],['nach „Brot"',false],['nach „Butter"',false]]],
// Englisch
['subject'=>'englisch','text'=>'What is "der Hund" in English?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['the dog',true],['the cat',false],['the horse',false],['the pig',false]]],
['subject'=>'englisch','text'=>'Which word means "groß"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['big',true],['small',false],['old',false],['fast',false]]],
['subject'=>'englisch','text'=>'"I ___ to school every day." Which word fits?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['go',true],['going',false],['goes',false],['gone',false]]],
['subject'=>'englisch','text'=>'What is the plural of "child"?','type'=>'multiple_choice','diff'=>2,'pts'=>10,
'options'=>[['children',true],['childs',false],['childes',false],['child',false]]],
['subject'=>'englisch','text'=>'What color is "gelb"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['yellow',true],['red',false],['green',false],['blue',false]]],
['subject'=>'englisch','text'=>'How do you say "Guten Morgen"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['Good morning',true],['Good night',false],['Good evening',false],['Good afternoon',false]]],
['subject'=>'englisch','text'=>'"She ___ a book yesterday." Which word fits?','type'=>'multiple_choice','diff'=>3,'pts'=>20,
'options'=>[['read',true],['reads',false],['reading',false],['is reading',false]]],
['subject'=>'englisch','text'=>'What is the opposite of "hot"?','type'=>'multiple_choice','diff'=>1,'pts'=>5,
'options'=>[['cold',true],['warm',false],['cool',false],['fast',false]]],
];
$subjectMap = Subject::pluck('id','slug')->all();
foreach ($questions as $i => $q) {
$question = Question::create([
'subject_id' => $subjectMap[$q['subject']],
'question_text' => $q['text'],
'type' => $q['type'],
'difficulty' => $q['diff'],
'points_value' => $q['pts'],
'active' => true,
]);
foreach ($q['options'] as $j => [$text, $correct]) {
AnswerOption::create([
'question_id' => $question->id,
'text' => $text,
'is_correct' => $correct,
'sort_order' => $j,
]);
}
}
$rewards = [
['name'=>'30 Min. Fernsehen', 'description'=>'30 Minuten Fernsehzeit', 'icon'=>'📺','points_cost'=>50, 'minutes'=>30],
['name'=>'1 Std. Fernsehen', 'description'=>'Eine Stunde Fernsehzeit', 'icon'=>'📺','points_cost'=>90, 'minutes'=>60],
['name'=>'30 Min. Konsole', 'description'=>'30 Minuten Spielkonsole', 'icon'=>'🎮','points_cost'=>75, 'minutes'=>30],
['name'=>'1 Std. Konsole', 'description'=>'Eine Stunde Spielkonsole', 'icon'=>'🎮','points_cost'=>130,'minutes'=>60],
['name'=>'Wunschdessert', 'description'=>'Ein Dessert deiner Wahl', 'icon'=>'🍰','points_cost'=>80, 'minutes'=>null],
['name'=>'1 Std. länger aufbleiben','description'=>'Heute eine Stunde später ins Bett','icon'=>'🌙','points_cost'=>120,'minutes'=>null],
];
foreach ($rewards as $r) {
Reward::create(array_merge($r, ['active' => true]));
}
}
}