diff --git a/clear_session.sh b/clear_session.sh old mode 100644 new mode 100755 index c721410..ad8bc98 --- a/clear_session.sh +++ b/clear_session.sh @@ -11,4 +11,4 @@ echo "location.reload();" echo "" echo "Luego vuelve a hacer login con:" echo " Email: juan.allende@gmail.com" -echo " Password: password123" +echo " Password: apoca11" diff --git a/docker-compose.yml b/docker-compose.yml index eef5966..b4209d1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -23,7 +23,7 @@ services: DATABASE_URL: postgresql://user:password@db:5432/openccb_cms JWT_SECRET: ${JWT_SECRET:-openccb_secret_key_2025_production} NEXT_PUBLIC_CMS_API_URL: ${NEXT_PUBLIC_CMS_API_URL:-http://localhost:3001} - RUST_LOG: info + RUST_LOG: debug LMS_INTERNAL_URL: http://experience:3002 volumes: - uploads_data:/app/uploads @@ -46,7 +46,7 @@ services: environment: DATABASE_URL: postgresql://user:password@db:5432/openccb_lms JWT_SECRET: ${JWT_SECRET:-openccb_secret_key_2025_production} - RUST_LOG: info + RUST_LOG: debug NEXT_PUBLIC_LMS_API_URL: ${NEXT_PUBLIC_LMS_API_URL:-http://localhost:3002} NEXT_PUBLIC_CMS_API_URL: ${NEXT_PUBLIC_CMS_API_URL:-http://localhost:3001} env_file: .env diff --git a/scripts/reset_db.sh b/scripts/reset_db.sh new file mode 100755 index 0000000..1f3e9c0 --- /dev/null +++ b/scripts/reset_db.sh @@ -0,0 +1,23 @@ +#!/bin/bash + +# Script para borrar y resetear las bases de datos de OpenCCB +# Uso: ./scripts/reset_db.sh + +set -e + +echo "🛑 Deteniendo servicios..." +docker compose stop studio experience + +echo "🧹 Borrando bases de datos..." +docker exec openccb-db-1 psql -U user -d openccb -c "DROP DATABASE IF EXISTS openccb_cms;" +docker exec openccb-db-1 psql -U user -d openccb -c "DROP DATABASE IF EXISTS openccb_lms;" + +echo "🏗️ Recreando bases de datos..." +docker exec openccb-db-1 psql -U user -d openccb -c "CREATE DATABASE openccb_cms;" +docker exec openccb-db-1 psql -U user -d openccb -c "CREATE DATABASE openccb_lms;" + +echo "🚀 Reiniciando servicios y aplicando migraciones..." +docker compose start studio experience + +echo "✅ Base de datos reseteada exitosamente." +echo "Nota: Las migraciones se ejecutarán automáticamente al iniciar los servicios." diff --git a/services/lms-service/migrations/20260216000001_student_notes.sql b/services/lms-service/migrations/20260216000001_student_notes.sql new file mode 100644 index 0000000..a68cf5c --- /dev/null +++ b/services/lms-service/migrations/20260216000001_student_notes.sql @@ -0,0 +1,17 @@ +-- Migration: Create Student Notes Table +-- Description: Allows students to save personal notes for each lesson. + +CREATE TABLE IF NOT EXISTS student_notes ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE, + lesson_id UUID NOT NULL REFERENCES lessons(id) ON DELETE CASCADE, + content TEXT NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE(user_id, lesson_id) +); + +-- Index for faster retrieval by user +CREATE INDEX IF NOT EXISTS idx_student_notes_user_id ON student_notes(user_id); +-- Index for faster retrieval by lesson (useful if we ever want to see all notes for a lesson as an instructor, though not requested yet) +CREATE INDEX IF NOT EXISTS idx_student_notes_lesson_id ON student_notes(lesson_id); diff --git a/services/lms-service/src/handlers_notes.rs b/services/lms-service/src/handlers_notes.rs new file mode 100644 index 0000000..7ead00f --- /dev/null +++ b/services/lms-service/src/handlers_notes.rs @@ -0,0 +1,53 @@ +use axum::{ + Json, + extract::{Path, State}, + http::StatusCode, +}; +use common::auth::Claims; +use common::models::{SaveNotePayload, StudentNote}; +use sqlx::PgPool; +use uuid::Uuid; + +pub async fn get_note( + claims: Claims, + Path(lesson_id): Path, + State(pool): State, +) -> Result>, (StatusCode, String)> { + let note = sqlx::query_as::<_, StudentNote>( + "SELECT * FROM student_notes WHERE user_id = $1 AND lesson_id = $2", + ) + .bind(claims.sub) + .bind(lesson_id) + .fetch_optional(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json(note)) +} + +pub async fn save_note( + claims: Claims, + Path(lesson_id): Path, + State(pool): State, + Json(payload): Json, +) -> Result, (StatusCode, String)> { + let note = sqlx::query_as::<_, StudentNote>( + r#" + INSERT INTO student_notes (user_id, lesson_id, content) + VALUES ($1, $2, $3) + ON CONFLICT (user_id, lesson_id) + DO UPDATE SET + content = EXCLUDED.content, + updated_at = NOW() + RETURNING * + "#, + ) + .bind(claims.sub) + .bind(lesson_id) + .bind(payload.content) + .fetch_one(&pool) + .await + .map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, e.to_string()))?; + + Ok(Json(note)) +} diff --git a/services/lms-service/src/main.rs b/services/lms-service/src/main.rs index a10e0c3..efabe48 100644 --- a/services/lms-service/src/main.rs +++ b/services/lms-service/src/main.rs @@ -2,6 +2,7 @@ mod db_util; mod handlers; mod handlers_announcements; mod handlers_discussions; +mod handlers_notes; mod handlers_payments; use axum::{ @@ -147,6 +148,8 @@ async fn main() { "/announcements/{id}", delete(handlers_announcements::delete_announcement), ) + .route("/lessons/{id}/notes", get(handlers_notes::get_note)) + .route("/lessons/{id}/notes", put(handlers_notes::save_note)) .route_layer(middleware::from_fn( common::middleware::org_extractor_middleware, )); diff --git a/shared/common/src/models.rs b/shared/common/src/models.rs index 11eca1d..6dcedcb 100644 --- a/shared/common/src/models.rs +++ b/shared/common/src/models.rs @@ -435,6 +435,22 @@ pub struct AnnouncementWithAuthor { pub author_avatar: Option, } +// Student Notes +#[derive(Debug, Serialize, Deserialize, sqlx::FromRow, Clone)] +pub struct StudentNote { + pub id: Uuid, + pub user_id: Uuid, + pub lesson_id: Uuid, + pub content: String, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +#[derive(Debug, Deserialize)] +pub struct SaveNotePayload { + pub content: String, +} + #[cfg(test)] mod tests { use super::*; diff --git a/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx b/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx index c43ed84..41ae383 100644 --- a/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx +++ b/web/experience/src/app/courses/[id]/lessons/[lessonId]/page.tsx @@ -19,9 +19,9 @@ import MemoryPlayer from "@/components/blocks/MemoryPlayer"; import DocumentPlayer from "@/components/blocks/DocumentPlayer"; import AudioResponsePlayer from "@/components/blocks/AudioResponsePlayer"; import InteractiveTranscript from "@/components/InteractiveTranscript"; -import AITutor from "@/components/AITutor"; import LessonLockedView from "@/components/LessonLockedView"; -import { ListMusic } from "lucide-react"; +import StudentNotes from "@/components/StudentNotes"; +import { ListMusic, StickyNote } from "lucide-react"; export default function LessonPlayerPage({ params }: { params: { id: string, lessonId: string } }) { const [lesson, setLesson] = useState(null); @@ -29,6 +29,7 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les const [loading, setLoading] = useState(true); const [sidebarOpen, setSidebarOpen] = useState(true); const [transcriptOpen, setTranscriptOpen] = useState(true); + const [notesOpen, setNotesOpen] = useState(false); const [currentTime, setCurrentTime] = useState(0); const [userGrade, setUserGrade] = useState(null); const [allGrades, setAllGrades] = useState([]); @@ -214,13 +215,26 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les {hasTranscription && ( )} +
@@ -458,14 +472,43 @@ export default function LessonPlayerPage({ params }: { params: { id: string, les
- {/* Interactive Transcript Panel */} - {hasTranscription && transcriptOpen && ( -