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,110 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Livewire\Dashboard;
|
||||
|
||||
use App\Enums\CategoryType;
|
||||
use App\Models\Category;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Component;
|
||||
|
||||
#[Layout('layouts.app')]
|
||||
class Index extends Component
|
||||
{
|
||||
public int $year;
|
||||
|
||||
public string $period = 'year'; // 'year' or '1'..'12'
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->year = (int) now()->year;
|
||||
$this->period = (string) now()->month;
|
||||
}
|
||||
|
||||
private function budgetFor(Category $category): float
|
||||
{
|
||||
$lines = $category->budgetLines()->where('year', $this->year)->get();
|
||||
|
||||
if ($this->period === 'year') {
|
||||
$default = (float) ($lines->firstWhere('month', null)->amount ?? 0);
|
||||
$total = 0.0;
|
||||
for ($m = 1; $m <= 12; $m++) {
|
||||
$override = $lines->firstWhere('month', $m);
|
||||
$total += $override ? (float) $override->amount : $default;
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
$month = (int) $this->period;
|
||||
$override = $lines->firstWhere('month', $month);
|
||||
|
||||
return $override ? (float) $override->amount : (float) ($lines->firstWhere('month', null)->amount ?? 0);
|
||||
}
|
||||
|
||||
private function trackedFor(Category $category): float
|
||||
{
|
||||
$query = $category->transactions()->whereYear('date_effective', $this->year);
|
||||
|
||||
if ($this->period !== 'year') {
|
||||
$query->whereMonth('date_effective', (int) $this->period);
|
||||
}
|
||||
|
||||
return (float) $query->sum('amount');
|
||||
}
|
||||
|
||||
public function render()
|
||||
{
|
||||
$household = auth()->user()->currentHousehold;
|
||||
|
||||
$sections = collect(CategoryType::cases())->mapWithKeys(function (CategoryType $type) use ($household) {
|
||||
$rows = $household->categories()
|
||||
->where('type', $type)
|
||||
->orderBy('position')
|
||||
->get()
|
||||
->map(function (Category $category) {
|
||||
$budget = $this->budgetFor($category);
|
||||
$tracked = $this->trackedFor($category);
|
||||
|
||||
return [
|
||||
'category' => $category,
|
||||
'tracked' => $tracked,
|
||||
'budget' => $budget,
|
||||
'pct' => $budget > 0 ? min(100, round($tracked / $budget * 100)) : 0,
|
||||
'reste' => max(0, $budget - $tracked),
|
||||
'manque' => max(0, $tracked - $budget),
|
||||
];
|
||||
});
|
||||
|
||||
return [$type->value => [
|
||||
'rows' => $rows,
|
||||
'totals' => [
|
||||
'tracked' => $rows->sum('tracked'),
|
||||
'budget' => $rows->sum('budget'),
|
||||
'reste' => $rows->sum('reste'),
|
||||
'manque' => $rows->sum('manque'),
|
||||
],
|
||||
]];
|
||||
});
|
||||
|
||||
$chartData = $sections->mapWithKeys(function ($section, $key) {
|
||||
$top = $section['rows']
|
||||
->filter(fn ($row) => $row['tracked'] > 0)
|
||||
->sortByDesc('tracked')
|
||||
->take(6)
|
||||
->values();
|
||||
|
||||
return [$key => [
|
||||
'labels' => $top->pluck('category.name')->values(),
|
||||
'data' => $top->pluck('tracked')->values(),
|
||||
]];
|
||||
});
|
||||
|
||||
return view('livewire.dashboard.index', [
|
||||
'types' => CategoryType::cases(),
|
||||
'sections' => $sections,
|
||||
'chartData' => $chartData,
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user