further development of the predefinedmutations
Some checks failed
linter / quality (push) Failing after 1m16s
tests / ci (8.3) (push) Failing after 59s
tests / ci (8.4) (push) Failing after 1m14s
tests / ci (8.5) (push) Failing after 1m15s

This commit is contained in:
Daan Meijer 2026-06-17 13:30:55 +02:00
parent ed23bb2a78
commit 11df4ef55c
9 changed files with 145 additions and 49 deletions

View File

@ -3,6 +3,7 @@
namespace App\Http\Controllers; namespace App\Http\Controllers;
use App\Models\Dynamic; use App\Models\Dynamic;
use App\Models\Ledger;
use App\Models\PredefinedMutation; use App\Models\PredefinedMutation;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\Request; use Illuminate\Http\Request;
@ -15,20 +16,21 @@ class PredefinedMutationController extends Controller
/** /**
* Display a listing of the resource. * Display a listing of the resource.
*/ */
public function index(Dynamic $dynamic) public function index(Dynamic $dynamic, Ledger $ledger)
{ {
$this->authorize('update', $dynamic); $this->authorize('update', $dynamic);
return Inertia::render('Dynamics/PredefinedMutations/Index', [ return Inertia::render('Ledgers/PredefinedMutations/Index', [
'dynamic' => $dynamic, 'dynamic' => $dynamic,
'predefined_mutations' => $dynamic->predefinedMutations()->latest()->get(), 'ledger' => $ledger,
'predefined_mutations' => $ledger->predefinedMutations()->latest()->get(),
]); ]);
} }
/** /**
* Store a newly created resource in storage. * Store a newly created resource in storage.
*/ */
public function store(Request $request, Dynamic $dynamic) public function store(Request $request, Dynamic $dynamic, Ledger $ledger)
{ {
$this->authorize('update', $dynamic); $this->authorize('update', $dynamic);
@ -36,11 +38,10 @@ class PredefinedMutationController extends Controller
'name' => ['required', 'string', 'max:255'], 'name' => ['required', 'string', 'max:255'],
'description' => ['nullable', 'string'], 'description' => ['nullable', 'string'],
'amount' => ['required', 'integer'], 'amount' => ['required', 'integer'],
'type' => ['required', 'string', 'in:reward,penalty'],
]); ]);
$dynamic->predefinedMutations()->create($request->all()); $ledger->predefinedMutations()->create($request->all());
return redirect()->route('dynamics.predefined-mutations.index', $dynamic); return redirect()->route('dynamics.ledgers.predefined-mutations.index', [$dynamic, $ledger]);
} }
} }

View File

@ -31,6 +31,11 @@ class Ledger extends Model
return $this->hasMany(Mutation::class); return $this->hasMany(Mutation::class);
} }
public function predefinedMutations(): HasMany
{
return $this->hasMany(PredefinedMutation::class);
}
public function media(): \Illuminate\Database\Eloquent\Relations\MorphMany public function media(): \Illuminate\Database\Eloquent\Relations\MorphMany
{ {
return $this->morphMany(Media::class, 'mediable'); return $this->morphMany(Media::class, 'mediable');

View File

@ -11,15 +11,14 @@ class PredefinedMutation extends Model
use HasFactory; use HasFactory;
protected $fillable = [ protected $fillable = [
'dynamic_id', 'ledger_id',
'name', 'name',
'description', 'description',
'amount', 'amount',
'type',
]; ];
public function dynamic(): BelongsTo public function ledger(): BelongsTo
{ {
return $this->belongsTo(Dynamic::class); return $this->belongsTo(Ledger::class);
} }
} }

View File

@ -13,11 +13,10 @@ return new class extends Migration
{ {
Schema::create('predefined_mutations', function (Blueprint $table) { Schema::create('predefined_mutations', function (Blueprint $table) {
$table->id(); $table->id();
$table->foreignId('dynamic_id')->constrained()->cascadeOnDelete(); $table->foreignId('ledger_id')->constrained()->cascadeOnDelete();
$table->string('name'); $table->string('name');
$table->text('description')->nullable(); $table->text('description')->nullable();
$table->integer('amount'); $table->integer('amount');
$table->string('type')->default('reward');
$table->timestamps(); $table->timestamps();
}); });
} }

View File

