Files
suivicpt/resources/views/livewire/suivi/index.blade.php
T
jeremy bayseandClaude Sonnet 5 97dc7cdb2a Allow editing an existing transaction
Desktop reuses the add-form with a Modifier link per row; mobile gets
a "Dernières opérations" list under the quick-entry form since no
table exists there.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-01 08:37:50 +02:00

423 lines
27 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, message: '' }"
x-on:transaction-added.window="message = 'Opération ajoutée'; toast = true; setTimeout(() => toast = false, 2000)"
x-on:transaction-updated.window="message = 'Opération modifiée'; toast = true; setTimeout(() => toast = false, 2000)"
>
<h1 id="mobile-form-top" class="text-xl font-semibold text-text">{{ $editingTransactionId ? 'Modifier l\'opération' : '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['pct'] }}%"
></span>
@endif
<span class="relative flex flex-col gap-0.5">
<span class="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['pct'] }}%</span>
@endif
</span>
@if ($progress !== null)
<span class="text-xs font-normal {{ $isActive ? 'text-white/70' : 'text-text-muted' }}">
{{ number_format($progress['remaining'], 0, ',', ' ') }} restants
</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>
@if ($members->isNotEmpty())
<fieldset>
<legend class="text-sm font-medium text-text-muted mb-2">Payé par</legend>
<div class="grid grid-cols-2 gap-2">
<button
type="button"
wire:click="$set('paidByUserId', '')"
aria-pressed="{{ $paidByUserId === '' ? 'true' : 'false' }}"
class="min-h-[44px] px-3 py-2 rounded-lg border text-sm font-medium transition focus:outline-none focus-visible:ring-2 ring-brand
{{ $paidByUserId === '' ? 'bg-brand text-white border-brand' : 'bg-surface border-border-strong text-text' }}"
>
Commun
</button>
@foreach ($members as $member)
<button
type="button"
wire:click="$set('paidByUserId', '{{ $member->id }}')"
aria-pressed="{{ (string) $paidByUserId === (string) $member->id ? 'true' : 'false' }}"
class="min-h-[44px] px-3 py-2 rounded-lg border text-sm font-medium transition focus:outline-none focus-visible:ring-2 ring-brand
{{ (string) $paidByUserId === (string) $member->id ? 'bg-brand text-white border-brand' : 'bg-surface border-border-strong text-text' }}"
>
{{ $member->name }}
</button>
@endforeach
</div>
</fieldset>
@endif
<div class="flex items-center gap-3">
<button
type="submit"
class="flex-1 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"
>
{{ $editingTransactionId ? 'Enregistrer' : 'Ajouter' }}
</button>
@if ($editingTransactionId)
<button
type="button"
wire:click="cancelEditTransaction"
class="py-4 px-4 rounded-lg border border-border-strong text-text text-sm font-medium focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
Annuler
</button>
@endif
</div>
</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"
>
<span x-text="'✓ ' + message"></span>
</div>
@if ($recentTransactions->isNotEmpty())
<div>
<p class="text-sm font-medium text-text-muted mb-2">Dernières opérations</p>
<ul class="space-y-2">
@foreach ($recentTransactions as $transaction)
<li class="bg-surface border border-border rounded-lg px-3 py-2 flex items-center justify-between gap-2" wire:key="recent-{{ $transaction->id }}">
<div class="min-w-0">
<p class="text-sm text-text truncate">
{{ $transaction->category->name }}
@if ($transaction->details)
<span class="text-text-muted"> {{ $transaction->details }}</span>
@endif
</p>
<p class="text-xs text-text-muted">{{ $transaction->date->translatedFormat('d M Y') }} · {{ number_format($transaction->amount, 2, ',', ' ') }} </p>
</div>
<div class="flex items-center gap-3 shrink-0">
<button
wire:click="startEditTransaction({{ $transaction->id }})"
x-on:click="$nextTick(() => document.getElementById('mobile-form-top')?.scrollIntoView({ behavior: 'smooth' }))"
aria-label="Modifier {{ $transaction->category->name }} du {{ $transaction->date->translatedFormat('d M Y') }}"
class="text-xs text-brand hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring"
>
Modifier
</button>
<button wire:click="deleteTransaction({{ $transaction->id }})" wire:confirm="Supprimer cet enregistrement ?" aria-label="Supprimer {{ $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>
</div>
</li>
@endforeach
</ul>
</div>
@endif
</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 ($paidBySummary->isNotEmpty())
<div class="bg-surface shadow rounded-lg p-4">
<p class="text-xs text-text-muted mb-2">Dépenses ce mois-ci, par personne</p>
<div class="flex flex-wrap gap-4">
@foreach ($paidBySummary as $entry)
<div>
<p class="text-xs text-text-muted">{{ $entry['label'] }}</p>
<p class="text-sm font-semibold text-negative">{{ number_format($entry['total'], 2, ',', ' ') }} </p>
</div>
@endforeach
</div>
</div>
@endif
@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">
<p class="text-sm font-medium text-text-muted mb-3">{{ $editingTransactionId ? 'Modifier l\'opération' : 'Nouvelle opération' }}</p>
<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>
<x-input-label for="paidByUserId" value="Payé par" />
<select wire:model="paidByUserId" id="paidByUserId" 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="">Commun</option>
@foreach ($members as $member)
<option value="{{ $member->id }}">{{ $member->name }}</option>
@endforeach
</select>
</div>
<div class="sm:col-span-6 flex items-center gap-3">
<x-primary-button>{{ $editingTransactionId ? 'Enregistrer' : 'Ajouter' }}</x-primary-button>
@if ($editingTransactionId)
<button type="button" wire:click="cancelEditTransaction" class="text-sm text-text-muted hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
Annuler
</button>
@endif
</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="flex items-center gap-2">
<label for="filterPaidBy" class="text-xs text-text-muted">Payé par</label>
<select wire:model.live="filterPaidBy" id="filterPaidBy" class="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="">Tout le monde</option>
<option value="common">Commun</option>
@foreach ($members as $member)
<option value="{{ $member->id }}">{{ $member->name }}</option>
@endforeach
</select>
</div>
<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-left px-4 py-2">Payé par</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-text-muted">{{ $transaction->paidBy?->name ?? 'Commun' }}</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 whitespace-nowrap">
<button wire:click="startEditTransaction({{ $transaction->id }})" aria-label="Modifier l'opération {{ $transaction->category->name }} du {{ $transaction->date->translatedFormat('d M Y') }}" class="text-xs text-brand hover:underline rounded focus:outline-none focus-visible:ring-2 focus-visible:ring-focus-ring">
Modifier
</button>
<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 ml-3">
Suppr.
</button>
</td>
</tr>
@empty
<tr><td colspan="9" class="px-4 py-3 text-sm text-text-muted">Aucun enregistrement.</td></tr>
@endforelse
</tbody>
</table>
</div>
</div>
</div>
</div>