From 97dc7cdb2af7fa3bf1e7752c85dd87f3eec4365f Mon Sep 17 00:00:00 2001 From: jeremy bayse Date: Sat, 1 Aug 2026 08:37:50 +0200 Subject: [PATCH] Allow editing an existing transaction MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Desktop reuses the add-form with a Modifier link per row; mobile gets a "Dernières opérations" list under the quick-entry form since no table exists there. Co-Authored-By: Claude Sonnet 5 --- app/Livewire/Suivi/Index.php | 63 +++++++++++--- .../views/livewire/suivi/index.blade.php | 83 ++++++++++++++---- tests/Feature/SuiviTest.php | 84 +++++++++++++++++++ 3 files changed, 205 insertions(+), 25 deletions(-) diff --git a/app/Livewire/Suivi/Index.php b/app/Livewire/Suivi/Index.php index ae15f9d..67c8786 100644 --- a/app/Livewire/Suivi/Index.php +++ b/app/Livewire/Suivi/Index.php @@ -30,6 +30,8 @@ class Index extends Component public string $filterPaidBy = ''; + public ?int $editingTransactionId = null; + public function mount(): void { $this->date = now()->toDateString(); @@ -51,21 +53,57 @@ class Index extends Component $category = $household->categories()->findOrFail($this->categoryId); $paidByUserId = $this->resolvePaidByUserId($household, $this->paidByUserId); - $household->transactions()->create([ - 'category_id' => $category->id, - 'user_id' => auth()->id(), - 'paid_by_user_id' => $paidByUserId, - 'date' => $this->date, - 'date_effective' => $this->dateEffective, - 'amount' => $this->amount, - 'details' => $this->details, - ]); + if ($this->editingTransactionId) { + $transaction = $household->transactions()->findOrFail($this->editingTransactionId); - $this->reset(['categoryId', 'amount', 'details', 'paidByUserId']); + $transaction->update([ + 'category_id' => $category->id, + 'paid_by_user_id' => $paidByUserId, + 'date' => $this->date, + 'date_effective' => $this->dateEffective, + 'amount' => $this->amount, + 'details' => $this->details, + ]); + } else { + $household->transactions()->create([ + 'category_id' => $category->id, + 'user_id' => auth()->id(), + 'paid_by_user_id' => $paidByUserId, + 'date' => $this->date, + 'date_effective' => $this->dateEffective, + 'amount' => $this->amount, + 'details' => $this->details, + ]); + } + + $wasEditing = $this->editingTransactionId !== null; + + $this->reset(['categoryId', 'amount', 'details', 'paidByUserId', 'editingTransactionId']); $this->date = now()->toDateString(); $this->dateEffective = now()->toDateString(); - $this->dispatch('transaction-added'); + $this->dispatch($wasEditing ? 'transaction-updated' : 'transaction-added'); + } + + public function startEditTransaction(int $transactionId): void + { + $household = auth()->user()->currentHousehold; + $transaction = $household->transactions()->findOrFail($transactionId); + + $this->editingTransactionId = $transaction->id; + $this->categoryId = $transaction->category_id; + $this->amount = (string) $transaction->amount; + $this->details = (string) $transaction->details; + $this->date = $transaction->date->toDateString(); + $this->dateEffective = $transaction->date_effective->toDateString(); + $this->paidByUserId = $transaction->paid_by_user_id ? (string) $transaction->paid_by_user_id : ''; + } + + public function cancelEditTransaction(): void + { + $this->reset(['categoryId', 'amount', 'details', 'paidByUserId', 'editingTransactionId']); + $this->date = now()->toDateString(); + $this->dateEffective = now()->toDateString(); } /** @@ -256,6 +294,8 @@ class Index extends Component ->sortByDesc('total') ->values(); + $recentTransactions = $allTransactions->sortByDesc('id')->take(5)->values(); + return view('livewire.suivi.index', [ 'transactions' => $transactions, 'running' => $running, @@ -263,6 +303,7 @@ class Index extends Component 'categoryProgress' => $categoryProgress, 'members' => $members, 'paidBySummary' => $paidBySummary, + 'recentTransactions' => $recentTransactions, 'nbrThisYear' => $allTransactions->filter(fn (Transaction $t) => $t->date->year === $year)->count(), 'nbrTotal' => $allTransactions->count(), 'lastEntry' => $allTransactions->sortByDesc('date')->first(), diff --git a/resources/views/livewire/suivi/index.blade.php b/resources/views/livewire/suivi/index.blade.php index e03f2d3..13e4967 100644 --- a/resources/views/livewire/suivi/index.blade.php +++ b/resources/views/livewire/suivi/index.blade.php @@ -2,10 +2,11 @@ {{-- Mobile: quick single-purpose entry form --}}
-

Nouvelle opération

+

{{ $editingTransactionId ? 'Modifier l\'opération' : 'Nouvelle opération' }}

@if ($pendingRecurrences->isNotEmpty())
@@ -151,12 +152,23 @@ @endif - +
+ + @if ($editingTransactionId) + + @endif +
- ✓ Opération ajoutée +
+ @if ($recentTransactions->isNotEmpty()) +
+

Dernières opérations

+
    + @foreach ($recentTransactions as $transaction) +
  • +
    +

    + {{ $transaction->category->name }} + @if ($transaction->details) + — {{ $transaction->details }} + @endif +

    +

    {{ $transaction->date->translatedFormat('d M Y') }} · {{ number_format($transaction->amount, 2, ',', ' ') }} €

    +
    +
    + + +
    +
  • + @endforeach +
+
+ @endif +
{{-- Desktop / tablet: full table view --}} @@ -255,6 +301,7 @@ @endif
+

{{ $editingTransactionId ? 'Modifier l\'opération' : 'Nouvelle opération' }}

@@ -292,8 +339,13 @@ @endforeach
-
- Ajouter +
+ {{ $editingTransactionId ? 'Enregistrer' : 'Ajouter' }} + @if ($editingTransactionId) + + @endif
@@ -350,8 +402,11 @@ {{ number_format($running[$transaction->id] ?? 0, 2, ',', ' ') }} € {{ $transaction->date_effective->translatedFormat('d M Y') }} - - + diff --git a/tests/Feature/SuiviTest.php b/tests/Feature/SuiviTest.php index 8037a1e..d10a142 100644 --- a/tests/Feature/SuiviTest.php +++ b/tests/Feature/SuiviTest.php @@ -2,6 +2,7 @@ use App\Livewire\Suivi\Index; use App\Models\User; +use Illuminate\Database\Eloquent\ModelNotFoundException; use Livewire\Livewire; it('records a transaction for the current household', function (): void { @@ -98,6 +99,89 @@ it('resets the form to defaults after adding a transaction', function (): void { expect($component->get('details'))->toBe(''); }); +it('prefills the form when starting to edit a transaction', function (): void { + $user = User::factory()->create(); + $household = $user->currentHousehold; + $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); + $transaction = $household->transactions()->create([ + 'category_id' => $category->id, 'user_id' => $user->id, + 'date' => '2026-03-05', 'date_effective' => '2026-03-06', 'amount' => 1200, 'details' => 'Loyer mars', + ]); + + $component = Livewire::actingAs($user)->test(Index::class)->call('startEditTransaction', $transaction->id); + + expect($component->get('editingTransactionId'))->toBe($transaction->id); + expect($component->get('categoryId'))->toBe($category->id); + expect($component->get('amount'))->toBe('1200.00'); + expect($component->get('details'))->toBe('Loyer mars'); + expect($component->get('date'))->toBe('2026-03-05'); + expect($component->get('dateEffective'))->toBe('2026-03-06'); +}); + +it('updates an existing transaction instead of creating a new one', function (): void { + $user = User::factory()->create(); + $household = $user->currentHousehold; + $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); + $newCategory = $household->categories()->create(['name' => 'Courses', 'type' => 'depense', 'position' => 2]); + $transaction = $household->transactions()->create([ + 'category_id' => $category->id, 'user_id' => $user->id, + 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 1200, 'details' => 'Loyer', + ]); + + Livewire::actingAs($user) + ->test(Index::class) + ->call('startEditTransaction', $transaction->id) + ->set('categoryId', $newCategory->id) + ->set('amount', '150') + ->set('details', 'Courses rectifiées') + ->call('addTransaction') + ->assertDispatched('transaction-updated'); + + expect($household->transactions()->count())->toBe(1); + + $transaction->refresh(); + expect($transaction->category_id)->toBe($newCategory->id); + expect((float) $transaction->amount)->toBe(150.0); + expect($transaction->details)->toBe('Courses rectifiées'); +}); + +it('cancels editing and resets the form without touching the transaction', function (): void { + $user = User::factory()->create(); + $household = $user->currentHousehold; + $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); + $transaction = $household->transactions()->create([ + 'category_id' => $category->id, 'user_id' => $user->id, + 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 1200, 'details' => 'Loyer', + ]); + + $component = Livewire::actingAs($user) + ->test(Index::class) + ->call('startEditTransaction', $transaction->id) + ->call('cancelEditTransaction'); + + expect($component->get('editingTransactionId'))->toBeNull(); + expect($component->get('categoryId'))->toBeNull(); + expect($component->get('amount'))->toBe(''); + + $transaction->refresh(); + expect((float) $transaction->amount)->toBe(1200.0); +}); + +it('does not allow editing a transaction belonging to another household', function (): void { + $owner = User::factory()->create(); + $intruder = User::factory()->create(); + $category = $owner->currentHousehold->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]); + $transaction = $owner->currentHousehold->transactions()->create([ + 'category_id' => $category->id, 'user_id' => $owner->id, + 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 1200, + ]); + + expect(fn () => Livewire::actingAs($intruder) + ->test(Index::class) + ->call('startEditTransaction', $transaction->id) + )->toThrow(ModelNotFoundException::class); +}); + it('lists active recurring transactions due this month as pending', function (): void { $this->travelTo(now()->setDate(2026, 3, 10));