@ -70,12 +70,6 @@ function submit() {
</form> </form>
</div> </div>
</div> </div>
<div class="mt-8">
<InertiaLink :href="route('dynamics.predefined-mutations.index', dynamic.id)" class="c-dynamic-settings__submit-btn">
Manage Predefined Mutations
</InertiaLink>
</div>
</div> </div>
</div> </div>
</template> </template>

View File

@ -9,12 +9,15 @@ const props = defineProps<{
id: number; id: number;
name: string; name: string;
}; };
ledger: {
id: number;
name: string;
};
predefined_mutations: Array<{ predefined_mutations: Array<{
id: number; id: number;
name: string; name: string;
description: string; description: string;
amount: number; amount: number;
type: string;
}>; }>;
}>(); }>();
@ -22,7 +25,6 @@ const form = useForm({
name: '', name: '',
description: '', description: '',
amount: 0, amount: 0,
type: 'reward',
}); });
const breadcrumbs = [ const breadcrumbs = [
@ -34,14 +36,18 @@ const breadcrumbs = [
name: props.dynamic.name, name: props.dynamic.name,
href: route('dynamics.show', props.dynamic.id), href: route('dynamics.show', props.dynamic.id),
}, },
{
name: props.ledger.name,
href: route('dynamics.ledgers.show', [props.dynamic.id, props.ledger.id]),
},
{ {
name: 'Predefined Mutations', name: 'Predefined Mutations',
href: route('dynamics.predefined-mutations.index', props.dynamic.id), href: route('dynamics.ledgers.predefined-mutations.index', [props.dynamic.id, props.ledger.id]),
}, },
]; ];
function submit() { function submit() {
form.post(route('dynamics.predefined-mutations.store', props.dynamic.id), { form.post(route('dynamics.ledgers.predefined-mutations.store', [props.dynamic.id, props.ledger.id]), {
onSuccess: () => form.reset(), onSuccess: () => form.reset(),
}); });
} }
@ -56,7 +62,7 @@ function submit() {
<div class="c-predefined-mutations__card"> <div class="c-predefined-mutations__card">
<div class="c-predefined-mutations__body"> <div class="c-predefined-mutations__body">
<h3 class="c-predefined-mutations__title"> <h3 class="c-predefined-mutations__title">
Predefined Mutations for {{ dynamic.name }} Predefined Mutations for {{ ledger.name }}
</h3> </h3>
<div class="c-predefined-mutations__list"> <div class="c-predefined-mutations__list">
@ -133,22 +139,6 @@ function submit() {
/> />
</div> </div>
<div class="c-predefined-mutations__field">
<label
for="type"
class="c-predefined-mutations__label"
>Type</label
>
<select
v-model="form.type"
id="type"
class="c-predefined-mutations__select"
>
<option value="reward">Reward</option>
<option value="penalty">Penalty</option>
</select>
</div>
<div class="c-predefined-mutations__actions"> <div class="c-predefined-mutations__actions">
<button <button
type="submit" type="submit"

View File

@ -1,5 +1,5 @@
<script setup lang="ts"> <script setup lang="ts">
import { Head } from '@inertiajs/vue3'; import { Head, Link as InertiaLink } from '@inertiajs/vue3';
import { useEcho } from '@laravel/echo-vue'; import { useEcho } from '@laravel/echo-vue';
import { ref } from 'vue'; import { ref } from 'vue';
import { route } from 'ziggy-js'; import { route } from 'ziggy-js';
@ -168,13 +168,25 @@ function isOwnerUser(userId: number): boolean {
<div class="c-ledger-show__container"> <div class="c-ledger-show__container">
<div class="c-ledger-show__card"> <div class="c-ledger-show__card">
<div class="c-ledger-show__body"> <div class="c-ledger-show__body">
<h3 class="c-ledger-show__title">{{ ledger.name }}</h3> <div class="flex flex-col sm:flex-row sm:justify-between sm:items-start gap-4">
<p class="c-ledger-show__score"> <div>
Score: {{ ledger.score }} <h3 class="c-ledger-show__title">{{ ledger.name }}</h3>
</p> <p class="c-ledger-show__score">
<p class="c-ledger-show__rules"> Score: {{ ledger.score }}
{{ ledger.rules }} </p>
</p> <p class="c-ledger-show__rules">
{{ ledger.rules }}
</p>
</div>
<div v-if="isOwner" class="flex flex-col gap-2">
<InertiaLink
:href="route('dynamics.ledgers.predefined-mutations.index', [dynamic.id, ledger.id])"
class="c-ledger-show__manage-btn"
>
Predefined Mutations
</InertiaLink>
</div>
</div>
<!-- Ledger Alignment Badge / Subtitle --> <!-- Ledger Alignment Badge / Subtitle -->
<div class="c-ledger-show__alignment-wrapper"> <div class="c-ledger-show__alignment-wrapper">
@ -288,6 +300,10 @@ function isOwnerUser(userId: number): boolean {
@apply py-12; @apply py-12;
} }
.c-ledger-show__manage-btn {
@apply inline-flex items-center rounded-md border border-transparent bg-gray-800 px-4 py-2 text-xs font-semibold tracking-widest text-white uppercase transition duration-150 ease-in-out hover:bg-gray-700 focus:bg-gray-700 focus:ring-2 focus:ring-indigo-500 focus:ring-offset-2 focus:outline-none active:bg-gray-900 dark:bg-gray-200 dark:text-gray-800 dark:hover:bg-white dark:focus:bg-white dark:focus:ring-offset-gray-800 dark:active:bg-gray-300;
}
.c-ledger-show__container { .c-ledger-show__container {
@apply mx-auto max-w-7xl sm:px-6 lg:px-8; @apply mx-auto max-w-7xl sm:px-6 lg:px-8;
} }

View File

@ -19,7 +19,7 @@ Route::middleware(['auth', 'verified'])->group(function () {
Route::get('/dynamics/{dynamic}/ledgers/create', [LedgerController::class, 'create'])->name('dynamics.ledgers.create'); Route::get('/dynamics/{dynamic}/ledgers/create', [LedgerController::class, 'create'])->name('dynamics.ledgers.create');
Route::resource('dynamics.ledgers', LedgerController::class)->scoped()->except(['create']); Route::resource('dynamics.ledgers', LedgerController::class)->scoped()->except(['create']);
Route::resource('dynamics.predefined-mutations', \App\Http\Controllers\PredefinedMutationController::class)->scoped(); Route::resource('dynamics.ledgers.predefined-mutations', \App\Http\Controllers\PredefinedMutationController::class)->scoped();
Route::resource('dynamics.ledgers.mutations', MutationController::class)->scoped(); Route::resource('dynamics.ledgers.mutations', MutationController::class)->scoped();

View File

@ -0,0 +1,92 @@
<?php
use App\Models\User;
use App\Models\Dynamic;
use App\Models\Ledger;
use App\Models\PredefinedMutation;
test('owner can view predefined mutations for ledger', function () {
$owner = User::factory()->create();
$dynamic = Dynamic::factory()->create();
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
$predefined = PredefinedMutation::create([
'ledger_id' => $ledger->id,
'name' => 'Weekly Room Cleaning',
'description' => 'Cleaned up the master bedroom',
'amount' => 20,
]);
$this->actingAs($owner);
$response = $this->get(route('dynamics.ledgers.predefined-mutations.index', [$dynamic->id, $ledger->id]));
$response->assertOk();
$response->assertInertia(fn ($page) => $page
->component('Ledgers/PredefinedMutations/Index')
->where('ledger.id', $ledger->id)
->has('predefined_mutations', 1)
->where('predefined_mutations.0.name', 'Weekly Room Cleaning')
);
});
test('non-owner cannot view predefined mutations for ledger', function () {
$owner = User::factory()->create();
$participant = User::factory()->create();
$dynamic = Dynamic::factory()->create();
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
$this->actingAs($participant);
$response = $this->get(route('dynamics.ledgers.predefined-mutations.index', [$dynamic->id, $ledger->id]));
$response->assertStatus(403);
});
test('owner can create predefined mutations for ledger', function () {
$owner = User::factory()->create();
$dynamic = Dynamic::factory()->create();
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
$this->actingAs($owner);
$response = $this->post(route('dynamics.ledgers.predefined-mutations.store', [$dynamic->id, $ledger->id]), [
'name' => 'Polished mirrors',
'description' => 'Mirror polishing in dungeon',
'amount' => 15,
]);
$response->assertRedirect();
$this->assertDatabaseHas('predefined_mutations', [
'ledger_id' => $ledger->id,
'name' => 'Polished mirrors',
'amount' => 15,
]);
});
test('non-owner cannot create predefined mutations for ledger', function () {
$owner = User::factory()->create();
$participant = User::factory()->create();
$dynamic = Dynamic::factory()->create();
$dynamic->participants()->attach($owner->id, ['role' => 'owner']);
$dynamic->participants()->attach($participant->id, ['role' => 'participant']);
$ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id]);
$this->actingAs($participant);
$response = $this->post(route('dynamics.ledgers.predefined-mutations.store', [$dynamic->id, $ledger->id]), [
'name' => 'Polished mirrors',
'description' => 'Mirror polishing in dungeon',
'amount' => 15,
]);
$response->assertStatus(403);
$this->assertDatabaseMissing('predefined_mutations', [
'ledger_id' => $ledger->id,
'name' => 'Polished mirrors',
]);
});