File: /var/www/api_matriculas/app/Services/DashboardService.php
<?php
namespace App\Services;
use App\Models\ContractDetail;
use App\Repositories\ContractRepository;
use App\Repositories\ProfileRepository;
use App\Repositories\StudentRepository;
use App\Repositories\UserRepository;
use App\Repositories\WebhookRepository;
use App\Models\PresencialPayment;
use Illuminate\Support\Facades\Log;
use Carbon\Carbon;
class DashboardService
{
protected $periodService;
protected $postulationService;
protected $contractService;
protected $studentService;
public function __construct(
PeriodService $periodService,
PostulationService $postulationService,
ContractService $contractService,
StudentService $studentService
) {
$this->periodService = $periodService;
$this->postulationService = $postulationService;
$this->contractService = $contractService;
$this->studentService = $studentService;
}
public function getData($periodId = null)
{
$data = [];
$students = new StudentRepository();
// Obtener período: si se pasa un ID usar ese, si no el activo
if ($periodId) {
$currentPeriod = $this->periodService->getPeriodDataById($periodId);
} else {
$currentPeriod = $this->periodService->getPeriodActivate();
}
if (auth()->check() && auth()->user()->profile->code === 'parent') {
$data['postulations'] = !empty($currentPeriod) ? $this->postulationService->list($currentPeriod['period_year'], null) : [];
// Obtener contrato del parent para el período seleccionado
if ($periodId) {
$contractRepo = new ContractRepository();
$register = $contractRepo->getByPeriodId($periodId);
if ($register) {
$data['contract'] = [
'code' => $register->code_contract,
'year' => $register->period->period_year,
'status_contract' => $register->statusContract->code,
'paid' => $register->statusPayment->code == 'completed',
'paid_at' => !empty($register->paid_at) ? sortDateHourForHuman($register->paid_at) : sortDateHourForHuman($register->updated_at),
'signed' => in_array($register->statusSignature->code, ['signed', 'manual_signed']),
'is_manual_signature' => in_array($register->statusSignature->code, ['pending_manual', 'manual_signed']),
'total_amount' => $register->total_amount,
'enrollment_type' => $register->enrollment_type,
];
} else {
$data['contract'] = null;
}
} else {
$data['contract'] = $this->contractService->getContractActive();
}
$allStudents = $students->getAll();
// CAE/CFE: alumnos con blocked_student (tienen prioridad sobre INACTIVE).
$blockedStudents = $allStudents->whereNotNull('blocked_student')
->map(function ($student) {
return [
'id' => $student['id'],
'rut' => $student['rut'],
'full_name' => "{$student['first_name']} {$student['last_name']}",
'blocked_student' => $student['blocked_student'],
];
});
// INACTIVE: alumnos con status=0 que NO estén ya en CAE/CFE.
$inactiveStudents = $allStudents
->where('status', 0)
->whereNull('blocked_student')
->map(function ($student) {
return [
'id' => $student['id'],
'rut' => $student['rut'],
'full_name' => "{$student['first_name']} {$student['last_name']}",
'blocked_student' => 'INACTIVE',
];
});
$grouped = $blockedStudents->groupBy('blocked_student');
if ($inactiveStudents->isNotEmpty()) {
$grouped['INACTIVE'] = $inactiveStudents->values();
}
$data['students'] = $grouped;
$configuration = getConfiguration('general');
$yearReplace = $currentPeriod['period_year'] ?? '';
$data['configuration'] = [
'whatsapp' => $configuration['whatsapp'],
'email' => $configuration['email'],
'cae_title' => str_replace('#PERIOD_YEAR', $yearReplace, $configuration['cae_title']),
'cae_message' => $configuration['cae_message'],
'cfe_title' => str_replace('#PERIOD_YEAR', $yearReplace, $configuration['cfe_title']),
'cfe_message' => $configuration['cfe_message'],
'inactive_title' => str_replace('#PERIOD_YEAR', $yearReplace, $configuration['inactive_title'] ?? 'Matrícula no disponible para #PERIOD_YEAR'),
'inactive_message' => $configuration['inactive_message'] ?? 'Tu alumno se encuentra inactivo en el sistema. Para reactivarlo o resolver dudas, contacta a administración.',
'enrollment_start_date' => $configuration['enrollment_start_date'] ?? null,
'enrollment_end_date' => $configuration['enrollment_end_date'] ?? null,
];
} else {
$contracts = new ContractRepository();
$users = new UserRepository();
$profile = new ProfileRepository();
$webhook = new WebhookRepository();
$profileParent = $profile->getByCode('parent');
$data['postulations'] = !empty($currentPeriod) ? $this->postulationService->list($currentPeriod['period_year']) : [];
// KPIs derivados de contratos del período
$activePeriodId = !empty($currentPeriod) ? ($currentPeriod['id'] ?? null) : null;
$contractsInPeriod = $contracts->getAll($activePeriodId);
$contractIds = $contractsInPeriod->pluck('id');
// Estudiantes: contar desde contracts_detail (student_id vive en el detalle)
$studentsCount = ContractDetail::whereIn('contract_id', $contractIds)
->whereNotNull('student_id')
->distinct('student_id')
->count('student_id');
$data['kpi'] = [
'students' => $studentsCount,
'contracts' => $contractsInPeriod->count(),
'parents' => $contractsInPeriod->pluck('financial_parent_id')->filter()->unique()->count(),
'users' => $profileParent ? $users->getAll($profileParent->id, '!=')->count() : $users->getAll()->count(),
];
// Status summary: contract counts grouped by status_contract code
$data['status_summary'] = $contractsInPeriod
->groupBy(fn($c) => $c->statusContract->code ?? 'unknown')
->map(fn($group, $code) => [
'code' => $code,
'label' => $group->first()->statusContract->description ?? $code,
'count' => $group->count(),
])
->values();
// Recent payments: last 10 presencial payments for contracts in this period
$data['recent_payments'] = PresencialPayment::whereIn('contract_id', $contractIds)
->with(['contract', 'paymentMethodRecord', 'registeredByUser'])
->orderBy('created_at', 'desc')
->limit(10)
->get()
->map(function ($p) {
// Currency: only TOKU (subscription) payments store amount in original currency (UF)
// Presencial payments always store amount in CLP regardless of concept currency
$currency = '$';
if ($p->subscription_status) {
$details = $p->receipt_data['details'] ?? [];
$currencies = array_unique(array_column($details, 'currency'));
$currency = count($currencies) === 1 ? $currencies[0] : '$';
}
return [
'id' => $p->id,
'created_at' => $p->created_at->format('d/m/Y H:i'),
'amount' => $p->amount,
'currency' => $currency,
'reconciliation_status' => $p->reconciliation_status,
'subscription_status' => $p->subscription_status,
'payment_method' => $p->paymentMethodRecord->payment_method ?? ($p->subscription_status ? 'TOKU' : 'N/A'),
'contract_code' => $p->contract->code_contract ?? null,
'parent_name' => trim(($p->contract->financial_parent_first_name ?? '') . ' ' . ($p->contract->financial_parent_last_name ?? '')),
];
});
// Alerts: counts of actionable pending items
$pendingPayment = $contractsInPeriod->filter(fn($c) =>
in_array($c->statusPayment->code ?? '', ['pending', 'partial'])
)->count();
$pendingSignature = $contractsInPeriod->filter(fn($c) =>
in_array($c->statusSignature->code ?? '', ['pending', 'sent', 'pending_manual'])
)->count();
$incompleteData = $contractsInPeriod->filter(fn($c) =>
in_array($c->statusContract->code ?? '', ['in_course', 'pending_payment', 'pending_signature'])
&& (empty($c->financial_parent_email) || empty($c->financial_parent_rut))
)->count();
$data['alerts'] = [
['key' => 'pending_payment', 'count' => $pendingPayment, 'label' => 'pendientes de pago'],
['key' => 'pending_signature', 'count' => $pendingSignature, 'label' => 'pendientes de firma'],
['key' => 'incomplete_data', 'count' => $incompleteData, 'label' => 'con datos incompletos'],
];
// Signature stats: requires_signature (non-playgroup) vs playgroup
$requiresSignature = $contractsInPeriod->filter(fn($c) => ($c->enrollment_type ?? '') !== 'playgroup');
$playgroup = $contractsInPeriod->filter(fn($c) => ($c->enrollment_type ?? '') === 'playgroup');
$signedElectronic = $requiresSignature->filter(fn($c) => ($c->statusSignature->code ?? '') === 'signed')->count();
$signedManual = $requiresSignature->filter(fn($c) => ($c->statusSignature->code ?? '') === 'manual_signed')->count();
$signed = $signedElectronic + $signedManual;
$pendingManual = $requiresSignature->filter(fn($c) => ($c->statusSignature->code ?? '') === 'pending_manual')->count();
$data['signature_stats'] = [
'requires_signature' => $requiresSignature->count(),
'signed' => $signed,
'signed_electronic' => $signedElectronic,
'signed_manual' => $signedManual,
'pending' => $requiresSignature->count() - $signed - $pendingManual,
'pending_manual' => $pendingManual,
'playgroup' => $playgroup->count(),
];
}
$data['period'] = $currentPeriod;
// Añadir información del segmento activo
if (!empty($currentPeriod) && isset($currentPeriod['segments'])) {
$today = Carbon::today();
$activeSegment = null;
foreach ($currentPeriod['segments'] as $segment) {
if ($segment['status'] == 1) {
$startDate = Carbon::parse($segment['start_date']);
$endDate = Carbon::parse($segment['end_date']);
if ($today->between($startDate, $endDate)) {
$activeSegment = $segment;
break;
}
}
}
$data['active_segment'] = [
'is_active' => !is_null($activeSegment),
'segment' => $activeSegment
];
} else {
$data['active_segment'] = [
'is_active' => false,
'segment' => null
];
}
return $data;
}
}