From e665dc5418bd4a89e1277690d3792dc8eb7c6933 Mon Sep 17 00:00:00 2001 From: AlejandroHenao2572 Date: Sat, 6 Dec 2025 23:10:02 -0500 Subject: [PATCH 001/149] feat(tutoring):show avaible tutors for student in /dashboard/student/tutoring --- CONEXION_TUTORIAS.md | 170 ++++++++++++++++++++++ app/lib/config/api.config.ts | 3 + app/lib/hooks/useTutores.ts | 25 ++++ app/lib/services/tutoria.service.ts | 23 +++ app/lib/types/tutoria.types.ts | 63 ++++++++ app/routes/dashboard/student/tutoring.tsx | 151 ++++++++++++++----- 6 files changed, 402 insertions(+), 33 deletions(-) create mode 100644 CONEXION_TUTORIAS.md create mode 100644 app/lib/hooks/useTutores.ts create mode 100644 app/lib/services/tutoria.service.ts create mode 100644 app/lib/types/tutoria.types.ts diff --git a/CONEXION_TUTORIAS.md b/CONEXION_TUTORIAS.md new file mode 100644 index 0000000..4349aee --- /dev/null +++ b/CONEXION_TUTORIAS.md @@ -0,0 +1,170 @@ +# Configuración de Conexión Backend - Microservicio de Tutorías + +## 📋 Resumen + +Se ha configurado la capa de comunicación con el backend para el microservicio de tutorías usando **Axios** y **React Query**. + +## 🔧 Componentes Implementados + +### 1. **Configuración de API** (`lib/config/api.config.ts`) +- ✅ Agregado endpoint: `TUTORIAS.TUTORES: '/wise/tutorias/tutores'` +- El cliente Axios ya estaba configurado con interceptors JWT en `lib/api/client.ts` + +### 2. **Tipos TypeScript** (`lib/types/tutoria.types.ts`) +Interfaces definidas para la respuesta del backend: + +```typescript +interface TutorProfile { + id: string; + email: string; + nombre: string; + apellido: string; + semestre: number; + rolId: number; + estadoId: number; + disponibilidad: DisponibilidadSemanal; + created_at: string; + updated_at: string; + rol: Rol; + estado: Estado; +} +``` + +### 3. **Servicio de Tutorías** (`lib/services/tutoria.service.ts`) +Función implementada: + +```typescript +export async function getTutores(): Promise +``` + +- Realiza petición GET a `/wise/tutorias/tutores` +- Maneja errores automáticamente +- Tipado estricto con TypeScript + +### 4. **Hook de React Query** (`lib/hooks/useTutores.ts`) +Hook personalizado para gestionar el estado: + +```typescript +export function useTutores(): UseQueryResult +``` + +**Características:** +- ✅ Cache automático (5 minutos) +- ✅ No refetch automático en focus +- ✅ Manejo de loading y error states +- ✅ Integración con React Query DevTools + +### 5. **Componente Actualizado** (`routes/dashboard/student/tutoring.tsx`) + +**Cambios realizados:** +1. Importación del hook `useTutores` +2. Función de transformación de datos: `transformTutorProfileToTutor()` +3. Estados de carga y error +4. Conexión con datos reales del backend + +**Uso:** +```tsx +const { data: tutoresData, isLoading, error } = useTutores(); +const tutors = tutoresData ? tutoresData.map(transformTutorProfileToTutor) : []; +``` + +## 🔒 Seguridad + +- ✅ **Token JWT**: Se adjunta automáticamente en cada petición mediante interceptor +- ✅ **Manejo 401**: Redirección automática a login si el token expira +- ✅ **HTTPS**: Forzado en producción (no localhost) +- ✅ **Timeout**: 30 segundos configurado + +## 📦 Estructura de Respuesta del Backend + +```json +[ + { + "id": "770e8400-e29b-41d4-a716-446655440002", + "email": "carlos.lopez@escuelaing.edu.co", + "nombre": "Carlos", + "apellido": "López", + "semestre": 10, + "rolId": 2, + "estadoId": 1, + "disponibilidad": { + "monday": [ + { + "start": "08:00", + "end": "10:00", + "modalidad": "VIRTUAL", + "lugar": "https://meet.google.com/abc-defg-hij" + } + ], + ... + }, + "rol": { "id": 2, "nombre": "Tutor", "activo": true }, + "estado": { "id": 1, "nombre": "Activo", "activo": true } + } +] +``` + +## 🧪 Pruebas + +Para probar la conexión: + +1. **Verificar variable de entorno:** + ``` + VITE_API_GATEWAY_URL=http://localhost:3000 + ``` + +2. **Navegar a:** + ``` + /dashboard/student/tutoring + ``` + +3. **Verificar en DevTools:** + - Network tab: Request a `/wise/tutorias/tutores` + - React Query DevTools: Estado de la query `['tutores']` + - Console: Logs de errores si los hay + +## 🚀 Próximos Endpoints + +Cuando necesites conectar más endpoints, sigue este patrón: + +1. **Agregar a `api.config.ts`:** + ```typescript + TUTORIAS: { + TUTORES: '/wise/tutorias/tutores', + NUEVO_ENDPOINT: '/wise/tutorias/nuevo-endpoint', // ← Agregar aquí + } + ``` + +2. **Crear función en `tutoria.service.ts`:** + ```typescript + export async function getNuevoEndpoint(): Promise { + const response = await apiClient.get(API_ENDPOINTS.TUTORIAS.NUEVO_ENDPOINT); + return response.data; + } + ``` + +3. **Crear hook si es necesario:** + ```typescript + export function useNuevoEndpoint() { + return useQuery({ + queryKey: ['nuevoEndpoint'], + queryFn: getNuevoEndpoint, + }); + } + ``` + +## ✅ Validación + +La conexión está lista cuando: +- [x] El componente carga sin errores de compilación +- [x] El spinner aparece mientras carga +- [x] Los datos se muestran correctamente +- [x] El token JWT se envía en los headers +- [x] Los errores se manejan apropiadamente + +## 📝 Notas Importantes + +- **Transformación de Datos**: La función `transformTutorProfileToTutor()` adapta la respuesta del backend al formato que espera el componente UI +- **React Query**: Gestiona automáticamente el cache, refetch y estados de loading/error +- **TypeScript**: Garantiza type-safety en toda la cadena de datos +- **Axios Interceptors**: Manejan JWT y errores globalmente, no necesitas configurarlos en cada petición diff --git a/app/lib/config/api.config.ts b/app/lib/config/api.config.ts index 071fd85..4bccf0f 100644 --- a/app/lib/config/api.config.ts +++ b/app/lib/config/api.config.ts @@ -51,4 +51,7 @@ export const API_ENDPOINTS = { SUSPEND: '/wise/gestion-usuarios/:id/estado', ACTIVATE: '/wise/gestion-usuarios/:id/estado', }, + TUTORIAS: { + TUTORES: '/wise/tutorias/tutores', + }, } as const; diff --git a/app/lib/hooks/useTutores.ts b/app/lib/hooks/useTutores.ts new file mode 100644 index 0000000..a699ffb --- /dev/null +++ b/app/lib/hooks/useTutores.ts @@ -0,0 +1,25 @@ +/** + * Custom Hook: useTutores + * Hook de React Query para obtener la lista de tutores disponibles + */ + +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { getTutores } from '../services/tutoria.service'; +import type { TutorProfile } from '../types/tutoria.types'; + +/** + * Query Key para tutores + */ +export const TUTORES_QUERY_KEY = ['tutores'] as const; + +/** + * Hook para obtener la lista de tutores disponibles + */ +export function useTutores(): UseQueryResult { + return useQuery({ + queryKey: TUTORES_QUERY_KEY, + queryFn: getTutores, + staleTime: 5 * 60 * 1000, // 5 minutos + refetchOnWindowFocus: false, + }); +} diff --git a/app/lib/services/tutoria.service.ts b/app/lib/services/tutoria.service.ts new file mode 100644 index 0000000..cbebfa0 --- /dev/null +++ b/app/lib/services/tutoria.service.ts @@ -0,0 +1,23 @@ +/** + * Tutoria Service + * Servicio para manejar operaciones relacionadas con el microservicio de tutorías + */ + +import apiClient from '../api/client'; +import { API_ENDPOINTS } from '../config/api.config'; +import type { TutorProfile } from '../types/tutoria.types'; + +/** + * Obtiene la lista de tutores disponibles en el sistema + */ +export async function getTutores(): Promise { + try { + const response = await apiClient.get( + API_ENDPOINTS.TUTORIAS.TUTORES, + ); + return response.data; + } catch (error) { + console.error('Error al obtener tutores:', error); + throw new Error('No se pudo obtener la lista de tutores'); + } +} diff --git a/app/lib/types/tutoria.types.ts b/app/lib/types/tutoria.types.ts new file mode 100644 index 0000000..3ed972d --- /dev/null +++ b/app/lib/types/tutoria.types.ts @@ -0,0 +1,63 @@ +/** + * Tutoria Types + * Tipos TypeScript para el microservicio de tutorías + */ + +/** + * Disponibilidad por día de la semana + */ +export interface DisponibilidadSlot { + start: string; // Formato: "HH:mm" + end: string; // Formato: "HH:mm" + modalidad: 'VIRTUAL' | 'PRESENCIAL' | 'HIBRIDA'; + lugar: string; // URL para virtual o ubicación física +} + +/** + * Disponibilidad semanal del tutor + */ +export interface DisponibilidadSemanal { + monday: DisponibilidadSlot[]; + tuesday: DisponibilidadSlot[]; + wednesday: DisponibilidadSlot[]; + thursday: DisponibilidadSlot[]; + friday: DisponibilidadSlot[]; + saturday: DisponibilidadSlot[]; + sunday: DisponibilidadSlot[]; +} + +/** + * Rol del usuario + */ +export interface Rol { + id: number; + nombre: string; + activo: boolean; +} + +/** + * Estado del usuario + */ +export interface Estado { + id: number; + nombre: string; + activo: boolean; +} + +/** + * Perfil completo del tutor + */ +export interface TutorProfile { + id: string; + email: string; + nombre: string; + apellido: string; + semestre: number; + rolId: number; + estadoId: number; + disponibilidad: DisponibilidadSemanal; + created_at: string; + updated_at: string; + rol: Rol; + estado: Estado; +} diff --git a/app/routes/dashboard/student/tutoring.tsx b/app/routes/dashboard/student/tutoring.tsx index 2cc8f46..eee964b 100644 --- a/app/routes/dashboard/student/tutoring.tsx +++ b/app/routes/dashboard/student/tutoring.tsx @@ -1,4 +1,4 @@ -import { Button, Card, CardBody, Input } from '@heroui/react'; +import { Button, Card, CardBody, Input, Spinner } from '@heroui/react'; import { BookOpen, Search } from 'lucide-react'; import type React from 'react'; import { useState } from 'react'; @@ -10,6 +10,8 @@ import ScheduledTutoringsModal, { import TutorCard from '~/components/tutor-card'; import TutorFilter from '~/components/tutor-filter'; import TutorScheduleModal from '~/components/tutor-schedule-modal'; +import { useTutores } from '~/lib/hooks/useTutores'; +import type { TutorProfile } from '~/lib/types/tutoria.types'; interface Tutor { id: number; @@ -32,37 +34,86 @@ interface TutorFilters { disponibilidad: string; } -// TODO: Conectar con API - Ejemplo con valores negativos para referencia -const mockTutors: Tutor[] = [ - { - id: 1, - name: 'Dr. María García', - title: 'Profesora de Matemáticas', - department: 'Ciencias Exactas', - avatarInitials: 'MG', - avatarColor: '#b81d24', - rating: 4.9, - reviews: 127, - tags: ['Cálculo', 'Álgebra', 'Geometría'], - availability: 'Lun-Vie 9:00-17:00', - isAvailableToday: true, - timeSlots: ['Lun 09:00', 'Lun 10:00', 'Mar 11:00', 'Mie 14:00'], - }, - { - id: 2, - name: 'Ing. Carlos Rodríguez', - title: 'Tutor de Programación', - department: 'Ingeniería', - avatarInitials: 'CR', - avatarColor: '#008000', - rating: 4.8, - reviews: 89, - tags: ['React', 'TypeScript', 'Node.js'], - availability: 'Mar-Sáb 14:00-20:00', - isAvailableToday: false, - timeSlots: ['Mar 14:00', 'Jue 16:00', 'Sab 10:00'], - }, -]; +/** + * Transforma un TutorProfile del backend al formato Tutor del componente + */ +const transformTutorProfileToTutor = (profile: TutorProfile): Tutor => { + // Validar que disponibilidad exista + const disponibilidad = profile.disponibilidad || { + monday: [], + tuesday: [], + wednesday: [], + thursday: [], + friday: [], + saturday: [], + sunday: [], + }; + + // Obtener los slots de disponibilidad de todos los días con tipado explícito + const allSlots = Object.entries(disponibilidad).flatMap(([day, slots]) => + slots.map( + (slot: { + start: string; + end: string; + modalidad: string; + lugar: string; + }) => ({ + day, + ...slot, + }), + ), + ); + + // Calcular disponibilidad en formato legible + const daysWithAvailability = Object.entries(disponibilidad) + .filter(([_, slots]) => slots.length > 0) + .map(([day]) => day); + + const availability = + daysWithAvailability.length > 0 + ? `Disponible: ${daysWithAvailability.join(', ')}` + : 'Sin disponibilidad'; + + // Verificar si está disponible hoy + const today = new Date() + .toLocaleDateString('en-US', { weekday: 'long' }) + .toLowerCase(); + const isAvailableToday = + (disponibilidad[today as keyof typeof disponibilidad] || []).length > 0; + + // Generar timeSlots en formato legible + const timeSlots = allSlots.map( + (slot) => `${slot.day} ${slot.start} - ${slot.end}`, + ); + + // Generar iniciales + const avatarInitials = + `${profile.nombre.charAt(0)}${profile.apellido.charAt(0)}`.toUpperCase(); + + // Colores aleatorios para avatar (basado en el ID) + const colors = ['#b81d24', '#008000', '#0073e6', '#f59e0b', '#8b5cf6']; + const avatarColor = + colors[Number.parseInt(profile.id.slice(-1), 16) % colors.length]; + + return { + id: + Number.parseInt(profile.id.replace(/\D/g, '').slice(0, 8)) || + Math.floor(Math.random() * 100000), + name: `${profile.nombre} ${profile.apellido}`, + title: `Tutor - Semestre ${profile.semestre}`, + department: profile.rol.nombre, + avatarInitials, + avatarColor, + rating: 4.5, // TODO: Implementar sistema de calificaciones + reviews: 0, // TODO: Implementar sistema de reseñas + tags: allSlots + .map((slot) => slot.modalidad) + .filter((v, i, a) => a.indexOf(v) === i), + availability, + isAvailableToday, + timeSlots, + }; +}; // Mock de tutorías agendadas (simulación de datos desde API) const mockScheduledTutorings: ScheduledTutoring[] = [ @@ -91,7 +142,14 @@ const mockScheduledTutorings: ScheduledTutoring[] = [ ]; const StudentTutoringPage: React.FC = () => { - const [tutors] = useState(mockTutors); + // Obtener tutores desde el backend + const { data: tutoresData, isLoading, error } = useTutores(); + + // Transformar los datos del backend al formato del componente + const tutors = tutoresData + ? tutoresData.map(transformTutorProfileToTutor) + : []; + const [searchValue, setSearchValue] = useState(''); const [selectedTutor, setSelectedTutor] = useState(null); const [isScheduleOpen, setIsScheduleOpen] = useState(false); @@ -136,6 +194,33 @@ const StudentTutoringPage: React.FC = () => { setScheduledTutorings(scheduledTutorings.filter((t) => t.id !== id)); }; + // Mostrar spinner mientras carga + if (isLoading) { + return ( +
+ +
+ +
+
+ ); + } + + // Mostrar error si falla la petición + if (error) { + return ( +
+ + + +

