Merge remote-tracking branch 'origin/issue/45-integrar-el-wizard-de-creacin-de-materia' into issue/42-que-tenga-persistencia-el-plan-de-estudios

This commit is contained in:
2026-02-04 07:29:35 -06:00
24 changed files with 1698 additions and 1576 deletions

View File

@@ -35,6 +35,8 @@ export const Route = createFileRoute('/planes/$planId/_detalle')({
} catch (e: any) {
// PGRST116: The result contains 0 rows
if (e?.code === 'PGRST116') {
console.log('not found on', Route.path)
throw notFound()
}
throw e

View File

@@ -10,7 +10,7 @@ import {
} from 'lucide-react'
import { useState, useMemo } from 'react'
import type { Asignatura } from '@/types/plan'
import type { Asignatura, AsignaturaStatus, TipoAsignatura } from '@/types/plan'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
@@ -33,17 +33,24 @@ import {
import { usePlanAsignaturas, usePlanLineas } from '@/data'
// --- Configuración de Estilos ---
const statusConfig: Record<string, { label: string; className: string }> = {
const statusConfig: Record<
AsignaturaStatus,
{ label: string; className: string }
> = {
borrador: { label: 'Borrador', className: 'bg-slate-100 text-slate-600' },
revisada: { label: 'Revisada', className: 'bg-amber-100 text-amber-700' },
aprobada: { label: 'Aprobada', className: 'bg-emerald-100 text-emerald-700' },
}
const tipoConfig: Record<string, { label: string; className: string }> = {
obligatoria: { label: 'Obligatoria', className: 'bg-blue-100 text-blue-700' },
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
}
const tipoConfig: Record<TipoAsignatura, { label: string; className: string }> =
{
obligatoria: {
label: 'Obligatoria',
className: 'bg-blue-100 text-blue-700',
},
optativa: { label: 'Optativa', className: 'bg-purple-100 text-purple-700' },
troncal: { label: 'Troncal', className: 'bg-slate-100 text-slate-700' },
}
// --- Mapeadores de API ---
const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
@@ -59,10 +66,13 @@ const mapAsignaturas = (asigApi: Array<any> = []): Array<Asignatura> => {
estado: 'borrador', // O el campo que venga de tu API
hd: Math.floor((asig.horas_semana ?? 0) / 2),
hi: Math.ceil((asig.horas_semana ?? 0) / 2),
prerrequisitos: Array.isArray(asig.prerrequisitos)
? asig.prerrequisitos
: [],
}))
}
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas')({
export const Route = createFileRoute('/planes/$planId/_detalle/asignaturas/')({
component: AsignaturasPage,
})
@@ -131,7 +141,17 @@ function AsignaturasPage() {
<Button variant="outline" size="sm">
<Copy className="mr-2 h-4 w-4" /> Clonar
</Button>
<Button className="bg-emerald-700 hover:bg-emerald-800">
<Button
onClick={() => {
console.log('planId desde asignaturas', planId)
navigate({
to: `/planes/${planId}/asignaturas/nueva`,
resetScroll: false,
})
}}
className="ring-offset-background bg-primary text-primary-foreground hover:bg-primary/90 inline-flex h-11 items-center justify-center gap-2 rounded-md px-8 text-sm font-medium shadow-md transition-colors"
>
<Plus className="mr-2 h-4 w-4" /> Nueva Asignatura
</Button>
</div>
@@ -262,17 +282,17 @@ function AsignaturasPage() {
<TableCell>
<Badge
variant="outline"
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo]?.className}`}
className={`capitalize shadow-sm ${tipoConfig[asignatura.tipo].className}`}
>
{tipoConfig[asignatura.tipo]?.label}
{tipoConfig[asignatura.tipo].label}
</Badge>
</TableCell>
<TableCell>
<Badge
variant="outline"
className={`capitalize shadow-sm ${statusConfig[asignatura.estado]?.className}`}
className={`capitalize shadow-sm ${statusConfig[asignatura.estado].className}`}
>
{statusConfig[asignatura.estado]?.label}
{statusConfig[asignatura.estado].label}
</Badge>
</TableCell>
<TableCell>

View File

@@ -3,12 +3,13 @@ import { createFileRoute } from '@tanstack/react-router'
import { NuevaAsignaturaModalContainer } from '@/features/asignaturas/nueva/NuevaAsignaturaModalContainer'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/_lista/nueva',
'/planes/$planId/_detalle/asignaturas/nueva',
)({
component: NuevaAsignaturaModal,
})
function NuevaAsignaturaModal() {
const { planId } = Route.useParams()
console.log('planId desde nueva', planId)
return <NuevaAsignaturaModalContainer planId={planId} />
}

View File

@@ -0,0 +1,44 @@
import { createFileRoute, notFound } from '@tanstack/react-router'
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
import { NotFoundPage } from '@/components/ui/NotFoundPage'
import { subjects_get } from '@/data/api/subjects.api'
import { qk } from '@/data/query/keys'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
)({
loader: async ({ context: { queryClient }, params: { asignaturaId } }) => {
try {
await queryClient.ensureQueryData({
queryKey: qk.asignatura(asignaturaId),
queryFn: () => subjects_get(asignaturaId),
})
} catch (e: any) {
// PGRST116: The result contains 0 rows (Supabase Single response error)
if (e?.code === 'PGRST116') {
throw notFound()
}
throw e
}
},
notFoundComponent: () => {
return (
<NotFoundPage
title="Materia no encontrada"
message="La asignatura que buscas no existe o fue eliminada."
/>
)
},
component: RouteComponent,
})
function RouteComponent() {
// const { planId, asignaturaId } = Route.useParams()
return (
<div>
<AsignaturaDetailPage></AsignaturaDetailPage>
</div>
)
}

View File

@@ -1,18 +0,0 @@
import AsignaturaDetailPage from '@/components/asignaturas/detalle/AsignaturaDetailPage'
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute(
'/planes/$planId/asignaturas/$asignaturaId',
)({
component: RouteComponent,
})
function RouteComponent() {
//const { planId, asignaturaId } = Route.useParams()
return (
<div>
<AsignaturaDetailPage></AsignaturaDetailPage>
</div>
)
}

View File

@@ -1,16 +0,0 @@
import { createFileRoute, Outlet } from '@tanstack/react-router'
export const Route = createFileRoute('/planes/$planId/asignaturas/_lista')({
component: RouteComponent,
})
function RouteComponent() {
return (
<main className="bg-background min-h-screen w-full">
<div className="mx-auto flex w-full max-w-7xl flex-col gap-4 px-4 py-6 md:px-6 lg:px-8">
<h1 className="text-foreground text-2xl font-semibold">Asignaturas</h1>
<Outlet />
</div>
</main>
)
}

View File

@@ -1,34 +0,0 @@
import { createFileRoute, Outlet, notFound } from '@tanstack/react-router'
import { NotFoundPage } from '@/components/ui/NotFoundPage'
import { plans_get } from '@/data/api/plans.api'
import { qk } from '@/data/query/keys'
export const Route = createFileRoute('/planes/$planId/asignaturas')({
loader: async ({ context: { queryClient }, params: { planId } }) => {
try {
await queryClient.ensureQueryData({
queryKey: qk.plan(planId),
queryFn: () => plans_get(planId),
})
} catch (e: any) {
if (e?.code === 'PGRST116') {
throw notFound()
}
throw e
}
},
notFoundComponent: () => {
return (
<NotFoundPage
title="Plan de Estudios no encontrado"
message="El plan de estudios que intentas consultar no existe o no tienes permisos para verlo."
/>
)
},
component: AsignaturasLayout,
})
function AsignaturasLayout() {
return <Outlet />
}