diff --git a/app/Livewire/Recurring/Index.php b/app/Livewire/Recurring/Index.php index 19acf39..4c3c413 100644 --- a/app/Livewire/Recurring/Index.php +++ b/app/Livewire/Recurring/Index.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Livewire\Recurring; +use App\Models\Household; use App\Models\RecurringTransaction; use Livewire\Attributes\Layout; use Livewire\Component; @@ -19,6 +20,8 @@ class Index extends Component public string $dayOfMonth = '1'; + public string $paidByUserId = ''; + public ?int $editingId = null; public string $editingName = ''; @@ -27,6 +30,23 @@ class Index extends Component public string $editingDayOfMonth = '1'; + public string $editingPaidByUserId = ''; + + /** + * Vide ou "0" = dépense commune (aucun membre précis). Vérifie que la + * valeur, si fournie, correspond bien à un membre du foyer courant. + */ + private function resolvePaidByUserId(Household $household, string $value): ?int + { + if ($value === '' || $value === '0') { + return null; + } + + $isMember = $household->members()->whereKey((int) $value)->exists(); + + return $isMember ? (int) $value : null; + } + public function add(): void { $this->validate([ @@ -34,6 +54,7 @@ class Index extends Component 'categoryId' => ['required', 'exists:categories,id'], 'amount' => ['required', 'numeric', 'min:0.01'], 'dayOfMonth' => ['required', 'integer', 'min:1', 'max:28'], + 'paidByUserId' => ['nullable', 'integer'], ]); $household = auth()->user()->currentHousehold; @@ -41,12 +62,13 @@ class Index extends Component $household->recurringTransactions()->create([ 'category_id' => $category->id, + 'paid_by_user_id' => $this->resolvePaidByUserId($household, $this->paidByUserId), 'name' => $this->name, 'amount' => $this->amount, 'day_of_month' => $this->dayOfMonth, ]); - $this->reset(['name', 'categoryId', 'amount', 'dayOfMonth']); + $this->reset(['name', 'categoryId', 'amount', 'dayOfMonth', 'paidByUserId']); $this->dayOfMonth = '1'; } @@ -59,6 +81,7 @@ class Index extends Component $this->editingName = $recurring->name; $this->editingAmount = (string) $recurring->amount; $this->editingDayOfMonth = (string) $recurring->day_of_month; + $this->editingPaidByUserId = $recurring->paid_by_user_id ? (string) $recurring->paid_by_user_id : ''; } public function saveEdit(): void @@ -70,12 +93,16 @@ class Index extends Component 'editingName' => ['required', 'string', 'max:255'], 'editingAmount' => ['required', 'numeric', 'min:0.01'], 'editingDayOfMonth' => ['required', 'integer', 'min:1', 'max:28'], + 'editingPaidByUserId' => ['nullable', 'integer'], ]); + $household = auth()->user()->currentHousehold; + $recurring->update([ 'name' => $this->editingName, 'amount' => $this->editingAmount, 'day_of_month' => $this->editingDayOfMonth, + 'paid_by_user_id' => $this->resolvePaidByUserId($household, $this->editingPaidByUserId), ]); $this->editingId = null; @@ -107,7 +134,7 @@ class Index extends Component $household = auth()->user()->currentHousehold; $recurrences = $household->recurringTransactions() - ->with('category') + ->with(['category', 'paidBy']) ->orderBy('day_of_month') ->get(); @@ -117,9 +144,12 @@ class Index extends Component ->orderBy('position') ->get(); + $members = $household->members()->orderBy('name')->get(); + return view('livewire.recurring.index', [ 'recurrences' => $recurrences, 'categories' => $categories, + 'members' => $members, ]); } } diff --git a/app/Livewire/Suivi/Index.php b/app/Livewire/Suivi/Index.php index 2212214..ae15f9d 100644 --- a/app/Livewire/Suivi/Index.php +++ b/app/Livewire/Suivi/Index.php @@ -7,6 +7,7 @@ namespace App\Livewire\Suivi; use App\Enums\CategoryType; use App\Models\BudgetLine; use App\Models\Category; +use App\Models\Household; use App\Models\RecurringTransaction; use App\Models\Transaction; use Livewire\Attributes\Layout; @@ -25,6 +26,10 @@ class Index extends Component public string $details = ''; + public string $paidByUserId = ''; + + public string $filterPaidBy = ''; + public function mount(): void { $this->date = now()->toDateString(); @@ -39,27 +44,45 @@ class Index extends Component 'categoryId' => ['required', 'exists:categories,id'], 'amount' => ['required', 'numeric'], 'details' => ['nullable', 'string', 'max:255'], + 'paidByUserId' => ['nullable', 'integer'], ]); $household = auth()->user()->currentHousehold; $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, ]); - $this->reset(['categoryId', 'amount', 'details']); + $this->reset(['categoryId', 'amount', 'details', 'paidByUserId']); $this->date = now()->toDateString(); $this->dateEffective = now()->toDateString(); $this->dispatch('transaction-added'); } + /** + * Vide ou "0" = dépense commune (aucun membre précis). Vérifie que la + * valeur, si fournie, correspond bien à un membre du foyer courant. + */ + private function resolvePaidByUserId(Household $household, string $value): ?int + { + if ($value === '' || $value === '0') { + return null; + } + + $isMember = $household->members()->whereKey((int) $value)->exists(); + + return $isMember ? (int) $value : null; + } + public function deleteTransaction(int $transactionId): void { $household = auth()->user()->currentHousehold; @@ -82,6 +105,7 @@ class Index extends Component 'category_id' => $recurring->category_id, 'recurring_transaction_id' => $recurring->id, 'user_id' => auth()->id(), + 'paid_by_user_id' => $recurring->paid_by_user_id, 'date' => $today->toDateString(), 'date_effective' => $today->toDateString(), 'amount' => $amount, @@ -139,13 +163,10 @@ class Index extends Component { $household = auth()->user()->currentHousehold; - $transactions = $household->transactions() - ->with('category') - ->orderByDesc('date') - ->orderByDesc('id') - ->get(); - - $chronological = $transactions->sortBy(['date', 'id'])->values(); + // Le solde cumulé porte sur l'ensemble des transactions, indépendamment + // du filtre "payé par" appliqué à la liste affichée. + $allTransactions = $household->transactions()->with('category')->get(); + $chronological = $allTransactions->sortBy(['date', 'id'])->values(); $running = []; $balance = 0.0; @@ -155,6 +176,20 @@ class Index extends Component $running[$t->id] = $balance; } + $transactionsQuery = $household->transactions() + ->with(['category', 'paidBy']); + + if ($this->filterPaidBy === 'common') { + $transactionsQuery->whereNull('paid_by_user_id'); + } elseif ($this->filterPaidBy !== '') { + $transactionsQuery->where('paid_by_user_id', (int) $this->filterPaidBy); + } + + $transactions = $transactionsQuery + ->orderByDesc('date') + ->orderByDesc('id') + ->get(); + $year = (int) now()->year; $month = (int) now()->month; @@ -205,14 +240,32 @@ class Index extends Component ]]; }); + $members = $household->members()->orderBy('name')->get(); + + $paidBySummary = $household->transactions() + ->with(['category', 'paidBy']) + ->whereHas('category', fn ($q) => $q->where('type', CategoryType::Depense)) + ->whereYear('date_effective', $year) + ->whereMonth('date_effective', $month) + ->get() + ->groupBy(fn (Transaction $t) => $t->paid_by_user_id ?? 'common') + ->map(fn ($group, $key) => [ + 'label' => $key === 'common' ? 'Commun' : $group->first()->paidBy->name, + 'total' => (float) $group->sum('amount'), + ]) + ->sortByDesc('total') + ->values(); + return view('livewire.suivi.index', [ 'transactions' => $transactions, 'running' => $running, 'categories' => $categories, 'categoryProgress' => $categoryProgress, - 'nbrThisYear' => $transactions->filter(fn (Transaction $t) => $t->date->year === $year)->count(), - 'nbrTotal' => $transactions->count(), - 'lastEntry' => $transactions->first(), + 'members' => $members, + 'paidBySummary' => $paidBySummary, + 'nbrThisYear' => $allTransactions->filter(fn (Transaction $t) => $t->date->year === $year)->count(), + 'nbrTotal' => $allTransactions->count(), + 'lastEntry' => $allTransactions->sortByDesc('date')->first(), 'unaffected' => $unaffected, 'pendingRecurrences' => $pendingRecurrences, ]); diff --git a/app/Models/RecurringTransaction.php b/app/Models/RecurringTransaction.php index 32ddf99..e26202d 100644 --- a/app/Models/RecurringTransaction.php +++ b/app/Models/RecurringTransaction.php @@ -11,7 +11,7 @@ use Illuminate\Database\Eloquent\Relations\HasMany; class RecurringTransaction extends Model { protected $fillable = [ - 'household_id', 'category_id', 'name', 'amount', 'day_of_month', + 'household_id', 'category_id', 'paid_by_user_id', 'name', 'amount', 'day_of_month', 'active', 'last_generated_year', 'last_generated_month', ]; @@ -33,6 +33,11 @@ class RecurringTransaction extends Model return $this->belongsTo(Category::class); } + public function paidBy(): BelongsTo + { + return $this->belongsTo(User::class, 'paid_by_user_id'); + } + public function transactions(): HasMany { return $this->hasMany(Transaction::class); diff --git a/app/Models/Transaction.php b/app/Models/Transaction.php index 72248f2..8cb6c23 100644 --- a/app/Models/Transaction.php +++ b/app/Models/Transaction.php @@ -9,7 +9,7 @@ use Illuminate\Database\Eloquent\Relations\BelongsTo; class Transaction extends Model { - protected $fillable = ['household_id', 'category_id', 'recurring_transaction_id', 'user_id', 'date', 'date_effective', 'amount', 'details']; + protected $fillable = ['household_id', 'category_id', 'recurring_transaction_id', 'user_id', 'paid_by_user_id', 'date', 'date_effective', 'amount', 'details']; protected function casts(): array { @@ -34,4 +34,9 @@ class Transaction extends Model { return $this->belongsTo(User::class); } + + public function paidBy(): BelongsTo + { + return $this->belongsTo(User::class, 'paid_by_user_id'); + } } diff --git a/database/migrations/2026_08_01_061025_add_paid_by_user_id_to_transactions_table.php b/database/migrations/2026_08_01_061025_add_paid_by_user_id_to_transactions_table.php new file mode 100644 index 0000000..50263c4 --- /dev/null +++ b/database/migrations/2026_08_01_061025_add_paid_by_user_id_to_transactions_table.php @@ -0,0 +1,29 @@ +foreignId('paid_by_user_id')->nullable()->after('user_id')->constrained('users')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('transactions', function (Blueprint $table) { + $table->dropConstrainedForeignId('paid_by_user_id'); + }); + } +}; diff --git a/database/migrations/2026_08_01_061052_add_paid_by_user_id_to_recurring_transactions_table.php b/database/migrations/2026_08_01_061052_add_paid_by_user_id_to_recurring_transactions_table.php new file mode 100644 index 0000000..4e1a6dd --- /dev/null +++ b/database/migrations/2026_08_01_061052_add_paid_by_user_id_to_recurring_transactions_table.php @@ -0,0 +1,28 @@ +foreignId('paid_by_user_id')->nullable()->after('category_id')->constrained('users')->nullOnDelete(); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + Schema::table('recurring_transactions', function (Blueprint $table) { + $table->dropConstrainedForeignId('paid_by_user_id'); + }); + } +}; diff --git a/resources/views/livewire/recurring/index.blade.php b/resources/views/livewire/recurring/index.blade.php index 0cec48e..c16cbc2 100644 --- a/resources/views/livewire/recurring/index.blade.php +++ b/resources/views/livewire/recurring/index.blade.php @@ -11,7 +11,7 @@ @forelse ($recurrences as $recurring)