Error al cargar tutores

+

{error.message}

+
+
+
+ ); + } + return (
{/* PageHeader */} From 907c933aa0765cefbfad72d9d149a51d1f172900 Mon Sep 17 00:00:00 2001 From: AlejandroHenao2572 Date: Mon, 8 Dec 2025 11:08:39 -0500 Subject: [PATCH 002/149] fix: pulled develop changes and fixed conflict with tutoring.tsx --- CONEXION_TUTORIAS.md | 170 ------------------ app/routes/dashboard/student/tutoring.tsx | 208 ++++++++++++++++------ 2 files changed, 152 insertions(+), 226 deletions(-) delete mode 100644 CONEXION_TUTORIAS.md diff --git a/CONEXION_TUTORIAS.md b/CONEXION_TUTORIAS.md deleted file mode 100644 index 4349aee..0000000 --- a/CONEXION_TUTORIAS.md +++ /dev/null @@ -1,170 +0,0 @@ -# Configuración de Conexión Backend - Microservicio de Tutorías - -## 📋 Resumen - -Se ha configurado la capa de comunicación con el backend para el microservicio de tutorías usando **Axios** y **React Query**. - -## 🔧 Componentes Implementados - -### 1. **Configuración de API** (`lib/config/api.config.ts`) -- ✅ Agregado endpoint: `TUTORIAS.TUTORES: '/wise/tutorias/tutores'` -- El cliente Axios ya estaba configurado con interceptors JWT en `lib/api/client.ts` - -### 2. **Tipos TypeScript** (`lib/types/tutoria.types.ts`) -Interfaces definidas para la respuesta del backend: - -```typescript -interface TutorProfile { - id: string; - email: string; - nombre: string; - apellido: string; - semestre: number; - rolId: number; - estadoId: number; - disponibilidad: DisponibilidadSemanal; - created_at: string; - updated_at: string; - rol: Rol; - estado: Estado; -} -``` - -### 3. **Servicio de Tutorías** (`lib/services/tutoria.service.ts`) -Función implementada: - -```typescript -export async function getTutores(): Promise -``` - -- Realiza petición GET a `/wise/tutorias/tutores` -- Maneja errores automáticamente -- Tipado estricto con TypeScript - -### 4. **Hook de React Query** (`lib/hooks/useTutores.ts`) -Hook personalizado para gestionar el estado: - -```typescript -export function useTutores(): UseQueryResult -``` - -**Características:** -- ✅ Cache automático (5 minutos) -- ✅ No refetch automático en focus -- ✅ Manejo de loading y error states -- ✅ Integración con React Query DevTools - -### 5. **Componente Actualizado** (`routes/dashboard/student/tutoring.tsx`) - -**Cambios realizados:** -1. Importación del hook `useTutores` -2. Función de transformación de datos: `transformTutorProfileToTutor()` -3. Estados de carga y error -4. Conexión con datos reales del backend - -**Uso:** -```tsx -const { data: tutoresData, isLoading, error } = useTutores(); -const tutors = tutoresData ? tutoresData.map(transformTutorProfileToTutor) : []; -``` - -## 🔒 Seguridad - -- ✅ **Token JWT**: Se adjunta automáticamente en cada petición mediante interceptor -- ✅ **Manejo 401**: Redirección automática a login si el token expira -- ✅ **HTTPS**: Forzado en producción (no localhost) -- ✅ **Timeout**: 30 segundos configurado - -## 📦 Estructura de Respuesta del Backend - -```json -[ - { - "id": "770e8400-e29b-41d4-a716-446655440002", - "email": "carlos.lopez@escuelaing.edu.co", - "nombre": "Carlos", - "apellido": "López", - "semestre": 10, - "rolId": 2, - "estadoId": 1, - "disponibilidad": { - "monday": [ - { - "start": "08:00", - "end": "10:00", - "modalidad": "VIRTUAL", - "lugar": "https://meet.google.com/abc-defg-hij" - } - ], - ... - }, - "rol": { "id": 2, "nombre": "Tutor", "activo": true }, - "estado": { "id": 1, "nombre": "Activo", "activo": true } - } -] -``` - -## 🧪 Pruebas - -Para probar la conexión: - -1. **Verificar variable de entorno:** - ``` - VITE_API_GATEWAY_URL=http://localhost:3000 - ``` - -2. **Navegar a:** - ``` - /dashboard/student/tutoring - ``` - -3. **Verificar en DevTools:** - - Network tab: Request a `/wise/tutorias/tutores` - - React Query DevTools: Estado de la query `['tutores']` - - Console: Logs de errores si los hay - -## 🚀 Próximos Endpoints - -Cuando necesites conectar más endpoints, sigue este patrón: - -1. **Agregar a `api.config.ts`:** - ```typescript - TUTORIAS: { - TUTORES: '/wise/tutorias/tutores', - NUEVO_ENDPOINT: '/wise/tutorias/nuevo-endpoint', // ← Agregar aquí - } - ``` - -2. **Crear función en `tutoria.service.ts`:** - ```typescript - export async function getNuevoEndpoint(): Promise { - const response = await apiClient.get(API_ENDPOINTS.TUTORIAS.NUEVO_ENDPOINT); - return response.data; - } - ``` - -3. **Crear hook si es necesario:** - ```typescript - export function useNuevoEndpoint() { - return useQuery({ - queryKey: ['nuevoEndpoint'], - queryFn: getNuevoEndpoint, - }); - } - ``` - -## ✅ Validación - -La conexión está lista cuando: -- [x] El componente carga sin errores de compilación -- [x] El spinner aparece mientras carga -- [x] Los datos se muestran correctamente -- [x] El token JWT se envía en los headers -- [x] Los errores se manejan apropiadamente - -## 📝 Notas Importantes - -- **Transformación de Datos**: La función `transformTutorProfileToTutor()` adapta la respuesta del backend al formato que espera el componente UI -- **React Query**: Gestiona automáticamente el cache, refetch y estados de loading/error -- **TypeScript**: Garantiza type-safety en toda la cadena de datos -- **Axios Interceptors**: Manejan JWT y errores globalmente, no necesitas configurarlos en cada petición diff --git a/app/routes/dashboard/student/tutoring.tsx b/app/routes/dashboard/student/tutoring.tsx index 080da96..35dccd7 100644 --- a/app/routes/dashboard/student/tutoring.tsx +++ b/app/routes/dashboard/student/tutoring.tsx @@ -435,37 +435,86 @@ const CancelSessionModal: React.FC<{ ); }; -// Conectar con API - Ejemplo con valores negativos para referencia -const mockTutors: Tutor[] = [ - { - id: 1, - name: 'Dr. Maria Garcia', - title: 'Profesora de Matematicas', - department: 'Ciencias Exactas', - avatarInitials: 'MG', - avatarColor: '#b81d24', - rating: 4.9, - reviews: 127, - tags: ['Calculo', 'Algebra', 'Geometria'], - availability: 'Lun-Vie 9:00-17:00', - isAvailableToday: true, - timeSlots: ['Lun 09:00', 'Lun 10:00', 'Mar 11:00', 'Mie 14:00'], - }, - { - id: 2, - name: 'Ing. Carlos Rodriguez', - title: 'Tutor de Programacion', - department: 'Ingenieria', - avatarInitials: 'CR', - avatarColor: '#008000', - rating: 4.8, - reviews: 89, - tags: ['React', 'TypeScript', 'Node.js'], - availability: 'Mar-Sab 14:00-20:00', - isAvailableToday: false, - timeSlots: ['Mar 14:00', 'Jue 16:00', 'Sab 10:00'], - }, -]; +/** + * Transforma un TutorProfile del backend al formato Tutor del componente + */ +const transformTutorProfileToTutor = (profile: TutorProfile): Tutor => { + // Validar que disponibilidad exista + const disponibilidad = profile.disponibilidad || { + monday: [], + tuesday: [], + wednesday: [], + thursday: [], + friday: [], + saturday: [], + sunday: [], + }; + + // Obtener los slots de disponibilidad de todos los días con tipado explícito + const allSlots = Object.entries(disponibilidad).flatMap(([day, slots]) => + slots.map( + (slot: { + start: string; + end: string; + modalidad: string; + lugar: string; + }) => ({ + day, + ...slot, + }), + ), + ); + + // Calcular disponibilidad en formato legible + const daysWithAvailability = Object.entries(disponibilidad) + .filter(([_, slots]) => slots.length > 0) + .map(([day]) => day); + + const availability = + daysWithAvailability.length > 0 + ? `Disponible: ${daysWithAvailability.join(', ')}` + : 'Sin disponibilidad'; + + // Verificar si está disponible hoy + const today = new Date() + .toLocaleDateString('en-US', { weekday: 'long' }) + .toLowerCase(); + const isAvailableToday = + (disponibilidad[today as keyof typeof disponibilidad] || []).length > 0; + + // Generar timeSlots en formato legible + const timeSlots = allSlots.map( + (slot) => `${slot.day} ${slot.start} - ${slot.end}`, + ); + + // Generar iniciales + const avatarInitials = + `${profile.nombre.charAt(0)}${profile.apellido.charAt(0)}`.toUpperCase(); + + // Colores aleatorios para avatar (basado en el ID) + const colors = ['#b81d24', '#008000', '#0073e6', '#f59e0b', '#8b5cf6']; + const avatarColor = + colors[Number.parseInt(profile.id.slice(-1), 16) % colors.length]; + + return { + id: + Number.parseInt(profile.id.replace(/\D/g, '').slice(0, 8)) || + Math.floor(Math.random() * 100000), + name: `${profile.nombre} ${profile.apellido}`, + title: `Tutor - Semestre ${profile.semestre}`, + department: profile.rol.nombre, + avatarInitials, + avatarColor, + rating: 4.5, // TODO: Implementar sistema de calificaciones + reviews: 0, // TODO: Implementar sistema de reseñas + tags: allSlots + .map((slot) => slot.modalidad) + .filter((v, i, a) => a.indexOf(v) === i), + availability, + isAvailableToday, + timeSlots, + }; +}; // Mock de tutorías agendadas (simulación de datos desde API) const mockScheduledTutorings: ScheduledTutoring[] = [ @@ -614,8 +663,18 @@ const mockSessions: StudentSession[] = [ ]; const StudentTutoringPage: React.FC = () => { - const [tutors] = useState(mockTutors); - // Inicializar tutors con datos del backend + // Obtener tutores desde el backend + const { + data: tutoresData, + isLoading: isLoadingTutors, + error: tutorsError, + } = useTutores(); + + // Transformar los datos del backend al formato del componente + const tutors = tutoresData + ? tutoresData.map(transformTutorProfileToTutor) + : []; + const [searchValue, setSearchValue] = useState(''); const [activeTab, setActiveTab] = useState<'search' | 'my-sessions'>( 'search', @@ -632,6 +691,7 @@ const StudentTutoringPage: React.FC = () => { const [scheduledTutorings, setScheduledTutorings] = useState< ScheduledTutoring[] >(mockScheduledTutorings); + const [isScheduleOpen, setIsScheduleOpen] = useState(false); const { isOpen, onOpen, onClose } = useDisclosure(); @@ -717,7 +777,6 @@ const StudentTutoringPage: React.FC = () => { return (
-
- {activeTab === 'search' && ( <> @@ -756,27 +814,53 @@ const StudentTutoringPage: React.FC = () => { -
-

- {tutors.length} tutores encontrados -

- -
+ {isLoadingTutors ? ( + + +

+ Cargando tutores disponibles... +

+
+
+ ) : tutorsError ? ( + + +

Error al cargar tutores

+

{tutorsError.message}

+
+
+ ) : ( + <> +
+

+ {tutors.length} tutores encontrados +

+ +
-
- {tutors.map((tutor) => ( - - ))} -
+
+ {tutors.map((tutor) => ( + { + setSelectedTutor(t); + setIsScheduleOpen(true); + }} + /> + ))} +
+ + )} - )} - + )}{' '} {activeTab === 'my-sessions' && (
@@ -815,7 +899,6 @@ const StudentTutoringPage: React.FC = () => { )}
)} - { onOpenConfirm(); }} /> - { handleCancelSession(session.id); }} /> + {/* Modal de agendar tutoría */} + setIsScheduleOpen(false)} + onSchedule={handleScheduleTutoring} + /> + {/* Modal de mis tutorías agendadas */} + {}} + onCancel={handleCancelTutoring} + />
); }; From 3752c0dd871caa8997aa8745b0eb02e34c51a291 Mon Sep 17 00:00:00 2001 From: AlejandroHenao2572 Date: Mon, 8 Dec 2025 12:58:50 -0500 Subject: [PATCH 003/149] feat(tutorig): show scheduled session in /dashboard/student/tutoring 'Mis Tutorias' panel --- app/lib/config/api.config.ts | 1 + app/lib/hooks/useStudentSessions.ts | 30 +++++ app/lib/services/tutoria.service.ts | 26 +++- app/lib/types/tutoria.types.ts | 47 +++++++ app/routes/dashboard/student/tutoring.tsx | 145 +++++++++++++++++----- 5 files changed, 217 insertions(+), 32 deletions(-) create mode 100644 app/lib/hooks/useStudentSessions.ts diff --git a/app/lib/config/api.config.ts b/app/lib/config/api.config.ts index d9bf402..c270e5d 100644 --- a/app/lib/config/api.config.ts +++ b/app/lib/config/api.config.ts @@ -56,5 +56,6 @@ export const API_ENDPOINTS = { }, TUTORIAS: { TUTORES: '/wise/tutorias/tutores', + STUDENT_SESSIONS: '/wise/tutorias/sessions/student/:studentId', }, } as const; diff --git a/app/lib/hooks/useStudentSessions.ts b/app/lib/hooks/useStudentSessions.ts new file mode 100644 index 0000000..3ddcd75 --- /dev/null +++ b/app/lib/hooks/useStudentSessions.ts @@ -0,0 +1,30 @@ +/** + * Custom Hook: useStudentSessions + * Hook de React Query para obtener las sesiones de tutoría de un estudiante + */ + +import { type UseQueryResult, useQuery } from '@tanstack/react-query'; +import { getStudentSessions } from '../services/tutoria.service'; +import type { StudentSession } from '../types/tutoria.types'; + +/** + * Query Key para sesiones del estudiante + */ +export const STUDENT_SESSIONS_QUERY_KEY = (studentId: string) => + ['student-sessions', studentId] as const; + +/** + * Hook para obtener las sesiones de tutoría de un estudiante + */ +export function useStudentSessions( + studentId: string, + enabled = true, +): UseQueryResult { + return useQuery({ + queryKey: STUDENT_SESSIONS_QUERY_KEY(studentId), + queryFn: () => getStudentSessions(studentId), + enabled: enabled && !!studentId, + staleTime: 2 * 60 * 1000, // 2 minutos + refetchOnWindowFocus: true, + }); +} diff --git a/app/lib/services/tutoria.service.ts b/app/lib/services/tutoria.service.ts index cbebfa0..337927e 100644 --- a/app/lib/services/tutoria.service.ts +++ b/app/lib/services/tutoria.service.ts @@ -5,7 +5,7 @@ import apiClient from '../api/client'; import { API_ENDPOINTS } from '../config/api.config'; -import type { TutorProfile } from '../types/tutoria.types'; +import type { StudentSession, TutorProfile } from '../types/tutoria.types'; /** * Obtiene la lista de tutores disponibles en el sistema @@ -21,3 +21,27 @@ export async function getTutores(): Promise { throw new Error('No se pudo obtener la lista de tutores'); } } + +/** + * Obtiene las sesiones de tutoría de un estudiante específico + */ +export async function getStudentSessions( + studentId: string, +): Promise { + try { + const url = API_ENDPOINTS.TUTORIAS.STUDENT_SESSIONS.replace( + ':studentId', + studentId, + ); + console.log('📡 Fetching student sessions:', { studentId, url }); + const response = await apiClient.get(url); + console.log('✅ Student sessions received:', response.data); + return response.data; + } catch (error) { + console.error('❌ Error al obtener sesiones del estudiante:', error); + if (error instanceof Error) { + throw error; + } + throw new Error('No se pudo obtener las sesiones del estudiante'); + } +} diff --git a/app/lib/types/tutoria.types.ts b/app/lib/types/tutoria.types.ts index 3ed972d..b632f82 100644 --- a/app/lib/types/tutoria.types.ts +++ b/app/lib/types/tutoria.types.ts @@ -61,3 +61,50 @@ export interface TutorProfile { rol: Rol; estado: Estado; } + +/** + * Estado de una sesión de tutoría + */ +export type SessionStatus = + | 'PENDIENTE' + | 'CONFIRMADA' + | 'CANCELADA' + | 'COMPLETADA'; + +/** + * Modo de una sesión de tutoría + */ +export type SessionMode = 'VIRTUAL' | 'PRESENCIAL'; + +/** + * Día de la semana en inglés + */ +export type WeekDay = + | 'monday' + | 'tuesday' + | 'wednesday' + | 'thursday' + | 'friday' + | 'saturday' + | 'sunday'; + +/** + * Sesión de tutoría del estudiante (respuesta del backend) + */ +export interface StudentSession { + id: string; + tutorId: string; + studentId: string; + materiaId: string; + codigoMateria: string; + scheduledAt: string; + day: WeekDay; + startTime: string; + endTime: string; + mode: SessionMode; + status: SessionStatus; + linkConexion: string | null; + lugar: string | null; + comentarios: string | null; + createdAt: string; +} diff --git a/app/routes/dashboard/student/tutoring.tsx b/app/routes/dashboard/student/tutoring.tsx index 35dccd7..77e2d61 100644 --- a/app/routes/dashboard/student/tutoring.tsx +++ b/app/routes/dashboard/student/tutoring.tsx @@ -22,8 +22,13 @@ import ScheduledTutoringsModal, { import TutorCard from '~/components/tutor-card'; import TutorFilter from '~/components/tutor-filter'; import TutorScheduleModal from '~/components/tutor-schedule-modal'; +import { useAuth } from '~/contexts/auth-context'; +import { useStudentSessions } from '~/lib/hooks/useStudentSessions'; import { useTutores } from '~/lib/hooks/useTutores'; -import type { TutorProfile } from '~/lib/types/tutoria.types'; +import type { + StudentSession as BackendStudentSession, + TutorProfile, +} from '~/lib/types/tutoria.types'; interface Tutor { id: number; @@ -435,6 +440,39 @@ const CancelSessionModal: React.FC<{ ); }; +/** + * Transforma una sesión del backend al formato StudentSession del componente + */ +const transformBackendSessionToComponentSession = ( + backendSession: BackendStudentSession, +): StudentSession => { + // Mapear el status del backend al formato del componente + const statusMap: Record = { + PENDIENTE: 'pendiente', + CONFIRMADA: 'confirmada', + CANCELADA: 'cancelada', + COMPLETADA: 'confirmada', + }; + + return { + id: backendSession.id, + tutorId: backendSession.tutorId, + studentId: backendSession.studentId, + tutorName: '', // Se llenará con datos del tutor si es necesario + codigoMateria: backendSession.codigoMateria, + subject: backendSession.codigoMateria, // Usar código como subject por defecto + topic: backendSession.comentarios || 'Sin tema especificado', + scheduledAt: backendSession.scheduledAt, + day: backendSession.day, + startTime: backendSession.startTime, + endTime: backendSession.endTime, + mode: backendSession.mode, + status: statusMap[backendSession.status] || 'pendiente', + location: backendSession.lugar || backendSession.linkConexion || undefined, + comentarios: backendSession.comentarios || undefined, + }; +}; + /** * Transforma un TutorProfile del backend al formato Tutor del componente */ @@ -663,6 +701,9 @@ const mockSessions: StudentSession[] = [ ]; const StudentTutoringPage: React.FC = () => { + // Obtener usuario autenticado + const { user } = useAuth(); + // Obtener tutores desde el backend const { data: tutoresData, @@ -670,17 +711,27 @@ const StudentTutoringPage: React.FC = () => { error: tutorsError, } = useTutores(); + // Obtener sesiones del estudiante desde el backend + const { + data: sessionsData, + isLoading: isLoadingSessions, + error: sessionsError, + } = useStudentSessions(user?.id || '', !!user?.id); + // Transformar los datos del backend al formato del componente const tutors = tutoresData ? tutoresData.map(transformTutorProfileToTutor) : []; + // Transformar las sesiones del backend + const sessions = sessionsData + ? sessionsData.map(transformBackendSessionToComponentSession) + : []; + const [searchValue, setSearchValue] = useState(''); const [activeTab, setActiveTab] = useState<'search' | 'my-sessions'>( 'search', ); - const [sessions, setSessions] = useState(mockSessions); - // Inicializar sessions con datos del backend const [selectedSession, setSelectedSession] = useState( null, ); @@ -728,11 +779,10 @@ const StudentTutoringPage: React.FC = () => { const handleSearch = (_filters: TutorFilters) => {}; - // Llamar al backend para cancelar la tutoria + // TODO: Llamar al backend para cancelar la tutoria const handleCancelSession = (id: string) => { - setSessions((prev) => - prev.map((s) => (s.id === id ? { ...s, status: 'cancelada' } : s)), - ); + console.log('Cancelar sesión:', id); + // TODO: Implementar cancelación en el backend y refetch de datos }; const openSessionDetails = (session: StudentSession) => { @@ -867,35 +917,68 @@ const StudentTutoringPage: React.FC = () => {

Sesiones programadas

- {futureSessions.length === 0 ? ( + {/* Loading state */} + {isLoadingSessions && ( +
+ {[...Array(3)].map((_, i) => ( + + + + ))} +
+ )} + + {/* Error state */} + {sessionsError && ( - -

- No tienes tutorias programadas -

-

- Cuando confirmes una tutoria aparecera aqui. +

+ Error al cargar las tutorías:{' '} + {sessionsError instanceof Error + ? sessionsError.message + : 'Error desconocido'}

-
- ) : ( -
- {futureSessions.map((session) => ( - { - setSessionToCancel(currentSession); - onOpenConfirm(); - }} - /> - ))} -
+ )} + + {/* Content */} + {!isLoadingSessions && !sessionsError && ( + <> + {futureSessions.length === 0 ? ( + + + +

+ No tienes tutorias programadas +

+

+ Cuando confirmes una tutoria aparecera aqui. +

+ +
+
+ ) : ( +
+ {futureSessions.map((session) => ( + { + setSessionToCancel(currentSession); + onOpenConfirm(); + }} + /> + ))} +
+ )} + )}
)} From 4891c25b80824e068d17082ed83ea57e8fc042b5 Mon Sep 17 00:00:00 2001 From: AlejandroHenao2572 Date: Mon, 8 Dec 2025 13:53:02 -0500 Subject: [PATCH 004/149] feat(tutorig): show tutor name in 'Mis tutorias' --- app/lib/config/api.config.ts | 1 + app/lib/services/tutoria.service.ts | 28 ++++- app/lib/types/tutoria.types.ts | 7 ++ app/routes/dashboard/student/tutoring.tsx | 129 ++++++++++++++-------- 4 files changed, 116 insertions(+), 49 deletions(-) diff --git a/app/lib/config/api.config.ts b/app/lib/config/api.config.ts index c270e5d..e4cfb39 100644 --- a/app/lib/config/api.config.ts +++ b/app/lib/config/api.config.ts @@ -57,5 +57,6 @@ export const API_ENDPOINTS = { TUTORIAS: { TUTORES: '/wise/tutorias/tutores', STUDENT_SESSIONS: '/wise/tutorias/sessions/student/:studentId', + TUTOR_NAME: '/wise/tutorias/nombre/{id}', }, } as const; diff --git a/app/lib/services/tutoria.service.ts b/app/lib/services/tutoria.service.ts index 337927e..4a1bd4a 100644 --- a/app/lib/services/tutoria.service.ts +++ b/app/lib/services/tutoria.service.ts @@ -5,7 +5,11 @@ import apiClient from '../api/client'; import { API_ENDPOINTS } from '../config/api.config'; -import type { StudentSession, TutorProfile } from '../types/tutoria.types'; +import type { + StudentSession, + TutorNameResponse, + TutorProfile, +} from '../types/tutoria.types'; /** * Obtiene la lista de tutores disponibles en el sistema @@ -33,15 +37,31 @@ export async function getStudentSessions( ':studentId', studentId, ); - console.log('📡 Fetching student sessions:', { studentId, url }); + console.log('Fetching student sessions:', { studentId, url }); const response = await apiClient.get(url); - console.log('✅ Student sessions received:', response.data); + console.log('Student sessions received:', response.data); return response.data; } catch (error) { - console.error('❌ Error al obtener sesiones del estudiante:', error); + console.error('Error al obtener sesiones del estudiante:', error); if (error instanceof Error) { throw error; } throw new Error('No se pudo obtener las sesiones del estudiante'); } } + +/** + * Obtiene el nombre de un tutor específico + */ +export async function getTutorName(tutorId: string): Promise { + try { + const url = API_ENDPOINTS.TUTORIAS.TUTOR_NAME.replace('{id}', tutorId); + console.log('Fetching tutor name:', { tutorId, url }); + const response = await apiClient.get(url); + console.log('Tutor name received:', response.data); + return response.data.nombreCompleto; + } catch (error) { + console.error('Error al obtener nombre del tutor:', error); + return 'Tutor no disponible'; + } +} diff --git a/app/lib/types/tutoria.types.ts b/app/lib/types/tutoria.types.ts index b632f82..3ef21b4 100644 --- a/app/lib/types/tutoria.types.ts +++ b/app/lib/types/tutoria.types.ts @@ -108,3 +108,10 @@ export interface StudentSession { comentarios: string | null; createdAt: string; } + +/** + * Respuesta del endpoint de nombre del tutor + */ +export interface TutorNameResponse { + nombreCompleto: string; +} diff --git a/app/routes/dashboard/student/tutoring.tsx b/app/routes/dashboard/student/tutoring.tsx index 77e2d61..0ab6e78 100644 --- a/app/routes/dashboard/student/tutoring.tsx +++ b/app/routes/dashboard/student/tutoring.tsx @@ -11,9 +11,8 @@ import { ModalHeader, useDisclosure, } from '@heroui/react'; -import { BookOpen, Calendar, Clock, MapPin, Search, Video } from 'lucide-react'; -import type React from 'react'; -import { useCallback, useMemo, useState } from 'react'; +import { Calendar, Clock, MapPin, Search, Video } from 'lucide-react'; +import React, { useCallback, useMemo, useState } from 'react'; import { useOutletContext } from 'react-router'; import { PageHeader } from '~/components/page-header'; import ScheduledTutoringsModal, { @@ -86,8 +85,9 @@ const getAvatarBg = (avatarColor?: string): string => { return colorMap[avatarColor ?? ''] || 'bg-red-500'; }; -const getInitials = (name: string, fallback?: string): string => { +const getInitials = (name: string | undefined, fallback?: string): string => { if (fallback) return fallback; + if (!name) return 'T'; const parts = name.split(' ').filter(Boolean); const first = parts[0]?.[0] ?? ''; const last = parts.slice(-1)[0]?.[0] ?? ''; @@ -249,13 +249,32 @@ const SessionCardItem: React.FC<{ onViewDetails: (session: StudentSession) => void; onCancel: (session: StudentSession) => void; }> = ({ session, onViewDetails, onCancel }) => { + const [tutorName, setTutorName] = React.useState('Cargando...'); const view = buildSessionViewModel(session); + React.useEffect(() => { + const fetchTutorName = async () => { + try { + const { getTutorName } = await import('~/lib/services/tutoria.service'); + const name = await getTutorName(session.tutorId); + setTutorName(name); + } catch (error) { + console.error('Error fetching tutor name:', error); + setTutorName('Tutor no disponible'); + } + }; + + fetchTutorName(); + }, [session.tutorId]); + + // Actualizar el tutorName en la vista + const viewWithTutorName = { ...view, tutorName }; + return ( - - - ) : ( -
- {futureSessions.map((session) => ( - { - setSessionToCancel(currentSession); - onOpenConfirm(); - }} - /> - ))} -
- )} - - )} + {!isLoadingSessions && + !sessionsError && + (futureSessions.length === 0 ? ( + + + +

+ No tienes tutorias programadas +

+

+ Cuando confirmes una tutoria aparecera aqui. +

+ +
+
+ ) : ( +
+ {futureSessions.map((session) => ( + { + setSessionToCancel(currentSession); + onOpenConfirm(); + }} + /> + ))} +
+ ))} )} Date: Mon, 8 Dec 2025 15:14:00 -0500 Subject: [PATCH 005/149] feat(tutorig): show subject code and name in details --- app/lib/config/api.config.ts | 1 + app/lib/services/tutoria.service.ts | 20 ++++++++++++ app/lib/types/tutoria.types.ts | 10 ++++++ app/routes/dashboard/student/tutoring.tsx | 39 +++++++++++++---------- 4 files changed, 53 insertions(+), 17 deletions(-) diff --git a/app/lib/config/api.config.ts b/app/lib/config/api.config.ts index e4cfb39..5fd104a 100644 --- a/app/lib/config/api.config.ts +++ b/app/lib/config/api.config.ts @@ -58,5 +58,6 @@ export const API_ENDPOINTS = { TUTORES: '/wise/tutorias/tutores', STUDENT_SESSIONS: '/wise/tutorias/sessions/student/:studentId', TUTOR_NAME: '/wise/tutorias/nombre/{id}', + MATERIA: '/wise/tutorias/materia/{codigo}', }, } as const; diff --git a/app/lib/services/tutoria.service.ts b/app/lib/services/tutoria.service.ts index 4a1bd4a..d965ad4 100644 --- a/app/lib/services/tutoria.service.ts +++ b/app/lib/services/tutoria.service.ts @@ -6,6 +6,7 @@ import apiClient from '../api/client'; import { API_ENDPOINTS } from '../config/api.config'; import type { + MateriaResponse, StudentSession, TutorNameResponse, TutorProfile, @@ -65,3 +66,22 @@ export async function getTutorName(tutorId: string): Promise { return 'Tutor no disponible'; } } + +/** + * Obtiene información de una materia por su código + */ +export async function getMateria( + codigoMateria: string, +): Promise { + try { + const url = API_ENDPOINTS.TUTORIAS.MATERIA.replace( + '{codigo}', + codigoMateria, + ); + const response = await apiClient.get(url); + return response.data; + } catch (error) { + console.error('Error al obtener materia:', error); + return null; + } +} diff --git a/app/lib/types/tutoria.types.ts b/app/lib/types/tutoria.types.ts index 3ef21b4..eba25d6 100644 --- a/app/lib/types/tutoria.types.ts +++ b/app/lib/types/tutoria.types.ts @@ -115,3 +115,13 @@ export interface StudentSession { export interface TutorNameResponse { nombreCompleto: string; } + +/** + * Respuesta del endpoint de materia + */ +export interface MateriaResponse { + id: string; + codigo: string; + nombre: string; + temas: string[]; +} diff --git a/app/routes/dashboard/student/tutoring.tsx b/app/routes/dashboard/student/tutoring.tsx index 0ab6e78..30d8e2e 100644 --- a/app/routes/dashboard/student/tutoring.tsx +++ b/app/routes/dashboard/student/tutoring.tsx @@ -203,7 +203,7 @@ const SessionHeader: React.FC<{ {subtitle && ( -

{subtitle}

+

{subtitle}

)} @@ -220,7 +220,7 @@ const SessionMeta: React.FC<{ className?: string; }> = ({ session, includeDay = false, className }) => (
@@ -310,27 +310,41 @@ const SessionDetailsModal: React.FC<{ onRequestCancel: (session: StudentSession) => void; }> = ({ session, isOpen, onClose, onRequestCancel }) => { const [tutorName, setTutorName] = React.useState('Cargando...'); + const [materiaName, setMateriaName] = React.useState(''); React.useEffect(() => { if (!session) return; - const fetchTutorName = async () => { + const fetchData = async () => { + const { getTutorName, getMateria } = await import( + '~/lib/services/tutoria.service' + ); + try { - const { getTutorName } = await import('~/lib/services/tutoria.service'); const name = await getTutorName(session.tutorId); setTutorName(name); } catch (error) { console.error('Error fetching tutor name:', error); setTutorName('Tutor no disponible'); } + + try { + const materia = await getMateria(session.codigoMateria); + setMateriaName(materia ? materia.nombre : session.codigoMateria); + } catch (error) { + console.error('Error fetching materia:', error); + setMateriaName(session.codigoMateria); + } }; - fetchTutorName(); + fetchData(); }, [session]); - const sessionWithTutorName = session ? { ...session, tutorName } : null; - const view = sessionWithTutorName - ? buildSessionViewModel(sessionWithTutorName) + const sessionWithUpdatedData = session + ? { ...session, tutorName, subject: materiaName || session.subject } + : null; + const view = sessionWithUpdatedData + ? buildSessionViewModel(sessionWithUpdatedData) : null; return ( @@ -370,15 +384,6 @@ const SessionDetailsModal: React.FC<{ -
- - Tutor ID: - - - {view.tutorId} - -
-

Comentarios From 40d5ed52bc4cbde103f183c3dec63b068f31ce9e Mon Sep 17 00:00:00 2001 From: AlejandroHenao2572 Date: Mon, 8 Dec 2025 15:25:46 -0500 Subject: [PATCH 006/149] feat(tutorig): make connection link to meeting directionable --- app/routes/dashboard/student/tutoring.tsx | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/app/routes/dashboard/student/tutoring.tsx b/app/routes/dashboard/student/tutoring.tsx index 30d8e2e..382d1bb 100644 --- a/app/routes/dashboard/student/tutoring.tsx +++ b/app/routes/dashboard/student/tutoring.tsx @@ -239,7 +239,24 @@ const SessionMeta: React.FC<{ )} {session.modalityLabel} - {session.location && - {session.location}} + {session.location && ( + <> + - + {session.modalityLabel === 'virtual' && + session.location.startsWith('http') ? ( + + {session.location} + + ) : ( + {session.location} + )} + + )}

); From c008ab3c4d4843c689b2402a7ed21e0c51b9dfcd Mon Sep 17 00:00:00 2001 From: AlejandroHenao2572 Date: Mon, 8 Dec 2025 18:32:05 -0500 Subject: [PATCH 007/149] feat(tutorig): submit tutoring form to request tutoring sesion --- app/components/tutor-card.tsx | 4 + app/components/tutor-schedule-modal.tsx | 261 ++++++++++++++++------ app/lib/config/api.config.ts | 1 + app/lib/hooks/useCreateSession.ts | 29 +++ app/lib/services/tutoria.service.ts | 49 ++++ app/lib/types/tutoria.types.ts | 20 ++ app/routes/dashboard/student/tutoring.tsx | 54 ++--- 7 files changed, 313 insertions(+), 105 deletions(-) create mode 100644 app/lib/hooks/useCreateSession.ts diff --git a/app/components/tutor-card.tsx b/app/components/tutor-card.tsx index 710a174..08ad1a9 100644 --- a/app/components/tutor-card.tsx +++ b/app/components/tutor-card.tsx @@ -1,9 +1,11 @@ import { Calendar, MessageCircle, Star } from 'lucide-react'; import type React from 'react'; import { Link } from 'react-router'; +import type { TutorProfile } from '~/lib/types/tutoria.types'; interface Tutor { id: number; + tutorId: string; name: string; title: string; department: string; @@ -14,6 +16,8 @@ interface Tutor { tags: string[]; availability: string; isAvailableToday: boolean; + timeSlots?: string[]; + disponibilidad?: TutorProfile['disponibilidad']; } interface TutorCardProps { diff --git a/app/components/tutor-schedule-modal.tsx b/app/components/tutor-schedule-modal.tsx index 3dfb47f..da5edf8 100644 --- a/app/components/tutor-schedule-modal.tsx +++ b/app/components/tutor-schedule-modal.tsx @@ -1,5 +1,6 @@ import { Avatar, + Badge, Button, Chip, Input, @@ -8,12 +9,20 @@ import { ModalContent, ModalFooter, ModalHeader, + Textarea, } from '@heroui/react'; import { X } from 'lucide-react'; import { useEffect, useState } from 'react'; +import { useCreateSession } from '~/lib/hooks/useCreateSession'; +import type { + CreateSessionRequest, + DisponibilidadSlot, + WeekDay, +} from '~/lib/types/tutoria.types'; interface Tutor { id: number; + tutorId?: string; name: string; title: string; department: string; @@ -25,67 +34,140 @@ interface Tutor { availability: string; isAvailableToday: boolean; timeSlots?: string[]; + disponibilidad?: Record; +} + +interface ScheduleSlot { + day: WeekDay; + dayLabel: string; + startTime: string; + endTime: string; + mode: 'VIRTUAL' | 'PRESENCIAL' | 'HIBRIDA'; + lugar: string; } interface Props { tutor: Tutor | null; isOpen: boolean; onClose: () => void; - onSchedule: (data: { - tutorId: number; - name: string; - email: string; - slot: string; - notes?: string; - }) => void; + studentId: string; + onSuccess?: () => void; } +const DAY_LABELS: Record = { + monday: 'Lunes', + tuesday: 'Martes', + wednesday: 'Miércoles', + thursday: 'Jueves', + friday: 'Viernes', + saturday: 'Sábado', + sunday: 'Domingo', +}; + +const MODE_COLORS: Record = { + VIRTUAL: 'primary', + PRESENCIAL: 'success', + HIBRIDA: 'warning', +}; + export default function TutorScheduleModal({ tutor, isOpen, onClose, - onSchedule, + studentId, + onSuccess, }: Props) { - const [selectedSlot, setSelectedSlot] = useState(null); - const [name, setName] = useState(''); - const [email, setEmail] = useState(''); - const [notes, setNotes] = useState(''); + const [selectedSlot, setSelectedSlot] = useState(null); + const [comentarios, setComentarios] = useState(''); + const [codigoMateria, setCodigoMateria] = useState(''); + + const { mutate: createSession, isPending } = useCreateSession(); useEffect(() => { - if (isOpen) { - // reset + if (isOpen && tutor) { setSelectedSlot(null); - setName(''); - setEmail(''); - setNotes(''); + setComentarios(''); + setCodigoMateria(''); } - }, [isOpen]); + }, [isOpen, tutor]); const handleConfirm = () => { - if (!tutor) return; - if (!selectedSlot) return alert('Selecciona un horario disponible.'); - if (!name || !email) return alert('Por favor ingresa tu nombre y correo.'); - - // Simular petición a API - const payload = { - tutorId: tutor.id, - name, - email, - slot: selectedSlot, - notes, + if (!tutor?.tutorId || !selectedSlot) { + alert('Por favor selecciona un horario disponible.'); + return; + } + + if (!codigoMateria.trim()) { + alert('Por favor ingresa el código de la materia.'); + return; + } + + // Construir scheduledAt en formato ISO + const now = new Date(); + const [hours, minutes] = selectedSlot.startTime.split(':').map(Number); + + // Calcular la fecha de la próxima ocurrencia del día seleccionado + const dayIndex = [ + 'sunday', + 'monday', + 'tuesday', + 'wednesday', + 'thursday', + 'friday', + 'saturday', + ].indexOf(selectedSlot.day); + const currentDay = now.getDay(); + const daysUntilTarget = (dayIndex - currentDay + 7) % 7 || 7; + + const targetDate = new Date(now); + targetDate.setDate(now.getDate() + daysUntilTarget); + targetDate.setHours(hours, minutes, 0, 0); + + const request: CreateSessionRequest = { + tutorId: tutor.tutorId, + studentId, + codigoMateria, + scheduledAt: targetDate.toISOString(), + day: selectedSlot.day, + startTime: selectedSlot.startTime, + endTime: selectedSlot.endTime, + mode: selectedSlot.mode as 'VIRTUAL' | 'PRESENCIAL', + comentarios: comentarios || undefined, }; - onSchedule(payload); - alert(`Tutoría agendada con ${tutor.name} el ${selectedSlot}.`); - onClose(); + + createSession(request, { + onSuccess: () => { + alert(`¡Tutoría agendada exitosamente con ${tutor.name}!`); + onSuccess?.(); + onClose(); + }, + onError: (error) => { + alert(`Error al agendar: ${error.message}`); + }, + }); }; if (!tutor) return null; - const slots = - tutor.timeSlots && tutor.timeSlots.length > 0 ? tutor.timeSlots : []; + // Convertir disponibilidad a slots organizados + const scheduleSlots: ScheduleSlot[] = []; + if (tutor.disponibilidad) { + Object.entries(tutor.disponibilidad).forEach(([day, slots]) => { + slots.forEach((slot) => { + scheduleSlots.push({ + day: day as WeekDay, + dayLabel: DAY_LABELS[day as WeekDay], + startTime: slot.start, + endTime: slot.end, + mode: slot.modalidad, + lugar: slot.lugar, + }); + }); + }); + } return ( - +
@@ -106,9 +188,9 @@ export default function TutorScheduleModal({
-
+ {/* Tags de materias */}
{tutor.tags.map((t) => ( @@ -117,59 +199,92 @@ export default function TutorScheduleModal({ ))}
-

{tutor.availability}

- + {/* Horarios disponibles */}
-

Horarios disponibles

- {slots.length === 0 ? ( +

Horarios disponibles

+ {scheduleSlots.length === 0 ? (

No hay horarios disponibles.

) : ( -
- {slots.map((slot) => ( - - ))} +
+ {scheduleSlots.map((slot, index) => { + const slotKey = `${slot.day}-${slot.startTime}-${index}`; + const isSelected = + selectedSlot?.day === slot.day && + selectedSlot?.startTime === slot.startTime && + selectedSlot?.endTime === slot.endTime; + + return ( + + ); + })}
)}
+ {/* Código de Materia */}
-

Tus datos

-
- - - -
+

Código de Materia *

+
-
- + {/* Comentarios */} +
+

Comentarios (opcional)

+