Refactor code structure for improved readability and maintainability
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
+10
-2
@@ -137,6 +137,14 @@
|
|||||||
- [x] **Studio lti-tools**: sección AGS colapsable en el formulario de creación — 4 campos opcionales (client_id, client_secret, token_url, lineitem_url).
|
- [x] **Studio lti-tools**: sección AGS colapsable en el formulario de creación — 4 campos opcionales (client_id, client_secret, token_url, lineitem_url).
|
||||||
- [x] **api.ts**: Interface `BbbRecording`, `getStudyRoomRecordings()` en Studio y Experience. Campos AGS en `LtiExternalTool`, `CreateLtiExternalToolPayload`, `UpdateLtiExternalToolPayload`.
|
- [x] **api.ts**: Interface `BbbRecording`, `getStudyRoomRecordings()` en Studio y Experience. Campos AGS en `LtiExternalTool`, `CreateLtiExternalToolPayload`, `UpdateLtiExternalToolPayload`.
|
||||||
|
|
||||||
|
### Fase 40: Edición Colaborativa de Documentos 📝 ✅
|
||||||
|
- [x] **Tabla `lesson_collaborative_docs`**: migración con campos `content TEXT`, `revision BIGINT`, `last_modified_by UUID`. Índice único por `(lesson_id, organization_id)`.
|
||||||
|
- [x] **Backend**: 3 endpoints — `GET /lessons/{id}/collaborative-doc`, `PUT /lessons/{id}/collaborative-doc` (concurrencia optimista por `revision`), `GET /lessons/{id}/collaborative-doc/stream` (SSE, polling 2s). Respuesta 409 con `server_content`/`server_revision` en conflicto.
|
||||||
|
- [x] **Experience — CollaborativeDocEditor**: componente SSE con autosave debounce 1.5s, toolbar Markdown básico (negrita, cursiva, H1/H2, listas), resolución de conflictos con diff visual (conservar la mía / usar la del servidor). Solo actualiza desde SSE si no hay cambios locales pendientes.
|
||||||
|
- [x] **Experience — Lesson Player**: sección "Documento Colaborativo" integrada en la página de la lección, después de la pizarra colaborativa.
|
||||||
|
- [x] **Studio**: página `/courses/[id]/lessons/[lessonId]/collaborative-doc` con metadatos (revisión, palabras, caracteres, última edición) + vista previa + botón "Borrar documento". Link desde el breadcrumb del editor de lección.
|
||||||
|
- [x] **api.ts Studio + Experience**: funciones `getLessonCollaborativeDoc`, `updateLessonCollaborativeDoc`, interfaces `CollaborativeDoc`, `UpdateCollaborativeDocPayload`, `UpdateCollaborativeDocResponse`.
|
||||||
|
|
||||||
**Próximas Prioridades**:
|
**Próximas Prioridades**:
|
||||||
1. **Edición Multiusuario** — documentos compartidos tipo Google Docs en lecciones.
|
1. **Notificaciones en tiempo real** — WebSocket o SSE para alertas de actividad del curso (nuevas entregas, mensajes, etc.).
|
||||||
2. **Notificaciones en tiempo real** — WebSocket o SSE para alertas de actividad del curso.
|
2. **Progreso del curso** — Endpoint de porcentaje de avance por alumno; barra de progreso en Experience.
|
||||||
@@ -1717,7 +1717,7 @@ pub async fn get_user_enrollments(
|
|||||||
user_id,
|
user_id,
|
||||||
org_ctx.id
|
org_ctx.id
|
||||||
);
|
);
|
||||||
let enrollments = sqlx::query_as::<_, Enrollment>(
|
let enrollments = match sqlx::query_as::<_, Enrollment>(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
e.id,
|
e.id,
|
||||||
@@ -1766,8 +1766,48 @@ pub async fn get_user_enrollments(
|
|||||||
.bind(user_id)
|
.bind(user_id)
|
||||||
.bind(org_ctx.id)
|
.bind(org_ctx.id)
|
||||||
.fetch_all(&pool)
|
.fetch_all(&pool)
|
||||||
|
.await {
|
||||||
|
Ok(rows) => rows,
|
||||||
|
Err(primary_err) => {
|
||||||
|
tracing::error!(
|
||||||
|
"get_user_enrollments: advanced query failed for user_id={} org_id={}: {}",
|
||||||
|
user_id,
|
||||||
|
org_ctx.id,
|
||||||
|
primary_err
|
||||||
|
);
|
||||||
|
|
||||||
|
// Fallback para esquemas legacy donde faltan tablas/columnas usadas en el cálculo avanzado.
|
||||||
|
sqlx::query_as::<_, Enrollment>(
|
||||||
|
r#"
|
||||||
|
SELECT
|
||||||
|
e.id,
|
||||||
|
e.user_id,
|
||||||
|
e.organization_id,
|
||||||
|
e.course_id,
|
||||||
|
NULL::INT AS external_id,
|
||||||
|
0.0::FLOAT4 AS progress,
|
||||||
|
e.enrolled_at
|
||||||
|
FROM enrollments e
|
||||||
|
WHERE e.user_id = $1
|
||||||
|
AND e.organization_id = $2
|
||||||
|
ORDER BY e.enrolled_at DESC
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.bind(user_id)
|
||||||
|
.bind(org_ctx.id)
|
||||||
|
.fetch_all(&pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
|
.map_err(|fallback_err| {
|
||||||
|
tracing::error!(
|
||||||
|
"get_user_enrollments: fallback query failed for user_id={} org_id={}: {}",
|
||||||
|
user_id,
|
||||||
|
org_ctx.id,
|
||||||
|
fallback_err
|
||||||
|
);
|
||||||
|
StatusCode::INTERNAL_SERVER_ERROR
|
||||||
|
})?
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
Ok(Json(enrollments))
|
Ok(Json(enrollments))
|
||||||
}
|
}
|
||||||
@@ -4884,7 +4924,7 @@ pub async fn update_lesson_collaborative_doc(
|
|||||||
return Err((StatusCode::NOT_FOUND, "Lección no encontrada".into()));
|
return Err((StatusCode::NOT_FOUND, "Lección no encontrada".into()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let user_id: Uuid = claims.sub.parse().map_err(|_| (StatusCode::UNAUTHORIZED, "Token inválido".into()))?;
|
let user_id: Uuid = claims.sub;
|
||||||
|
|
||||||
// Intento de actualización optimista
|
// Intento de actualización optimista
|
||||||
let rows_updated = sqlx::query(
|
let rows_updated = sqlx::query(
|
||||||
|
|||||||
@@ -430,6 +430,13 @@ export default function LessonEditor({ params }: { params: { id: string; lessonI
|
|||||||
<Link href={`/courses/${params.id}`} className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">Outline</Link>
|
<Link href={`/courses/${params.id}`} className="text-slate-400 hover:text-blue-600 dark:hover:text-blue-400 transition-colors">Outline</Link>
|
||||||
<span className="text-slate-300 dark:text-gray-700">/</span>
|
<span className="text-slate-300 dark:text-gray-700">/</span>
|
||||||
<span className="text-blue-600 dark:text-blue-500">Activity Builder</span>
|
<span className="text-blue-600 dark:text-blue-500">Activity Builder</span>
|
||||||
|
<span className="text-slate-300 dark:text-gray-700">/</span>
|
||||||
|
<Link
|
||||||
|
href={`/courses/${params.id}/lessons/${params.lessonId}/collaborative-doc`}
|
||||||
|
className="text-slate-400 hover:text-purple-600 dark:hover:text-purple-400 transition-colors"
|
||||||
|
>
|
||||||
|
Doc. Colaborativo
|
||||||
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
{editingId === 'lesson-title' ? (
|
{editingId === 'lesson-title' ? (
|
||||||
|
|||||||
@@ -1855,7 +1855,9 @@ export const lmsApi = {
|
|||||||
apiFetch(`/courses/${courseId}/study-rooms/${roomId}`, { method: 'DELETE' }, true),
|
apiFetch(`/courses/${courseId}/study-rooms/${roomId}`, { method: 'DELETE' }, true),
|
||||||
getStudyRoomRecordings: (courseId: string, roomId: string): Promise<BbbRecording[]> =>
|
getStudyRoomRecordings: (courseId: string, roomId: string): Promise<BbbRecording[]> =>
|
||||||
apiFetch(`/courses/${courseId}/study-rooms/${roomId}/recordings`, {}, true),
|
apiFetch(`/courses/${courseId}/study-rooms/${roomId}/recordings`, {}, true),
|
||||||
};
|
updateLessonCollaborativeDoc: (lessonId: string, payload: UpdateCollaborativeDocPayload): Promise<UpdateCollaborativeDocResponse> =>
|
||||||
|
apiFetch(`/lessons/${lessonId}/collaborative-doc`, { method: 'PUT', body: JSON.stringify(payload) }, true),
|
||||||
|
};
|
||||||
|
|
||||||
export interface StudyRoom {
|
export interface StudyRoom {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user