Saltar a contenido

API Documentation: Get Paginated Students (Frontend)

Endpoint: GET /api/students/administrative
Versión: 2.0.0 (Actualizado: 2025-10-02)
Autenticación: Required


Descripción

Este endpoint devuelve una lista paginada de estudiantes con información completa sobre matrículas, centros, horarios y profesores asignados en el curso escolar actual.

Nuevas funcionalidades (Versión 2.0.0)

Filtro centerId: Filtra estudiantes por centro educativo
Filtro hasSchedule: Filtra estudiantes que tienen/no tienen horarios asignados
Filtro employeeId: Filtra estudiantes por profesor asignado
Búsqueda mejorada: Ahora busca por nombre, apellido, DNI y correo electrónico
Campo centros: Array con nombres de centros donde el estudiante tiene matrículas
Campo horarios: Array con horarios/eventos asignados al estudiante


Request

URL

GET /api/students/administrative

Query Parameters

Parámetro Tipo Requerido Default Descripción
page number No 1 Número de página (mínimo: 1)
limit number No 20 Elementos por página (máximo: 100)
search string No - Buscar por nombre, apellido, DNI o email del estudiante
status string No - Filtrar por estado(s). Valores separados por coma
subjectId number No - Filtrar estudiantes por asignatura
schoolCourseId number No último curso ID del curso escolar (usa último curso por defecto)
renews boolean No - Filtrar por renovación (true=renuevan, false=no renuevan)
centerId number No - NUEVO: Filtrar estudiantes por centro educativo
hasSchedule boolean No - NUEVO: Filtrar por horarios (true=con horarios, false=sin horarios)
employeeId number No - NUEVO: Filtrar estudiantes por profesor asignado

Valores válidos para status

  • active - Estudiante activo
  • inactive - Estudiante inactivo
  • pending - Estudiante pendiente
  • restricted - Estudiante con restricciones
  • delete / deleted - Estudiante eliminado

Nota: Puedes enviar múltiples estados separados por comas: ?status=active,pending


Response

Status Code: 200 OK

Response Body

{
  data: Array<{
    id: number;
    name: string;
    lastname: string;
    email: string;
    dni: string;
    dateOfBirth: string;          // ISO 8601 format
    phone: string | number;
    province: string;
    city: string;
    zipcode: string | null;
    address: string;
    urlContract: string | null;
    needTutor: boolean;
    userId: number | null;
    monthlyFee: number | null;
    feeStatus: string | null;
    status: string;
    guardian: {
      id: number;
      userId: number;
      name: string;
      lastName: string;
      dateOfBirth: string;
      dni: string;
      phoneNumber: string;
      email: string;
    } | null;
    renews: boolean;              // true si tiene matrículas en curso actual
    centros: string[];            // ✨ NUEVO: Array de nombres de centros donde está matriculado
    horarios: Array<{             // ✨ NUEVO: Horarios/eventos del estudiante
      id: number;
      titulo: string;
      inicio: string;             // ISO 8601 format
      duracion: string;           // formato HH:MM
      centro: string | null;
      materia: string | null;
      profesor: string | null;
    }>;
  }>;
  meta: {
    totalItems: number;           // Total de estudiantes en la búsqueda
    totalPages: number;           // Total de páginas
    currentPage: number;          // Página actual
    pageSize: number;             // Elementos en esta página
    statusCounts: {               // ✨ NUEVO: Contadores por estado (respeta filtros aplicados)
      all: number;                // Total de estudiantes (todos los estados)
      active: number;             // Estudiantes con estado "active"
      pending: number;            // Estudiantes con estado "pending"
      inactive: number;           // Estudiantes con estado "inactive"
      deleted: number;            // Estudiantes con estado "delete" o "deleted"
    };
  };
}

Ejemplos de Uso

Ejemplo 1: Obtener primera página (básico)

Request:

GET /api/students/administrative?page=1&limit=20

Response:

