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>
110 lines
3.1 KiB
PHP
110 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Livewire\Plan;
|
|
|
|
use App\Enums\CategoryType;
|
|
use App\Models\BudgetLine;
|
|
use App\Models\Category;
|
|
use Livewire\Attributes\Layout;
|
|
use Livewire\Component;
|
|
|
|
#[Layout('layouts.app')]
|
|
class Index extends Component
|
|
{
|
|
public int $year;
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->year = (int) now()->year;
|
|
}
|
|
|
|
public function setYear(int $year): void
|
|
{
|
|
$this->year = $year;
|
|
}
|
|
|
|
public function updateAmount(int $categoryId, ?int $month, string $amount): void
|
|
{
|
|
$category = Category::findOrFail($categoryId);
|
|
$household = auth()->user()->currentHousehold;
|
|
|
|
if ($category->household_id !== $household->id) {
|
|
abort(403);
|
|
}
|
|
|
|
$amount = is_numeric($amount) ? (float) $amount : 0.0;
|
|
|
|
BudgetLine::updateOrCreate(
|
|
[
|
|
'household_id' => $household->id,
|
|
'category_id' => $categoryId,
|
|
'year' => $this->year,
|
|
'month' => $month,
|
|
],
|
|
['amount' => $amount],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<int|string, float> keyed 'default',1..12
|
|
*/
|
|
private function resolveRow(Category $category): array
|
|
{
|
|
$lines = $category->budgetLines()->where('year', $this->year)->get();
|
|
$default = (float) ($lines->firstWhere('month', null)->amount ?? 0);
|
|
|
|
$row = ['default' => $default];
|
|
|
|
for ($m = 1; $m <= 12; $m++) {
|
|
$override = $lines->firstWhere('month', $m);
|
|
$row[$m] = $override ? (float) $override->amount : $default;
|
|
}
|
|
|
|
return $row;
|
|
}
|
|
|
|
public function render()
|
|
{
|
|
$household = auth()->user()->currentHousehold;
|
|
|
|
$sections = collect(CategoryType::cases())->mapWithKeys(function (CategoryType $type) use ($household) {
|
|
$categories = $household->categories()
|
|
->where('type', $type)
|
|
->orderBy('position')
|
|
->get()
|
|
->map(fn (Category $category) => [
|
|
'category' => $category,
|
|
'row' => $this->resolveRow($category),
|
|
]);
|
|
|
|
$totals = ['default' => 0.0];
|
|
for ($m = 1; $m <= 12; $m++) {
|
|
$totals[$m] = 0.0;
|
|
}
|
|
foreach ($categories as $entry) {
|
|
foreach ($entry['row'] as $key => $value) {
|
|
$totals[$key] += $value;
|
|
}
|
|
}
|
|
|
|
return [$type->value => ['categories' => $categories, 'totals' => $totals]];
|
|
});
|
|
|
|
$remaining = [];
|
|
for ($m = 1; $m <= 12; $m++) {
|
|
$remaining[$m] = $sections['revenu']['totals'][$m]
|
|
- $sections['depense']['totals'][$m]
|
|
- $sections['epargne']['totals'][$m];
|
|
}
|
|
$sections['revenu'] = [...$sections['revenu'], 'remaining' => $remaining];
|
|
|
|
return view('livewire.plan.index', [
|
|
'types' => CategoryType::cases(),
|
|
'sections' => $sections,
|
|
'months' => ['Janv', 'Févr', 'Mars', 'Avr', 'Mai', 'Juin', 'Juil', 'Août', 'Sept', 'Oct', 'Nov', 'Déc'],
|
|
]);
|
|
}
|
|
}
|