## New Features - Email notifications for overdue and upcoming payments - User preferences page for notification settings - Daily scheduler to send alerts at user-configured time - Smart analysis: payable items, transfer suggestions between accounts ## Backend - Migration for user_preferences table - SendDuePaymentsAlert Artisan command - DuePaymentsAlert Mailable with HTML/text templates - UserPreferenceController with test-notification endpoint - Scheduler config for notify:due-payments command ## Frontend - Preferences.jsx page with notification toggle - API service for preferences - Route and menu link for settings - Translations (PT-BR, EN, ES) ## Server - Cron configured for Laravel scheduler Version: 1.44.5
255 lines
7.7 KiB
PHP
255 lines
7.7 KiB
PHP
<?php
|
|
|
|
namespace App\Models;
|
|
|
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
|
use Illuminate\Database\Eloquent\Model;
|
|
use Carbon\Carbon;
|
|
|
|
class Budget extends Model
|
|
{
|
|
use HasFactory;
|
|
|
|
protected $fillable = [
|
|
'user_id',
|
|
'category_id',
|
|
'subcategory_id',
|
|
'cost_center_id',
|
|
'name',
|
|
'amount',
|
|
'currency',
|
|
'year',
|
|
'month',
|
|
'period_type',
|
|
'is_cumulative',
|
|
'is_active',
|
|
'notes',
|
|
];
|
|
|
|
protected $casts = [
|
|
'amount' => 'decimal:2',
|
|
'is_active' => 'boolean',
|
|
'is_cumulative' => 'boolean',
|
|
];
|
|
|
|
protected $appends = [
|
|
'spent_amount',
|
|
'remaining_amount',
|
|
'usage_percentage',
|
|
'is_exceeded',
|
|
'period_label',
|
|
];
|
|
|
|
// ============================================
|
|
// Relaciones
|
|
// ============================================
|
|
|
|
public function user()
|
|
{
|
|
return $this->belongsTo(User::class);
|
|
}
|
|
|
|
public function category()
|
|
{
|
|
return $this->belongsTo(Category::class);
|
|
}
|
|
|
|
public function subcategory()
|
|
{
|
|
return $this->belongsTo(Category::class, 'subcategory_id');
|
|
}
|
|
|
|
public function costCenter()
|
|
{
|
|
return $this->belongsTo(CostCenter::class);
|
|
}
|
|
|
|
// ============================================
|
|
// Accessors
|
|
// ============================================
|
|
|
|
public function getSpentAmountAttribute()
|
|
{
|
|
// Calcular el gasto real de las transacciones
|
|
$query = Transaction::where('user_id', $this->user_id)
|
|
->where('type', 'debit');
|
|
|
|
// Definir el rango de fechas según el tipo de período
|
|
$startDate = Carbon::create($this->year, $this->month, 1);
|
|
|
|
if ($this->is_cumulative) {
|
|
// Cumulativo: desde el inicio del año hasta el final del período actual
|
|
$endDate = $this->getPeriodEndDate();
|
|
$query->whereYear('effective_date', $this->year)
|
|
->whereDate('effective_date', '<=', $endDate);
|
|
} else {
|
|
// No cumulativo: solo el período específico
|
|
switch ($this->period_type) {
|
|
case 'monthly':
|
|
$query->whereYear('effective_date', $this->year)
|
|
->whereMonth('effective_date', $this->month);
|
|
break;
|
|
|
|
case 'bimestral':
|
|
// 2 meses: mes actual + anterior (o siguiente según configuración)
|
|
$endDate = $startDate->copy()->addMonths(2)->subDay();
|
|
$query->whereBetween('effective_date', [$startDate, $endDate]);
|
|
break;
|
|
|
|
case 'trimestral':
|
|
// 3 meses
|
|
$endDate = $startDate->copy()->addMonths(3)->subDay();
|
|
$query->whereBetween('effective_date', [$startDate, $endDate]);
|
|
break;
|
|
|
|
case 'semestral':
|
|
// 6 meses
|
|
$endDate = $startDate->copy()->addMonths(6)->subDay();
|
|
$query->whereBetween('effective_date', [$startDate, $endDate]);
|
|
break;
|
|
|
|
case 'yearly':
|
|
$query->whereYear('effective_date', $this->year);
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Se tem subcategoria específica, usa apenas ela
|
|
if ($this->subcategory_id) {
|
|
$query->where('category_id', $this->subcategory_id);
|
|
}
|
|
// Se tem apenas categoria, inclui todas as subcategorias
|
|
elseif ($this->category_id) {
|
|
$categoryIds = [$this->category_id];
|
|
$subcategories = Category::where('parent_id', $this->category_id)->pluck('id')->toArray();
|
|
$categoryIds = array_merge($categoryIds, $subcategories);
|
|
|
|
$query->whereIn('category_id', $categoryIds);
|
|
}
|
|
|
|
// Filtrar por centro de custos se especificado
|
|
if ($this->cost_center_id) {
|
|
$query->where('cost_center_id', $this->cost_center_id);
|
|
}
|
|
|
|
return abs($query->sum('amount'));
|
|
}
|
|
|
|
/**
|
|
* Obtém a data final do período
|
|
*/
|
|
private function getPeriodEndDate()
|
|
{
|
|
$startDate = Carbon::create($this->year, $this->month, 1);
|
|
|
|
switch ($this->period_type) {
|
|
case 'monthly':
|
|
return $startDate->endOfMonth();
|
|
case 'bimestral':
|
|
return $startDate->copy()->addMonths(2)->subDay();
|
|
case 'trimestral':
|
|
return $startDate->copy()->addMonths(3)->subDay();
|
|
case 'semestral':
|
|
return $startDate->copy()->addMonths(6)->subDay();
|
|
case 'yearly':
|
|
return $startDate->endOfYear();
|
|
default:
|
|
return $startDate->endOfMonth();
|
|
}
|
|
}
|
|
|
|
public function getRemainingAmountAttribute()
|
|
{
|
|
return $this->amount - $this->spent_amount;
|
|
}
|
|
|
|
public function getUsagePercentageAttribute()
|
|
{
|
|
if ($this->amount <= 0) return 0;
|
|
return round(($this->spent_amount / $this->amount) * 100, 1);
|
|
}
|
|
|
|
public function getIsExceededAttribute()
|
|
{
|
|
return $this->spent_amount > $this->amount;
|
|
}
|
|
|
|
public function getPeriodLabelAttribute()
|
|
{
|
|
$months = [
|
|
1 => 'Enero', 2 => 'Febrero', 3 => 'Marzo', 4 => 'Abril',
|
|
5 => 'Mayo', 6 => 'Junio', 7 => 'Julio', 8 => 'Agosto',
|
|
9 => 'Septiembre', 10 => 'Octubre', 11 => 'Noviembre', 12 => 'Diciembre'
|
|
];
|
|
|
|
$monthName = $months[$this->month] ?? '';
|
|
|
|
switch ($this->period_type) {
|
|
case 'yearly':
|
|
return $this->year;
|
|
case 'bimestral':
|
|
return $monthName . ' ' . $this->year . ' (Bimestral)';
|
|
case 'trimestral':
|
|
return $monthName . ' ' . $this->year . ' (Trimestral)';
|
|
case 'semestral':
|
|
return $monthName . ' ' . $this->year . ' (Semestral)';
|
|
default:
|
|
return $monthName . ' ' . $this->year;
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Methods
|
|
// ============================================
|
|
|
|
public static function copyToNextMonth($userId, $fromYear, $fromMonth)
|
|
{
|
|
$nextMonth = $fromMonth === 12 ? 1 : $fromMonth + 1;
|
|
$nextYear = $fromMonth === 12 ? $fromYear + 1 : $fromYear;
|
|
|
|
$budgets = self::where('user_id', $userId)
|
|
->where('year', $fromYear)
|
|
->where('month', $fromMonth)
|
|
->where('is_active', true)
|
|
->get();
|
|
|
|
foreach ($budgets as $budget) {
|
|
self::firstOrCreate([
|
|
'user_id' => $userId,
|
|
'category_id' => $budget->category_id,
|
|
'year' => $nextYear,
|
|
'month' => $nextMonth,
|
|
], [
|
|
'name' => $budget->name,
|
|
'amount' => $budget->amount,
|
|
'currency' => $budget->currency,
|
|
'period_type' => 'monthly',
|
|
'is_active' => true,
|
|
]);
|
|
}
|
|
}
|
|
|
|
// ============================================
|
|
// Scopes
|
|
// ============================================
|
|
|
|
public function scopeActive($query)
|
|
{
|
|
return $query->where('is_active', true);
|
|
}
|
|
|
|
public function scopeForPeriod($query, $year, $month = null)
|
|
{
|
|
$query->where('year', $year);
|
|
if ($month) {
|
|
$query->where('month', $month);
|
|
}
|
|
return $query;
|
|
}
|
|
|
|
public function scopeForUser($query, $userId)
|
|
{
|
|
return $query->where('user_id', $userId);
|
|
}
|
|
}
|