diff --git a/app/Livewire/Suivi/Index.php b/app/Livewire/Suivi/Index.php
index 9c64f80..8e13c1b 100644
--- a/app/Livewire/Suivi/Index.php
+++ b/app/Livewire/Suivi/Index.php
@@ -6,6 +6,7 @@ namespace App\Livewire\Suivi;
use App\Enums\CategoryType;
use App\Models\BudgetLine;
+use App\Models\Category;
use App\Models\RecurringTransaction;
use App\Models\Transaction;
use Livewire\Attributes\Layout;
@@ -108,6 +109,32 @@ class Index extends Component
]);
}
+ /**
+ * Budget planifié pour cette catégorie sur le mois en cours, en résolvant
+ * l'override du mois s'il existe sinon le montant par défaut de l'année.
+ */
+ private function budgetForCurrentMonth(Category $category, int $year, int $month): ?float
+ {
+ $lines = $category->budgetLines()->where('year', $year)->get();
+
+ $override = $lines->firstWhere('month', $month);
+ if ($override) {
+ return (float) $override->amount;
+ }
+
+ $default = $lines->firstWhere('month', null);
+
+ return $default ? (float) $default->amount : null;
+ }
+
+ private function trackedForCategoryThisMonth(Category $category, int $year, int $month): float
+ {
+ return (float) $category->transactions()
+ ->whereYear('date_effective', $year)
+ ->whereMonth('date_effective', $month)
+ ->sum('amount');
+ }
+
public function render()
{
$household = auth()->user()->currentHousehold;
@@ -159,10 +186,27 @@ class Index extends Component
->sortBy('day_of_month')
->values();
+ $categories = $household->categories()->orderBy('type')->orderBy('position')->get();
+
+ $categoryProgress = $categories
+ ->whereIn('type', [CategoryType::Depense, CategoryType::Epargne])
+ ->mapWithKeys(function (Category $category) use ($year, $month) {
+ $budget = $this->budgetForCurrentMonth($category, $year, $month);
+
+ if ($budget === null || $budget <= 0) {
+ return [$category->id => null];
+ }
+
+ $tracked = $this->trackedForCategoryThisMonth($category, $year, $month);
+
+ return [$category->id => min(100, round($tracked / $budget * 100))];
+ });
+
return view('livewire.suivi.index', [
'transactions' => $transactions,
'running' => $running,
- 'categories' => $household->categories()->orderBy('type')->orderBy('position')->get(),
+ 'categories' => $categories,
+ 'categoryProgress' => $categoryProgress,
'nbrThisYear' => $transactions->filter(fn (Transaction $t) => $t->date->year === $year)->count(),
'nbrTotal' => $transactions->count(),
'lastEntry' => $transactions->first(),
diff --git a/resources/views/livewire/suivi/index.blade.php b/resources/views/livewire/suivi/index.blade.php
index a90c01d..d52e62b 100644
--- a/resources/views/livewire/suivi/index.blade.php
+++ b/resources/views/livewire/suivi/index.blade.php
@@ -55,14 +55,30 @@
{{ $meta['label'] }}
@foreach ($typeCategories as $category)
+ @php
+ $isActive = (string) $categoryId === (string) $category->id;
+ $progress = $categoryProgress[$category->id] ?? null;
+ @endphp
@endforeach
diff --git a/tests/Feature/SuiviTest.php b/tests/Feature/SuiviTest.php
index 302d5c1..000b44f 100644
--- a/tests/Feature/SuiviTest.php
+++ b/tests/Feature/SuiviTest.php
@@ -184,6 +184,54 @@ it('shows the recurrence again the following month after being confirmed', funct
expect($component->viewData('pendingRecurrences')->pluck('id'))->toContain($recurring->id);
});
+it('computes the percentage of budget already tracked for a category this month', function (): void {
+ $this->travelTo(now()->setDate(2026, 3, 10));
+
+ $user = User::factory()->create();
+ $household = $user->currentHousehold;
+ $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]);
+
+ $category->budgetLines()->create(['household_id' => $household->id, 'year' => 2026, 'month' => null, 'amount' => 1000]);
+ $household->transactions()->create([
+ 'category_id' => $category->id, 'user_id' => $user->id,
+ 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 600,
+ ]);
+
+ $component = Livewire::actingAs($user)->test(Index::class);
+
+ expect($component->viewData('categoryProgress')[$category->id])->toBe(60.0);
+});
+
+it('caps the category progress percentage at 100', function (): void {
+ $this->travelTo(now()->setDate(2026, 3, 10));
+
+ $user = User::factory()->create();
+ $household = $user->currentHousehold;
+ $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]);
+
+ $category->budgetLines()->create(['household_id' => $household->id, 'year' => 2026, 'month' => null, 'amount' => 1000]);
+ $household->transactions()->create([
+ 'category_id' => $category->id, 'user_id' => $user->id,
+ 'date' => '2026-03-05', 'date_effective' => '2026-03-05', 'amount' => 1500,
+ ]);
+
+ $component = Livewire::actingAs($user)->test(Index::class);
+
+ expect($component->viewData('categoryProgress')[$category->id])->toBe(100);
+});
+
+it('returns no progress for a category without a budget defined', function (): void {
+ $this->travelTo(now()->setDate(2026, 3, 10));
+
+ $user = User::factory()->create();
+ $household = $user->currentHousehold;
+ $category = $household->categories()->create(['name' => 'Loyer', 'type' => 'depense', 'position' => 1]);
+
+ $component = Livewire::actingAs($user)->test(Index::class);
+
+ expect($component->viewData('categoryProgress')[$category->id])->toBeNull();
+});
+
it('does not show another household transactions', function (): void {
$ownerA = User::factory()->create();
$ownerB = User::factory()->create();