Se añadió el botón de eliminar asignatura, y se borra adecuadamente

This commit is contained in:
2025-10-21 17:05:45 -06:00
parent ce2cd6b397
commit d491100c73
5 changed files with 62 additions and 13 deletions

View File

@@ -114,7 +114,7 @@ export function DetailDialog({
return ( return (
<Dialog open={open} onOpenChange={(o) => !o && onClose()}> <Dialog open={open} onOpenChange={(o) => !o && onClose()}>
<DialogContent className="max-w-3xl"> <DialogContent className="max-w-fit">
<DialogHeader> <DialogHeader>
<DialogTitle className="font-mono">{row?.titulo_archivo ?? "(Sin título)"}</DialogTitle> <DialogTitle className="font-mono">{row?.titulo_archivo ?? "(Sin título)"}</DialogTitle>
<DialogDescription>{row?.descripcion || "Sin descripción"}</DialogDescription> <DialogDescription>{row?.descripcion || "Sin descripción"}</DialogDescription>
@@ -134,13 +134,13 @@ export function DetailDialog({
</span> </span>
)} )}
</div> </div>
{/* Tags
{row.tags?.length ? ( {row.tags?.length ? (
<div className="text-xs text-neutral-600"> <div className="text-xs text-neutral-600">
<span className="font-medium">Tags: </span> <span className="font-medium">Tags: </span>
{row.tags.join(", ")} {row.tags.join(", ")}
</div> </div>
) : null} ) : null} */}
<div> <div>
<Label className="text-xs text-neutral-600">Instrucciones</Label> <Label className="text-xs text-neutral-600">Instrucciones</Label>

View File

@@ -312,6 +312,7 @@ export function CreatePlanDialog({ open, onOpenChange }: { open: boolean; onOpen
) : ( ) : (
<p className="text-xs text-neutral-500">Fecha desconocida</p> <p className="text-xs text-neutral-500">Fecha desconocida</p>
)} )}
{file.tags && file.tags.length > 0 && ( {file.tags && file.tags.length > 0 && (
<div className="mt-2 flex flex-wrap gap-1.5"> <div className="mt-2 flex flex-wrap gap-1.5">
{file.tags.map((tag, i) => ( {file.tags.map((tag, i) => (

View File

@@ -167,6 +167,7 @@ function RouteComponent() {
<p className="text-sm text-neutral-700 line-clamp-3">{r.descripcion}</p> <p className="text-sm text-neutral-700 line-clamp-3">{r.descripcion}</p>
)} )}
{/* Tags
{r.tags && r.tags.length > 0 && ( {r.tags && r.tags.length > 0 && (
<div className="flex flex-wrap gap-1.5"> <div className="flex flex-wrap gap-1.5">
{r.tags.map((t, i) => ( {r.tags.map((t, i) => (
@@ -175,7 +176,7 @@ function RouteComponent() {
</span> </span>
))} ))}
</div> </div>
)} )} */}
<div className="mt-auto flex items-center justify-between gap-2"> <div className="mt-auto flex items-center justify-between gap-2">
<Button variant="ghost" size="sm" onClick={() => setViewing(r)}> <Button variant="ghost" size="sm" onClick={() => setViewing(r)}>

View File

@@ -1,4 +1,5 @@
// routes/_authenticated/asignatura/$asignaturaId.tsx // routes/_authenticated/asignatura/$asignaturaId.tsx
import { useQueryClient } from "@tanstack/react-query";
import { createFileRoute, Link, useRouter } from "@tanstack/react-router" import { createFileRoute, Link, useRouter } from "@tanstack/react-router"
import * as Icons from "lucide-react" import * as Icons from "lucide-react"
import { useCallback, useEffect, useMemo, useRef, useState } from "react" import { useCallback, useEffect, useMemo, useRef, useState } from "react"
@@ -165,6 +166,7 @@ function Page() {
</Button> </Button>
<EditAsignaturaButton asignatura={a} onUpdate={setA} /> <EditAsignaturaButton asignatura={a} onUpdate={setA} />
<MejorarAIButton asignaturaId={a.id} onApply={(nuevo) => setA(nuevo)} /> <MejorarAIButton asignaturaId={a.id} onApply={(nuevo) => setA(nuevo)} />
<BorrarAsignaturaButton asignatura_id={a.id} />
</div> </div>
</div> </div>
@@ -191,7 +193,7 @@ function Page() {
)} )}
{/* Syllabus */} {/* Syllabus */}
{unidades.length > 0 && (
<Section id="syllabus" title="Programa / Contenidos" icon={Icons.ListTree}> <Section id="syllabus" title="Programa / Contenidos" icon={Icons.ListTree}>
<div className="flex items-center gap-2 mb-2"> <div className="flex items-center gap-2 mb-2">
<div className="relative flex-1"> <div className="relative flex-1">
@@ -285,7 +287,7 @@ function Page() {
) )
})()} })()}
</Section> </Section>
)}
{/* Bibliografía */} {/* Bibliografía */}
@@ -578,6 +580,51 @@ function MejorarAIButton({ asignaturaId, onApply }: {
) )
} }
function BorrarAsignaturaButton({ asignatura_id, onDeleted }: { asignatura_id: string; onDeleted?: () => void }) {
const [confirm, setConfirm] = useState(false)
const [loading, setLoading] = useState(false)
const router = useRouter()
const queryClient = useQueryClient()
async function handleDelete() {
setLoading(true)
try {
const { error, status, statusText } = await supabase.from("asignaturas").delete().eq("id", asignatura_id)
console.log({ status, statusText });
if (error) throw error
setConfirm(false)
queryClient.invalidateQueries({ queryKey: ["asignaturas"] })
if (onDeleted) onDeleted()
router.navigate({ to: "/asignaturas", search: {
q: "", // Término de búsqueda vacío
planId: "", // ID del plan (vacío si no aplica)
carreraId: "", // ID de la carrera (vacío si no aplica)
facultadId: "", // ID de la facultad (vacío si no aplica)
f: "", // Filtro vacío
}})
} catch (e: any) {
alert(e?.message || "Error al eliminar la asignatura")
} finally {
setLoading(false)
}
}
return confirm ? (
<div className="flex gap-2">
<Button variant="destructive" onClick={handleDelete} disabled={loading}>
{loading ? "Eliminando…" : "Confirmar eliminación"}
</Button>
<Button variant="outline" onClick={() => setConfirm(false)} disabled={loading}>Cancelar</Button>
</div>
) : (
<Button variant="outline" onClick={() => setConfirm(true)}>
Eliminar asignatura
</Button>
)
}
function Field({ label, children }: { label: string; children: React.ReactNode }) { function Field({ label, children }: { label: string; children: React.ReactNode }) {
return ( return (
<div className="space-y-1"> <div className="space-y-1">
@@ -632,9 +679,9 @@ export function EditContenidosButton({
} }
return { title, temas } return { title, temas }
}) })
return entries.length ? entries : [{ title: "Unidad 1", temas: [] }] return entries.length ? entries : [{ title: "", temas: [] }]
} catch { } catch {
return [{ title: "Unidad 1", temas: [] }] return [{ title: "", temas: [] }]
} }
}, []) }, [])
@@ -650,7 +697,7 @@ export function EditContenidosButton({
.forEach((t, i) => { .forEach((t, i) => {
sub[String(i + 1)] = t sub[String(i + 1)] = t
}) })
out[k] = { titulo: (u.title || "").trim() || `Unidad ${k}`, subtemas: sub } out[k] = { titulo: (u.title || "").trim(), subtemas: sub }
}) })
return out return out
}, []) }, [])
@@ -669,7 +716,7 @@ export function EditContenidosButton({
return true return true
}) })
return { return {
title: (u.title || "").trim() || `Unidad ${idx + 1}`, title: (u.title || "").trim(),
temas, temas,
} }
}) })
@@ -839,7 +886,7 @@ export function EditContenidosButton({
<Button <Button
variant="secondary" variant="secondary"
onClick={() => onClick={() =>
setUnits((prev) => [...prev, { title: `Unidad ${prev.length + 1}`, temas: [] }]) setUnits((prev) => [...prev, { title: "", temas: [] }])
} }
> >
<Icons.Plus className="w-4 h-4 mr-2" /> Agregar unidad <Icons.Plus className="w-4 h-4 mr-2" /> Agregar unidad
@@ -850,7 +897,7 @@ export function EditContenidosButton({
<DialogFooter className="px-6 pb-5"> <DialogFooter className="px-6 pb-5">
<Button variant="outline" onClick={cancel}>Cancelar</Button> <Button variant="outline" onClick={cancel}>Cancelar</Button>
<Button onClick={save} disabled={saving}> <Button onClick={save} disabled={saving || !hasChanges || units.some(u => !u.title.trim())}>
{saving ? ( {saving ? (
<span className="inline-flex items-center gap-2"> <span className="inline-flex items-center gap-2">
<Icons.Loader2 className="h-4 w-4 animate-spin" /> Guardando <Icons.Loader2 className="h-4 w-4 animate-spin" /> Guardando

View File

@@ -115,7 +115,7 @@ function RouteComponent() {
id: role.id, id: role.id,
label: role.label, label: role.label,
Icon: (Icons as any)[role.icono] || Icons.Cpu, // Icono por defecto si no está definido Icon: (Icons as any)[role.icono] || Icons.Cpu, // Icono por defecto si no está definido
className: role.nombre_clase || "bg-gray-500 text-white", // Clase por defecto si no está definida className: /* role.nombre_clase || */ "bg-gray-500 text-white", // Clase por defecto si no está definida
}; };
return acc; return acc;
}, {} as Record<string, { id: string; label: string; Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>; className: string }>); }, {} as Record<string, { id: string; label: string; Icon: React.ComponentType<React.SVGProps<SVGSVGElement>>; className: string }>);