diff --git a/app/Http/Controllers/MutationController.php b/app/Http/Controllers/MutationController.php index 8a9c824..bcc9fc8 100644 --- a/app/Http/Controllers/MutationController.php +++ b/app/Http/Controllers/MutationController.php @@ -67,6 +67,30 @@ class MutationController extends Controller return $mutation; }); + // Log to Mutation and Dynamic chats + $user = $request->user(); + + $mutationMsg = $mutation->chat->messages()->create([ + 'user_id' => $user->id, + 'content' => $status === 'approved' + ? "System: Entry was created by {$user->name}." + : "System: Suggestion was created by {$user->name}.", + ]); + broadcast(new \App\Events\MessageSent($mutationMsg)); + + if ($status === 'approved') { + $dynamicMsg = $dynamic->chat->messages()->create([ + 'user_id' => $user->id, + 'content' => "System: {$user->name} added entry \"{$mutation->description}\" for " . ($mutation->amount >= 0 ? '+' : '') . "{$mutation->amount} points on \"{$ledger->name}\" ledger.", + ]); + } else { + $dynamicMsg = $dynamic->chat->messages()->create([ + 'user_id' => $user->id, + 'content' => "System: {$user->name} suggested \"{$mutation->description}\" for " . ($mutation->amount >= 0 ? '+' : '') . "{$mutation->amount} points on \"{$ledger->name}\" ledger.", + ]); + } + broadcast(new \App\Events\MessageSent($dynamicMsg)); + // Broadcast the real-time creation event! broadcast(new \App\Events\MutationCreated($mutation)); diff --git a/resources/js/app.ts b/resources/js/app.ts index b0c9c1f..3816a62 100644 --- a/resources/js/app.ts +++ b/resources/js/app.ts @@ -1,10 +1,10 @@ import { createInertiaApp } from '@inertiajs/vue3'; +import { configureEcho } from '@laravel/echo-vue'; import { initializeTheme } from '@/composables/useAppearance'; import AppLayout from '@/layouts/AppLayout.vue'; import AuthLayout from '@/layouts/AuthLayout.vue'; import SettingsLayout from '@/layouts/settings/Layout.vue'; import { initializeFlashToast } from '@/lib/flashToast'; -import { configureEcho } from '@laravel/echo-vue'; configureEcho({ broadcaster: 'reverb', diff --git a/resources/js/components/AddMutationForm.vue b/resources/js/components/AddMutationForm.vue new file mode 100644 index 0000000..f096c72 --- /dev/null +++ b/resources/js/components/AddMutationForm.vue @@ -0,0 +1,136 @@ + + + diff --git a/resources/js/components/Chat.vue b/resources/js/components/Chat.vue index 8fb1b16..28f298b 100644 --- a/resources/js/components/Chat.vue +++ b/resources/js/components/Chat.vue @@ -1,9 +1,9 @@ + + diff --git a/resources/js/components/LedgerList.vue b/resources/js/components/LedgerList.vue new file mode 100644 index 0000000..2c87ab8 --- /dev/null +++ b/resources/js/components/LedgerList.vue @@ -0,0 +1,73 @@ + + + diff --git a/resources/js/components/MutationList.vue b/resources/js/components/MutationList.vue new file mode 100644 index 0000000..0a9b91a --- /dev/null +++ b/resources/js/components/MutationList.vue @@ -0,0 +1,170 @@ + + + diff --git a/resources/js/components/ParticipantsList.vue b/resources/js/components/ParticipantsList.vue new file mode 100644 index 0000000..ec14a3f --- /dev/null +++ b/resources/js/components/ParticipantsList.vue @@ -0,0 +1,25 @@ + + + diff --git a/resources/js/pages/Dynamics/Show.vue b/resources/js/pages/Dynamics/Show.vue index faf3bda..fda6607 100644 --- a/resources/js/pages/Dynamics/Show.vue +++ b/resources/js/pages/Dynamics/Show.vue @@ -1,7 +1,10 @@ diff --git a/resources/js/pages/Ledgers/Show.vue b/resources/js/pages/Ledgers/Show.vue index c58fdd0..7486df0 100644 --- a/resources/js/pages/Ledgers/Show.vue +++ b/resources/js/pages/Ledgers/Show.vue @@ -1,9 +1,10 @@ @@ -248,232 +215,18 @@ function isOwnerUser(userId: number): boolean { -
-

- Add Mutation -

