886 lines
38 KiB
PHP
886 lines
38 KiB
PHP
<?php
|
|
|
|
namespace App\Http\Controllers\Api;
|
|
|
|
use App\Http\Controllers\Controller;
|
|
use App\Models\Transaction;
|
|
use App\Models\Account;
|
|
use App\Models\RecurringInstance;
|
|
use App\Models\LiabilityInstallment;
|
|
use Illuminate\Http\Request;
|
|
use Illuminate\Http\JsonResponse;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Carbon\Carbon;
|
|
|
|
class DashboardController extends Controller
|
|
{
|
|
/**
|
|
* Fluxo de caixa mensal
|
|
* Retorna receitas, despesas e saldo por mês
|
|
* Ignora transações marcadas como transferências entre contas
|
|
*/
|
|
public function cashflow(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
|
|
// Parâmetros de período (padrão: últimos 12 meses)
|
|
$months = min((int) $request->get('months', 12), 24); // máximo 24 meses
|
|
$endDate = Carbon::parse($request->get('end_date', now()->endOfMonth()));
|
|
$startDate = $endDate->copy()->subMonths($months - 1)->startOfMonth();
|
|
|
|
// Buscar dados mensais de transações completed (excluindo transferências)
|
|
// Usar strftime para SQLite ou DATE_FORMAT para MySQL
|
|
$driver = DB::connection()->getDriverName();
|
|
$monthExpression = $driver === 'sqlite'
|
|
? "strftime('%Y-%m', effective_date)"
|
|
: "DATE_FORMAT(effective_date, '%Y-%m')";
|
|
|
|
$monthlyData = Transaction::ofUser($userId)
|
|
->completed()
|
|
->where('is_transfer', false) // Ignorar transferências entre contas
|
|
->whereBetween('effective_date', [$startDate, $endDate])
|
|
->select(
|
|
DB::raw("$monthExpression as month"),
|
|
DB::raw("SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income"),
|
|
DB::raw("SUM(CASE WHEN type = 'debit' THEN amount ELSE 0 END) as expense")
|
|
)
|
|
->groupBy('month')
|
|
->orderBy('month')
|
|
->get()
|
|
->keyBy('month');
|
|
|
|
// Criar array com todos os meses (mesmo sem dados)
|
|
$result = [];
|
|
$current = $startDate->copy();
|
|
$cumulativeBalance = 0;
|
|
|
|
// Calcular saldo inicial (antes do período) - excluindo transferências
|
|
$initialBalance = Transaction::ofUser($userId)
|
|
->completed()
|
|
->where('is_transfer', false)
|
|
->where('effective_date', '<', $startDate)
|
|
->selectRaw("SUM(CASE WHEN type = 'credit' THEN amount ELSE -amount END) as balance")
|
|
->value('balance') ?? 0;
|
|
|
|
$cumulativeBalance = (float) $initialBalance;
|
|
|
|
while ($current <= $endDate) {
|
|
$monthKey = $current->format('Y-m');
|
|
$data = $monthlyData->get($monthKey);
|
|
|
|
$income = (float) ($data->income ?? 0);
|
|
$expense = (float) ($data->expense ?? 0);
|
|
$balance = $income - $expense;
|
|
$cumulativeBalance += $balance;
|
|
|
|
$result[] = [
|
|
'month' => $monthKey,
|
|
'month_label' => $current->translatedFormat('M/Y'),
|
|
'income' => $income,
|
|
'expense' => $expense,
|
|
'balance' => $balance,
|
|
'cumulative_balance' => $cumulativeBalance,
|
|
];
|
|
|
|
$current->addMonth();
|
|
}
|
|
|
|
// Totais do período
|
|
$totals = [
|
|
'income' => array_sum(array_column($result, 'income')),
|
|
'expense' => array_sum(array_column($result, 'expense')),
|
|
'balance' => array_sum(array_column($result, 'balance')),
|
|
'average_income' => count($result) > 0 ? array_sum(array_column($result, 'income')) / count($result) : 0,
|
|
'average_expense' => count($result) > 0 ? array_sum(array_column($result, 'expense')) / count($result) : 0,
|
|
];
|
|
|
|
return response()->json([
|
|
'period' => [
|
|
'start' => $startDate->format('Y-m-d'),
|
|
'end' => $endDate->format('Y-m-d'),
|
|
'months' => $months,
|
|
],
|
|
'data' => $result,
|
|
'totals' => $totals,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Resumo geral do dashboard
|
|
* Ignora transações marcadas como transferências entre contas
|
|
*/
|
|
public function summary(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
|
|
// Saldo total das contas
|
|
$totalBalance = Account::where('user_id', $userId)
|
|
->where('is_active', true)
|
|
->where('include_in_total', true)
|
|
->sum('current_balance');
|
|
|
|
// Transações do mês atual (excluindo transferências)
|
|
$currentMonth = [
|
|
now()->startOfMonth()->format('Y-m-d'),
|
|
now()->endOfMonth()->format('Y-m-d')
|
|
];
|
|
|
|
$monthlyStats = Transaction::ofUser($userId)
|
|
->completed()
|
|
->where('is_transfer', false) // Ignorar transferências entre contas
|
|
->whereBetween('effective_date', $currentMonth)
|
|
->select(
|
|
DB::raw("SUM(CASE WHEN type = 'credit' THEN amount ELSE 0 END) as income"),
|
|
DB::raw("SUM(CASE WHEN type = 'debit' THEN amount ELSE 0 END) as expense"),
|
|
DB::raw("COUNT(*) as transactions_count")
|
|
)
|
|
->first();
|
|
|
|
// Pendentes (excluindo transferências)
|
|
$pending = Transaction::ofUser($userId)
|
|
->pending()
|
|
->where('is_transfer', false)
|
|
->select(
|
|
DB::raw("SUM(CASE WHEN type = 'credit' THEN planned_amount ELSE 0 END) as income"),
|
|
DB::raw("SUM(CASE WHEN type = 'debit' THEN planned_amount ELSE 0 END) as expense"),
|
|
DB::raw("COUNT(*) as count")
|
|
)
|
|
->first();
|
|
|
|
// Atrasadas (vencidas) - excluindo transferências
|
|
$overdue = Transaction::ofUser($userId)
|
|
->pending()
|
|
->where('is_transfer', false)
|
|
->where('planned_date', '<', now()->startOfDay())
|
|
->select(
|
|
DB::raw("SUM(planned_amount) as total"),
|
|
DB::raw("COUNT(*) as count")
|
|
)
|
|
->first();
|
|
|
|
return response()->json([
|
|
'total_balance' => (float) $totalBalance,
|
|
'current_month' => [
|
|
'income' => (float) ($monthlyStats->income ?? 0),
|
|
'expense' => (float) ($monthlyStats->expense ?? 0),
|
|
'balance' => (float) (($monthlyStats->income ?? 0) - ($monthlyStats->expense ?? 0)),
|
|
'transactions_count' => (int) ($monthlyStats->transactions_count ?? 0),
|
|
],
|
|
'pending' => [
|
|
'income' => (float) ($pending->income ?? 0),
|
|
'expense' => (float) ($pending->expense ?? 0),
|
|
'count' => (int) ($pending->count ?? 0),
|
|
],
|
|
'overdue' => [
|
|
'total' => (float) ($overdue->total ?? 0),
|
|
'count' => (int) ($overdue->count ?? 0),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Despesas por categoria (últimos N meses)
|
|
* Ignora transações marcadas como transferências entre contas
|
|
*/
|
|
public function expensesByCategory(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
$months = min((int) $request->get('months', 3), 12);
|
|
|
|
$startDate = now()->subMonths($months - 1)->startOfMonth();
|
|
$endDate = now()->endOfMonth();
|
|
|
|
$data = Transaction::ofUser($userId)
|
|
->completed()
|
|
->where('is_transfer', false) // Ignorar transferências entre contas
|
|
->where('type', 'debit')
|
|
->whereBetween('effective_date', [$startDate, $endDate])
|
|
->whereNotNull('category_id')
|
|
->select(
|
|
'category_id',
|
|
DB::raw('SUM(amount) as total'),
|
|
DB::raw('COUNT(*) as count')
|
|
)
|
|
->groupBy('category_id')
|
|
->with('category:id,name,color,icon')
|
|
->orderByDesc('total')
|
|
->limit(10)
|
|
->get();
|
|
|
|
$total = $data->sum('total');
|
|
|
|
$result = $data->map(function ($item) use ($total) {
|
|
return [
|
|
'category_id' => $item->category_id,
|
|
'category_name' => $item->category->name ?? 'Sem categoria',
|
|
'category_color' => $item->category->color ?? '#6b7280',
|
|
'category_icon' => $item->category->icon ?? 'bi-tag',
|
|
'total' => (float) $item->total,
|
|
'count' => (int) $item->count,
|
|
'percentage' => $total > 0 ? round(($item->total / $total) * 100, 1) : 0,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'period' => [
|
|
'start' => $startDate->format('Y-m-d'),
|
|
'end' => $endDate->format('Y-m-d'),
|
|
'months' => $months,
|
|
],
|
|
'data' => $result,
|
|
'total' => (float) $total,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Receitas por categoria (últimos N meses)
|
|
* Ignora transações marcadas como transferências entre contas
|
|
*/
|
|
public function incomeByCategory(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
$months = min((int) $request->get('months', 3), 12);
|
|
|
|
$startDate = now()->subMonths($months - 1)->startOfMonth();
|
|
$endDate = now()->endOfMonth();
|
|
|
|
$data = Transaction::ofUser($userId)
|
|
->completed()
|
|
->where('is_transfer', false) // Ignorar transferências entre contas
|
|
->where('type', 'credit')
|
|
->whereBetween('effective_date', [$startDate, $endDate])
|
|
->whereNotNull('category_id')
|
|
->select(
|
|
'category_id',
|
|
DB::raw('SUM(amount) as total'),
|
|
DB::raw('COUNT(*) as count')
|
|
)
|
|
->groupBy('category_id')
|
|
->with('category:id,name,color,icon')
|
|
->orderByDesc('total')
|
|
->limit(10)
|
|
->get();
|
|
|
|
$total = $data->sum('total');
|
|
|
|
$result = $data->map(function ($item) use ($total) {
|
|
return [
|
|
'category_id' => $item->category_id,
|
|
'category_name' => $item->category->name ?? 'Sem categoria',
|
|
'category_color' => $item->category->color ?? '#6b7280',
|
|
'category_icon' => $item->category->icon ?? 'bi-tag',
|
|
'total' => (float) $item->total,
|
|
'count' => (int) $item->count,
|
|
'percentage' => $total > 0 ? round(($item->total / $total) * 100, 1) : 0,
|
|
];
|
|
});
|
|
|
|
return response()->json([
|
|
'period' => [
|
|
'start' => $startDate->format('Y-m-d'),
|
|
'end' => $endDate->format('Y-m-d'),
|
|
'months' => $months,
|
|
],
|
|
'data' => $result,
|
|
'total' => (float) $total,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Análise de diferenças entre valores planejados e efetivos
|
|
* Mostra sobrepagamentos e subpagamentos
|
|
*/
|
|
public function paymentVariances(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
$months = min((int) $request->get('months', 3), 12);
|
|
|
|
$startDate = now()->subMonths($months - 1)->startOfMonth();
|
|
$endDate = now()->endOfMonth();
|
|
|
|
// Buscar transações completed com diferença entre planned_amount e amount
|
|
$transactions = Transaction::ofUser($userId)
|
|
->completed()
|
|
->where('is_transfer', false)
|
|
->whereBetween('effective_date', [$startDate, $endDate])
|
|
->whereRaw('ABS(amount - planned_amount) > 0.01') // Diferença maior que 1 centavo
|
|
->with(['category:id,name,color,icon', 'account:id,name'])
|
|
->orderByRaw('ABS(amount - planned_amount) DESC')
|
|
->limit(50)
|
|
->get();
|
|
|
|
$result = $transactions->map(function ($t) {
|
|
$variance = $t->amount - $t->planned_amount;
|
|
$variancePercent = $t->planned_amount > 0
|
|
? round(($variance / $t->planned_amount) * 100, 2)
|
|
: 0;
|
|
|
|
// Calcular dias de atraso (diferença entre effective_date e planned_date)
|
|
$delayDays = null;
|
|
if ($t->planned_date && $t->effective_date) {
|
|
$delayDays = $t->planned_date->diffInDays($t->effective_date, false);
|
|
// Positivo = pago depois do planejado (atrasado)
|
|
// Negativo = pago antes do planejado (adiantado)
|
|
}
|
|
|
|
return [
|
|
'id' => $t->id,
|
|
'description' => $t->description,
|
|
'type' => $t->type,
|
|
'planned_amount' => (float) $t->planned_amount,
|
|
'actual_amount' => (float) $t->amount,
|
|
'variance' => (float) $variance,
|
|
'variance_percent' => $variancePercent,
|
|
'effective_date' => $t->effective_date->format('Y-m-d'),
|
|
'planned_date' => $t->planned_date ? $t->planned_date->format('Y-m-d') : null,
|
|
'delay_days' => $delayDays,
|
|
'category' => $t->category ? [
|
|
'id' => $t->category->id,
|
|
'name' => $t->category->name,
|
|
'color' => $t->category->color,
|
|
] : null,
|
|
'account' => $t->account ? [
|
|
'id' => $t->account->id,
|
|
'name' => $t->account->name,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Calcular totais
|
|
$overpayments = $result->filter(fn($t) => $t['variance'] > 0);
|
|
$underpayments = $result->filter(fn($t) => $t['variance'] < 0);
|
|
|
|
// Agrupar por mês para o gráfico
|
|
$byMonth = $transactions->groupBy(function ($t) {
|
|
return $t->effective_date->format('Y-m');
|
|
})->map(function ($items, $month) {
|
|
$over = $items->filter(fn($t) => $t->amount > $t->planned_amount)
|
|
->sum(fn($t) => $t->amount - $t->planned_amount);
|
|
$under = $items->filter(fn($t) => $t->amount < $t->planned_amount)
|
|
->sum(fn($t) => $t->planned_amount - $t->amount);
|
|
|
|
return [
|
|
'month' => $month,
|
|
'overpayment' => round($over, 2),
|
|
'underpayment' => round($under, 2),
|
|
'net' => round($over - $under, 2),
|
|
'count' => $items->count(),
|
|
];
|
|
})->sortKeys()->values();
|
|
|
|
return response()->json([
|
|
'period' => [
|
|
'start' => $startDate->format('Y-m-d'),
|
|
'end' => $endDate->format('Y-m-d'),
|
|
'months' => $months,
|
|
],
|
|
'summary' => [
|
|
'total_overpayment' => round($overpayments->sum('variance'), 2),
|
|
'total_underpayment' => round(abs($underpayments->sum('variance')), 2),
|
|
'net_variance' => round($result->sum('variance'), 2),
|
|
'overpayment_count' => $overpayments->count(),
|
|
'underpayment_count' => $underpayments->count(),
|
|
],
|
|
'by_month' => $byMonth,
|
|
'transactions' => $result,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Dados do calendário para o dashboard
|
|
* Retorna transações e instâncias recorrentes pendentes por data
|
|
*/
|
|
public function calendar(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
|
|
// Período: mês atual por padrão, ou o mês especificado
|
|
$year = (int) $request->get('year', now()->year);
|
|
$month = (int) $request->get('month', now()->month);
|
|
|
|
$startDate = Carbon::create($year, $month, 1)->startOfMonth();
|
|
$endDate = $startDate->copy()->endOfMonth();
|
|
|
|
// Buscar transações do período
|
|
$transactions = Transaction::ofUser($userId)
|
|
->whereBetween('effective_date', [$startDate, $endDate])
|
|
->with(['account:id,name,currency', 'category:id,name,color,icon'])
|
|
->orderBy('effective_date')
|
|
->get()
|
|
->map(function ($t) {
|
|
return [
|
|
'id' => $t->id,
|
|
'type' => 'transaction',
|
|
'date' => $t->effective_date->format('Y-m-d'),
|
|
'description' => $t->description,
|
|
'amount' => (float) $t->amount,
|
|
'transaction_type' => $t->type,
|
|
'status' => $t->status,
|
|
'is_transfer' => $t->is_transfer,
|
|
'account' => $t->account ? [
|
|
'id' => $t->account->id,
|
|
'name' => $t->account->name,
|
|
'currency' => $t->account->currency,
|
|
] : null,
|
|
'category' => $t->category ? [
|
|
'id' => $t->category->id,
|
|
'name' => $t->category->name,
|
|
'color' => $t->category->color,
|
|
'icon' => $t->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Buscar instâncias recorrentes pendentes do período
|
|
$recurringInstances = RecurringInstance::where('user_id', $userId)
|
|
->whereBetween('due_date', [$startDate, $endDate])
|
|
->where('status', 'pending')
|
|
->whereNull('transaction_id') // Não reconciliadas
|
|
->with(['template:id,name,type,planned_amount,account_id,category_id', 'template.account:id,name,currency', 'template.category:id,name,color,icon'])
|
|
->orderBy('due_date')
|
|
->get()
|
|
->map(function ($ri) {
|
|
return [
|
|
'id' => $ri->id,
|
|
'type' => 'recurring',
|
|
'date' => $ri->due_date->format('Y-m-d'),
|
|
'description' => $ri->template->name ?? 'Recorrência',
|
|
'amount' => (float) $ri->planned_amount,
|
|
'transaction_type' => $ri->template->type ?? 'debit',
|
|
'status' => $ri->status,
|
|
'occurrence_number' => $ri->occurrence_number,
|
|
'template_id' => $ri->recurring_template_id,
|
|
'account' => $ri->template && $ri->template->account ? [
|
|
'id' => $ri->template->account->id,
|
|
'name' => $ri->template->account->name,
|
|
'currency' => $ri->template->account->currency,
|
|
] : null,
|
|
'category' => $ri->template && $ri->template->category ? [
|
|
'id' => $ri->template->category->id,
|
|
'name' => $ri->template->category->name,
|
|
'color' => $ri->template->category->color,
|
|
'icon' => $ri->template->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Combinar e agrupar por data
|
|
$allItems = $transactions->concat($recurringInstances);
|
|
|
|
$byDate = $allItems->groupBy('date')->map(function ($items, $date) {
|
|
return [
|
|
'date' => $date,
|
|
'items' => $items->values(),
|
|
'total_credit' => $items->where('transaction_type', 'credit')->sum('amount'),
|
|
'total_debit' => $items->where('transaction_type', 'debit')->sum('amount'),
|
|
'has_transactions' => $items->where('type', 'transaction')->count() > 0,
|
|
'has_recurring' => $items->where('type', 'recurring')->count() > 0,
|
|
'pending_count' => $items->whereIn('status', ['pending', 'scheduled'])->count(),
|
|
];
|
|
})->values();
|
|
|
|
// Resumo do mês (excluindo transferências entre contas)
|
|
$nonTransferTransactions = $transactions->where('is_transfer', false);
|
|
$summary = [
|
|
'transactions_count' => $nonTransferTransactions->count(),
|
|
'recurring_count' => $recurringInstances->count(),
|
|
'total_income' => $nonTransferTransactions->where('transaction_type', 'credit')->sum('amount'),
|
|
'total_expense' => $nonTransferTransactions->where('transaction_type', 'debit')->sum('amount'),
|
|
'pending_recurring' => $recurringInstances->count(),
|
|
'pending_recurring_amount' => $recurringInstances->sum('amount'),
|
|
];
|
|
|
|
return response()->json([
|
|
'period' => [
|
|
'year' => $year,
|
|
'month' => $month,
|
|
'start' => $startDate->format('Y-m-d'),
|
|
'end' => $endDate->format('Y-m-d'),
|
|
],
|
|
'by_date' => $byDate,
|
|
'summary' => $summary,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Transações e recorrências de um dia específico
|
|
*/
|
|
public function calendarDay(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
$date = Carbon::parse($request->get('date', now()->format('Y-m-d')));
|
|
|
|
// Buscar transações do dia
|
|
$transactions = Transaction::ofUser($userId)
|
|
->whereDate('effective_date', $date)
|
|
->with(['account:id,name,currency', 'category:id,name,color,icon'])
|
|
->orderBy('effective_date')
|
|
->orderBy('created_at')
|
|
->get()
|
|
->map(function ($t) {
|
|
return [
|
|
'id' => $t->id,
|
|
'type' => 'transaction',
|
|
'date' => $t->effective_date->format('Y-m-d'),
|
|
'description' => $t->description,
|
|
'amount' => (float) $t->amount,
|
|
'transaction_type' => $t->type,
|
|
'status' => $t->status,
|
|
'is_transfer' => $t->is_transfer,
|
|
'notes' => $t->notes,
|
|
'account' => $t->account ? [
|
|
'id' => $t->account->id,
|
|
'name' => $t->account->name,
|
|
'currency' => $t->account->currency,
|
|
] : null,
|
|
'category' => $t->category ? [
|
|
'id' => $t->category->id,
|
|
'name' => $t->category->name,
|
|
'color' => $t->category->color,
|
|
'icon' => $t->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Buscar instâncias recorrentes pendentes do dia
|
|
$recurringInstances = RecurringInstance::where('user_id', $userId)
|
|
->whereDate('due_date', $date)
|
|
->where('status', 'pending')
|
|
->whereNull('transaction_id')
|
|
->with(['template:id,name,type,planned_amount,account_id,category_id,description,transaction_description', 'template.account:id,name,currency', 'template.category:id,name,color,icon'])
|
|
->orderBy('due_date')
|
|
->get()
|
|
->map(function ($ri) {
|
|
return [
|
|
'id' => $ri->id,
|
|
'type' => 'recurring',
|
|
'date' => $ri->due_date->format('Y-m-d'),
|
|
'description' => $ri->template->name ?? 'Recorrência',
|
|
'amount' => (float) $ri->planned_amount,
|
|
'transaction_type' => $ri->template->type ?? 'debit',
|
|
'status' => $ri->status,
|
|
'occurrence_number' => $ri->occurrence_number,
|
|
'template_id' => $ri->recurring_template_id,
|
|
'notes' => $ri->template->description ?? null,
|
|
'account' => $ri->template && $ri->template->account ? [
|
|
'id' => $ri->template->account->id,
|
|
'name' => $ri->template->account->name,
|
|
'currency' => $ri->template->account->currency,
|
|
] : null,
|
|
'category' => $ri->template && $ri->template->category ? [
|
|
'id' => $ri->template->category->id,
|
|
'name' => $ri->template->category->name,
|
|
'color' => $ri->template->category->color,
|
|
'icon' => $ri->template->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Combinar
|
|
$allItems = $transactions->concat($recurringInstances);
|
|
|
|
// Para o resumo, excluir transferências entre contas
|
|
$nonTransferItems = $allItems->filter(fn($item) => !($item['is_transfer'] ?? false));
|
|
|
|
return response()->json([
|
|
'date' => $date->format('Y-m-d'),
|
|
'date_formatted' => $date->translatedFormat('l, d F Y'),
|
|
'items' => $allItems->values(),
|
|
'summary' => [
|
|
'transactions_count' => $transactions->count(),
|
|
'recurring_count' => $recurringInstances->count(),
|
|
'total_credit' => $nonTransferItems->where('transaction_type', 'credit')->sum('amount'),
|
|
'total_debit' => $nonTransferItems->where('transaction_type', 'debit')->sum('amount'),
|
|
],
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Transações pendentes dos próximos dias (incluindo hoje)
|
|
*/
|
|
public function upcomingTransactions(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
$days = min((int) $request->get('days', 7), 30); // máximo 30 dias
|
|
|
|
$startDate = now()->startOfDay();
|
|
$endDate = now()->addDays($days - 1)->endOfDay();
|
|
|
|
// Buscar transações pendentes do período
|
|
$transactions = Transaction::ofUser($userId)
|
|
->whereIn('status', ['pending', 'scheduled'])
|
|
->whereBetween('effective_date', [$startDate, $endDate])
|
|
->with(['account:id,name,currency', 'category:id,name,color,icon'])
|
|
->orderBy('effective_date')
|
|
->orderBy('created_at')
|
|
->get()
|
|
->map(function ($t) {
|
|
return [
|
|
'id' => $t->id,
|
|
'type' => 'transaction',
|
|
'date' => $t->effective_date->format('Y-m-d'),
|
|
'date_formatted' => $t->effective_date->translatedFormat('D, d M'),
|
|
'description' => $t->description,
|
|
'amount' => (float) $t->amount,
|
|
'transaction_type' => $t->type,
|
|
'status' => $t->status,
|
|
'is_transfer' => $t->is_transfer,
|
|
'days_until' => (int) now()->startOfDay()->diffInDays($t->effective_date, false),
|
|
'account' => $t->account ? [
|
|
'id' => $t->account->id,
|
|
'name' => $t->account->name,
|
|
'currency' => $t->account->currency,
|
|
] : null,
|
|
'category' => $t->category ? [
|
|
'id' => $t->category->id,
|
|
'name' => $t->category->name,
|
|
'color' => $t->category->color,
|
|
'icon' => $t->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Buscar instâncias recorrentes pendentes do período
|
|
$recurringInstances = RecurringInstance::where('user_id', $userId)
|
|
->where('status', 'pending')
|
|
->whereNull('transaction_id')
|
|
->whereBetween('due_date', [$startDate, $endDate])
|
|
->with(['template:id,name,type,planned_amount,account_id,category_id', 'template.account:id,name,currency', 'template.category:id,name,color,icon'])
|
|
->orderBy('due_date')
|
|
->get()
|
|
->map(function ($ri) {
|
|
return [
|
|
'id' => $ri->id,
|
|
'type' => 'recurring',
|
|
'date' => $ri->due_date->format('Y-m-d'),
|
|
'date_formatted' => $ri->due_date->translatedFormat('D, d M'),
|
|
'description' => $ri->template->name ?? 'Recorrência',
|
|
'amount' => (float) $ri->planned_amount,
|
|
'transaction_type' => $ri->template->type ?? 'debit',
|
|
'status' => $ri->status,
|
|
'occurrence_number' => $ri->occurrence_number,
|
|
'template_id' => $ri->recurring_template_id,
|
|
'days_until' => (int) now()->startOfDay()->diffInDays($ri->due_date, false),
|
|
'account' => $ri->template && $ri->template->account ? [
|
|
'id' => $ri->template->account->id,
|
|
'name' => $ri->template->account->name,
|
|
'currency' => $ri->template->account->currency,
|
|
] : null,
|
|
'category' => $ri->template && $ri->template->category ? [
|
|
'id' => $ri->template->category->id,
|
|
'name' => $ri->template->category->name,
|
|
'color' => $ri->template->category->color,
|
|
'icon' => $ri->template->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Combinar e ordenar por data
|
|
$allItems = $transactions->concat($recurringInstances)
|
|
->sortBy('date')
|
|
->values();
|
|
|
|
// Agrupar por data
|
|
$byDate = $allItems->groupBy('date')->map(function ($items, $date) {
|
|
$carbonDate = Carbon::parse($date);
|
|
$daysUntil = (int) now()->startOfDay()->diffInDays($carbonDate, false);
|
|
|
|
return [
|
|
'date' => $date,
|
|
'date_formatted' => $carbonDate->translatedFormat('l, d M'),
|
|
'days_until' => $daysUntil,
|
|
'is_today' => $daysUntil === 0,
|
|
'items' => $items->values(),
|
|
'total_credit' => $items->where('transaction_type', 'credit')->where('is_transfer', '!==', true)->sum('amount'),
|
|
'total_debit' => $items->where('transaction_type', 'debit')->where('is_transfer', '!==', true)->sum('amount'),
|
|
];
|
|
})->values();
|
|
|
|
// Totais gerais (excluindo transferências)
|
|
$nonTransferItems = $allItems->filter(fn($item) => !($item['is_transfer'] ?? false));
|
|
$summary = [
|
|
'total_items' => $allItems->count(),
|
|
'transactions_count' => $transactions->count(),
|
|
'recurring_count' => $recurringInstances->count(),
|
|
'total_credit' => $nonTransferItems->where('transaction_type', 'credit')->sum('amount'),
|
|
'total_debit' => $nonTransferItems->where('transaction_type', 'debit')->sum('amount'),
|
|
];
|
|
|
|
return response()->json([
|
|
'period' => [
|
|
'start' => $startDate->format('Y-m-d'),
|
|
'end' => $endDate->format('Y-m-d'),
|
|
'days' => $days,
|
|
],
|
|
'by_date' => $byDate,
|
|
'items' => $allItems,
|
|
'summary' => $summary,
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* Transações em atraso (vencidas e não pagas)
|
|
*/
|
|
public function overdueTransactions(Request $request): JsonResponse
|
|
{
|
|
$userId = $request->user()->id;
|
|
$limit = min((int) $request->get('limit', 50), 100); // máximo 100
|
|
|
|
$today = now()->startOfDay();
|
|
|
|
// Buscar transações pendentes com data planejada no passado
|
|
$transactions = Transaction::ofUser($userId)
|
|
->whereIn('status', ['pending', 'scheduled'])
|
|
->where('is_transfer', false)
|
|
->where('planned_date', '<', $today)
|
|
->with(['account:id,name,currency', 'category:id,name,color,icon'])
|
|
->orderBy('planned_date')
|
|
->limit($limit)
|
|
->get()
|
|
->map(function ($t) use ($today) {
|
|
$plannedDate = Carbon::parse($t->planned_date);
|
|
$daysOverdue = (int) $plannedDate->diffInDays($today);
|
|
|
|
return [
|
|
'id' => $t->id,
|
|
'type' => 'transaction',
|
|
'planned_date' => $t->planned_date->format('Y-m-d'),
|
|
'planned_date_formatted' => $t->planned_date->translatedFormat('D, d M Y'),
|
|
'description' => $t->description,
|
|
'amount' => (float) ($t->planned_amount ?? $t->amount),
|
|
'transaction_type' => $t->type,
|
|
'status' => $t->status,
|
|
'days_overdue' => $daysOverdue,
|
|
'account' => $t->account ? [
|
|
'id' => $t->account->id,
|
|
'name' => $t->account->name,
|
|
'currency' => $t->account->currency,
|
|
] : null,
|
|
'category' => $t->category ? [
|
|
'id' => $t->category->id,
|
|
'name' => $t->category->name,
|
|
'color' => $t->category->color,
|
|
'icon' => $t->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Buscar instâncias recorrentes em atraso
|
|
$recurringInstances = RecurringInstance::where('user_id', $userId)
|
|
->where('status', 'pending')
|
|
->whereNull('transaction_id')
|
|
->where('due_date', '<', $today)
|
|
->with(['template:id,name,type,planned_amount,account_id,category_id', 'template.account:id,name,currency', 'template.category:id,name,color,icon'])
|
|
->orderBy('due_date')
|
|
->limit($limit)
|
|
->get()
|
|
->map(function ($ri) use ($today) {
|
|
$dueDate = Carbon::parse($ri->due_date);
|
|
$daysOverdue = (int) $dueDate->diffInDays($today);
|
|
|
|
return [
|
|
'id' => $ri->id,
|
|
'type' => 'recurring',
|
|
'planned_date' => $ri->due_date->format('Y-m-d'),
|
|
'planned_date_formatted' => $ri->due_date->translatedFormat('D, d M Y'),
|
|
'description' => $ri->template->name ?? 'Recorrência',
|
|
'amount' => (float) $ri->planned_amount,
|
|
'transaction_type' => $ri->template->type ?? 'debit',
|
|
'status' => $ri->status,
|
|
'occurrence_number' => $ri->occurrence_number,
|
|
'template_id' => $ri->recurring_template_id,
|
|
'days_overdue' => $daysOverdue,
|
|
'account' => $ri->template && $ri->template->account ? [
|
|
'id' => $ri->template->account->id,
|
|
'name' => $ri->template->account->name,
|
|
'currency' => $ri->template->account->currency,
|
|
] : null,
|
|
'category' => $ri->template && $ri->template->category ? [
|
|
'id' => $ri->template->category->id,
|
|
'name' => $ri->template->category->name,
|
|
'color' => $ri->template->category->color,
|
|
'icon' => $ri->template->category->icon,
|
|
] : null,
|
|
];
|
|
});
|
|
|
|
// Buscar parcelas de passivo em atraso
|
|
$liabilityInstallments = LiabilityInstallment::whereHas('liabilityAccount', function ($query) use ($userId) {
|
|
$query->where('user_id', $userId);
|
|
})
|
|
->where('status', 'pending')
|
|
->where('due_date', '<', $today)
|
|
->with(['liabilityAccount:id,name,creditor,currency'])
|
|
->orderBy('due_date')
|
|
->limit($limit)
|
|
->get()
|
|
->map(function ($li) use ($today) {
|
|
$dueDate = Carbon::parse($li->due_date);
|
|
$daysOverdue = (int) $dueDate->diffInDays($today);
|
|
|
|
return [
|
|
'id' => $li->id,
|
|
'type' => 'liability',
|
|
'planned_date' => $li->due_date->format('Y-m-d'),
|
|
'planned_date_formatted' => $li->due_date->translatedFormat('D, d M Y'),
|
|
'description' => $li->liabilityAccount->name . ' - Parcela ' . $li->installment_number,
|
|
'amount' => (float) $li->amount,
|
|
'transaction_type' => 'debit',
|
|
'status' => $li->status,
|
|
'installment_number' => $li->installment_number,
|
|
'liability_account_id' => $li->liability_account_id,
|
|
'creditor' => $li->liabilityAccount->creditor,
|
|
'days_overdue' => $daysOverdue,
|
|
'account' => null,
|
|
'category' => null,
|
|
];
|
|
});
|
|
|
|
// Combinar e ordenar por dias em atraso (mais antigo primeiro)
|
|
$allItems = $transactions->concat($recurringInstances)->concat($liabilityInstallments)
|
|
->sortByDesc('days_overdue')
|
|
->values();
|
|
|
|
// Agrupar por faixa de atraso
|
|
$byRange = collect([
|
|
['key' => 'critical', 'min' => 30, 'max' => PHP_INT_MAX, 'label' => '> 30 dias'],
|
|
['key' => 'high', 'min' => 15, 'max' => 29, 'label' => '15-30 dias'],
|
|
['key' => 'medium', 'min' => 7, 'max' => 14, 'label' => '7-14 dias'],
|
|
['key' => 'low', 'min' => 1, 'max' => 6, 'label' => '1-6 dias'],
|
|
])->map(function ($range) use ($allItems) {
|
|
$items = $allItems->filter(function ($item) use ($range) {
|
|
return $item['days_overdue'] >= $range['min'] && $item['days_overdue'] <= $range['max'];
|
|
})->values();
|
|
|
|
return [
|
|
'key' => $range['key'],
|
|
'label' => $range['label'],
|
|
'min_days' => $range['min'],
|
|
'max_days' => $range['max'] === PHP_INT_MAX ? null : $range['max'],
|
|
'items' => $items,
|
|
'count' => $items->count(),
|
|
'total' => $items->sum('amount'),
|
|
];
|
|
})->filter(fn($range) => $range['count'] > 0)->values();
|
|
|
|
// Totais gerais
|
|
$summary = [
|
|
'total_items' => $allItems->count(),
|
|
'transactions_count' => $transactions->count(),
|
|
'recurring_count' => $recurringInstances->count(),
|
|
'liability_count' => $liabilityInstallments->count(),
|
|
'total_amount' => $allItems->sum('amount'),
|
|
'total_credit' => $allItems->where('transaction_type', 'credit')->sum('amount'),
|
|
'total_debit' => $allItems->where('transaction_type', 'debit')->sum('amount'),
|
|
'oldest_date' => $allItems->isNotEmpty() ? $allItems->first()['planned_date'] : null,
|
|
'max_days_overdue' => $allItems->isNotEmpty() ? $allItems->first()['days_overdue'] : 0,
|
|
];
|
|
|
|
return response()->json([
|
|
'by_range' => $byRange,
|
|
'items' => $allItems->take($limit),
|
|
'summary' => $summary,
|
|
]);
|
|
}
|
|
}
|