Bicycle/App/Controller/BudgetsController.php
2026-08-15 10:39:22 +03:00

157 lines
6.2 KiB
PHP
Raw Permalink 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.

<?php
/**
* @package Bicycle
* @author Egor Isaev
* @description BudgetsController.php
* @copyright (c) 13/08/2026
*/
namespace App\Controller;
use App\Repositories\BudgetRepository;
use App\Repositories\CategoryGroupRepository;
use App\Repositories\CategoryRepository;
use App\Repositories\UserRepository;
use Services\Auth;
use System\Classes\Controller;
use System\Classes\HTTP\HTTPException;
use System\Classes\MyException;
use System\Classes\Request;
/**
* Годовой план бюджета по месяцам — сетка «статья × 12 месяцев», сохраняется одной кнопкой
* (см. App/view/Budgets/budgets.html, App/media/js/budgets.js). Факт (реальные transactions)
* здесь не показывается — это отдельная фича, см. бюджет_текущий_план.md.
*/
class BudgetsController extends Controller
{
protected bool $_auth_protection = true;
/**
* @return string
* @throws MyException
*/
public function indexAction(): string
{
$user = $this->currentUser();
$year = (int)(Request::$current->query('year') ?: (int)date('Y'));
$category_repo = new CategoryRepository();
$group_repo = new CategoryGroupRepository();
$budget_repo = new BudgetRepository();
$tree = $category_repo->getTree($user->id);
$groups = $group_repo->getForUser($user->id);
$plan = $budget_repo->getYear($user->id, $year);
$archived = $category_repo->getList('*', ['user_id' => $user->id, 'is_delete' => 1], 'title');
$archived_groups = $group_repo->getList('*', ['user_id' => $user->id, 'is_delete' => 1], 'title');
return $this->render('budgets', [
'tree' => $tree,
'groups' => $groups,
'plan' => $plan,
'year' => $year,
'archived' => $archived ?: [],
'archived_groups' => $archived_groups ?: [],
]);
}
/**
* Сохраняет всю сетку одним запросом: plan[categorie_id][month] = сумма. Для статей дохода
* со связанной статьёй расхода (formula_pct) — автоматически пересчитывает и перезаписывает
* план связанной статьи расхода тем же процентом (см. бюджет_текущий_план.md → Categories).
*
* @return string JSON
* @throws HTTPException|MyException
*/
public function saveAction(): string
{
$request = Request::$current;
$user = $this->currentUser();
$year = (int)$request->post('year');
$plan_input = $request->post('plan');
if ($year < 2000 || $year > 2100 || !is_array($plan_input)) {
return $this->json(['success' => false, 'errors' => ['year' => 'Некорректный год']], 422);
}
$category_repo = new CategoryRepository();
$budget_repo = new BudgetRepository();
$categories = [];
foreach (array_keys($plan_input) as $categorie_id) {
$category = $category_repo->get((int)$categorie_id);
if (!$category || (int)$category->user_id !== (int)$user->id) {
throw HTTPException::factory(404);
}
$categories[(int)$categorie_id] = $category;
}
// Пересчитанные формулой ячейки связанной статьи расхода — возвращаются клиенту, чтобы
// JS обновил их в сетке сразу, без перезагрузки страницы (см. budgets.js).
$recalculated = [];
$budget_repo->transaction(static function () use ($categories, $plan_input, $year, $budget_repo, &$recalculated) {
// 1. Прямые значения из формы — как есть.
foreach ($categories as $categorie_id => $category) {
foreach ($plan_input[$categorie_id] ?? [] as $month => $summ) {
$month = (int)$month;
if ($month < 1 || $month > 12) {
continue;
}
$budget_repo->upsert($categorie_id, $year, $month, (float)$summ, $category->type);
}
}
// 2. Формульные статьи дохода — пересчитывают связанную статью расхода поверх,
// последним шагом, чтобы формула всегда побеждала независимо от порядка обхода.
foreach ($categories as $category) {
if ($category->linked_expense_categorie_id === null || $category->formula_pct === null) {
continue;
}
foreach ($plan_input[(int)$category->id] ?? [] as $month => $summ) {
$month = (int)$month;
if ($month < 1 || $month > 12) {
continue;
}
$expense_summ = round((float)$summ * (float)$category->formula_pct / 100, 2);
$target_id = (int)$category->linked_expense_categorie_id;
$budget_repo->upsert($target_id, $year, $month, $expense_summ, 'expense', (float)$category->formula_pct);
$recalculated[$target_id][$month] = $expense_summ;
}
}
});
return $this->json(['success' => true, 'recalculated' => $recalculated]);
}
/**
* Бюджетный пользователь, связанный с текущим логином Services\Auth.
*
* @return object
* @throws HTTPException|MyException
*/
protected function currentUser(): object
{
$auth_user = Auth::instance()->getUser();
$user = (new UserRepository())->getByLogin($auth_user['login']);
if (!$user) {
throw HTTPException::factory(404);
}
return $user;
}
}