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>
101 lines
6.9 KiB
PHP
101 lines
6.9 KiB
PHP
<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">‹</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">›</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>
|