webmoney/backend/app/Http/Controllers/Api/ReportController.php

454 lines
17 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('effective_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('effective_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('effective_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('effective_date', '>=', now()->subMonths($months)->startOfMonth())
->selectRaw("
DATE_FORMAT(effective_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('effective_date', '>=', now()->subMonths($months))
->selectRaw("
DAYOFWEEK(effective_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('effective_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->effective_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('effective_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('effective_date', 'desc')
->limit(5)
->get(['id', 'description', 'amount', 'type', 'effective_date']);
// Movimientos del mes
$monthStats = Transaction::where('account_id', $account->id)
->whereMonth('effective_date', now()->month)
->whereYear('effective_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('effective_date', '>=', now()->subMonths($months)->startOfMonth())
->where('effective_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('effective_date', now()->month)
->whereYear('effective_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,
],
]);
}
}