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,109 @@
|
||||
<?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'],
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user