diff --git a/backend/app/main.py b/backend/app/main.py index 86ab314..3431b3f 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -8,7 +8,7 @@ from fastapi.responses import FileResponse, PlainTextResponse from backend.app.config import settings from backend.app.managers.knowledge_manager import DuplicateSourceError, KnowledgeManager -from backend.app.models.schemas import MarkdownUpdate, SubjectCreate, WeekCreate, WeekUpdate +from backend.app.models.schemas import MarkdownUpdate, SubjectCreate, SubjectUpdate, WeekCreate, WeekUpdate from backend.app.services.ingestion_service import IngestionService app = FastAPI(title="Knowledge Station", version="0.1.0") @@ -60,6 +60,14 @@ def get_subject(subject_id: int) -> dict: raise HTTPException(status_code=404, detail=str(exc)) from exc +@app.put("/subjects/{subject_id}") +def update_subject(subject_id: int, payload: SubjectUpdate) -> dict: + try: + return manager.update_subject(subject_id, payload.name) + except ValueError as exc: + raise HTTPException(status_code=400, detail=str(exc)) from exc + + @app.delete("/subjects/{subject_id}") def delete_subject(subject_id: int) -> dict: try: diff --git a/backend/app/managers/knowledge_manager.py b/backend/app/managers/knowledge_manager.py index 5813122..ab83253 100644 --- a/backend/app/managers/knowledge_manager.py +++ b/backend/app/managers/knowledge_manager.py @@ -154,6 +154,14 @@ class KnowledgeManager: raise ValueError("Materia no encontrada") return dict(row) + def update_subject(self, subject_id: int, name: str) -> dict[str, Any]: + if not name.strip(): + raise ValueError("El nombre de la materia no puede estar vacio") + self.get_subject(subject_id) + with self._connect() as conn: + conn.execute("update subjects set name = ? where id = ?", (name.strip(), subject_id)) + return self.get_subject(subject_id) + def delete_subject(self, subject_id: int) -> None: subject = self.get_subject(subject_id) with self._connect() as conn: @@ -224,6 +232,8 @@ class KnowledgeManager: def update_week(self, subject_id: int, current_number: int, new_number: int, title: str | None = None) -> dict[str, Any]: subject = self.get_subject(subject_id) week = self.get_week(subject_id, current_number) + old_dir: Path | None = None + new_dir: Path | None = None if new_number != current_number: with self._connect() as conn: existing = conn.execute( @@ -248,6 +258,22 @@ class KnowledgeManager: "update weeks set number = ?, title = ? where id = ?", (new_number, title, week["id"]), ) + if old_dir and new_dir: + old_prefix = str(old_dir) + new_prefix = str(new_dir) + conn.execute( + "update sources set stored_path = replace(stored_path, ?, ?) where week_id = ? and stored_path like ?", + (old_prefix, new_prefix, week["id"], f"{old_prefix}%"), + ) + conn.execute( + """ + update documents + set markdown_path = replace(markdown_path, ?, ?) + where source_id in (select id from sources where week_id = ?) + and markdown_path like ? + """, + (old_prefix, new_prefix, week["id"], f"{old_prefix}%"), + ) return self.get_week(subject_id, new_number) def week_dir(self, subject_slug: str, week_number: int) -> Path: diff --git a/backend/app/models/schemas.py b/backend/app/models/schemas.py index 61a88bb..86f5c11 100644 --- a/backend/app/models/schemas.py +++ b/backend/app/models/schemas.py @@ -8,6 +8,10 @@ class SubjectCreate(BaseModel): name: str = Field(min_length=1) +class SubjectUpdate(BaseModel): + name: str = Field(min_length=1) + + class Subject(BaseModel): id: int name: str diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx index 66d40e9..9400cc7 100644 --- a/frontend/app/page.tsx +++ b/frontend/app/page.tsx @@ -10,6 +10,8 @@ export default function DashboardPage() { const [subjects, setSubjects] = useState([]); const [settings, setSettings] = useState(null); const [name, setName] = useState(""); + const [editingSubject, setEditingSubject] = useState(null); + const [editName, setEditName] = useState(""); const [loading, setLoading] = useState(true); const [error, setError] = useState(""); @@ -31,9 +33,47 @@ export default function DashboardPage() { async function createSubject(event: FormEvent) { event.preventDefault(); if (!name.trim()) return; - await api.createSubject(name.trim()); - setName(""); - await load(); + try { + setError(""); + await api.createSubject(name.trim()); + setName(""); + await load(); + } catch (err) { + setError(err instanceof Error ? err.message : "No se pudo crear la materia"); + } + } + + function startEdit(subject: Subject) { + setEditingSubject(subject); + setEditName(subject.name); + setError(""); + } + + async function saveEdit(event: FormEvent) { + event.preventDefault(); + if (!editingSubject || !editName.trim()) return; + try { + setError(""); + await api.updateSubject(editingSubject.id, editName.trim()); + setEditingSubject(null); + setEditName(""); + await load(); + } catch (err) { + setError(err instanceof Error ? err.message : "No se pudo editar la materia"); + } + } + + async function deleteSubject(subject: Subject) { + const confirmed = window.confirm(`Eliminar la materia "${subject.name}" tambien borrara todas sus semanas, archivos subidos, Markdown procesado y exports. Esta accion no se puede deshacer.`); + if (!confirmed) return; + try { + setError(""); + await api.deleteSubject(subject.id); + if (editingSubject?.id === subject.id) setEditingSubject(null); + await load(); + } catch (err) { + setError(err instanceof Error ? err.message : "No se pudo eliminar la materia"); + } } return ( @@ -50,13 +90,15 @@ export default function DashboardPage() { {loading ? Cargando... : (
{subjects.map((subject) => ( - - -
{subject.name}
-
{subject.slug}
-
Abrir materia
-
- + +
{subject.name}
+
{subject.slug}
+
+ Abrir materia + + +
+
))} {subjects.length === 0 && No hay materias todavia. Crea la primera para empezar.}
@@ -64,6 +106,21 @@ export default function DashboardPage() {