Add recurring transactions with monthly confirmation flow
New "Récurrences" section lets users define recurring depense/epargne models (name, category, default amount, day of month). Nothing is created automatically: from the day a recurrence is due, it appears as a pending proposal in Suivi (desktop + mobile) with an editable amount, so variable bills (electricity, etc.) can be adjusted before confirming. Dismissing a proposal skips it for the current month without creating a transaction, and it reappears the following month. - recurring_transactions table (household/category scoped, active flag, last_generated_year/month to prevent duplicate generation) - transactions.recurring_transaction_id nullable FK to trace origin - RecurringTransactionPolicy mirrors CategoryPolicy tenant scoping - 10 new Pest tests covering CRUD, tenant isolation, and the pending/confirm/dismiss lifecycle across month boundaries (travelTo) 58/58 tests pass, Pint clean, verified end-to-end against production data (test recurrence created and cleaned up). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
674c43afea
commit
b78c314569
@@ -7,6 +7,43 @@
|
||||
>
|
||||
<h1 class="text-xl font-semibold text-text">Nouvelle opération</h1>
|
||||
|
||||
@if ($pendingRecurrences->isNotEmpty())
|
||||
<div class="space-y-3">
|
||||
<p class="text-sm font-medium text-text-muted">À confirmer ce mois-ci</p>
|
||||
@foreach ($pendingRecurrences as $recurring)
|
||||
<div class="bg-surface-alt border border-border-strong rounded-lg p-3" wire:key="pending-mobile-{{ $recurring->id }}">
|
||||
<p class="text-sm text-text">{{ $recurring->name }} <span class="text-text-muted">— {{ $recurring->category->name }}</span></p>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<label for="pending-amount-mobile-{{ $recurring->id }}" class="sr-only">Montant pour {{ $recurring->name }}</label>
|
||||
<input
|
||||
x-ref="amount{{ $recurring->id }}"
|
||||
id="pending-amount-mobile-{{ $recurring->id }}"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value="{{ $recurring->amount }}"
|
||||
class="flex-1 bg-surface border-border-strong text-text rounded-md text-sm py-1.5"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
x-on:click="$wire.confirmRecurrence({{ $recurring->id }}, $refs.amount{{ $recurring->id }}.value)"
|
||||
class="text-xs text-white bg-brand px-3 py-1.5 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
>
|
||||
Confirmer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="dismissRecurrence({{ $recurring->id }})"
|
||||
wire:confirm="Ignorer cette récurrence pour ce mois ?"
|
||||
class="text-xs text-text-muted hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
>
|
||||
Ignorer
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<form wire:submit="addTransaction" class="space-y-5">
|
||||
<fieldset>
|
||||
<legend class="text-sm font-medium text-text-muted mb-2">Catégorie</legend>
|
||||
@@ -108,6 +145,50 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($pendingRecurrences->isNotEmpty())
|
||||
<div class="bg-surface shadow rounded-lg overflow-hidden">
|
||||
<div class="px-4 py-2 bg-brand">
|
||||
<h2 class="text-white font-semibold text-sm">À confirmer ce mois-ci</h2>
|
||||
</div>
|
||||
<ul class="divide-y divide-border">
|
||||
@foreach ($pendingRecurrences as $recurring)
|
||||
<li class="px-4 py-3 flex items-center justify-between gap-3" wire:key="pending-desktop-{{ $recurring->id }}">
|
||||
<div class="min-w-0">
|
||||
<p class="text-sm text-text">{{ $recurring->name }} <span class="text-text-muted">— {{ $recurring->category->name }}</span></p>
|
||||
<p class="text-xs text-text-muted">le {{ $recurring->day_of_month }} de chaque mois</p>
|
||||
</div>
|
||||
<div class="flex items-center gap-2 shrink-0">
|
||||
<label for="pending-amount-{{ $recurring->id }}" class="sr-only">Montant pour {{ $recurring->name }}</label>
|
||||
<input
|
||||
x-ref="amountDesktop{{ $recurring->id }}"
|
||||
id="pending-amount-{{ $recurring->id }}"
|
||||
type="number"
|
||||
step="0.01"
|
||||
value="{{ $recurring->amount }}"
|
||||
class="w-28 bg-surface border-border-strong text-text rounded-md text-sm py-1"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
x-on:click="$wire.confirmRecurrence({{ $recurring->id }}, $refs.amountDesktop{{ $recurring->id }}.value)"
|
||||
class="text-xs text-white bg-brand px-3 py-1.5 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
>
|
||||
Confirmer
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
wire:click="dismissRecurrence({{ $recurring->id }})"
|
||||
wire:confirm="Ignorer cette récurrence pour ce mois ?"
|
||||
class="text-xs text-text-muted hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
|
||||
>
|
||||
Ignorer
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
@endforeach
|
||||
</ul>
|
||||
</div>
|
||||
@endif
|
||||
|
||||
<div class="bg-surface shadow rounded-lg p-4">
|
||||
<form wire:submit="addTransaction" class="grid grid-cols-1 sm:grid-cols-6 gap-3 items-end">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user