Servicios
Ruta: /teacher/services · Atajo: g v · Sidebar: Servicios
La pagina de Servicios gestiona el catalogo completo del profesor. Cada servicio se define por 3 dimensiones ortogonales: deliveryMode x groupType x structure. Incluye KPIs del mes, dos modos de vista, y workflow de estados.
Que hay
Section titled “Que hay”Tabs principales
Section titled “Tabs principales”- Catalogo (default) — Gestion de servicios
- Legal — Gestion de documentos legales (contratos, politicas)
KPIs (4 tarjetas)
Section titled “KPIs (4 tarjetas)”Grid responsive con metricas del mes actual vs. anterior:
| KPI | Datos | Tendencia |
|---|---|---|
| Ingresos del mes | Total revenue del mes en curso | % vs mes anterior |
| Enrollments activos | Count de enrollments activos | — |
| Enrollments del mes | Nuevos enrollments este mes | % vs mes anterior |
| Sesiones del mes | Sesiones completadas/programadas | % vs mes anterior |
API: GET /teacher/services/stats
Filtros y controles
Section titled “Filtros y controles”- Busqueda por nombre: Input con debounce 300ms. Filtro client-side sobre el nombre del servicio. Muestra empty state si no hay coincidencias.
- Pills de estado: Todos, Active, Draft, Paused, Archived (seleccion unica)
- Date Range Picker: Presets (7 dias, 30 dias, 3 meses) o rango custom
- Toggle de vista: Cards View vs Analytics View
Vista Cards (tabla plana)
Section titled “Vista Cards (tabla plana)”Tabla de 14 columnas en desktop:
| Columna | Contenido |
|---|---|
| Grip | Handle de arrastre para reordenar (dnd-kit) |
| Checkbox | Seleccion multiple para acciones en masa |
| Toggle | Switch activo/pausado (inline, optimista) |
| Iconos | 3 badges: delivery mode, group type, structure |
| Nombre | Nombre + badges (Trial, Unlisted, Private) |
| Menu | Tres puntos (Editar, Duplicar, Archivar, Eliminar) |
| Precio | Formateado segun pricing model |
| Vendidos | Total enrollments historicos |
| Activos | Enrollments activos actuales |
| Revenue | Ingresos totales (lifetime) |
| Impartidas | Sesiones completadas |
| Restantes | Creditos pendientes |
| Perdidas | Sesiones forfeited (rojo si mayor que 0) |
| Este periodo | Sesiones en el rango seleccionado |
Fila de totales al final si hay 2+ servicios. Columnas ordenables (click en header).
Reordenacion manual (DnD)
Section titled “Reordenacion manual (DnD)”Cada fila en la Vista Cards tiene un grip handle (icono GripVertical) a la izquierda. El profesor puede arrastrar y soltar para cambiar el orden de sus servicios.
Implementacion:
DndContext+SortableContextconverticalListSortingStrategyenvuelven la lista de servicios- Cada fila es un
SortableServiceRowque usauseSortablede dnd-kit PointerSensorconactivationConstraint: { distance: 5 }evita activacion accidental- Al soltar (
handleDragEnd): se usaarrayMovepara reordenar el estado local (localOrder) de forma optimista y se llamaPATCH /teacher/services/:idcon el nuevosortOrderpara cada servicio movido - Si el servidor falla, se resetea
localOrdera null y se muestra un toast de error DragOverlaymuestra una copia semitransparente de la fila durante el arrastre
API: PATCH /teacher/services/:id con { sortOrder: number }
Acciones en masa
Section titled “Acciones en masa”Cuando hay servicios en la Vista Cards, se puede seleccionar uno o varios con los checkboxes de la columna izquierda. Al seleccionar al menos uno, aparece una barra de acciones fija en la parte superior de la lista con:
- Contador de seleccionados (“N seleccionados”)
- Publicar todos — llama a
POST /teacher/services/:id/publishpara cada servicio seleccionado viaPromise.all. Solo aplica a servicios endraftopaused. - Pausar todos — llama a
POST /teacher/services/:id/pausepara cada servicio seleccionado viaPromise.all. Solo aplica a serviciosactive. - X — cancela la seleccion
El header de la tabla incluye un checkbox de “seleccionar todo” que alterna entre seleccionar todos los servicios visibles y deseleccionar todos.
Estado: selectedIds: Set<string> + isBulkPublishing: boolean + isBulkPausing: boolean
Validacion de slug en tiempo real
Section titled “Validacion de slug en tiempo real”El campo URL del servicio (slug) en el formulario de edicion (y creacion) incluye validacion en tiempo real:
- Formato: Regex
/^[a-z0-9-]+$/. Si el slug contiene caracteres invalidos, muestra error “Solo letras minusculas, numeros y guiones” inmediatamente. - Disponibilidad: Con un debounce de 400ms, llama a
GET /teacher/services/slug-check?slug=...&excludeId=...para verificar conflictos. - Indicadores visuales:
- Comprobando — spinner
Loader2+ borde neutro - Disponible —
CheckCircle2verde + borde verde - En uso —
AlertCirclerojo + borde rojo - Invalido —
AlertCirclerojo + borde rojo
- Comprobando — spinner
El parametro excludeId excluye el propio servicio al editar (su slug actual siempre es “disponible” para si mismo).
API: GET /teacher/services/slug-check?slug=xxx&excludeId=yyy
Vista Analytics (agrupada)
Section titled “Vista Analytics (agrupada)”Servicios agrupados en 4 secciones colapsables por estructura:
- Single — Clases sueltas
- Package — Paquetes multi-sesion
- Course — Cursos con secuencia ordenada
- Subscription — Suscripciones recurrentes
Cada grupo muestra badge de conteo, subtotales, y barra de total general.
Duplicar servicio
Section titled “Duplicar servicio”El menu de tres puntos incluye la opcion “Duplicar” (icono Copy). Llama a POST /teacher/services/:id/duplicate.
Copia todos los campos del servicio excepto id, status (queda en draft), name (agrega ” (copia)”) y slug.
Wizard de creacion
Section titled “Wizard de creacion”Flujo de 2 pasos:
- Selector de preset (26 presets agrupados por delivery + group) o modo custom
- Formulario de configuracion con todos los campos. Incluye
TagMultiSelectpara asignar tags al servicio.
Schedule Items — editor de secuencia para cursos
Section titled “Schedule Items — editor de secuencia para cursos”Cuando se edita un servicio con structure=course, aparece la seccion Schedule Items dentro del sheet de edicion.
Permite definir la secuencia ordenada de eventos del curso:
| Tipo de item | Icono | Color |
|---|---|---|
live_session | Video | Azul |
async_content | FileText | Ambar |
milestone | Flag | Esmeralda |
Cada item tiene: numero de secuencia, tipo, titulo, descripcion, duracion (minutos), flag isOptional.
Drag-and-drop con dnd-kit: los items son reordenables arrastrando el grip handle. Al soltar, se hace una actualizacion optimista del cache (queryClient.setQueryData) y se llama a POST /teacher/services/:id/schedule/reorder con el array de ids en el nuevo orden.
Formulario de creacion/edicion en un Sheet secundario con: selector de tipo, input de titulo, textarea de descripcion, duracion (oculto para milestones), checkbox opcional.
API usada:
GET /teacher/services/:id/schedule— lista itemsPOST /teacher/services/:id/schedule— crear itemPATCH /teacher/services/:id/schedule/:itemId— editar itemDELETE /teacher/services/:id/schedule/:itemId— borrar item (conConfirmDialog)POST /teacher/services/:id/schedule/reorder— reordenar
Query key: ['teacher', 'service-schedule', serviceId]
Roster de alumnos por servicio
Section titled “Roster de alumnos por servicio”Todos los servicios en edicion muestran la seccion Roster de alumnos al final del sheet. Lista los enrollments del servicio via GET /teacher/enrollments?serviceId=:id.
Columnas de la tabla:
| Columna | Datos |
|---|---|
| Alumno | Nombre + email |
| Estado | Badge de color por status de enrollment |
| Completadas | sessionsCompleted |
| Restantes | sessionsTotal - sessionsCompleted - sessionsScheduled - sessionsForfeited |
| Inscrito | Fecha de enrolledAt o createdAt |
Estados de enrollment con colores: pending_payment (ambar), active (esmeralda), completed (azul), cancelled/expired (gris), suspended (ambar).
Query key: ['teacher', 'service-enrollments', serviceId]
Tags en servicios
Section titled “Tags en servicios”TagMultiSelect disponible en el wizard de creacion y en el formulario de edicion. Las tags se sincronizan via la junction table service_tags. El campo tags ya estaba soportado en el backend.
3 dimensiones de servicio
Section titled “3 dimensiones de servicio”| Dimension | Opciones |
|---|---|
| Delivery Mode | live (video), async (self-paced), hybrid (mixto) |
| Group Type | individual (1:1), group (limitado), open (ilimitado) |
| Structure | single, package, course, subscription |
Workflow de estados
Section titled “Workflow de estados”draft → active (publish)active → paused (pause)paused → active (publish)active/paused → archived (archive)draft → deleted (soft-delete, solo draft)Que falta
Section titled “Que falta”| Feature | Descripcion | Estado | Implementado |
|---|---|---|---|
| Drill-down de metricas | Click en revenue/enrollments deberia mostrar detalle | Implementado ✅ | |
| KPIs por servicio | Tendencias MoM por servicio individual, no solo agregado | Implementado ✅ |
Que falla
Section titled “Que falla”| Bug | Descripcion | Estado | Corregido |
|---|---|---|---|
| Moneda hardcodeada en precios | El formato de precio usaba currency del servicio pero el wizard podia defaultear a EUR. Ahora usa teacher.defaultCurrency | ✅ | Batch 4 |
| Servicios inactivos al 50% de opacidad | Los servicios pausados/archivados se mostraban con opacity: 50%, dificultando leer las metricas. FIXED: opacity-50 removed. Status badges (Pausado/Archivado) added to ServiceNameCell component instead, preserving metric readability | ✅ | Batch 4 |
Que cambiaria
Section titled “Que cambiaria”| Mejora | Descripcion | Dificultad | Estado | Implementado |
|---|---|---|---|---|
| Confirmacion al pausar servicios con enrollments | ConfirmDialog aparece cuando service.activeEnrollments > 0 al pausar, mostrando el conteo de inscripciones activas afectadas | Facil | ✅ | Batch 4 |
Referencia tecnica
Section titled “Referencia tecnica”Archivos clave
Section titled “Archivos clave”| Archivo | Proposito |
|---|---|
apps/web/src/routes/teacher/services.lazy.tsx | Pagina completa (catalogo + KPIs + vistas + schedule items + roster) |
apps/web/src/components/services/service-wizard.tsx | Wizard de creacion (3 dimensiones) |
apps/api/src/services/scheduling/service-catalog.ts | CRUD + stats aggregation |
apps/api/src/services/scheduling/service-schedule.ts | CRUD de schedule items |
apps/api/src/routes/teacher/services.ts | Rutas HTTP |
packages/shared/src/schemas/service.ts | Schemas Zod |
Componentes internos del archivo de servicios
Section titled “Componentes internos del archivo de servicios”| Componente | Proposito |
|---|---|
ServicesPage | Pagina principal — tabs, KPIs, filtros, vistas, DnD, bulk actions |
SortableServiceRow | Fila arrastrable de servicio (grip + checkbox + datos). Usa useSortable de dnd-kit |
ServiceForm | Sheet de edicion/creacion de servicio (incluye validacion de slug en tiempo real) |
ScheduleItemsSection | Editor DnD de items de secuencia (solo cursos) |
SortableScheduleItem | Item individual arrastrable (dnd-kit useSortable) |
StudentRosterSection | Tabla de enrollments del servicio |
ServiceMenu | Popover de 3 puntos (editar, archivar, borrar) |
ServiceIconsCell | 3 badges de dimensiones (delivery, group, structure) |
ServiceNameCell | Nombre + badges (Trial, Unlisted, Private) |
SortableHeader | Header de columna ordenable con icono de direccion |
PriceCell | Precio formateado segun pricing model |
| Endpoint | Metodo | Proposito |
|---|---|---|
/teacher/services | GET | Lista con stats embebidas |
/teacher/services/stats | GET | KPIs del mes |
/teacher/services/slug-check?slug=&excludeId= | GET | Verifica disponibilidad de slug (excluye el propio servicio al editar) |
/teacher/services/:id | GET/PATCH/DELETE | CRUD individual (PATCH acepta sortOrder para reordenar) |
/teacher/services | POST | Crear servicio |
/teacher/services/:id/duplicate | POST | Duplicar servicio (crea draft con todos los campos) |
/teacher/services/:id/publish | POST | Publicar (draft/paused → active) |
/teacher/services/:id/pause | POST | Pausar (active → paused) |
/teacher/services/:id/archive | POST | Archivar |
/teacher/services/:id/schedule | GET/POST | Items de schedule (cursos) |
/teacher/services/:id/schedule/:itemId | PATCH/DELETE | Item individual |
/teacher/services/:id/schedule/reorder | POST | Reordenar items |
Queries (TanStack Query)
Section titled “Queries (TanStack Query)”// Lista de servicios con stats (filtros de status y rango de fechas)useQuery({ queryKey: ['teacher', 'services', filterStatus, startDateStr, endDateStr], queryFn: ... })
// KPIs globales del mesuseQuery({ queryKey: ['teacher', 'services', 'stats'], queryFn: ... })
// Detalle de un servicio (para el form de edicion)useQuery({ queryKey: ['teacher', 'service-detail', serviceId], queryFn: ... })
// Schedule items de un cursouseQuery({ queryKey: ['teacher', 'service-schedule', serviceId], queryFn: ... })
// Enrollments de un servicio (roster)useQuery({ queryKey: ['teacher', 'service-enrollments', serviceId], queryFn: ... })