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,56 @@
<div class="max-w-3xl mx-auto py-6 space-y-6">
<div class="bg-surface shadow rounded-lg p-6">
<h2 class="text-lg font-semibold text-text">{{ $household->name }}</h2>
<h3 class="mt-6 text-sm font-medium text-text-muted">Membres</h3>
<ul class="mt-2 divide-y divide-border">
@foreach ($household->members as $member)
<li class="py-2 flex items-center justify-between">
<div>
<span class="text-sm text-text">{{ $member->name }}</span>
<span class="text-xs text-text-muted ml-2">{{ $member->pivot->role }}</span>
</div>
@if ($member->id !== $household->owner_id && $member->id !== auth()->id())
<button wire:click="removeMember({{ $member->id }})" wire:confirm="Retirer ce membre du foyer ?" aria-label="Retirer {{ $member->name }} du foyer" class="text-xs text-danger hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
Retirer
</button>
@endif
</li>
@endforeach
</ul>
@if ($household->invites->isNotEmpty())
<h3 class="mt-6 text-sm font-medium text-text-muted">Invitations en attente</h3>
<ul class="mt-2 divide-y divide-border">
@foreach ($household->invites as $pendingInvite)
<li class="py-2 flex items-center justify-between">
<span class="text-sm text-text-muted">{{ $pendingInvite->email }}</span>
<button wire:click="revokeInvite({{ $pendingInvite->id }})" aria-label="Annuler l'invitation de {{ $pendingInvite->email }}" class="text-xs text-danger hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
Annuler
</button>
</li>
@endforeach
</ul>
@endif
<form wire:submit="invite" class="mt-6 flex items-end gap-3">
<div class="flex-1">
<x-input-label for="inviteEmail" value="Inviter par email" />
<x-text-input wire:model="inviteEmail" id="inviteEmail" type="email" class="block mt-1 w-full" />
<x-input-error :messages="$errors->get('inviteEmail')" class="mt-2" />
</div>
<div>
<x-input-label for="inviteRole" value="Rôle" />
<select wire:model="inviteRole" id="inviteRole" class="block mt-1 w-full rounded-md bg-surface border-border-strong text-text text-sm focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
<option value="member">Membre</option>
<option value="owner">Propriétaire</option>
</select>
</div>
<x-primary-button>Envoyer</x-primary-button>
</form>
<div x-data="{ shown: false }" x-on:invite-sent.window="shown = true; setTimeout(() => shown = false, 3000)" x-show="shown" x-cloak role="status" class="mt-3 text-sm text-positive">
Invitation envoyée.
</div>
</div>
</div>