Files
suivicpt/resources/views/livewire/suivi/index.blade.php
T
jeremy bayseandClaude Sonnet 5 613220f43a Add budget progress gauge inside mobile category buttons
Each depense/epargne category button on the mobile quick-entry form
now shows a fill bar plus percentage of its monthly budget already
tracked, so the user sees at a glance which categories are close to
their limit while picking one. Categories without a budget defined
in Plan show no gauge (avoids a misleading 0%). Capped at 100% —
overspend detail already lives on the dashboard's "Manque" column.

3 new Pest tests for the percentage calculation (normal, capped,
no-budget cases). 61/61 tests pass, Pint clean, verified on mobile
against production data.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-26 12:48:29 +02:00

297 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<div>
{{-- Mobile: quick single-purpose entry form --}}
<div
class="sm:hidden max-w-md mx-auto px-4 py-6 space-y-5"
x-data="{ toast: false }"
x-on:transaction-added.window="toast = true; setTimeout(() => toast = false, 2000)"
>
<h1 class="text-xl font-semibold text-text">Nouvelle opération</h1>
@if ($pendingRecurrences->isNotEmpty())
<div class="space-y-3">
<p class="text-sm font-medium text-text-muted">À confirmer ce mois-ci</p>
@foreach ($pendingRecurrences as $recurring)
<div class="bg-surface-alt border border-border-strong rounded-lg p-3" wire:key="pending-mobile-{{ $recurring->id }}">
<p class="text-sm text-text">{{ $recurring->name }} <span class="text-text-muted"> {{ $recurring->category->name }}</span></p>
<div class="mt-2 flex items-center gap-2">
<label for="pending-amount-mobile-{{ $recurring->id }}" class="sr-only">Montant pour {{ $recurring->name }}</label>
<input
x-ref="amount{{ $recurring->id }}"
id="pending-amount-mobile-{{ $recurring->id }}"
type="number"
step="0.01"
value="{{ $recurring->amount }}"
class="flex-1 bg-surface border-border-strong text-text rounded-md text-sm py-1.5"
/>
<button
type="button"
x-on:click="$wire.confirmRecurrence({{ $recurring->id }}, $refs.amount{{ $recurring->id }}.value)"
class="text-xs text-white bg-brand px-3 py-1.5 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
Confirmer
</button>
<button
type="button"
wire:click="dismissRecurrence({{ $recurring->id }})"
wire:confirm="Ignorer cette récurrence pour ce mois ?"
class="text-xs text-text-muted hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
Ignorer
</button>
</div>
</div>
@endforeach
</div>
@endif
<form wire:submit="addTransaction" class="space-y-5">
<fieldset>
<legend class="text-sm font-medium text-text-muted mb-2">Catégorie</legend>
<div class="space-y-4">
@foreach (['revenu' => ['label' => 'Revenus', 'ring' => 'ring-positive', 'active' => 'bg-positive-fill text-white border-positive-fill'], 'depense' => ['label' => 'Dépenses', 'ring' => 'ring-negative', 'active' => 'bg-negative-fill text-white border-negative-fill'], 'epargne' => ['label' => 'Épargne', 'ring' => 'ring-info', 'active' => 'bg-info-fill text-white border-info-fill']] as $typeValue => $meta)
@php $typeCategories = $categories->where('type', $typeValue); @endphp
@if ($typeCategories->isNotEmpty())
<div>
<p class="text-xs text-text-muted mb-1.5">{{ $meta['label'] }}</p>
<div class="grid grid-cols-2 gap-2">
@foreach ($typeCategories as $category)
@php
$isActive = (string) $categoryId === (string) $category->id;
$progress = $categoryProgress[$category->id] ?? null;
@endphp
<button
type="button"
wire:click="$set('categoryId', {{ $category->id }})"
aria-pressed="{{ $isActive ? 'true' : 'false' }}"
class="relative min-h-[52px] px-3 py-2 rounded-lg border text-sm font-medium text-left transition overflow-hidden focus:outline-none focus-visible:ring-2 {{ $meta['ring'] }}
{{ $isActive ? $meta['active'] : 'bg-surface border-border-strong text-text' }}"
>
@if ($progress !== null)
<span
aria-hidden="true"
class="absolute inset-y-0 left-0 {{ $isActive ? 'bg-white/25' : 'bg-text/10' }}"
style="width: {{ $progress }}%"
></span>
@endif
<span class="relative flex items-center justify-between gap-2">
<span>{{ $category->name }}</span>
@if ($progress !== null)
<span class="text-xs font-normal {{ $isActive ? 'text-white/80' : 'text-text-muted' }}">{{ $progress }}%</span>
@endif
</span>
</button>
@endforeach
</div>
</div>
@endif
@endforeach
</div>
<x-input-error :messages="$errors->get('categoryId')" class="mt-2" />
</fieldset>
<div>
<label for="mobile-amount" class="text-sm font-medium text-text-muted">Montant</label>
<div class="mt-1 relative">
<input
wire:model="amount"
id="mobile-amount"
type="number"
inputmode="decimal"
step="0.01"
placeholder="0.00"
class="w-full text-3xl font-semibold text-center py-4 rounded-lg bg-surface border-border-strong text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
<span class="absolute right-4 top-1/2 -translate-y-1/2 text-2xl text-text-muted"></span>
</div>
<x-input-error :messages="$errors->get('amount')" class="mt-2" />
</div>
<div>
<label for="mobile-details" class="text-sm font-medium text-text-muted">Détails (optionnel)</label>
<input
wire:model="details"
id="mobile-details"
type="text"
class="mt-1 w-full py-3 px-3 rounded-lg bg-surface border-border-strong text-text focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
/>
</div>
<button
type="submit"
class="w-full py-4 rounded-lg bg-brand text-white text-lg font-semibold focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring focus-visible:ring-offset-2 focus-visible:ring-offset-surface"
>
Ajouter
</button>
</form>
<div
x-show="toast"
x-cloak
x-transition
role="status"
class="fixed bottom-6 left-1/2 -translate-x-1/2 bg-positive-fill text-white text-sm font-medium px-4 py-2 rounded-full shadow-lg"
>
Opération ajoutée
</div>
</div>
{{-- Desktop / tablet: full table view --}}
<div class="hidden sm:block max-w-6xl mx-auto py-6 px-4 sm:px-6 space-y-6">
<h1 class="text-xl font-semibold text-text">Suivi</h1>
<div class="grid grid-cols-2 sm:grid-cols-4 gap-4">
<div class="bg-surface shadow rounded-lg p-4">
<p class="text-xs text-text-muted">Date du jour</p>
<p class="mt-1 text-lg font-semibold text-text">{{ now()->translatedFormat('d M Y') }}</p>
</div>
<div class="bg-surface shadow rounded-lg p-4">
<p class="text-xs text-text-muted">Dernier enreg.</p>
<p class="mt-1 text-lg font-semibold text-brand">{{ $lastEntry?->date?->translatedFormat('d M Y') ?? '—' }}</p>
</div>
<div class="bg-surface shadow rounded-lg p-4">
<p class="text-xs text-text-muted">Nbr enreg.</p>
<p class="mt-1 text-lg font-semibold text-brand">{{ $nbrTotal }}</p>
<p class="text-xs text-text-muted">{{ $nbrThisYear }} cette année</p>
</div>
<div class="bg-surface shadow rounded-lg p-4">
<p class="text-xs text-text-muted">Affectation</p>
<p class="mt-1 text-lg font-semibold text-text">{{ number_format($unaffected, 2, ',', ' ') }} </p>
<p class="text-xs text-text-muted">revenus restent à affecter</p>
</div>
</div>
@if ($pendingRecurrences->isNotEmpty())
<div class="bg-surface shadow rounded-lg overflow-hidden">
<div class="px-4 py-2 bg-brand">
<h2 class="text-white font-semibold text-sm">À confirmer ce mois-ci</h2>
</div>
<ul class="divide-y divide-border">
@foreach ($pendingRecurrences as $recurring)
<li class="px-4 py-3 flex items-center justify-between gap-3" wire:key="pending-desktop-{{ $recurring->id }}">
<div class="min-w-0">
<p class="text-sm text-text">{{ $recurring->name }} <span class="text-text-muted"> {{ $recurring->category->name }}</span></p>
<p class="text-xs text-text-muted">le {{ $recurring->day_of_month }} de chaque mois</p>
</div>
<div class="flex items-center gap-2 shrink-0">
<label for="pending-amount-{{ $recurring->id }}" class="sr-only">Montant pour {{ $recurring->name }}</label>
<input
x-ref="amountDesktop{{ $recurring->id }}"
id="pending-amount-{{ $recurring->id }}"
type="number"
step="0.01"
value="{{ $recurring->amount }}"
class="w-28 bg-surface border-border-strong text-text rounded-md text-sm py-1"
/>
<button
type="button"
x-on:click="$wire.confirmRecurrence({{ $recurring->id }}, $refs.amountDesktop{{ $recurring->id }}.value)"
class="text-xs text-white bg-brand px-3 py-1.5 rounded-md focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
Confirmer
</button>
<button
type="button"
wire:click="dismissRecurrence({{ $recurring->id }})"
wire:confirm="Ignorer cette récurrence pour ce mois ?"
class="text-xs text-text-muted hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
Ignorer
</button>
</div>
</li>
@endforeach
</ul>
</div>
@endif
<div class="bg-surface shadow rounded-lg p-4">
<form wire:submit="addTransaction" class="grid grid-cols-1 sm:grid-cols-6 gap-3 items-end">
<div>
<x-input-label for="date" value="Date" />
<x-text-input wire:model="date" id="date" type="date" class="block mt-1 w-full text-sm" />
</div>
<div>
<x-input-label for="categoryId" value="Catégorie" />
<select wire:model="categoryId" id="categoryId" 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=""></option>
@foreach ($categories as $category)
<option value="{{ $category->id }}">{{ $category->type->label() }} {{ $category->name }}</option>
@endforeach
</select>
<x-input-error :messages="$errors->get('categoryId')" class="mt-1" />
</div>
<div>
<x-input-label for="amount" value="Montant" />
<x-text-input wire:model="amount" id="amount" type="number" step="0.01" class="block mt-1 w-full text-sm" />
<x-input-error :messages="$errors->get('amount')" class="mt-1" />
</div>
<div class="sm:col-span-2">
<x-input-label for="details" value="Détails" />
<x-text-input wire:model="details" id="details" type="text" class="block mt-1 w-full text-sm" />
</div>
<div>
<x-input-label for="dateEffective" value="Date effective" />
<x-text-input wire:model="dateEffective" id="dateEffective" type="date" class="block mt-1 w-full text-sm" />
</div>
<div class="sm:col-span-6">
<x-primary-button>Ajouter</x-primary-button>
</div>
</form>
</div>
@php
$amountColor = fn ($t) => match ($t->category->type->value) {
'depense' => 'text-negative',
'epargne' => 'text-info',
default => 'text-positive',
};
$amountPrefix = fn ($t) => $t->category->type->value === 'depense' ? '' : '+';
@endphp
<div class="bg-surface shadow rounded-lg overflow-hidden">
<div class="overflow-x-auto">
<table class="min-w-full text-sm">
<caption class="sr-only">Historique des transactions du foyer</caption>
<thead>
<tr class="bg-surface-alt text-xs text-text-muted uppercase">
<th scope="col" class="text-left px-4 py-2">Date</th>
<th scope="col" class="text-left px-4 py-2">Type</th>
<th scope="col" class="text-left px-4 py-2">Catégorie</th>
<th scope="col" class="text-right px-4 py-2">Montant</th>
<th scope="col" class="text-left px-4 py-2">Détails</th>
<th scope="col" class="text-right px-4 py-2">Balance</th>
<th scope="col" class="text-left px-4 py-2">Date effective</th>
<th scope="col" class="px-4 py-2"><span class="sr-only">Actions</span></th>
</tr>
</thead>
<tbody>
@forelse ($transactions as $transaction)
<tr class="border-t border-border/60">
<td class="px-4 py-2 whitespace-nowrap text-text">{{ $transaction->date->translatedFormat('d M Y') }}</td>
<td class="px-4 py-2 text-text">{{ $transaction->category->type->label() }}</td>
<td class="px-4 py-2 text-text">{{ $transaction->category->name }}</td>
<td class="px-4 py-2 text-right font-medium {{ $amountColor($transaction) }}">
{{ $amountPrefix($transaction) }}{{ number_format($transaction->amount, 2, ',', ' ') }}
</td>
<td class="px-4 py-2 text-text-muted">{{ $transaction->details }}</td>
<td class="px-4 py-2 text-right font-semibold {{ ($running[$transaction->id] ?? 0) < 0 ? 'text-negative' : 'text-text' }}">
{{ number_format($running[$transaction->id] ?? 0, 2, ',', ' ') }}
</td>
<td class="px-4 py-2 whitespace-nowrap text-text-muted">{{ $transaction->date_effective->translatedFormat('d M Y') }}</td>
<td class="px-4 py-2 text-right">
<button wire:click="deleteTransaction({{ $transaction->id }})" wire:confirm="Supprimer cet enregistrement ?" aria-label="Supprimer l'opération {{ $transaction->category->name }} du {{ $transaction->date->translatedFormat('d M Y') }}" class="text-xs text-danger hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
Suppr.
</button>
</td>
</tr>
@empty
<tr><td colspan="8" class="px-4 py-3 text-sm text-text-muted">Aucun enregistrement.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>