<?php
/**
* Working English System - Módulo de Estudiantes
*
* @package WorkingEnglishSystem
* @subpackage Modules/Students
* @version 1.0.0
*/
if (!defined('ABSPATH')) {
exit;
}
class WES_Students_Module {
private $db;
private $table_students;
private $table_enrollments;
private $table_payments;
public function __construct() {
global $wpdb;
$this->db = $wpdb;
$this->table_students = $wpdb->prefix . 'wes_students';
$this->table_enrollments = $wpdb->prefix . 'wes_enrollments';
$this->table_payments = $wpdb->prefix . 'wes_payments';
$this->init_hooks();
}
/**
* Inicializar hooks y acciones
*/
private function init_hooks() {
add_action('wp_ajax_wes_get_students', array($this, 'ajax_get_students'));
add_action('wp_ajax_wes_save_student', array($this, 'ajax_save_student'));
add_action('wp_ajax_wes_delete_student', array($this, 'ajax_delete_student'));
add_action('wp_ajax_wes_get_student', array($this, 'ajax_get_student'));
add_action('wp_ajax_wes_search_students', array($this, 'ajax_search_students'));
add_action('wp_ajax_wes_export_students', array($this, 'ajax_export_students'));
add_action('wp_ajax_wes_update_student_status', array($this, 'ajax_update_student_status'));
add_action('wp_ajax_wes_get_students_stats', array($this, 'ajax_get_students_stats'));
// AJAX para frontend (no-priv significa usuarios en frontend)
add_action('wp_ajax_nopriv_wes_get_students', array($this, 'ajax_get_students'));
add_action('wp_ajax_nopriv_wes_save_student', array($this, 'ajax_save_student'));
add_action('wp_ajax_nopriv_wes_delete_student', array($this, 'ajax_delete_student'));
add_action('wp_ajax_nopriv_wes_get_student', array($this, 'ajax_get_student'));
add_action('wp_ajax_nopriv_wes_search_students', array($this, 'ajax_search_students'));
add_action('wp_ajax_nopriv_wes_export_students', array($this, 'ajax_export_students'));
add_action('wp_ajax_nopriv_wes_update_student_status', array($this, 'ajax_update_student_status'));
add_action('wp_ajax_nopriv_wes_get_students_stats', array($this, 'ajax_get_students_stats'));
// Registrar scripts y estilos específicos del módulo
add_action('admin_enqueue_scripts', array($this, 'enqueue_scripts'));
}
/**
* Cargar scripts y estilos del módulo
*/
public function enqueue_scripts($hook) {
if (strpos($hook, 'working-english-system') !== false) {
wp_enqueue_script(
'wes-students-js',
WES_PLUGIN_URL . 'modules/students/assets/students.js',
array('jquery', 'wes-main-js'),
WES_VERSION,
true
);
wp_enqueue_style(
'wes-students-css',
WES_PLUGIN_URL . 'modules/students/assets/students.css',
array('wes-main-css'),
WES_VERSION
);
}
}
/**
* Renderizar la página principal del módulo de estudiantes
*/
public function render_students_page() {
// Verificar permisos
if (!current_user_can('manage_options')) {
wp_die(__('No tienes permisos para acceder a esta página.', 'working-english-system'));
}
include WES_PLUGIN_PATH . 'modules/students/templates/students-main.php';
}
/**
* AJAX: Obtener lista de estudiantes con paginación y filtros
*/
public function ajax_get_students() {
check_ajax_referer('wes_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$page = intval($_POST['page'] ?? 1);
$per_page = intval($_POST['per_page'] ?? 20);
$search = sanitize_text_field($_POST['search'] ?? '');
$status = sanitize_text_field($_POST['status'] ?? '');
$country = sanitize_text_field($_POST['country'] ?? '');
$sort_by = sanitize_text_field($_POST['sort_by'] ?? 'created_at');
$sort_order = sanitize_text_field($_POST['sort_order'] ?? 'DESC');
$offset = ($page - 1) * $per_page;
// Construir consulta base
$where_conditions = array("s.deleted_at IS NULL");
$params = array();
// Filtro de búsqueda ampliado
if (!empty($search)) {
$where_conditions[] = "(s.first_name LIKE %s OR s.last_name LIKE %s OR s.email LIKE %s OR s.phone LIKE %s OR s.student_id LIKE %s OR s.city LIKE %s OR s.identity_document LIKE %s)";
$search_term = '%' . $this->db->esc_like($search) . '%';
$params = array_merge($params, array($search_term, $search_term, $search_term, $search_term, $search_term, $search_term, $search_term));
}
// Filtro de estado
if (!empty($status)) {
$where_conditions[] = "s.status = %s";
$params[] = $status;
}
// Filtro por país
if (!empty($country)) {
$where_conditions[] = "s.country_of_origin = %s";
$params[] = $country;
}
$where_clause = 'WHERE ' . implode(' AND ', $where_conditions);
// Validar columnas de ordenamiento
$allowed_sort_columns = array('student_id', 'first_name', 'last_name', 'email', 'country_of_origin', 'city', 'status', 'created_at');
if (!in_array($sort_by, $allowed_sort_columns)) {
$sort_by = 'created_at';
}
$sort_order = strtoupper($sort_order) === 'ASC' ? 'ASC' : 'DESC';
// Consulta principal
$query = "
SELECT s.*
FROM {$this->table_students} s
{$where_clause}
ORDER BY s.{$sort_by} {$sort_order}
LIMIT %d OFFSET %d
";
$params[] = $per_page;
$params[] = $offset;
$students = $this->db->get_results($this->db->prepare($query, $params));
// Consulta para el total de registros
$count_query = "
SELECT COUNT(s.id) as total
FROM {$this->table_students} s
{$where_clause}
";
$count_params = array_slice($params, 0, -2); // Remover LIMIT y OFFSET
$total_students = $this->db->get_var($this->db->prepare($count_query, $count_params));
// Formatear datos de estudiantes
$formatted_students = array();
foreach ($students as $student) {
$formatted_students[] = array(
'id' => $student->id,
'student_id' => $student->student_id,
'first_name' => $student->first_name,
'last_name' => $student->last_name,
'full_name' => $student->first_name . ' ' . $student->last_name,
'email' => $student->email,
'phone' => $student->phone,
'date_of_birth' => $student->date_of_birth,
'gender' => $student->gender,
'country_of_origin' => $student->country_of_origin,
'nationality' => $student->nationality,
'identity_document' => $student->identity_document,
'city' => $student->city,
'address' => $student->address,
'emergency_contact' => $student->emergency_contact,
'emergency_email' => $student->emergency_email,
'emergency_phone' => $student->emergency_phone,
'emergency_relationship' => $student->emergency_relationship,
'how_did_you_hear' => $student->how_did_you_hear,
'referred_by' => $student->referred_by,
'special_notes' => $student->special_notes,
'current_level' => $student->current_level,
'status' => $student->status,
'notes' => $student->notes,
'created_at' => $student->created_at,
'updated_at' => $student->updated_at,
'status_label' => $this->get_status_label($student->status)
);
}
wp_send_json_success(array(
'students' => $formatted_students,
'pagination' => array(
'current_page' => $page,
'per_page' => $per_page,
'total_items' => intval($total_students),
'total_pages' => ceil($total_students / $per_page)
)
));
}
/**
* AJAX: Guardar estudiante (crear o actualizar)
*/
public function ajax_save_student() {
check_ajax_referer('wes_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$student_id = intval($_POST['student_id'] ?? 0);
$student_data = $this->sanitize_student_data($_POST);
// Validar datos requeridos
$validation_result = $this->validate_student_data($student_data, $student_id);
if (!$validation_result['valid']) {
wp_send_json_error($validation_result['message']);
}
if ($student_id > 0) {
// Actualizar estudiante existente
$result = $this->update_student($student_id, $student_data);
} else {
// Crear nuevo estudiante
$result = $this->create_student($student_data);
}
if ($result['success']) {
wp_send_json_success($result['data']);
} else {
wp_send_json_error($result['message']);
}
}
/**
* AJAX: Eliminar estudiante (soft delete)
*/
public function ajax_delete_student() {
check_ajax_referer('wes_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$student_id = intval($_POST['student_id'] ?? 0);
if ($student_id <= 0) {
wp_send_json_error('ID de estudiante inválido');
}
// Verificar si el estudiante existe
$student = $this->get_student_by_id($student_id);
if (!$student) {
wp_send_json_error('Estudiante no encontrado');
}
// Realizar soft delete
$result = $this->db->update(
$this->table_students,
array(
'deleted_at' => current_time('mysql'),
'updated_at' => current_time('mysql')
),
array('id' => $student_id),
array('%s', '%s'),
array('%d')
);
if ($result !== false) {
wp_send_json_success('Estudiante eliminado correctamente');
} else {
wp_send_json_error('Error al eliminar el estudiante');
}
}
/**
* AJAX: Obtener datos de un estudiante específico
*/
public function ajax_get_student() {
check_ajax_referer('wes_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$student_id = intval($_POST['student_id'] ?? 0);
if ($student_id <= 0) {
wp_send_json_error('ID de estudiante inválido');
}
$student = $this->get_student_by_id($student_id);
if (!$student) {
wp_send_json_error('Estudiante no encontrado');
}
wp_send_json_success($student);
}
/**
* AJAX: Búsqueda rápida de estudiantes
*/
public function ajax_search_students() {
check_ajax_referer('wes_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$search_term = sanitize_text_field($_POST['search'] ?? '');
$limit = intval($_POST['limit'] ?? 10);
if (strlen($search_term) < 2) {
wp_send_json_success(array());
}
$query = "
SELECT id, student_id, first_name, last_name, email, phone, city, country_of_origin, status
FROM {$this->table_students}
WHERE deleted_at IS NULL
AND (first_name LIKE %s OR last_name LIKE %s OR email LIKE %s OR student_id LIKE %s OR city LIKE %s OR phone LIKE %s)
ORDER BY first_name ASC, last_name ASC
LIMIT %d
";
$search_like = '%' . $this->db->esc_like($search_term) . '%';
$results = $this->db->get_results($this->db->prepare(
$query,
$search_like,
$search_like,
$search_like,
$search_like,
$search_like,
$search_like,
$limit
));
$formatted_results = array();
foreach ($results as $student) {
$formatted_results[] = array(
'id' => $student->id,
'student_id' => $student->student_id,
'name' => $student->first_name . ' ' . $student->last_name,
'email' => $student->email,
'phone' => $student->phone,
'city' => $student->city,
'country' => $student->country_of_origin,
'status' => $student->status
);
}
wp_send_json_success($formatted_results);
}
/**
* AJAX: Actualizar estado de estudiante
*/
public function ajax_update_student_status() {
check_ajax_referer('wes_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
$student_id = intval($_POST['student_id'] ?? 0);
$new_status = sanitize_text_field($_POST['status'] ?? '');
if ($student_id <= 0) {
wp_send_json_error('ID de estudiante inválido');
}
$allowed_statuses = array('active', 'inactive', 'graduated', 'suspended', 'retired');
if (!in_array($new_status, $allowed_statuses)) {
wp_send_json_error('Estado inválido');
}
$result = $this->db->update(
$this->table_students,
array(
'status' => $new_status,
'updated_at' => current_time('mysql')
),
array('id' => $student_id),
array('%s', '%s'),
array('%d')
);
if ($result !== false) {
wp_send_json_success('Estado actualizado correctamente');
} else {
wp_send_json_error('Error al actualizar el estado');
}
}
/**
* AJAX: Obtener estadísticas de estudiantes
*/
public function ajax_get_students_stats() {
check_ajax_referer('wes_nonce', 'nonce');
if (!current_user_can('manage_options')) {
wp_send_json_error('Permisos insuficientes');
}
global $wpdb;
// Consulta optimizada para obtener todas las estadísticas
$stats_query = "
SELECT
COUNT(*) as total_students,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) as active_students,
SUM(CASE WHEN status = 'graduated' THEN 1 ELSE 0 END) as graduated_students,
SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) as inactive_students,
SUM(CASE WHEN status = 'suspended' THEN 1 ELSE 0 END) as suspended_students,
SUM(CASE WHEN status = 'retired' THEN 1 ELSE 0 END) as retired_students,
SUM(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) THEN 1 ELSE 0 END) as new_students_30_days,
SUM(CASE WHEN created_at >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) THEN 1 ELSE 0 END) as new_students_7_days
FROM {$this->table_students}
WHERE deleted_at IS NULL
";
$stats = $this->db->get_row($stats_query);
if (!$stats) {
wp_send_json_error('Error al obtener estadísticas');
}
wp_send_json_success(array(
'total_students' => intval($stats->total_students),
'active_students' => intval($stats->active_students),
'graduated_students' => intval($stats->graduated_students),
'inactive_students' => intval($stats->inactive_students),
'suspended_students' => intval($stats->suspended_students),
'retired_students' => intval($stats->retired_students),
'new_students_30_days' => intval($stats->new_students_30_days),
'new_students_7_days' => intval($stats->new_students_7_days),
'timestamp' => current_time('mysql')
));
}
/**
* Crear nuevo estudiante
*/
private function create_student($data) {
// Generar ID único de estudiante
$data['student_id'] = $this->generate_student_id();
$data['created_at'] = current_time('mysql');
$data['updated_at'] = current_time('mysql');
$result = $this->db->insert($this->table_students, $data);
if ($result) {
$student_id = $this->db->insert_id;
$student = $this->get_student_by_id($student_id);
return array(
'success' => true,
'data' => $student,
'message' => 'Estudiante creado correctamente'
);
} else {
return array(
'success' => false,
'message' => 'Error al crear el estudiante'
);
}
}
/**
* Actualizar estudiante existente
*/
private function update_student($student_id, $data) {
$data['updated_at'] = current_time('mysql');
$result = $this->db->update(
$this->table_students,
$data,
array('id' => $student_id),
null,
array('%d')
);
if ($result !== false) {
$student = $this->get_student_by_id($student_id);
return array(
'success' => true,
'data' => $student,
'message' => 'Estudiante actualizado correctamente'
);
} else {
return array(
'success' => false,
'message' => 'Error al actualizar el estudiante'
);
}
}
/**
* Obtener estudiante por ID
*/
private function get_student_by_id($student_id) {
$query = "SELECT * FROM {$this->table_students} WHERE id = %d AND deleted_at IS NULL";
return $this->db->get_row($this->db->prepare($query, $student_id));
}
/**
* Generar ID único de estudiante
*/
private function generate_student_id() {
$prefix = 'WES';
$year = date('Y');
// Obtener el último número secuencial del año
$last_student = $this->db->get_row($this->db->prepare(
"SELECT student_id FROM {$this->table_students}
WHERE student_id LIKE %s
ORDER BY student_id DESC LIMIT 1",
$prefix . $year . '%'
));
if ($last_student) {
$last_number = intval(substr($last_student->student_id, -4));
$new_number = $last_number + 1;
} else {
$new_number = 1;
}
return $prefix . $year . str_pad($new_number, 4, '0', STR_PAD_LEFT);
}
/**
* Sanitizar datos del estudiante
*/
private function sanitize_student_data($data) {
return array(
// Datos Personales
'first_name' => sanitize_text_field($data['first_name'] ?? ''),
'last_name' => sanitize_text_field($data['last_name'] ?? ''),
'date_of_birth' => sanitize_text_field($data['date_of_birth'] ?? ''),
'gender' => sanitize_text_field($data['gender'] ?? ''),
'country_of_origin' => sanitize_text_field($data['country_of_origin'] ?? ''),
'nationality' => sanitize_text_field($data['nationality'] ?? ''),
'identity_document' => sanitize_text_field($data['identity_document'] ?? ''),
// Información de Contacto
'email' => sanitize_email($data['email'] ?? ''),
'phone' => sanitize_text_field($data['phone'] ?? ''),
'city' => sanitize_text_field($data['city'] ?? ''),
'address' => sanitize_textarea_field($data['address'] ?? ''),
// Responsables/Contacto de Emergencia
'emergency_contact' => sanitize_text_field($data['emergency_contact'] ?? ''),
'emergency_email' => sanitize_email($data['emergency_email'] ?? ''),
'emergency_phone' => sanitize_text_field($data['emergency_phone'] ?? ''),
'emergency_relationship' => sanitize_text_field($data['emergency_relationship'] ?? ''),
// Información Adicional
'how_did_you_hear' => sanitize_text_field($data['how_did_you_hear'] ?? ''),
'referred_by' => sanitize_text_field($data['referred_by'] ?? ''),
'special_notes' => sanitize_textarea_field($data['special_notes'] ?? ''),
// Campos del sistema
'current_level' => sanitize_text_field($data['current_level'] ?? 'beginner'),
'status' => sanitize_text_field($data['status'] ?? 'active'),
'notes' => sanitize_textarea_field($data['notes'] ?? '')
);
}
/**
* Validar datos del estudiante
*/
private function validate_student_data($data, $student_id = 0) {
if (empty($data['first_name'])) {
return array('valid' => false, 'message' => 'El nombre es requerido');
}
if (empty($data['last_name'])) {
return array('valid' => false, 'message' => 'El apellido es requerido');
}
// Email opcional - solo validar si se proporciona
if (!empty($data['email']) && !is_email($data['email'])) {
return array('valid' => false, 'message' => 'Formato de email inválido');
}
// Verificar email único solo si se proporciona
if (!empty($data['email'])) {
$email_exists = $this->db->get_var($this->db->prepare(
"SELECT id FROM {$this->table_students}
WHERE email = %s AND id != %d AND deleted_at IS NULL",
$data['email'],
$student_id
));
if ($email_exists) {
return array('valid' => false, 'message' => 'El email ya está registrado');
}
}
// Validar email de emergencia si se proporciona
if (!empty($data['emergency_email']) && !is_email($data['emergency_email'])) {
return array('valid' => false, 'message' => 'Email de emergencia inválido');
}
if (!empty($data['date_of_birth']) && !$this->validate_date($data['date_of_birth'])) {
return array('valid' => false, 'message' => 'Fecha de nacimiento inválida');
}
return array('valid' => true);
}
/**
* Validar formato de fecha
*/
private function validate_date($date) {
$d = DateTime::createFromFormat('Y-m-d', $date);
return $d && $d->format('Y-m-d') === $date;
}
/**
* Obtener etiqueta de estado
*/
private function get_status_label($status) {
$labels = array(
'active' => 'Activo',
'inactive' => 'Inactivo',
'graduated' => 'Graduado',
'suspended' => 'Suspendido',
'retired' => 'Retirado'
);
return $labels[$status] ?? $status;
}
/**
* Obtener etiqueta de relación de emergencia
*/
private function get_relationship_label($relationship) {
$labels = array(
'parent' => 'Padre/Madre',
'guardian' => 'Tutor',
'spouse' => 'Cónyuge',
'sibling' => 'Hermano/a',
'friend' => 'Amigo/a',
'other' => 'Otro'
);
return $labels[$relationship] ?? $relationship;
}
}
// Inicializar el módulo
new WES_Students_Module();