{
  "data": [
    {
      "id": 795,
      "name": "Teresa",
      "lastname": "Barazal Díaz",
      "dni": "53280325C",
      "email": "teresa.20160608@musicaymaestro.com",
      "status": "restricted",
      "phone": "34655756548",
      "province": "Córdoba",
      "city": "Madrid",
      "address": "Calle navarro Sáez 32",
      "zipcode": null,
      "dateOfBirth": "2016-06-08",
      "urlContract": null,
      "needTutor": true,
      "userId": 834,
      "guardian": null,
      "monthlyFee": null,
      "feeStatus": null,
      "renews": false
    },
    {
      "id": 6,
      "name": "Teresa",
      "lastname": "Merino Bobillo",
      "dni": "12190942E",
      "email": "tmerinobobillo@hotmail.com",
      "status": "active",
      "phone": "34636082956",
      "province": "Córdoba",
      "city": "Peñarroya-Pueblonuevo",
      "address": "C/ Eucaliptos, 10",
      "zipcode": "14200",
      "dateOfBirth": "1970-10-20",
      "urlContract": "https://storage.googleapis.com/...",
      "needTutor": false,
      "userId": 41,
      "guardian": null,
      "monthlyFee": 18,
      "feeStatus": "success_subscription",
      "renews": true
    }
  ],
  "meta": {
    "totalItems": 850,
    "totalPages": 43,
    "currentPage": 1,
    "pageSize": 20,
    "statusCounts": {
      "all": 1502,
      "active": 1123,
      "pending": 30,
      "inactive": 271,
      "deleted": 60
    }
  }
}


Ejemplo 2: Buscar estudiantes por nombre, DNI o email ✨

Request:

# Buscar por nombre
GET /api/students/administrative?search=Teresa&page=1&limit=10

# Buscar por DNI
GET /api/students/administrative?search=53280325C&page=1&limit=10

# Buscar por email
GET /api/students/administrative?search=teresa@musicaymaestro.com&page=1&limit=10

Response:

{
  "data": [
    {
      "id": 795,
      "name": "Teresa",
      "lastname": "Barazal Díaz",
      "dni": "53280325C",
      "email": "teresa.20160608@musicaymaestro.com",
      "renews": false,
      "centros": [],
      "horarios": []
    }
  ],
  "meta": {
    "totalItems": 1,
    "totalPages": 1,
    "currentPage": 1,
    "pageSize": 10
  }
}


Ejemplo 3: Filtrar por estado (solo activos)

Request:

GET /api/students/administrative?status=active&page=1&limit=20


Ejemplo 4: Filtrar solo estudiantes que NO renuevan ✨

Request:

GET /api/students/administrative?renews=false&page=1&limit=20

Response:

{
  "data": [
    {
      "id": 795,
      "name": "Teresa",
      "lastname": "Barazal Díaz",
      "status": "restricted",
      "renews": false,
      // ... otros campos
    },
    {
      "id": 13,
      "name": "Teresa",
      "lastname": "Escobar Moriega",
      "status": "inactive",
      "renews": false,
      // ... otros campos
    }
  ],
  "meta": {
    "totalItems": 45,
    "totalPages": 3,
    "currentPage": 1,
    "pageSize": 20
  }
}


Ejemplo 5: Filtrar solo estudiantes que SÍ renuevan ✨

Request:

GET /api/students/administrative?renews=true&page=1&limit=20

Response:

{
  "data": [
    {
      "id": 6,
      "name": "Teresa",
      "lastname": "Merino Bobillo",
      "status": "active",
      "renews": true,
      // ... otros campos
    }
  ],
  "meta": {
    "totalItems": 120,
    "totalPages": 6,
    "currentPage": 1,
    "pageSize": 20
  }
}


Ejemplo 6: Combinar filtro de renovación con estado ✨

Request:

GET /api/students/administrative?status=active&renews=false&page=1&limit=20

Esto devolverá estudiantes que: - ✅ Tengan estado "active" - ✅ Y NO estén renovando matrícula


Ejemplo 7: Filtrar por centro ✨

Request:

GET /api/students/administrative?centerId=16&page=1&limit=20

Response:

{
  "data": [
    {
      "id": 1680,
      "name": "Geronimo",
      "lastname": "Marin López",
      "renews": true,
      "centros": ["Escuela Municipal de Música Guadalcázar"],
      "horarios": [
        {
          "id": 928,
          "titulo": "Piano 1º",
          "inicio": "2025-08-20T15:00:00.000Z",
          "duracion": "01:00",
          "centro": "Escuela Municipal de Música Guadalcázar",
          "materia": "Piano 1º",
          "profesor": "Carlos López Arregui"
        }
      ]
    }
  ],
  "meta": {
    "totalItems": 45,
    "totalPages": 3,
    "currentPage": 1,
    "pageSize": 20
  }
}

