feat: implement asignaturas management with dynamic routing and UI updates
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import { StrictMode } from 'react'
|
import { StrictMode } from 'react'
|
||||||
import ReactDOM from 'react-dom/client'
|
import ReactDOM from 'react-dom/client'
|
||||||
import { RouterProvider, createRouter } from '@tanstack/react-router'
|
import { RouterProvider, createRouteMask, createRouter } from '@tanstack/react-router'
|
||||||
|
|
||||||
// Import the generated route tree
|
// Import the generated route tree
|
||||||
import { routeTree } from './routeTree.gen'
|
import { routeTree } from './routeTree.gen'
|
||||||
@@ -15,7 +15,7 @@ const router = createRouter({
|
|||||||
scrollRestoration: true,
|
scrollRestoration: true,
|
||||||
defaultStructuralSharing: true,
|
defaultStructuralSharing: true,
|
||||||
defaultPreloadStaleTime: 0,
|
defaultPreloadStaleTime: 0,
|
||||||
context:{
|
context: {
|
||||||
auth: undefined!,
|
auth: undefined!,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -16,10 +16,9 @@ import { Route as AuthenticatedUsuariosRouteImport } from './routes/_authenticat
|
|||||||
import { Route as AuthenticatedPlanesRouteImport } from './routes/_authenticated/planes'
|
import { Route as AuthenticatedPlanesRouteImport } from './routes/_authenticated/planes'
|
||||||
import { Route as AuthenticatedFacultadesRouteImport } from './routes/_authenticated/facultades'
|
import { Route as AuthenticatedFacultadesRouteImport } from './routes/_authenticated/facultades'
|
||||||
import { Route as AuthenticatedDashboardRouteImport } from './routes/_authenticated/dashboard'
|
import { Route as AuthenticatedDashboardRouteImport } from './routes/_authenticated/dashboard'
|
||||||
import { Route as AuthenticatedAsignaturasRouteImport } from './routes/_authenticated/asignaturas'
|
|
||||||
import { Route as AuthenticatedPlanPlanIdRouteImport } from './routes/_authenticated/plan/$planId'
|
import { Route as AuthenticatedPlanPlanIdRouteImport } from './routes/_authenticated/plan/$planId'
|
||||||
import { Route as AuthenticatedFacultadFacultadIdRouteImport } from './routes/_authenticated/facultad/$facultadId'
|
import { Route as AuthenticatedFacultadFacultadIdRouteImport } from './routes/_authenticated/facultad/$facultadId'
|
||||||
import { Route as AuthenticatedPlanPlanIdModalRouteImport } from './routes/_authenticated/plan/$planId/modal'
|
import { Route as AuthenticatedAsignaturasPlanIdRouteImport } from './routes/_authenticated/asignaturas/$planId'
|
||||||
|
|
||||||
const LoginRoute = LoginRouteImport.update({
|
const LoginRoute = LoginRouteImport.update({
|
||||||
id: '/login',
|
id: '/login',
|
||||||
@@ -55,12 +54,6 @@ const AuthenticatedDashboardRoute = AuthenticatedDashboardRouteImport.update({
|
|||||||
path: '/dashboard',
|
path: '/dashboard',
|
||||||
getParentRoute: () => AuthenticatedRoute,
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthenticatedAsignaturasRoute =
|
|
||||||
AuthenticatedAsignaturasRouteImport.update({
|
|
||||||
id: '/asignaturas',
|
|
||||||
path: '/asignaturas',
|
|
||||||
getParentRoute: () => AuthenticatedRoute,
|
|
||||||
} as any)
|
|
||||||
const AuthenticatedPlanPlanIdRoute = AuthenticatedPlanPlanIdRouteImport.update({
|
const AuthenticatedPlanPlanIdRoute = AuthenticatedPlanPlanIdRouteImport.update({
|
||||||
id: '/plan/$planId',
|
id: '/plan/$planId',
|
||||||
path: '/plan/$planId',
|
path: '/plan/$planId',
|
||||||
@@ -72,89 +65,83 @@ const AuthenticatedFacultadFacultadIdRoute =
|
|||||||
path: '/facultad/$facultadId',
|
path: '/facultad/$facultadId',
|
||||||
getParentRoute: () => AuthenticatedRoute,
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
const AuthenticatedPlanPlanIdModalRoute =
|
const AuthenticatedAsignaturasPlanIdRoute =
|
||||||
AuthenticatedPlanPlanIdModalRouteImport.update({
|
AuthenticatedAsignaturasPlanIdRouteImport.update({
|
||||||
id: '/modal',
|
id: '/asignaturas/$planId',
|
||||||
path: '/modal',
|
path: '/asignaturas/$planId',
|
||||||
getParentRoute: () => AuthenticatedPlanPlanIdRoute,
|
getParentRoute: () => AuthenticatedRoute,
|
||||||
} as any)
|
} as any)
|
||||||
|
|
||||||
export interface FileRoutesByFullPath {
|
export interface FileRoutesByFullPath {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/asignaturas': typeof AuthenticatedAsignaturasRoute
|
|
||||||
'/dashboard': typeof AuthenticatedDashboardRoute
|
'/dashboard': typeof AuthenticatedDashboardRoute
|
||||||
'/facultades': typeof AuthenticatedFacultadesRoute
|
'/facultades': typeof AuthenticatedFacultadesRoute
|
||||||
'/planes': typeof AuthenticatedPlanesRoute
|
'/planes': typeof AuthenticatedPlanesRoute
|
||||||
'/usuarios': typeof AuthenticatedUsuariosRoute
|
'/usuarios': typeof AuthenticatedUsuariosRoute
|
||||||
|
'/asignaturas/$planId': typeof AuthenticatedAsignaturasPlanIdRoute
|
||||||
'/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
|
'/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
|
||||||
'/plan/$planId': typeof AuthenticatedPlanPlanIdRouteWithChildren
|
'/plan/$planId': typeof AuthenticatedPlanPlanIdRoute
|
||||||
'/plan/$planId/modal': typeof AuthenticatedPlanPlanIdModalRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesByTo {
|
export interface FileRoutesByTo {
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/asignaturas': typeof AuthenticatedAsignaturasRoute
|
|
||||||
'/dashboard': typeof AuthenticatedDashboardRoute
|
'/dashboard': typeof AuthenticatedDashboardRoute
|
||||||
'/facultades': typeof AuthenticatedFacultadesRoute
|
'/facultades': typeof AuthenticatedFacultadesRoute
|
||||||
'/planes': typeof AuthenticatedPlanesRoute
|
'/planes': typeof AuthenticatedPlanesRoute
|
||||||
'/usuarios': typeof AuthenticatedUsuariosRoute
|
'/usuarios': typeof AuthenticatedUsuariosRoute
|
||||||
|
'/asignaturas/$planId': typeof AuthenticatedAsignaturasPlanIdRoute
|
||||||
'/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
|
'/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
|
||||||
'/plan/$planId': typeof AuthenticatedPlanPlanIdRouteWithChildren
|
'/plan/$planId': typeof AuthenticatedPlanPlanIdRoute
|
||||||
'/plan/$planId/modal': typeof AuthenticatedPlanPlanIdModalRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRoutesById {
|
export interface FileRoutesById {
|
||||||
__root__: typeof rootRouteImport
|
__root__: typeof rootRouteImport
|
||||||
'/': typeof IndexRoute
|
'/': typeof IndexRoute
|
||||||
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
'/_authenticated': typeof AuthenticatedRouteWithChildren
|
||||||
'/login': typeof LoginRoute
|
'/login': typeof LoginRoute
|
||||||
'/_authenticated/asignaturas': typeof AuthenticatedAsignaturasRoute
|
|
||||||
'/_authenticated/dashboard': typeof AuthenticatedDashboardRoute
|
'/_authenticated/dashboard': typeof AuthenticatedDashboardRoute
|
||||||
'/_authenticated/facultades': typeof AuthenticatedFacultadesRoute
|
'/_authenticated/facultades': typeof AuthenticatedFacultadesRoute
|
||||||
'/_authenticated/planes': typeof AuthenticatedPlanesRoute
|
'/_authenticated/planes': typeof AuthenticatedPlanesRoute
|
||||||
'/_authenticated/usuarios': typeof AuthenticatedUsuariosRoute
|
'/_authenticated/usuarios': typeof AuthenticatedUsuariosRoute
|
||||||
|
'/_authenticated/asignaturas/$planId': typeof AuthenticatedAsignaturasPlanIdRoute
|
||||||
'/_authenticated/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
|
'/_authenticated/facultad/$facultadId': typeof AuthenticatedFacultadFacultadIdRoute
|
||||||
'/_authenticated/plan/$planId': typeof AuthenticatedPlanPlanIdRouteWithChildren
|
'/_authenticated/plan/$planId': typeof AuthenticatedPlanPlanIdRoute
|
||||||
'/_authenticated/plan/$planId/modal': typeof AuthenticatedPlanPlanIdModalRoute
|
|
||||||
}
|
}
|
||||||
export interface FileRouteTypes {
|
export interface FileRouteTypes {
|
||||||
fileRoutesByFullPath: FileRoutesByFullPath
|
fileRoutesByFullPath: FileRoutesByFullPath
|
||||||
fullPaths:
|
fullPaths:
|
||||||
| '/'
|
| '/'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/asignaturas'
|
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/facultades'
|
| '/facultades'
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/usuarios'
|
| '/usuarios'
|
||||||
|
| '/asignaturas/$planId'
|
||||||
| '/facultad/$facultadId'
|
| '/facultad/$facultadId'
|
||||||
| '/plan/$planId'
|
| '/plan/$planId'
|
||||||
| '/plan/$planId/modal'
|
|
||||||
fileRoutesByTo: FileRoutesByTo
|
fileRoutesByTo: FileRoutesByTo
|
||||||
to:
|
to:
|
||||||
| '/'
|
| '/'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/asignaturas'
|
|
||||||
| '/dashboard'
|
| '/dashboard'
|
||||||
| '/facultades'
|
| '/facultades'
|
||||||
| '/planes'
|
| '/planes'
|
||||||
| '/usuarios'
|
| '/usuarios'
|
||||||
|
| '/asignaturas/$planId'
|
||||||
| '/facultad/$facultadId'
|
| '/facultad/$facultadId'
|
||||||
| '/plan/$planId'
|
| '/plan/$planId'
|
||||||
| '/plan/$planId/modal'
|
|
||||||
id:
|
id:
|
||||||
| '__root__'
|
| '__root__'
|
||||||
| '/'
|
| '/'
|
||||||
| '/_authenticated'
|
| '/_authenticated'
|
||||||
| '/login'
|
| '/login'
|
||||||
| '/_authenticated/asignaturas'
|
|
||||||
| '/_authenticated/dashboard'
|
| '/_authenticated/dashboard'
|
||||||
| '/_authenticated/facultades'
|
| '/_authenticated/facultades'
|
||||||
| '/_authenticated/planes'
|
| '/_authenticated/planes'
|
||||||
| '/_authenticated/usuarios'
|
| '/_authenticated/usuarios'
|
||||||
|
| '/_authenticated/asignaturas/$planId'
|
||||||
| '/_authenticated/facultad/$facultadId'
|
| '/_authenticated/facultad/$facultadId'
|
||||||
| '/_authenticated/plan/$planId'
|
| '/_authenticated/plan/$planId'
|
||||||
| '/_authenticated/plan/$planId/modal'
|
|
||||||
fileRoutesById: FileRoutesById
|
fileRoutesById: FileRoutesById
|
||||||
}
|
}
|
||||||
export interface RootRouteChildren {
|
export interface RootRouteChildren {
|
||||||
@@ -214,13 +201,6 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthenticatedDashboardRouteImport
|
preLoaderRoute: typeof AuthenticatedDashboardRouteImport
|
||||||
parentRoute: typeof AuthenticatedRoute
|
parentRoute: typeof AuthenticatedRoute
|
||||||
}
|
}
|
||||||
'/_authenticated/asignaturas': {
|
|
||||||
id: '/_authenticated/asignaturas'
|
|
||||||
path: '/asignaturas'
|
|
||||||
fullPath: '/asignaturas'
|
|
||||||
preLoaderRoute: typeof AuthenticatedAsignaturasRouteImport
|
|
||||||
parentRoute: typeof AuthenticatedRoute
|
|
||||||
}
|
|
||||||
'/_authenticated/plan/$planId': {
|
'/_authenticated/plan/$planId': {
|
||||||
id: '/_authenticated/plan/$planId'
|
id: '/_authenticated/plan/$planId'
|
||||||
path: '/plan/$planId'
|
path: '/plan/$planId'
|
||||||
@@ -235,48 +215,34 @@ declare module '@tanstack/react-router' {
|
|||||||
preLoaderRoute: typeof AuthenticatedFacultadFacultadIdRouteImport
|
preLoaderRoute: typeof AuthenticatedFacultadFacultadIdRouteImport
|
||||||
parentRoute: typeof AuthenticatedRoute
|
parentRoute: typeof AuthenticatedRoute
|
||||||
}
|
}
|
||||||
'/_authenticated/plan/$planId/modal': {
|
'/_authenticated/asignaturas/$planId': {
|
||||||
id: '/_authenticated/plan/$planId/modal'
|
id: '/_authenticated/asignaturas/$planId'
|
||||||
path: '/modal'
|
path: '/asignaturas/$planId'
|
||||||
fullPath: '/plan/$planId/modal'
|
fullPath: '/asignaturas/$planId'
|
||||||
preLoaderRoute: typeof AuthenticatedPlanPlanIdModalRouteImport
|
preLoaderRoute: typeof AuthenticatedAsignaturasPlanIdRouteImport
|
||||||
parentRoute: typeof AuthenticatedPlanPlanIdRoute
|
parentRoute: typeof AuthenticatedRoute
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AuthenticatedPlanPlanIdRouteChildren {
|
|
||||||
AuthenticatedPlanPlanIdModalRoute: typeof AuthenticatedPlanPlanIdModalRoute
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthenticatedPlanPlanIdRouteChildren: AuthenticatedPlanPlanIdRouteChildren =
|
|
||||||
{
|
|
||||||
AuthenticatedPlanPlanIdModalRoute: AuthenticatedPlanPlanIdModalRoute,
|
|
||||||
}
|
|
||||||
|
|
||||||
const AuthenticatedPlanPlanIdRouteWithChildren =
|
|
||||||
AuthenticatedPlanPlanIdRoute._addFileChildren(
|
|
||||||
AuthenticatedPlanPlanIdRouteChildren,
|
|
||||||
)
|
|
||||||
|
|
||||||
interface AuthenticatedRouteChildren {
|
interface AuthenticatedRouteChildren {
|
||||||
AuthenticatedAsignaturasRoute: typeof AuthenticatedAsignaturasRoute
|
|
||||||
AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRoute
|
AuthenticatedDashboardRoute: typeof AuthenticatedDashboardRoute
|
||||||
AuthenticatedFacultadesRoute: typeof AuthenticatedFacultadesRoute
|
AuthenticatedFacultadesRoute: typeof AuthenticatedFacultadesRoute
|
||||||
AuthenticatedPlanesRoute: typeof AuthenticatedPlanesRoute
|
AuthenticatedPlanesRoute: typeof AuthenticatedPlanesRoute
|
||||||
AuthenticatedUsuariosRoute: typeof AuthenticatedUsuariosRoute
|
AuthenticatedUsuariosRoute: typeof AuthenticatedUsuariosRoute
|
||||||
|
AuthenticatedAsignaturasPlanIdRoute: typeof AuthenticatedAsignaturasPlanIdRoute
|
||||||
AuthenticatedFacultadFacultadIdRoute: typeof AuthenticatedFacultadFacultadIdRoute
|
AuthenticatedFacultadFacultadIdRoute: typeof AuthenticatedFacultadFacultadIdRoute
|
||||||
AuthenticatedPlanPlanIdRoute: typeof AuthenticatedPlanPlanIdRouteWithChildren
|
AuthenticatedPlanPlanIdRoute: typeof AuthenticatedPlanPlanIdRoute
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
const AuthenticatedRouteChildren: AuthenticatedRouteChildren = {
|
||||||
AuthenticatedAsignaturasRoute: AuthenticatedAsignaturasRoute,
|
|
||||||
AuthenticatedDashboardRoute: AuthenticatedDashboardRoute,
|
AuthenticatedDashboardRoute: AuthenticatedDashboardRoute,
|
||||||
AuthenticatedFacultadesRoute: AuthenticatedFacultadesRoute,
|
AuthenticatedFacultadesRoute: AuthenticatedFacultadesRoute,
|
||||||
AuthenticatedPlanesRoute: AuthenticatedPlanesRoute,
|
AuthenticatedPlanesRoute: AuthenticatedPlanesRoute,
|
||||||
AuthenticatedUsuariosRoute: AuthenticatedUsuariosRoute,
|
AuthenticatedUsuariosRoute: AuthenticatedUsuariosRoute,
|
||||||
|
AuthenticatedAsignaturasPlanIdRoute: AuthenticatedAsignaturasPlanIdRoute,
|
||||||
AuthenticatedFacultadFacultadIdRoute: AuthenticatedFacultadFacultadIdRoute,
|
AuthenticatedFacultadFacultadIdRoute: AuthenticatedFacultadFacultadIdRoute,
|
||||||
AuthenticatedPlanPlanIdRoute: AuthenticatedPlanPlanIdRouteWithChildren,
|
AuthenticatedPlanPlanIdRoute: AuthenticatedPlanPlanIdRoute,
|
||||||
}
|
}
|
||||||
|
|
||||||
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
const AuthenticatedRouteWithChildren = AuthenticatedRoute._addFileChildren(
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import { createFileRoute } from '@tanstack/react-router'
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authenticated/asignaturas')({
|
|
||||||
component: RouteComponent,
|
|
||||||
})
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
return <div>Hello "/_authenticated/asignaturas"!</div>
|
|
||||||
}
|
|
||||||
142
src/routes/_authenticated/asignaturas/$planId.tsx
Normal file
142
src/routes/_authenticated/asignaturas/$planId.tsx
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
import { createFileRoute, useRouter } from "@tanstack/react-router"
|
||||||
|
import { supabase } from "@/auth/supabase"
|
||||||
|
import * as Icons from "lucide-react"
|
||||||
|
import { useMemo, useState } from "react"
|
||||||
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
|
import { Input } from "@/components/ui/input"
|
||||||
|
import { Badge } from "@/components/ui/badge"
|
||||||
|
|
||||||
|
type Asignatura = {
|
||||||
|
id: string
|
||||||
|
nombre: string
|
||||||
|
semestre: number | null
|
||||||
|
creditos: number | null
|
||||||
|
horas_teoricas: number | null
|
||||||
|
horas_practicas: number | null
|
||||||
|
}
|
||||||
|
|
||||||
|
type ModalData = {
|
||||||
|
planId: string
|
||||||
|
planNombre: string
|
||||||
|
asignaturas: Asignatura[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Route = createFileRoute("/_authenticated/asignaturas/$planId")({
|
||||||
|
component: ModalComponent,
|
||||||
|
loader: async ({ params }): Promise<ModalData> => {
|
||||||
|
const planId = params.planId
|
||||||
|
|
||||||
|
const { data: plan, error: planErr } = await supabase
|
||||||
|
.from("plan_estudios")
|
||||||
|
.select("id, nombre")
|
||||||
|
.eq("id", planId)
|
||||||
|
.single()
|
||||||
|
if (planErr || !plan) throw planErr ?? new Error("Plan no encontrado")
|
||||||
|
|
||||||
|
const { data: asignaturas, error: aErr } = await supabase
|
||||||
|
.from("asignaturas")
|
||||||
|
.select("id, nombre, semestre, creditos, horas_teoricas, horas_practicas")
|
||||||
|
.eq("plan_id", planId)
|
||||||
|
.order("semestre", { ascending: true })
|
||||||
|
.order("nombre", { ascending: true })
|
||||||
|
|
||||||
|
if (aErr) throw aErr
|
||||||
|
|
||||||
|
return {
|
||||||
|
planId,
|
||||||
|
planNombre: plan.nombre,
|
||||||
|
asignaturas: (asignaturas ?? []) as Asignatura[],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function ModalComponent() {
|
||||||
|
const { planId, planNombre, asignaturas } = Route.useLoaderData() as ModalData
|
||||||
|
const router = useRouter()
|
||||||
|
const [q, setQ] = useState("")
|
||||||
|
|
||||||
|
const filtered = useMemo(() => {
|
||||||
|
const t = q.trim().toLowerCase()
|
||||||
|
if (!t) return asignaturas
|
||||||
|
return asignaturas.filter(a =>
|
||||||
|
[a.nombre, a.semestre, a.creditos]
|
||||||
|
.filter(Boolean)
|
||||||
|
.some(v => String(v).toLowerCase().includes(t))
|
||||||
|
)
|
||||||
|
}, [q, asignaturas])
|
||||||
|
|
||||||
|
// Agrupar por semestre
|
||||||
|
const groups = useMemo(() => {
|
||||||
|
const m = new Map<number | string, Asignatura[]>()
|
||||||
|
for (const a of filtered) {
|
||||||
|
const k = a.semestre ?? "—"
|
||||||
|
if (!m.has(k)) m.set(k, [])
|
||||||
|
m.get(k)!.push(a)
|
||||||
|
}
|
||||||
|
return Array.from(m.entries()).sort(([a], [b]) => {
|
||||||
|
if (a === "—") return 1
|
||||||
|
if (b === "—") return -1
|
||||||
|
return Number(a) - Number(b)
|
||||||
|
})
|
||||||
|
}, [filtered])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog
|
||||||
|
open
|
||||||
|
onOpenChange={() =>
|
||||||
|
router.navigate({ to: "/plan/$planId", params: { planId }, replace: true })
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<DialogContent className="w-[min(92vw,900px)]">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<Icons.BookOpen className="w-5 h-5" />
|
||||||
|
Asignaturas · <span className="font-normal">{planNombre}</span>
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="flex items-center gap-2 pb-3">
|
||||||
|
<Input
|
||||||
|
value={q}
|
||||||
|
onChange={(e) => setQ(e.target.value)}
|
||||||
|
placeholder="Buscar por nombre, semestre…"
|
||||||
|
className="w-full"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="max-h-[65vh] overflow-auto pr-1">
|
||||||
|
{groups.length === 0 && (
|
||||||
|
<div className="text-sm text-neutral-500 py-8 text-center">Sin asignaturas</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="space-y-5">
|
||||||
|
{groups.map(([sem, items]) => (
|
||||||
|
<div key={String(sem)}>
|
||||||
|
<div className="mb-2 text-xs font-semibold text-neutral-500">
|
||||||
|
Semestre {sem}
|
||||||
|
</div>
|
||||||
|
<ul className="grid gap-2 sm:grid-cols-2">
|
||||||
|
{items.map(a => (
|
||||||
|
<li key={a.id} className="rounded-xl border p-3 bg-white/70 dark:bg-neutral-900/60">
|
||||||
|
<div className="font-medium truncate">{a.nombre}</div>
|
||||||
|
<div className="mt-1 flex flex-wrap items-center gap-2 text-xs text-neutral-600">
|
||||||
|
{a.creditos != null && (
|
||||||
|
<Badge variant="outline" className="bg-white/60">Créditos: {a.creditos}</Badge>
|
||||||
|
)}
|
||||||
|
{(a.horas_teoricas ?? 0) + (a.horas_practicas ?? 0) > 0 && (
|
||||||
|
<Badge variant="secondary" className="bg-neutral-100 text-neutral-800 dark:bg-neutral-800 dark:text-neutral-100">
|
||||||
|
Hrs T/P: {a.horas_teoricas ?? 0}/{a.horas_practicas ?? 0}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ import { createFileRoute, Link } from '@tanstack/react-router'
|
|||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
import { supabase } from '@/auth/supabase'
|
import { supabase } from '@/auth/supabase'
|
||||||
import { useMemo } from 'react'
|
import { useMemo } from 'react'
|
||||||
|
import { useTheme } from '@/components/theme-provider'
|
||||||
|
|
||||||
type Facultad = { id: string; nombre: string; icon: string; color?: string | null }
|
type Facultad = { id: string; nombre: string; icon: string; color?: string | null }
|
||||||
type Plan = {
|
type Plan = {
|
||||||
@@ -121,13 +122,14 @@ function gradientFrom(color?: string | null) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ====== UI helpers ======
|
// ====== UI helpers ======
|
||||||
function ProgressRing({ pct }: { pct: number }) {
|
function ProgressRing({ pct, color }: { pct: number, color: string }) {
|
||||||
const r = 42, c = 2 * Math.PI * r
|
const r = 42, c = 2 * Math.PI * r
|
||||||
const offset = c * (1 - Math.min(Math.max(pct, 0), 100) / 100)
|
const offset = c * (1 - Math.min(Math.max(pct, 0), 100) / 100)
|
||||||
|
// Puedes ajustar el color del stroke según el tema
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-4">
|
<div className="flex items-center gap-4">
|
||||||
<svg width="112" height="112" viewBox="0 0 112 112" className="drop-shadow">
|
<svg width="112" height="112" viewBox="0 0 112 112" className="drop-shadow">
|
||||||
<circle cx="56" cy="56" r={r} fill="none" stroke="rgba(0,0,0,.08)" strokeWidth="12" />
|
<circle cx="56" cy="56" r={r} fill="none" stroke={color} strokeWidth="12" />
|
||||||
<circle cx="56" cy="56" r={r} fill="none" stroke="currentColor" strokeWidth="12"
|
<circle cx="56" cy="56" r={r} fill="none" stroke="currentColor" strokeWidth="12"
|
||||||
strokeDasharray={c} strokeDashoffset={offset} strokeLinecap="round"
|
strokeDasharray={c} strokeDashoffset={offset} strokeLinecap="round"
|
||||||
transform="rotate(-90 56 56)" />
|
transform="rotate(-90 56 56)" />
|
||||||
@@ -143,7 +145,7 @@ function ProgressRing({ pct }: { pct: number }) {
|
|||||||
function HealthItem({ label, value, to }: { label: string; value: number; to: string }) {
|
function HealthItem({ label, value, to }: { label: string; value: number; to: string }) {
|
||||||
const warn = value > 0
|
const warn = value > 0
|
||||||
return (
|
return (
|
||||||
<Link to={to} className={`flex items-center justify-between rounded-xl px-4 py-3 border ${warn ? 'border-amber-300 bg-amber-50 text-amber-800' : 'border-neutral-200 bg-white'}`}>
|
<Link to={to} className={`flex items-center justify-between rounded-xl px-4 py-3 border ${warn ? 'border-amber-300 bg-amber-50 text-amber-800' : 'border-neutral-200'}`}>
|
||||||
<span className="text-sm">{label}</span>
|
<span className="text-sm">{label}</span>
|
||||||
<span className="text-lg font-semibold tabular-nums">{value}</span>
|
<span className="text-lg font-semibold tabular-nums">{value}</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -158,7 +160,7 @@ function RouteComponent() {
|
|||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-8">
|
<div className="p-6 space-y-8">
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<div className="relative rounded-3xl overflow-hidden text-white shadow-xl" style={headerBg}>
|
<div className="relative rounded-3xl overflow-hidden shadow-xl text-white" style={headerBg}>
|
||||||
<div className="absolute inset-0 opacity-20" style={{ background: 'radial-gradient(800px 300px at 20% -10%, #fff, transparent 60%)' }} />
|
<div className="absolute inset-0 opacity-20" style={{ background: 'radial-gradient(800px 300px at 20% -10%, #fff, transparent 60%)' }} />
|
||||||
<div className="relative p-8 flex items-center gap-5">
|
<div className="relative p-8 flex items-center gap-5">
|
||||||
<HeaderIcon className="w-16 h-16 md:w-20 md:h-20 drop-shadow" />
|
<HeaderIcon className="w-16 h-16 md:w-20 md:h-20 drop-shadow" />
|
||||||
@@ -179,14 +181,14 @@ function RouteComponent() {
|
|||||||
|
|
||||||
{/* Calidad + Salud */}
|
{/* Calidad + Salud */}
|
||||||
<div className="grid gap-6 lg:grid-cols-3">
|
<div className="grid gap-6 lg:grid-cols-3">
|
||||||
<div className="rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5 lg:col-span-2">
|
<div className="rounded-2xl shadow-lg ring-1 ring-black/5 p-5 lg:col-span-2">
|
||||||
<div className="font-semibold mb-3">Calidad de planes</div>
|
<div className="font-semibold mb-3">Calidad de planes</div>
|
||||||
<ProgressRing pct={calidadPlanesPct} />
|
<ProgressRing pct={calidadPlanesPct} color={facultad.color || 'white'}/>
|
||||||
<div className="mt-3 text-sm text-neutral-600">
|
<div className="mt-3 text-sm text-neutral-600">
|
||||||
Considera <span className="font-medium">objetivo general, perfiles, sistema de evaluación y créditos</span>.
|
Considera <span className="font-medium">objetivo general, perfiles, sistema de evaluación y créditos</span>.
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5">
|
<div className="rounded-2xl shadow-lg ring-1 ring-black/5 p-5">
|
||||||
<div className="font-semibold mb-3">Salud de asignaturas</div>
|
<div className="font-semibold mb-3">Salud de asignaturas</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<HealthItem label="Sin bibliografía" value={saludAsignaturas.sinBibliografia} to={`/_authenticated/asignaturas?facultadId=${facultad.id}&f=sinBibliografia`} />
|
<HealthItem label="Sin bibliografía" value={saludAsignaturas.sinBibliografia} to={`/_authenticated/asignaturas?facultadId=${facultad.id}&f=sinBibliografia`} />
|
||||||
@@ -197,7 +199,7 @@ function RouteComponent() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Actividad reciente */}
|
{/* Actividad reciente */}
|
||||||
<div className="rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5">
|
<div className="rounded-2xl shadow-lg ring-1 ring-black/5 p-5">
|
||||||
<div className="font-semibold mb-3">Actividad reciente</div>
|
<div className="font-semibold mb-3">Actividad reciente</div>
|
||||||
<ul className="space-y-2">
|
<ul className="space-y-2">
|
||||||
{recientes.length === 0 && <li className="text-sm text-neutral-500">Sin actividad</li>}
|
{recientes.length === 0 && <li className="text-sm text-neutral-500">Sin actividad</li>}
|
||||||
@@ -221,12 +223,12 @@ function RouteComponent() {
|
|||||||
// Tarjeta métrica (igual a tu StatTile)
|
// Tarjeta métrica (igual a tu StatTile)
|
||||||
function Metric({ to, label, value, Icon }:{ to: string; label: string; value: number; Icon: any }) {
|
function Metric({ to, label, value, Icon }:{ to: string; label: string; value: number; Icon: any }) {
|
||||||
return (
|
return (
|
||||||
<Link to={to} className="group rounded-2xl bg-white shadow-lg ring-1 ring-black/5 p-5 flex items-center justify-between hover:-translate-y-0.5 transition-all">
|
<Link to={to} className="group rounded-2xl shadow-lg ring-1 ring-black/5 p-5 flex items-center justify-between hover:-translate-y-0.5 transition-all">
|
||||||
<div>
|
<div>
|
||||||
<div className="text-sm text-neutral-500">{label}</div>
|
<div className="text-sm text-neutral-500">{label}</div>
|
||||||
<div className="text-3xl font-bold tabular-nums">{value}</div>
|
<div className="text-3xl font-bold tabular-nums">{value}</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-3 rounded-xl bg-neutral-100 group-hover:bg-neutral-200">
|
<div className="p-3 rounded-xl bg-neutral-100 dark:bg-neutral-800 group-hover:bg-neutral-200">
|
||||||
<Icon className="w-7 h-7" />
|
<Icon className="w-7 h-7" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { createFileRoute, Link, useParams } from '@tanstack/react-router'
|
import { createFileRoute, Link } from '@tanstack/react-router'
|
||||||
import { supabase, useSupabaseAuth } from '@/auth/supabase'
|
import { supabase, useSupabaseAuth } from '@/auth/supabase'
|
||||||
import * as Icons from 'lucide-react'
|
import * as Icons from 'lucide-react'
|
||||||
import { useEffect, useMemo, useRef, useState } from 'react'
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
||||||
@@ -25,7 +25,8 @@ type PlanFull = {
|
|||||||
estado: string | null; fecha_creacion: string | null;
|
estado: string | null; fecha_creacion: string | null;
|
||||||
carreras: { id: string; nombre: string; facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null } | null
|
carreras: { id: string; nombre: string; facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null } | null
|
||||||
}
|
}
|
||||||
type LoaderData = { plan: PlanFull; asignaturasCount: number }
|
type AsignaturaLite = { id: string; nombre: string; semestre: number | null; creditos: number | null }
|
||||||
|
type LoaderData = { plan: PlanFull; asignaturasCount: number; asignaturasPreview: AsignaturaLite[] }
|
||||||
|
|
||||||
/* ============== ROUTE ============== */
|
/* ============== ROUTE ============== */
|
||||||
export const Route = createFileRoute('/_authenticated/plan/$planId')({
|
export const Route = createFileRoute('/_authenticated/plan/$planId')({
|
||||||
@@ -49,7 +50,19 @@ export const Route = createFileRoute('/_authenticated/plan/$planId')({
|
|||||||
.select('*', { count: 'exact', head: true })
|
.select('*', { count: 'exact', head: true })
|
||||||
.eq('plan_id', params.planId)
|
.eq('plan_id', params.planId)
|
||||||
|
|
||||||
return { plan: plan as unknown as PlanFull, asignaturasCount: count ?? 0 }
|
const { data: asignaturasPreview } = await supabase
|
||||||
|
.from('asignaturas')
|
||||||
|
.select('id, nombre, semestre, creditos')
|
||||||
|
.eq('plan_id', params.planId)
|
||||||
|
.order('semestre', { ascending: true })
|
||||||
|
.order('nombre', { ascending: true })
|
||||||
|
.limit(8)
|
||||||
|
|
||||||
|
return {
|
||||||
|
plan: plan as unknown as PlanFull,
|
||||||
|
asignaturasCount: count ?? 0,
|
||||||
|
asignaturasPreview: (asignaturasPreview ?? []) as AsignaturaLite[],
|
||||||
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -112,7 +125,7 @@ function GradientMesh({ color }: { color?: string | null }) {
|
|||||||
|
|
||||||
/* ============== PAGE ============== */
|
/* ============== PAGE ============== */
|
||||||
function RouteComponent() {
|
function RouteComponent() {
|
||||||
const { plan, asignaturasCount } = Route.useLoaderData() as LoaderData
|
const { plan, asignaturasCount, asignaturasPreview } = Route.useLoaderData() as LoaderData
|
||||||
const auth = useSupabaseAuth()
|
const auth = useSupabaseAuth()
|
||||||
const showFacultad = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria'
|
const showFacultad = auth.claims?.role === 'lci' || auth.claims?.role === 'vicerrectoria'
|
||||||
const showCarrera = auth.claims?.role === 'secretario_academico'
|
const showCarrera = auth.claims?.role === 'secretario_academico'
|
||||||
@@ -144,7 +157,7 @@ function RouteComponent() {
|
|||||||
// Stats y campos con ScrollTrigger
|
// Stats y campos con ScrollTrigger
|
||||||
if (statsRef.current) {
|
if (statsRef.current) {
|
||||||
const ctx = gsap.context(() => {
|
const ctx = gsap.context(() => {
|
||||||
gsap.from('.kv', {
|
gsap.from('.academics', {
|
||||||
y: 14, opacity: 0, stagger: .08, duration: .4,
|
y: 14, opacity: 0, stagger: .08, duration: .4,
|
||||||
scrollTrigger: { trigger: statsRef.current, start: 'top 85%' }
|
scrollTrigger: { trigger: statsRef.current, start: 'top 85%' }
|
||||||
})
|
})
|
||||||
@@ -204,8 +217,8 @@ function RouteComponent() {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<Link
|
<Link
|
||||||
to="/asignaturas"
|
to="/asignaturas/$planId"
|
||||||
search={{ planId: plan.id }}
|
params={{ planId: plan.id }}
|
||||||
className="inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
|
className="inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
|
||||||
>
|
>
|
||||||
<Icons.BookOpen className="w-4 h-4" /> Ver asignaturas
|
<Icons.BookOpen className="w-4 h-4" /> Ver asignaturas
|
||||||
@@ -220,24 +233,59 @@ function RouteComponent() {
|
|||||||
{/* stats */}
|
{/* stats */}
|
||||||
<CardContent
|
<CardContent
|
||||||
ref={statsRef}
|
ref={statsRef}
|
||||||
className="relative z-10 grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]"
|
|
||||||
>
|
>
|
||||||
|
<div className="academics relative z-10 grid gap-3 [grid-template-columns:repeat(auto-fit,minmax(180px,1fr))]">
|
||||||
|
|
||||||
<StatCard label="Nivel" value={plan.nivel ?? "—"} Icon={Icons.GraduationCap} accent={facColor} />
|
<StatCard label="Nivel" value={plan.nivel ?? "—"} Icon={Icons.GraduationCap} accent={facColor} />
|
||||||
<StatCard label="Duración" value={plan.duracion ?? "—"} Icon={Icons.Clock} accent={facColor} />
|
<StatCard label="Duración" value={plan.duracion ?? "—"} Icon={Icons.Clock} accent={facColor} />
|
||||||
<StatCard label="Créditos" value={fmt(plan.total_creditos)} Icon={Icons.Coins} accent={facColor} />
|
<StatCard label="Créditos" value={fmt(plan.total_creditos)} Icon={Icons.Coins} accent={facColor} />
|
||||||
<StatCard label="Asignaturas" value={fmt(asignaturasCount)} Icon={Icons.BookOpen} accent={facColor} />
|
|
||||||
<StatCard
|
<StatCard
|
||||||
label="Creado"
|
label="Creado"
|
||||||
value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : "—"}
|
value={plan.fecha_creacion ? new Date(plan.fecha_creacion).toLocaleDateString() : "—"}
|
||||||
Icon={Icons.CalendarDays}
|
Icon={Icons.CalendarDays}
|
||||||
accent={facColor}
|
accent={facColor}
|
||||||
/>
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<div className="academics">
|
||||||
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
|
<AcademicSections planId={plan.id} plan={plan} color={fac?.color} />
|
||||||
</div>
|
</div>
|
||||||
|
<Card className="border shadow-sm">
|
||||||
|
<CardHeader className="flex items-center justify-between gap-2">
|
||||||
|
<CardTitle className="text-base">Asignaturas ({asignaturasCount})</CardTitle>
|
||||||
|
|
||||||
|
{/* Abre el modal enmascarado */}
|
||||||
|
<Link
|
||||||
|
to="/asignaturas/$planId"
|
||||||
|
params={{ planId: plan.id }}
|
||||||
|
className="inline-flex items-center gap-2 rounded-xl border px-3 py-2 text-sm hover:bg-neutral-50"
|
||||||
|
>
|
||||||
|
<Icons.BookOpen className="w-4 h-4" /> Ver todas
|
||||||
|
</Link>
|
||||||
|
</CardHeader>
|
||||||
|
|
||||||
|
<CardContent className="flex flex-wrap gap-2">
|
||||||
|
{asignaturasPreview.length === 0 && (
|
||||||
|
<div className="text-sm text-neutral-500">Sin asignaturas</div>
|
||||||
|
)}
|
||||||
|
{asignaturasPreview.map(a => (
|
||||||
|
<Link
|
||||||
|
key={a.id}
|
||||||
|
to="/asignaturas/$planId"
|
||||||
|
params={{ planId: plan.id }}
|
||||||
|
className="rounded-full border px-3 py-1 text-xs bg-white/70 hover:bg-white transition"
|
||||||
|
title={a.nombre}
|
||||||
|
>
|
||||||
|
{a.semestre ? `S${a.semestre} · ` : ''}{a.nombre}
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,112 +0,0 @@
|
|||||||
import { createFileRoute, useRouter } from "@tanstack/react-router"
|
|
||||||
import { supabase } from "@/auth/supabase"
|
|
||||||
import * as Icons from "lucide-react"
|
|
||||||
|
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
|
||||||
import { Badge } from "@/components/ui/badge"
|
|
||||||
|
|
||||||
|
|
||||||
type PlanDetail = {
|
|
||||||
id: string
|
|
||||||
nombre: string
|
|
||||||
nivel: string | null
|
|
||||||
duracion: string | null
|
|
||||||
total_creditos: number | null
|
|
||||||
estado: string | null
|
|
||||||
carreras: {
|
|
||||||
id: string
|
|
||||||
nombre: string
|
|
||||||
facultades?: { id: string; nombre: string; color?: string | null; icon?: string | null } | null
|
|
||||||
} | null
|
|
||||||
}
|
|
||||||
|
|
||||||
export const Route = createFileRoute('/_authenticated/plan/$planId/modal')({
|
|
||||||
component: RouteComponent,
|
|
||||||
loader: async ({ params }) => {
|
|
||||||
const { data, error } = await supabase
|
|
||||||
.from('plan_estudios')
|
|
||||||
.select(`
|
|
||||||
id, nombre, nivel, duracion, total_creditos, estado,
|
|
||||||
carreras ( id, nombre, facultades:facultades ( id, nombre, color, icon ) )
|
|
||||||
`)
|
|
||||||
.eq('id', params.planId)
|
|
||||||
.single()
|
|
||||||
if (error) throw error
|
|
||||||
return data
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
function gradientFrom(color?: string | null) {
|
|
||||||
const base = (color && /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.test(color)) ? color : "#2563eb"
|
|
||||||
return `linear-gradient(135deg, ${base} 0%, ${base}CC 45%, ${base}99 75%, ${base}66 100%)`
|
|
||||||
}
|
|
||||||
|
|
||||||
function RouteComponent() {
|
|
||||||
const plan = Route.useLoaderData() as PlanDetail
|
|
||||||
const router = useRouter()
|
|
||||||
const fac = plan.carreras?.facultades
|
|
||||||
const IconComp = (fac?.icon && (Icons as any)[fac.icon]) || Icons.Building2
|
|
||||||
const headerBg = { background: gradientFrom(fac?.color) }
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog
|
|
||||||
open
|
|
||||||
onOpenChange={() =>
|
|
||||||
router.navigate({
|
|
||||||
to: '/plan/$planId',
|
|
||||||
params: { planId: plan.id },
|
|
||||||
replace: true,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<DialogContent className="max-w-2xl p-0 overflow-hidden" aria-describedby="">
|
|
||||||
{/* Header con color/ícono de facultad */}
|
|
||||||
<div className="p-6 text-white" style={headerBg}>
|
|
||||||
<div className="flex items-center gap-3">
|
|
||||||
<span className="inline-flex items-center justify-center rounded-2xl bg-white/15 backdrop-blur px-3 py-2">
|
|
||||||
<IconComp className="w-6 h-6" />
|
|
||||||
</span>
|
|
||||||
<div className="min-w-0">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="truncate">{plan.nombre}</DialogTitle>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="text-xs opacity-90 truncate">
|
|
||||||
{plan.carreras?.nombre ?? "—"} {fac?.nombre ? `· ${fac?.nombre}` : ""}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{plan.estado && (
|
|
||||||
<Badge variant="outline" className="ml-auto bg-white/10 text-white border-white/40">
|
|
||||||
{plan.estado}
|
|
||||||
</Badge>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{/* Cuerpo */}
|
|
||||||
<div className="p-6 space-y-4">
|
|
||||||
<div className="grid grid-cols-2 gap-3 text-sm">
|
|
||||||
<div><span className="text-neutral-500">Nivel:</span> <span className="font-medium">{plan.nivel ?? "—"}</span></div>
|
|
||||||
<div><span className="text-neutral-500">Duración:</span> <span className="font-medium">{plan.duracion ?? "—"}</span></div>
|
|
||||||
<div><span className="text-neutral-500">Créditos:</span> <span className="font-medium">{plan.total_creditos ?? "—"}</span></div>
|
|
||||||
<div><span className="text-neutral-500">Facultad:</span> <span className="font-medium">{fac?.nombre ?? "—"}</span></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<a
|
|
||||||
href={`/_authenticated/planes/${plan.id}`}
|
|
||||||
className="inline-flex items-center gap-2 rounded-xl bg-black text-white px-4 py-2 hover:opacity-90"
|
|
||||||
>
|
|
||||||
<Icons.FileText className="w-4 h-4" /> Ver ficha
|
|
||||||
</a>
|
|
||||||
<a
|
|
||||||
href={`/_authenticated/asignaturas?planId=${plan.id}`}
|
|
||||||
className="inline-flex items-center gap-2 rounded-xl border px-4 py-2 hover:bg-neutral-50"
|
|
||||||
>
|
|
||||||
<Icons.BookOpen className="w-4 h-4" /> Ver asignaturas
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -122,7 +122,7 @@ function RouteComponent() {
|
|||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
key={p.id}
|
key={p.id}
|
||||||
to="/plan/$planId/modal"
|
to="/plan/$planId"
|
||||||
mask={{ to: '/plan/$planId', params: { planId: p.id } }}
|
mask={{ to: '/plan/$planId', params: { planId: p.id } }}
|
||||||
className="group relative overflow-hidden rounded-3xl bg-white shadow-sm ring-1 transition-all hover:shadow-md hover:-translate-y-0.5"
|
className="group relative overflow-hidden rounded-3xl bg-white shadow-sm ring-1 transition-all hover:shadow-md hover:-translate-y-0.5"
|
||||||
params={{ planId: p.id }}
|
params={{ planId: p.id }}
|
||||||
|
|||||||
Reference in New Issue
Block a user