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:
@@ -0,0 +1,35 @@
|
||||
<div class="max-w-md mx-auto mt-10 bg-surface shadow rounded-lg p-6 text-center">
|
||||
@if ($invalid)
|
||||
<h1 class="text-lg font-semibold text-text">Invitation invalide ou expirée</h1>
|
||||
<p class="mt-2 text-sm text-text-muted">Demandez une nouvelle invitation à l'administrateur du foyer.</p>
|
||||
<a href="{{ route('login') }}" wire:navigate class="mt-4 inline-block text-brand underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">Retour à la connexion</a>
|
||||
@else
|
||||
<h1 class="text-lg font-semibold text-text">
|
||||
Rejoindre {{ $invite->household->name }}
|
||||
</h1>
|
||||
<p class="mt-2 text-sm text-text-muted">
|
||||
{{ $invite->inviter->name }} vous invite à rejoindre ce foyer sur SuiviCPT.
|
||||
</p>
|
||||
|
||||
@auth
|
||||
@if (auth()->user()->email === $invite->email)
|
||||
<button wire:click="accept" class="mt-4 inline-flex items-center px-4 py-2 bg-brand text-white text-sm rounded-md hover:bg-brand-emphasis focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface">
|
||||
Accepter l'invitation
|
||||
</button>
|
||||
@else
|
||||
<p class="mt-4 text-sm text-danger">
|
||||
Cette invitation est destinée à {{ $invite->email }}. Déconnectez-vous pour l'accepter avec le bon compte.
|
||||
</p>
|
||||
@endif
|
||||
@else
|
||||
<div class="mt-4 flex flex-col gap-2">
|
||||
<a href="{{ route('register', ['invite' => $invite->token]) }}" wire:navigate class="inline-flex items-center justify-center px-4 py-2 bg-brand text-white text-sm rounded-md hover:bg-brand-emphasis focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface">
|
||||
Créer un compte et rejoindre
|
||||
</a>
|
||||
<a href="{{ route('login') }}" wire:navigate class="text-sm text-brand underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
|
||||
J'ai déjà un compte
|
||||
</a>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@@ -0,0 +1,12 @@
|
||||
<div class="max-w-md mx-auto mt-10 bg-surface shadow rounded-lg p-6">
|
||||
<h1 class="text-lg font-semibold text-text">Créer votre foyer</h1>
|
||||
<p class="mt-1 text-sm text-text-muted">Vous n'appartenez à aucun foyer. Créez-en un pour commencer.</p>
|
||||
|
||||
<form wire:submit="create" class="mt-4">
|
||||
<x-input-label for="name" value="Nom du foyer" />
|
||||
<x-text-input wire:model="name" id="name" class="block mt-1 w-full" autofocus />
|
||||
<x-input-error :messages="$errors->get('name')" class="mt-2" />
|
||||
|
||||
<x-primary-button class="mt-4">Créer</x-primary-button>
|
||||
</form>
|
||||
</div>
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user