- FinancialHealthController: Fix column name in queries - ReportController: Fix column name in queries - Budget model: Fix getSpentAmountAttribute query
454 lines
18 KiB
PHP
454 lines
18 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Transaction;
|
|
use App\Models\Category;
|
|
use App\Models\Account;
|
|
use App\Models\LiabilityAccount;
|
|
use App\Models\RecurringTemplate;
|
|
use App\Models\Budget;
|
|
use App\Models\FinancialGoal;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Support\Facades\Auth;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Carbon\Carbon;
|
|
|
|
class ReportController extends Controller
|
|
{
|
|
/**
|
|
* Resumen general de reportes
|
|
*/
|
|
public function summary(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
$year = $request->get('year', now()->year);
|
|
|
|
// Ingresos y gastos del año
|
|
$yearData = Transaction::where('user_id', $userId)
|
|
->whereYear('transaction_date', $year)
|
|
->selectRaw("
|
|
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income,
|
|
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) as expense
|
|
")
|
|
->first();
|
|
|
|
// Comparar con año anterior
|
|
$lastYearData = Transaction::where('user_id', $userId)
|
|
->whereYear('transaction_date', $year - 1)
|
|
->selectRaw("
|
|
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income,
|
|
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) as expense
|
|
")
|
|
->first();
|
|
|
|
return response()->json([
|
|
'year' => $year,
|
|
'current' => [
|
|
'income' => $yearData->income ?? 0,
|
|
'expense' => $yearData->expense ?? 0,
|
|
'balance' => ($yearData->income ?? 0) - ($yearData->expense ?? 0),
|
|
],
|
|
'previous' => [
|
|
'income' => $lastYearData->income ?? 0,
|
|
'expense' => $lastYearData->expense ?? 0,
|
|
'balance' => ($lastYearData->income ?? 0) - ($lastYearData->expense ?? 0),
|
|
],
|
|
'variation' => [
|
|
'income' => $lastYearData->income > 0
|
|
? round((($yearData->income - $lastYearData->income) / $lastYearData->income) * 100, 1)
|
|
: 0,
|
|
'expense' => $lastYearData->expense > 0
|
|
? round((($yearData->expense - $lastYearData->expense) / $lastYearData->expense) * 100, 1)
|
|
: 0,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reporte por categorías
|
|
*/
|
|
public function byCategory(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
$startDate = $request->get('start_date', now()->startOfYear()->format('Y-m-d'));
|
|
$endDate = $request->get('end_date', now()->format('Y-m-d'));
|
|
$type = $request->get('type', 'debit'); // debit o credit
|
|
|
|
$data = Transaction::where('user_id', $userId)
|
|
->whereBetween('transaction_date', [$startDate, $endDate])
|
|
->where('type', $type)
|
|
->whereNotNull('category_id')
|
|
->join('categories', 'transactions.category_id', '=', 'categories.id')
|
|
->selectRaw("
|
|
COALESCE(categories.parent_id, categories.id) as category_group_id,
|
|
SUM(ABS(transactions.amount)) as total
|
|
")
|
|
->groupBy('category_group_id')
|
|
->orderByDesc('total')
|
|
->get();
|
|
|
|
// Obtener nombres de categorías
|
|
$categoryIds = $data->pluck('category_group_id')->unique();
|
|
$categories = Category::whereIn('id', $categoryIds)->get()->keyBy('id');
|
|
|
|
$result = $data->map(function($item) use ($categories) {
|
|
$category = $categories->get($item->category_group_id);
|
|
return [
|
|
'category_id' => $item->category_group_id,
|
|
'category_name' => $category ? $category->name : 'Sin categoría',
|
|
'icon' => $category ? $category->icon : 'bi-tag',
|
|
'color' => $category ? $category->color : '#6b7280',
|
|
'total' => round($item->total, 2),
|
|
];
|
|
});
|
|
|
|
$grandTotal = $result->sum('total');
|
|
|
|
// Añadir porcentajes
|
|
$result = $result->map(function($item) use ($grandTotal) {
|
|
$item['percentage'] = $grandTotal > 0 ? round(($item['total'] / $grandTotal) * 100, 1) : 0;
|
|
return $item;
|
|
});
|
|
|
|
return response()->json([
|
|
'data' => $result->values(),
|
|
'total' => $grandTotal,
|
|
'period' => [
|
|
'start' => $startDate,
|
|
'end' => $endDate,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reporte de evolución mensual
|
|
*/
|
|
public function monthlyEvolution(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
$months = $request->get('months', 12);
|
|
|
|
$data = Transaction::where('user_id', $userId)
|
|
->where('transaction_date', '>=', now()->subMonths($months)->startOfMonth())
|
|
->selectRaw("
|
|
DATE_FORMAT(transaction_date, '%Y-%m') as month,
|
|
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income,
|
|
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) as expense
|
|
")
|
|
->groupBy('month')
|
|
->orderBy('month')
|
|
->get();
|
|
|
|
// Calcular balance acumulado y tasa de ahorro
|
|
$result = $data->map(function($item) {
|
|
$balance = $item->income - $item->expense;
|
|
$savingsRate = $item->income > 0 ? round(($balance / $item->income) * 100, 1) : 0;
|
|
|
|
return [
|
|
'month' => $item->month,
|
|
'month_label' => Carbon::parse($item->month . '-01')->format('M Y'),
|
|
'income' => round($item->income, 2),
|
|
'expense' => round($item->expense, 2),
|
|
'balance' => round($balance, 2),
|
|
'savings_rate' => $savingsRate,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'data' => $result,
|
|
'averages' => [
|
|
'income' => round($result->avg('income'), 2),
|
|
'expense' => round($result->avg('expense'), 2),
|
|
'balance' => round($result->avg('balance'), 2),
|
|
'savings_rate' => round($result->avg('savings_rate'), 1),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reporte de gastos por día de la semana
|
|
*/
|
|
public function byDayOfWeek(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
$months = $request->get('months', 6);
|
|
|
|
$data = Transaction::where('user_id', $userId)
|
|
->where('type', 'debit')
|
|
->where('transaction_date', '>=', now()->subMonths($months))
|
|
->selectRaw("
|
|
DAYOFWEEK(transaction_date) as day_num,
|
|
COUNT(*) as count,
|
|
SUM(ABS(amount)) as total,
|
|
AVG(ABS(amount)) as average
|
|
")
|
|
->groupBy('day_num')
|
|
->orderBy('day_num')
|
|
->get();
|
|
|
|
$days = ['Domingo', 'Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado'];
|
|
|
|
$result = $data->map(function($item) use ($days) {
|
|
return [
|
|
'day' => $days[$item->day_num - 1],
|
|
'day_num' => $item->day_num,
|
|
'count' => $item->count,
|
|
'total' => round($item->total, 2),
|
|
'average' => round($item->average, 2),
|
|
];
|
|
});
|
|
|
|
return response()->json($result);
|
|
}
|
|
|
|
/**
|
|
* Top gastos
|
|
*/
|
|
public function topExpenses(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
$startDate = $request->get('start_date', now()->startOfMonth()->format('Y-m-d'));
|
|
$endDate = $request->get('end_date', now()->format('Y-m-d'));
|
|
$limit = $request->get('limit', 20);
|
|
|
|
$transactions = Transaction::where('user_id', $userId)
|
|
->where('type', 'debit')
|
|
->whereBetween('transaction_date', [$startDate, $endDate])
|
|
->with(['category', 'account'])
|
|
->orderByRaw('ABS(amount) DESC')
|
|
->limit($limit)
|
|
->get();
|
|
|
|
return response()->json([
|
|
'data' => $transactions->map(function($t) {
|
|
return [
|
|
'id' => $t->id,
|
|
'description' => $t->description,
|
|
'amount' => abs($t->amount),
|
|
'date' => $t->transaction_date->format('Y-m-d'),
|
|
'category' => $t->category ? $t->category->name : null,
|
|
'account' => $t->account ? $t->account->name : null,
|
|
];
|
|
}),
|
|
'total' => $transactions->sum(fn($t) => abs($t->amount)),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Comparativa de períodos
|
|
*/
|
|
public function comparePeriods(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
$period1Start = $request->get('period1_start');
|
|
$period1End = $request->get('period1_end');
|
|
$period2Start = $request->get('period2_start');
|
|
$period2End = $request->get('period2_end');
|
|
|
|
// Si no se especifican, comparar mes actual vs anterior
|
|
if (!$period1Start) {
|
|
$period1Start = now()->startOfMonth()->format('Y-m-d');
|
|
$period1End = now()->endOfMonth()->format('Y-m-d');
|
|
$period2Start = now()->subMonth()->startOfMonth()->format('Y-m-d');
|
|
$period2End = now()->subMonth()->endOfMonth()->format('Y-m-d');
|
|
}
|
|
|
|
$getPeriodData = function($start, $end) use ($userId) {
|
|
return Transaction::where('user_id', $userId)
|
|
->whereBetween('transaction_date', [$start, $end])
|
|
->selectRaw("
|
|
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income,
|
|
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) as expense,
|
|
COUNT(*) as transactions
|
|
")
|
|
->first();
|
|
};
|
|
|
|
$period1 = $getPeriodData($period1Start, $period1End);
|
|
$period2 = $getPeriodData($period2Start, $period2End);
|
|
|
|
return response()->json([
|
|
'period1' => [
|
|
'label' => Carbon::parse($period1Start)->format('M Y'),
|
|
'start' => $period1Start,
|
|
'end' => $period1End,
|
|
'income' => $period1->income ?? 0,
|
|
'expense' => $period1->expense ?? 0,
|
|
'balance' => ($period1->income ?? 0) - ($period1->expense ?? 0),
|
|
'transactions' => $period1->transactions ?? 0,
|
|
],
|
|
'period2' => [
|
|
'label' => Carbon::parse($period2Start)->format('M Y'),
|
|
'start' => $period2Start,
|
|
'end' => $period2End,
|
|
'income' => $period2->income ?? 0,
|
|
'expense' => $period2->expense ?? 0,
|
|
'balance' => ($period2->income ?? 0) - ($period2->expense ?? 0),
|
|
'transactions' => $period2->transactions ?? 0,
|
|
],
|
|
'variation' => [
|
|
'income' => ($period2->income ?? 0) > 0
|
|
? round(((($period1->income ?? 0) - ($period2->income ?? 0)) / ($period2->income ?? 1)) * 100, 1)
|
|
: 0,
|
|
'expense' => ($period2->expense ?? 0) > 0
|
|
? round(((($period1->expense ?? 0) - ($period2->expense ?? 0)) / ($period2->expense ?? 1)) * 100, 1)
|
|
: 0,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reporte de cuentas
|
|
*/
|
|
public function accountsReport(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
|
|
$accounts = Account::where('user_id', $userId)
|
|
->withCount('transactions')
|
|
->get();
|
|
|
|
$result = $accounts->map(function($account) {
|
|
// Últimas transacciones
|
|
$recentActivity = Transaction::where('account_id', $account->id)
|
|
->orderBy('transaction_date', 'desc')
|
|
->limit(5)
|
|
->get(['id', 'description', 'amount', 'type', 'transaction_date']);
|
|
|
|
// Movimientos del mes
|
|
$monthStats = Transaction::where('account_id', $account->id)
|
|
->whereMonth('transaction_date', now()->month)
|
|
->whereYear('transaction_date', now()->year)
|
|
->selectRaw("
|
|
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income,
|
|
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) as expense
|
|
")
|
|
->first();
|
|
|
|
return [
|
|
'id' => $account->id,
|
|
'name' => $account->name,
|
|
'type' => $account->type,
|
|
'currency' => $account->currency,
|
|
'balance' => $account->current_balance,
|
|
'transactions_count' => $account->transactions_count,
|
|
'month_income' => $monthStats->income ?? 0,
|
|
'month_expense' => $monthStats->expense ?? 0,
|
|
'recent_activity' => $recentActivity,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'accounts' => $result,
|
|
'summary' => [
|
|
'total_accounts' => $accounts->count(),
|
|
'total_balance' => $accounts->where('include_in_total', true)->sum('current_balance'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Proyección de gastos
|
|
*/
|
|
public function projection(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
$months = $request->get('months', 3);
|
|
|
|
// Obtener promedio de los últimos meses
|
|
$historical = Transaction::where('user_id', $userId)
|
|
->where('transaction_date', '>=', now()->subMonths($months)->startOfMonth())
|
|
->where('transaction_date', '<', now()->startOfMonth())
|
|
->selectRaw("
|
|
AVG(CASE WHEN type = 'credit' THEN amount ELSE NULL END) as avg_income,
|
|
AVG(CASE WHEN type = 'debit' THEN ABS(amount) ELSE NULL END) as avg_expense,
|
|
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) / ? as monthly_income,
|
|
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) / ? as monthly_expense
|
|
", [$months, $months])
|
|
->first();
|
|
|
|
// Gastos del mes actual
|
|
$currentMonth = Transaction::where('user_id', $userId)
|
|
->whereMonth('transaction_date', now()->month)
|
|
->whereYear('transaction_date', now()->year)
|
|
->selectRaw("
|
|
SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income,
|
|
SUM(CASE WHEN type = 'debit' THEN ABS(amount) ELSE 0 END) as expense
|
|
")
|
|
->first();
|
|
|
|
// Días transcurridos y restantes
|
|
$daysElapsed = now()->day;
|
|
$daysInMonth = now()->daysInMonth;
|
|
$daysRemaining = $daysInMonth - $daysElapsed;
|
|
|
|
// Proyección del mes
|
|
$projectedExpense = ($currentMonth->expense / $daysElapsed) * $daysInMonth;
|
|
$projectedIncome = ($currentMonth->income / $daysElapsed) * $daysInMonth;
|
|
|
|
return response()->json([
|
|
'historical_average' => [
|
|
'income' => round($historical->monthly_income ?? 0, 2),
|
|
'expense' => round($historical->monthly_expense ?? 0, 2),
|
|
],
|
|
'current_month' => [
|
|
'income' => round($currentMonth->income ?? 0, 2),
|
|
'expense' => round($currentMonth->expense ?? 0, 2),
|
|
'days_elapsed' => $daysElapsed,
|
|
'days_remaining' => $daysRemaining,
|
|
],
|
|
'projection' => [
|
|
'income' => round($projectedIncome, 2),
|
|
'expense' => round($projectedExpense, 2),
|
|
'balance' => round($projectedIncome - $projectedExpense, 2),
|
|
],
|
|
'vs_average' => [
|
|
'income' => ($historical->monthly_income ?? 0) > 0
|
|
? round((($projectedIncome - ($historical->monthly_income ?? 0)) / ($historical->monthly_income ?? 1)) * 100, 1)
|
|
: 0,
|
|
'expense' => ($historical->monthly_expense ?? 0) > 0
|
|
? round((($projectedExpense - ($historical->monthly_expense ?? 0)) / ($historical->monthly_expense ?? 1)) * 100, 1)
|
|
: 0,
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Reporte de recurrencias
|
|
*/
|
|
public function recurringReport(Request $request)
|
|
{
|
|
$userId = Auth::id();
|
|
|
|
$templates = RecurringTemplate::where('user_id', $userId)
|
|
->where('is_active', true)
|
|
->with(['category', 'account'])
|
|
->get();
|
|
|
|
$monthlyIncome = $templates->where('type', 'credit')->sum('amount');
|
|
$monthlyExpense = $templates->where('type', 'debit')->sum(fn($t) => abs($t->amount));
|
|
|
|
return response()->json([
|
|
'templates' => $templates->map(function($t) {
|
|
return [
|
|
'id' => $t->id,
|
|
'description' => $t->description,
|
|
'amount' => abs($t->amount),
|
|
'type' => $t->type,
|
|
'frequency' => $t->frequency,
|
|
'category' => $t->category ? $t->category->name : null,
|
|
'next_date' => $t->next_occurrence_date,
|
|
];
|
|
}),
|
|
'summary' => [
|
|
'total_recurring' => $templates->count(),
|
|
'monthly_income' => $monthlyIncome,
|
|
'monthly_expense' => $monthlyExpense,
|
|
'net_recurring' => $monthlyIncome - $monthlyExpense,
|
|
],
|
|
]);
|
|
}
|
|
}
|