-
-
- - -
- {{ form.errors.amount }} -
-
+ + -
- - -
- {{ form.errors.description }} -
-
- - -
- - -
-
- {{ - file.name - }} - -
-
-
- -
- -
- -
- -
-

- Mutations -

- -
- No mutations found for this ledger. -
-
+ + diff --git a/tests/Feature/MutationTest.php b/tests/Feature/MutationTest.php new file mode 100644 index 0000000..4e78350 --- /dev/null +++ b/tests/Feature/MutationTest.php @@ -0,0 +1,117 @@ +create(); + $dynamic = Dynamic::factory()->create(); + $dynamic->participants()->attach($owner->id, ['role' => 'owner']); + $ledger = Ledger::factory()->create(['dynamic_id' => $dynamic->id, 'score' => 100]); + + $this->actingAs($owner); + + $response = $this->post(route('dynamics.ledgers.mutations.store', [$dynamic, $ledger]), [ + 'amount' => 15, + 'description' => 'Direct point reward', + ]); + + $response->assertRedirect(route('dynamics.ledgers.show', [$dynamic, $ledger])); + + $mutation = Mutation::firstWhere('description', 'Direct point reward'); + + expect($mutation)->not->toBeNull(); + expect($mutation->status)->toBe('approved'); + + // Score should be updated immediately + $ledger->refresh(); + expect($ledger->score)->toBe(115); + + // Verify chat messages (should NOT say "approved" or "Approved") + $mutationChatMessages = $mutation->chat->messages; + expect($mutationChatMessages)->toHaveCount(1); + expect($mutationChatMessages->first()->content)->toBe("System: Entry was created by {$owner->name}."); + + $dynamicChatMessages = $dynamic->chat->messages; + expect($dynamicChatMessages)->toHaveCount(1); + expect($dynamicChatMessages->first()->content)->toBe("System: {$owner->name} added entry \"Direct point reward\" for +15 points on \"{$ledger->name}\" ledger."); +}); + +test('non-owner participant creates a suggestion which defaults to pending and says suggested', 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, 'score' => 100]); + + $this->actingAs($participant); + + $response = $this->post(route('dynamics.ledgers.mutations.store', [$dynamic, $ledger]), [ + 'amount' => 10, + 'description' => 'Suggested point reward', + ]); + + $response->assertRedirect(route('dynamics.ledgers.show', [$dynamic, $ledger])); + + $mutation = Mutation::firstWhere('description', 'Suggested point reward'); + + expect($mutation)->not->toBeNull(); + expect($mutation->status)->toBe('pending'); + + // Score should NOT be updated immediately + $ledger->refresh(); + expect($ledger->score)->toBe(100); + + // Verify chat messages + $mutationChatMessages = $mutation->chat->messages; + expect($mutationChatMessages)->toHaveCount(1); + expect($mutationChatMessages->first()->content)->toBe("System: Suggestion was created by {$participant->name}."); + + $dynamicChatMessages = $dynamic->chat->messages; + expect($dynamicChatMessages)->toHaveCount(1); + expect($dynamicChatMessages->first()->content)->toBe("System: {$participant->name} suggested \"Suggested point reward\" for +10 points on \"{$ledger->name}\" ledger."); +}); + +test('owner can approve a pending suggestion and it is updated and logged', 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, 'score' => 100]); + + // Create a pending mutation for the participant + $mutation = Mutation::factory()->create([ + 'ledger_id' => $ledger->id, + 'user_id' => $participant->id, + 'amount' => 20, + 'description' => 'Polished dungeon floors', + 'status' => 'pending', + ]); + + $this->actingAs($owner); + + $response = $this->put(route('dynamics.ledgers.mutations.update', [$dynamic, $ledger, $mutation]), [ + 'status' => 'approved', + ]); + + $response->assertRedirect(); + + $mutation->refresh(); + expect($mutation->status)->toBe('approved'); + + $ledger->refresh(); + expect($ledger->score)->toBe(120); + + // Verify system logs (should have the manual approval log now) + $mutationChatMessages = $mutation->chat->messages; + // Note: one from boot created (empty or via seeder, but in our factory it starts with 0 messages if not manually logged, + // actually our model booted hook creates the chat but doesn't log on boot, the update method creates 1 message) + expect($mutationChatMessages->last()->content)->toBe("System: Suggestion was APPROVED by {$owner->name}."); + + $dynamicChatMessages = $dynamic->chat->messages; + expect($dynamicChatMessages->last()->content)->toBe("System: {$owner->name} APPROVED the suggestion \"Polished dungeon floors\" for +20 points on \"{$ledger->name}\" ledger."); +});