Initial commit: SuiviCPT personal finance tracker

Laravel 11 + Livewire 3/Volt + Alpine.js + Tailwind app for household
budget planning, transaction tracking, and financial dashboards.

- Multi-tenant households with member invites
- Plan: monthly/yearly budget per category with overrides
- Suivi: transaction log with running balance (desktop + mobile quick-entry)
- Dashboard: budget vs tracked breakdown + Chart.js donuts
- Flux: Sankey diagram of income allocation
- WCAG 2.1 AA color tokens, dark mode, accessible tables/forms
- 50 Pest tests

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
jeremy bayse
2026-07-25 22:39:42 +02:00
co-authored by Claude Sonnet 5
commit d8dc57a5df
164 changed files with 19880 additions and 0 deletions
@@ -0,0 +1,100 @@
<div class="max-w-[1600px] mx-auto py-6 px-4 sm:px-6 space-y-6">
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-text">Plan</h1>
<div class="flex items-center gap-2">
<button wire:click="setYear({{ $year - 1 }})" aria-label="Année précédente" class="px-2 py-1 text-sm rounded-md border border-border-strong text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">&lsaquo;</button>
<span class="text-sm font-medium text-text-muted w-16 text-center">{{ $year }}</span>
<button wire:click="setYear({{ $year + 1 }})" aria-label="Année suivante" class="px-2 py-1 text-sm rounded-md border border-border-strong text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">&rsaquo;</button>
</div>
</div>
@php
$colors = [
'revenu' => ['bg' => 'bg-positive-fill', 'row' => 'bg-positive-surface/40'],
'depense' => ['bg' => 'bg-negative-fill', 'row' => 'bg-negative-surface/40'],
'epargne' => ['bg' => 'bg-info-fill', 'row' => 'bg-info-surface/40'],
];
@endphp
@foreach ($types as $type)
@php $section = $sections[$type->value]; @endphp
<div class="bg-surface shadow rounded-lg overflow-hidden">
<div class="{{ $colors[$type->value]['bg'] }} px-4 py-2">
<h2 class="text-white font-semibold">{{ $type->label() }}</h2>
</div>
<div class="p-2">
<table class="w-full table-fixed text-[11px] sm:text-xs border-collapse">
<caption class="sr-only">Budget {{ Str::lower($type->label()) }} par mois pour {{ $year }}</caption>
<colgroup>
<col style="width: 12%">
<col style="width: 6.77%">
@for ($m = 1; $m <= 12; $m++)
<col style="width: 6.77%">
@endfor
</colgroup>
<thead>
<tr class="text-text-muted border-b border-border">
<th scope="col" class="text-left px-1 py-2 truncate">Catégorie</th>
<th scope="col" class="px-0.5 py-2">Défaut</th>
@foreach ($months as $m)
<th scope="col" class="px-0.5 py-2">{{ $m }}</th>
@endforeach
</tr>
</thead>
<tbody>
@forelse ($section['categories'] as $entry)
<tr class="border-b border-border/60">
<th scope="row" class="px-1 py-1 truncate text-text font-normal text-left" title="{{ $entry['category']->name }}">{{ $entry['category']->name }}</th>
<td class="px-0.5 py-1">
<label class="sr-only" for="amount-{{ $entry['category']->id }}-default">Montant par défaut pour {{ $entry['category']->name }}</label>
<input
id="amount-{{ $entry['category']->id }}-default"
type="number" step="0.01"
value="{{ $entry['row']['default'] }}"
wire:change="updateAmount({{ $entry['category']->id }}, null, $event.target.value)"
class="w-full min-w-0 bg-surface text-right text-[11px] sm:text-xs px-1 py-0.5 rounded border-border-strong text-text font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
</td>
@for ($m = 1; $m <= 12; $m++)
<td class="px-0.5 py-1">
<label class="sr-only" for="amount-{{ $entry['category']->id }}-{{ $m }}">Montant pour {{ $entry['category']->name }}, {{ $months[$m - 1] }}</label>
<input
id="amount-{{ $entry['category']->id }}-{{ $m }}"
type="number" step="0.01"
value="{{ $entry['row'][$m] }}"
wire:change="updateAmount({{ $entry['category']->id }}, {{ $m }}, $event.target.value)"
class="w-full min-w-0 bg-surface text-right text-[11px] sm:text-xs px-1 py-0.5 rounded border-border-strong text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
</td>
@endfor
</tr>
@empty
<tr><td colspan="14" class="px-4 py-3 text-sm text-text-muted">Aucune catégorie. Ajoutez-en dans <a href="{{ route('categories') }}" wire:navigate class="underline text-brand rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">Catégories</a>.</td></tr>
@endforelse
</tbody>
@if ($section['categories']->isNotEmpty())
<tfoot>
<tr class="font-semibold {{ $colors[$type->value]['row'] }} text-text">
<th scope="row" class="px-1 py-2 truncate font-semibold text-left">Total</th>
<td class="px-0.5 py-2 text-right">{{ number_format($section['totals']['default'], 0, ',', ' ') }} </td>
@for ($m = 1; $m <= 12; $m++)
<td class="px-0.5 py-2 text-right">{{ number_format($section['totals'][$m], 0, ',', ' ') }} </td>
@endfor
</tr>
@if ($type->value === 'revenu')
<tr class="text-[11px] sm:text-xs text-text-muted">
<th scope="row" class="px-1 py-1 truncate font-normal text-left">Reste à affecter</th>
<td class="px-0.5 py-1 text-right"></td>
@for ($m = 1; $m <= 12; $m++)
<td class="px-0.5 py-1 text-right {{ $section['remaining'][$m] < 0 ? 'text-danger font-semibold' : '' }}">{{ $section['remaining'][$m] < 0 ? '' : '+' }}{{ number_format(abs($section['remaining'][$m]), 0, ',', ' ') }} </td>
@endfor
</tr>
@endif
</tfoot>
@endif
</table>
</div>
</div>
@endforeach
</div>