Esto devolverá solo estudiantes con matrículas en el centro ID 16.


Ejemplo 8: Filtrar estudiantes con horarios ✨

Request:

# Estudiantes CON horarios
GET /api/students/administrative?hasSchedule=true&page=1&limit=20

# Estudiantes SIN horarios
GET /api/students/administrative?hasSchedule=false&page=1&limit=20

Útil para: - Identificar estudiantes que necesitan asignación de horarios - Ver qué estudiantes ya tienen clases programadas - Generar reportes de cobertura de horarios


Ejemplo 9: Filtrar estudiantes por profesor ✨

Request:

GET /api/students/administrative?employeeId=4&page=1&limit=20

Response:

{
  "data": [
    {
      "id": 1680,
      "name": "Geronimo",
      "lastname": "Marin López",
      "renews": true,
      "centros": ["Escuela Municipal de Música Guadalcázar"],
      "horarios": [
        {
          "id": 928,
          "titulo": "Piano 1º",
          "inicio": "2025-08-20T15:00:00.000Z",
          "duracion": "01:00",
          "centro": "Escuela Municipal de Música Guadalcázar",
          "materia": "Piano 1º",
          "profesor": "Carlos López Arregui"
        }
      ]
    }
  ]
}

Esto devolverá solo estudiantes con eventos del profesor ID 4.


Ejemplo 10: Combinar múltiples filtros ✨

Request:

GET /api/students/administrative?centerId=16&employeeId=4&hasSchedule=true&status=active&page=1&limit=20

Esto devolverá estudiantes que: - ✅ Tengan matrículas en el centro ID 16 - ✅ Y tengan horarios con el profesor ID 4 - ✅ Y tengan horarios asignados - ✅ Y su estado sea "active"


Ejemplo 11: Búsqueda avanzada combinada

Request:

GET /api/students/administrative?search=37219841E&centerId=1&renews=true&page=1&limit=20

Esto devolverá estudiantes que: - ✅ Tengan "37219841E" en nombre, apellido, DNI o email - ✅ Y estén matriculados en el centro ID 1 - ✅ Y SÍ estén renovando matrícula


Casos de Uso Frontend

1. Mostrar tabla de estudiantes

interface Student {
  id: number;
  name: string;
  lastname: string;
  email: string;
  status: string;
  renews: boolean;
  // ... otros campos
}

interface PaginatedResponse {
  data: Student[];
  meta: {
    totalItems: number;
    totalPages: number;
    currentPage: number;
    pageSize: number;
  };
}

async function fetchStudents(page: number = 1, limit: number = 20): Promise<PaginatedResponse> {
  const response = await fetch(
    `/api/students/administrative?page=${page}&limit=${limit}`,
    {
      headers: {
        'Authorization': `Bearer ${token}`
      }
    }
  );

  if (!response.ok) {
    throw new Error('Error al obtener estudiantes');
  }

  return response.json();
}

2. Implementar búsqueda con debounce

import { useState, useEffect } from 'react';
import { debounce } from 'lodash';

function StudentsList() {
  const [students, setStudents] = useState<Student[]>([]);
  const [search, setSearch] = useState('');
  const [page, setPage] = useState(1);
  const [meta, setMeta] = useState(null);

  // Debounce para evitar múltiples requests
  const debouncedSearch = debounce(async (searchTerm: string) => {
    const response = await fetchStudents(1, 20, searchTerm);
    setStudents(response.data);
    setMeta(response.meta);
    setPage(1);
  }, 500);

  useEffect(() => {
    if (search) {
      debouncedSearch(search);
    }
  }, [search]);

  return (
    <div>
      <input 
        type="text" 
        placeholder="Buscar estudiante..."
        value={search}
        onChange={(e) => setSearch(e.target.value)}
      />

      <table>
        <thead>
          <tr>
            <th>Nombre</th>
            <th>Apellido</th>
            <th>Email</th>
            <th>Estado</th>
            <th>Renueva</th>
          </tr>
        </thead>
        <tbody>
          {students.map(student => (
            <tr key={student.id}>
              <td>{student.name}</td>
              <td>{student.lastname}</td>
              <td>{student.email}</td>
              <td>
                <span className={`badge badge-${student.status}`}>
                  {student.status}
                </span>
              </td>
              <td>
                {student.renews ? (
                  <span className="badge badge-success"> Renueva</span>
                ) : (
                  <span className="badge badge-warning"> No renueva</span>
                )}
              </td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* Paginación */}
      <div className="pagination">
        <button 
          disabled={page === 1}
          onClick={() => setPage(page - 1)}
        >
          Anterior
        </button>
        <span>Página {page} de {meta?.totalPages}</span>
        <button 
          disabled={page === meta?.totalPages}
          onClick={() => setPage(page + 1)}
        >
          Siguiente
        </button>
      </div>
    </div>
  );
}

3. Filtrar por estado con select

function StudentsFilter() {
  const [status, setStatus] = useState<string[]>([]);

  const statusOptions = [
    { value: 'active', label: 'Activos' },
    { value: 'inactive', label: 'Inactivos' },
    { value: 'pending', label: 'Pendientes' },
    { value: 'restricted', label: 'Restringidos' },
  ];

  const handleStatusChange = (selectedStatus: string[]) => {
    setStatus(selectedStatus);

    // Construir query string
    const statusQuery = selectedStatus.length > 0 
      ? `&status=${selectedStatus.join(',')}`
      : '';

    fetchStudents(1, 20, '', statusQuery);
  };

  return (
    <MultiSelect
      options={statusOptions}
      value={status}
      onChange={handleStatusChange}
      placeholder="Filtrar por estado..."
    />
  );
}

4. Badge visual para "renews"

function RenewBadge({ renews }: { renews: boolean }) {
  if (renews) {
    return (
      <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-green-100 text-green-800">
        <svg className="mr-1.5 h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
          <circle cx="4" cy="4" r="3" />
        </svg>
        Renueva
      </span>
    );
  }

  return (
    <span className="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">
      <svg className="mr-1.5 h-2 w-2" fill="currentColor" viewBox="0 0 8 8">
        <circle cx="4" cy="4" r="3" />
      </svg>
      No renueva
    </span>
  );
}

// Uso:
<RenewBadge renews={student.renews} />

5. Tabs para filtrar por renovación ✨

import { useState } from 'react';

type RenewsFilter = 'all' | 'renews' | 'not_renews';

function StudentsPage() {
  const [activeTab, setActiveTab] = useState<RenewsFilter>('all');
  const [students, setStudents] = useState([]);
  const [meta, setMeta] = useState(null);

  const fetchStudents = async (renewsFilter: RenewsFilter) => {
    let url = '/api/students/administrative?page=1&limit=20';

    if (renewsFilter === 'renews') {
      url += '&renews=true';
    } else if (renewsFilter === 'not_renews') {
      url += '&renews=false';
    }

    const response = await fetch(url);
    const data = await response.json();
    setStudents(data.data);
    setMeta(data.meta);
  };

  return (
    <div>
      <div className="tabs">
        <button 
          className={activeTab === 'all' ? 'active' : ''}
          onClick={() => {
            setActiveTab('all');
            fetchStudents('all');
          }}
        >
          Todos ({meta?.totalItems || 0})
        </button>

        <button 
          className={activeTab === 'renews' ? 'active' : ''}
          onClick={() => {
            setActiveTab('renews');
            fetchStudents('renews');
          }}
        >
           Renuevan
        </button>

        <button 
          className={activeTab === 'not_renews' ? 'active' : ''}
          onClick={() => {
            setActiveTab('not_renews');
            fetchStudents('not_renews');
          }}
        >
           No Renuevan
        </button>
      </div>

      <table>
        {/* Tabla de estudiantes */}
      </table>
    </div>
  );
}

6. Tabs con contadores por estado (usando statusCounts) ✨

import { useState, useEffect } from 'react';

interface StatusTab {
  key: string;
  label: string;
  status?: string[];
  count: number;
}

function StudentsWithStatusTabs() {
  const [activeStatus, setActiveStatus] = useState<string | null>(null);
  const [students, setStudents] = useState([]);
  const [meta, setMeta] = useState(null);
  const [tabs, setTabs] = useState<StatusTab[]>([]);

  const fetchStudents = async (status?: string[]) => {
    let url = '/api/students/administrative?page=1&limit=20';
    if (status) {
      url += `&status=${status.join(',')}`;
    }

    const response = await fetch(url);
    const data = await response.json();

    setStudents(data.data);
    setMeta(data.meta);

    // Actualizar tabs con los contadores desde statusCounts
    setTabs([
      { key: 'all', label: 'Todos', count: data.meta.statusCounts.all },
      { key: 'active', label: 'Activo', status: ['active'], count: data.meta.statusCounts.active },
      { key: 'pending', label: 'Pendiente', status: ['pending'], count: data.meta.statusCounts.pending },
      { key: 'inactive', label: 'Inactivo', status: ['inactive'], count: data.meta.statusCounts.inactive },
      { key: 'deleted', label: 'Eliminados', status: ['delete', 'deleted'], count: data.meta.statusCounts.deleted }
    ]);
  };

  useEffect(() => {
    fetchStudents();
  }, []);

  const handleTabClick = (tab: StatusTab) => {
    setActiveStatus(tab.key);
    fetchStudents(tab.status);
  };

  return (
    <div>
      {/* Tabs con contadores */}
      <div className="tabs-container">
        {tabs.map(tab => (
          <button
            key={tab.key}
            className={`tab ${activeStatus === tab.key ? 'active' : ''}`}
            onClick={() => handleTabClick(tab)}
          >
            {tab.label}
            <span className="badge">{tab.count}</span>
          </button>
        ))}
      </div>

      {/* Tabla de estudiantes */}
      <table>
        <thead>
          <tr>
            <th>Nombre</th>
            <th>Estado</th>
            <th>Centros</th>
          </tr>
        </thead>
        <tbody>
          {students.map(student => (
            <tr key={student.id}>
              <td>{student.name} {student.lastname}</td>
              <td><span className={`status-badge ${student.status}`}>{student.status}</span></td>
              <td>{student.centros.join(', ')}</td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* Paginación */}
      <div className="pagination">
        Página {meta?.currentPage} de {meta?.totalPages}
      </div>
    </div>
  );
}

7. Dashboard de estadísticas ✨

interface DashboardStats {
  total: number;
  renewing: number;
  notRenewing: number;
  renewalRate: number;
}

async function getDashboardStats(): Promise<DashboardStats> {
  // Obtener todos
  const allResponse = await fetch('/api/students/administrative?page=1&limit=1');
  const allData = await allResponse.json();

  // Obtener los que renuevan
  const renewingResponse = await fetch('/api/students/administrative?renews=true&page=1&limit=1');
  const renewingData = await renewingResponse.json();

  // Obtener los que NO renuevan
  const notRenewingResponse = await fetch('/api/students/administrative?renews=false&page=1&limit=1');
  const notRenewingData = await notRenewingResponse.json();

  const total = allData.meta.totalItems;
  const renewing = renewingData.meta.totalItems;
  const notRenewing = notRenewingData.meta.totalItems;

  return {
    total,
    renewing,
    notRenewing,
    renewalRate: (renewing / total) * 100
  };
}

function Dashboard() {
  const [stats, setStats] = useState<DashboardStats | null>(null);

  useEffect(() => {
    getDashboardStats().then(setStats);
  }, []);

  if (!stats) return <div>Cargando...</div>;

  return (
    <div className="dashboard">
      <div className="stat-card">
        <h3>Total Estudiantes</h3>
        <p className="stat-number">{stats.total}</p>
      </div>

      <div className="stat-card success">
        <h3>Renuevan</h3>
        <p className="stat-number">{stats.renewing}</p>
      </div>

      <div className="stat-card warning">
        <h3>No Renuevan</h3>
        <p className="stat-number">{stats.notRenewing}</p>
      </div>

      <div className="stat-card info">
        <h3>Tasa de Renovación</h3>
        <p className="stat-number">{stats.renewalRate.toFixed(1)}%</p>
      </div>
    </div>
  );
}

Lógica de Negocio: ¿Qué significa renews?

renews: true

El estudiante SÍ tiene matrículas activas en el curso escolar actual (2025-2026).

Significa: - ✅ El estudiante está inscrito en al menos una asignatura este año - ✅ Tiene registros (registers) asociados al curso actual - ✅ Probablemente pagará cuota mensual este año

renews: false

El estudiante NO tiene matrículas activas en el curso escolar actual.

Puede significar: - ❌ No se ha inscrito este año (podría ser estudiante antiguo) - ❌ Aún no ha completado el proceso de renovación - ❌ Ya no asiste a la escuela


Notas Importantes

⚠️ Límite de resultados

El límite máximo por página es 100 estudiantes. Si solicitas más, se limitará automáticamente a 100.

📅 Curso escolar actual

El endpoint usa automáticamente el último curso escolar de la base de datos. Todos los filtros (centros, horarios, renews, hasSchedule) se aplican sobre el curso actual.

🔍 Búsqueda mejorada

La búsqueda es case-insensitive y busca en: - Nombre del estudiante - Apellido del estudiante - DNI (búsqueda parcial) - Email (búsqueda parcial)

🎯 Filtros combinables

Todos los filtros son combinables para búsquedas avanzadas: - status + centerId + employeeId → Estudiantes activos de un centro y profesor - renews + hasSchedule → Estudiantes que renuevan pero sin horarios - search + múltiples filtros → Búsqueda específica con contexto

🏫 Filtro por centro (centerId)

  • Filtra estudiantes que tienen matrículas activas en ese centro en el curso actual
  • Solo muestra estudiantes con registros (registers) en ese centro

⏰ Filtro por horarios (hasSchedule)

  • true: Estudiantes con eventos/horarios asignados en el curso actual
  • false: Estudiantes sin eventos/horarios asignados (útil para asignación pendiente)

👨‍🏫 Filtro por profesor (employeeId)

  • Filtra estudiantes que tienen eventos con ese profesor en el curso actual
  • Útil para ver la lista de alumnos de un profesor específico

📊 Campos adicionales

  • centros: Array con nombres únicos de centros donde el estudiante tiene matrículas
  • horarios: Array completo con todos los eventos/horarios del estudiante (incluye centro, materia y profesor)

🔢 Contadores por estado (statusCounts)

La respuesta siempre incluye contadores de estudiantes por estado en meta.statusCounts: - Los contadores respetan todos los filtros aplicados (search, centerId, employeeId, hasSchedule, renews) - Los contadores NO respetan el filtro de status (para poder mostrar todos los badges) - Útil para mostrar tabs/badges con contadores como: "Todos (1502)", "Activos (1123)", "Pendientes (30)"

Ejemplo: Si filtras por ?centerId=16, los contadores mostrarán: - all: Total de estudiantes en el centro 16 - active: Estudiantes activos en el centro 16 - pending: Estudiantes pendientes en el centro 16 - Etc.


Manejo de Errores

Error 401 - No autorizado

{
  "status": "error",
  "message": "No estás autorizado para acceder a este recurso"
}

Solución: Verifica que el token de autenticación sea válido.


Error 400 - Parámetros inválidos

{
  "status": "error",
  "message": "Parámetros inválidos"
}

Solución: Verifica que page y limit sean números positivos.


Changelog

v2.0.0 (2025-10-02) - MAJOR UPDATE

  • NUEVO: Filtro centerId - Filtra estudiantes por centro educativo
  • NUEVO: Filtro hasSchedule - Filtra estudiantes con/sin horarios asignados
  • NUEVO: Filtro employeeId - Filtra estudiantes por profesor asignado
  • NUEVO: Campo centros - Array con nombres de centros del estudiante
  • NUEVO: Campo horarios - Array completo con horarios/eventos del estudiante
  • Incluye: id, título, inicio, duración, centro, materia y profesor
  • NUEVO: meta.statusCounts - Contadores automáticos por estado
  • Incluye: all, active, pending, inactive, deleted
  • Respeta filtros aplicados (excepto status)
  • Ideal para mostrar tabs con badges de contadores
  • 🔍 MEJORADO: Búsqueda ahora incluye DNI y email (además de nombre y apellido)
  • 📚 11 ejemplos completos de uso con diferentes combinaciones de filtros
  • 💡 Ejemplo de componente React con tabs usando statusCounts
  • 🎯 Todos los filtros son combinables para búsquedas avanzadas

v1.2.0 (2025-10-02)

  • NUEVO: Parámetro renews para filtrar estudiantes por renovación
  • renews=true → Solo estudiantes que renuevan
  • renews=false → Solo estudiantes que NO renuevan
  • 📚 Ejemplos completos con tabs y dashboard de estadísticas
  • 🔧 Paginación optimizada para el filtro de renovación

v1.1.0 (2025-10-02)

  • ✨ Agregada propiedad renews para indicar si el estudiante renueva matrícula
  • 🔧 Filtro automático de registros por curso escolar actual
  • 📝 Los estudiantes sin registros ahora se devuelven con renews: false

v1.0.0

  • 🎉 Versión inicial
  • Paginación básica
  • Filtros por estado y búsqueda

Última actualización: 2025-10-02 (Versión 2.0.0)