Se termina seccion de detalle de plan
This commit is contained in:
@@ -1,5 +1,7 @@
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
"use client"
|
||||
|
||||
import * as React from "react"
|
||||
import * as AvatarPrimitive from "@radix-ui/react-avatar"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva } from "class-variance-authority"
|
||||
import * as React from "react"
|
||||
|
||||
import type {VariantProps} from "class-variance-authority";
|
||||
import { Slot } from "@radix-ui/react-slot"
|
||||
import { cva, type VariantProps } from "class-variance-authority"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
|
||||
56
src/components/ui/scroll-area.tsx
Normal file
56
src/components/ui/scroll-area.tsx
Normal file
@@ -0,0 +1,56 @@
|
||||
import * as React from "react"
|
||||
import * as ScrollAreaPrimitive from "@radix-ui/react-scroll-area"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function ScrollArea({
|
||||
className,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.Root
|
||||
data-slot="scroll-area"
|
||||
className={cn("relative", className)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.Viewport
|
||||
data-slot="scroll-area-viewport"
|
||||
className="focus-visible:ring-ring/50 size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:outline-1"
|
||||
>
|
||||
{children}
|
||||
</ScrollAreaPrimitive.Viewport>
|
||||
<ScrollBar />
|
||||
<ScrollAreaPrimitive.Corner />
|
||||
</ScrollAreaPrimitive.Root>
|
||||
)
|
||||
}
|
||||
|
||||
function ScrollBar({
|
||||
className,
|
||||
orientation = "vertical",
|
||||
...props
|
||||
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
||||
return (
|
||||
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
||||
data-slot="scroll-area-scrollbar"
|
||||
orientation={orientation}
|
||||
className={cn(
|
||||
"flex touch-none p-px transition-colors select-none",
|
||||
orientation === "vertical" &&
|
||||
"h-full w-2.5 border-l border-l-transparent",
|
||||
orientation === "horizontal" &&
|
||||
"h-2.5 flex-col border-t border-t-transparent",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
>
|
||||
<ScrollAreaPrimitive.ScrollAreaThumb
|
||||
data-slot="scroll-area-thumb"
|
||||
className="bg-border relative flex-1 rounded-full"
|
||||
/>
|
||||
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
||||
)
|
||||
}
|
||||
|
||||
export { ScrollArea, ScrollBar }
|
||||
114
src/components/ui/table.tsx
Normal file
114
src/components/ui/table.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Table({ className, ...props }: React.ComponentProps<"table">) {
|
||||
return (
|
||||
<div
|
||||
data-slot="table-container"
|
||||
className="relative w-full overflow-x-auto"
|
||||
>
|
||||
<table
|
||||
data-slot="table"
|
||||
className={cn("w-full caption-bottom text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHeader({ className, ...props }: React.ComponentProps<"thead">) {
|
||||
return (
|
||||
<thead
|
||||
data-slot="table-header"
|
||||
className={cn("[&_tr]:border-b", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableBody({ className, ...props }: React.ComponentProps<"tbody">) {
|
||||
return (
|
||||
<tbody
|
||||
data-slot="table-body"
|
||||
className={cn("[&_tr:last-child]:border-0", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableFooter({ className, ...props }: React.ComponentProps<"tfoot">) {
|
||||
return (
|
||||
<tfoot
|
||||
data-slot="table-footer"
|
||||
className={cn(
|
||||
"bg-muted/50 border-t font-medium [&>tr]:last:border-b-0",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
|
||||
return (
|
||||
<tr
|
||||
data-slot="table-row"
|
||||
className={cn(
|
||||
"hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableHead({ className, ...props }: React.ComponentProps<"th">) {
|
||||
return (
|
||||
<th
|
||||
data-slot="table-head"
|
||||
className={cn(
|
||||
"text-foreground h-10 px-2 text-left align-middle font-medium whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCell({ className, ...props }: React.ComponentProps<"td">) {
|
||||
return (
|
||||
<td
|
||||
data-slot="table-cell"
|
||||
className={cn(
|
||||
"p-2 align-middle whitespace-nowrap [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
function TableCaption({
|
||||
className,
|
||||
...props
|
||||
}: React.ComponentProps<"caption">) {
|
||||
return (
|
||||
<caption
|
||||
data-slot="table-caption"
|
||||
className={cn("text-muted-foreground mt-4 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export {
|
||||
Table,
|
||||
TableHeader,
|
||||
TableBody,
|
||||
TableFooter,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableCell,
|
||||
TableCaption,
|
||||
}
|
||||
18
src/components/ui/textarea.tsx
Normal file
18
src/components/ui/textarea.tsx
Normal file
@@ -0,0 +1,18 @@
|
||||
import * as React from "react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
|
||||
return (
|
||||
<textarea
|
||||
data-slot="textarea"
|
||||
className={cn(
|
||||
"border-input placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive dark:bg-input/30 flex field-sizing-content min-h-16 w-full rounded-md border bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export { Textarea }
|
||||
@@ -17,7 +17,12 @@ import { Route as Planes2IndexRouteImport } from './routes/planes2/index'
|
||||
import { Route as DemoTanstackQueryRouteImport } from './routes/demo/tanstack-query'
|
||||
import { Route as Planes2PlanIdRouteRouteImport } from './routes/planes2/$planId/route'
|
||||
import { Route as Planes2PlanIdIndexRouteImport } from './routes/planes2/$planId/index'
|
||||
import { Route as Planes2PlanIdMateriasRouteImport } from './routes/planes2/$planId/materias'
|
||||
import { Route as Planes2PlanIdMapaRouteImport } from './routes/planes2/$planId/mapa'
|
||||
import { Route as Planes2PlanIdIaplanRouteImport } from './routes/planes2/$planId/iaplan'
|
||||
import { Route as Planes2PlanIdHistorialRouteImport } from './routes/planes2/$planId/historial'
|
||||
import { Route as Planes2PlanIdFlujoRouteImport } from './routes/planes2/$planId/flujo'
|
||||
import { Route as Planes2PlanIdDocumentoRouteImport } from './routes/planes2/$planId/documento'
|
||||
|
||||
const PlanesRoute = PlanesRouteImport.update({
|
||||
id: '/planes',
|
||||
@@ -59,11 +64,36 @@ const Planes2PlanIdIndexRoute = Planes2PlanIdIndexRouteImport.update({
|
||||
path: '/',
|
||||
getParentRoute: () => Planes2PlanIdRouteRoute,
|
||||
} as any)
|
||||
const Planes2PlanIdMateriasRoute = Planes2PlanIdMateriasRouteImport.update({
|
||||
id: '/materias',
|
||||
path: '/materias',
|
||||
getParentRoute: () => Planes2PlanIdRouteRoute,
|
||||
} as any)
|
||||
const Planes2PlanIdMapaRoute = Planes2PlanIdMapaRouteImport.update({
|
||||
id: '/mapa',
|
||||
path: '/mapa',
|
||||
getParentRoute: () => Planes2PlanIdRouteRoute,
|
||||
} as any)
|
||||
const Planes2PlanIdIaplanRoute = Planes2PlanIdIaplanRouteImport.update({
|
||||
id: '/iaplan',
|
||||
path: '/iaplan',
|
||||
getParentRoute: () => Planes2PlanIdRouteRoute,
|
||||
} as any)
|
||||
const Planes2PlanIdHistorialRoute = Planes2PlanIdHistorialRouteImport.update({
|
||||
id: '/historial',
|
||||
path: '/historial',
|
||||
getParentRoute: () => Planes2PlanIdRouteRoute,
|
||||
} as any)
|
||||
const Planes2PlanIdFlujoRoute = Planes2PlanIdFlujoRouteImport.update({
|
||||
id: '/flujo',
|
||||
path: '/flujo',
|
||||
getParentRoute: () => Planes2PlanIdRouteRoute,
|
||||
} as any)
|
||||
const Planes2PlanIdDocumentoRoute = Planes2PlanIdDocumentoRouteImport.update({
|
||||
id: '/documento',
|
||||
path: '/documento',
|
||||
getParentRoute: () => Planes2PlanIdRouteRoute,
|
||||
} as any)
|
||||
|
||||
export interface FileRoutesByFullPath {
|
||||
'/': typeof IndexRoute
|
||||
@@ -73,7 +103,12 @@ export interface FileRoutesByFullPath {
|
||||
'/planes2/$planId': typeof Planes2PlanIdRouteRouteWithChildren
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/planes2': typeof Planes2IndexRoute
|
||||
'/planes2/$planId/documento': typeof Planes2PlanIdDocumentoRoute
|
||||
'/planes2/$planId/flujo': typeof Planes2PlanIdFlujoRoute
|
||||
'/planes2/$planId/historial': typeof Planes2PlanIdHistorialRoute
|
||||
'/planes2/$planId/iaplan': typeof Planes2PlanIdIaplanRoute
|
||||
'/planes2/$planId/mapa': typeof Planes2PlanIdMapaRoute
|
||||
'/planes2/$planId/materias': typeof Planes2PlanIdMateriasRoute
|
||||
'/planes2/$planId/': typeof Planes2PlanIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesByTo {
|
||||
@@ -83,7 +118,12 @@ export interface FileRoutesByTo {
|
||||
'/planes': typeof PlanesRoute
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/planes2': typeof Planes2IndexRoute
|
||||
'/planes2/$planId/documento': typeof Planes2PlanIdDocumentoRoute
|
||||
'/planes2/$planId/flujo': typeof Planes2PlanIdFlujoRoute
|
||||
'/planes2/$planId/historial': typeof Planes2PlanIdHistorialRoute
|
||||
'/planes2/$planId/iaplan': typeof Planes2PlanIdIaplanRoute
|
||||
'/planes2/$planId/mapa': typeof Planes2PlanIdMapaRoute
|
||||
'/planes2/$planId/materias': typeof Planes2PlanIdMateriasRoute
|
||||
'/planes2/$planId': typeof Planes2PlanIdIndexRoute
|
||||
}
|
||||
export interface FileRoutesById {
|
||||
@@ -95,7 +135,12 @@ export interface FileRoutesById {
|
||||
'/planes2/$planId': typeof Planes2PlanIdRouteRouteWithChildren
|
||||
'/demo/tanstack-query': typeof DemoTanstackQueryRoute
|
||||
'/planes2/': typeof Planes2IndexRoute
|
||||
'/planes2/$planId/documento': typeof Planes2PlanIdDocumentoRoute
|
||||
'/planes2/$planId/flujo': typeof Planes2PlanIdFlujoRoute
|
||||
'/planes2/$planId/historial': typeof Planes2PlanIdHistorialRoute
|
||||
'/planes2/$planId/iaplan': typeof Planes2PlanIdIaplanRoute
|
||||
'/planes2/$planId/mapa': typeof Planes2PlanIdMapaRoute
|
||||
'/planes2/$planId/materias': typeof Planes2PlanIdMateriasRoute
|
||||
'/planes2/$planId/': typeof Planes2PlanIdIndexRoute
|
||||
}
|
||||
export interface FileRouteTypes {
|
||||
@@ -108,7 +153,12 @@ export interface FileRouteTypes {
|
||||
| '/planes2/$planId'
|
||||
| '/demo/tanstack-query'
|
||||
| '/planes2'
|
||||
| '/planes2/$planId/documento'
|
||||
| '/planes2/$planId/flujo'
|
||||
| '/planes2/$planId/historial'
|
||||
| '/planes2/$planId/iaplan'
|
||||
| '/planes2/$planId/mapa'
|
||||
| '/planes2/$planId/materias'
|
||||
| '/planes2/$planId/'
|
||||
fileRoutesByTo: FileRoutesByTo
|
||||
to:
|
||||
@@ -118,7 +168,12 @@ export interface FileRouteTypes {
|
||||
| '/planes'
|
||||
| '/demo/tanstack-query'
|
||||
| '/planes2'
|
||||
| '/planes2/$planId/documento'
|
||||
| '/planes2/$planId/flujo'
|
||||
| '/planes2/$planId/historial'
|
||||
| '/planes2/$planId/iaplan'
|
||||
| '/planes2/$planId/mapa'
|
||||
| '/planes2/$planId/materias'
|
||||
| '/planes2/$planId'
|
||||
id:
|
||||
| '__root__'
|
||||
@@ -129,7 +184,12 @@ export interface FileRouteTypes {
|
||||
| '/planes2/$planId'
|
||||
| '/demo/tanstack-query'
|
||||
| '/planes2/'
|
||||
| '/planes2/$planId/documento'
|
||||
| '/planes2/$planId/flujo'
|
||||
| '/planes2/$planId/historial'
|
||||
| '/planes2/$planId/iaplan'
|
||||
| '/planes2/$planId/mapa'
|
||||
| '/planes2/$planId/materias'
|
||||
| '/planes2/$planId/'
|
||||
fileRoutesById: FileRoutesById
|
||||
}
|
||||
@@ -201,6 +261,13 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof Planes2PlanIdIndexRouteImport
|
||||
parentRoute: typeof Planes2PlanIdRouteRoute
|
||||
}
|
||||
'/planes2/$planId/materias': {
|
||||
id: '/planes2/$planId/materias'
|
||||
path: '/materias'
|
||||
fullPath: '/planes2/$planId/materias'
|
||||
preLoaderRoute: typeof Planes2PlanIdMateriasRouteImport
|
||||
parentRoute: typeof Planes2PlanIdRouteRoute
|
||||
}
|
||||
'/planes2/$planId/mapa': {
|
||||
id: '/planes2/$planId/mapa'
|
||||
path: '/mapa'
|
||||
@@ -208,16 +275,54 @@ declare module '@tanstack/react-router' {
|
||||
preLoaderRoute: typeof Planes2PlanIdMapaRouteImport
|
||||
parentRoute: typeof Planes2PlanIdRouteRoute
|
||||
}
|
||||
'/planes2/$planId/iaplan': {
|
||||
id: '/planes2/$planId/iaplan'
|
||||
path: '/iaplan'
|
||||
fullPath: '/planes2/$planId/iaplan'
|
||||
preLoaderRoute: typeof Planes2PlanIdIaplanRouteImport
|
||||
parentRoute: typeof Planes2PlanIdRouteRoute
|
||||
}
|
||||
'/planes2/$planId/historial': {
|
||||
id: '/planes2/$planId/historial'
|
||||
path: '/historial'
|
||||
fullPath: '/planes2/$planId/historial'
|
||||
preLoaderRoute: typeof Planes2PlanIdHistorialRouteImport
|
||||
parentRoute: typeof Planes2PlanIdRouteRoute
|
||||
}
|
||||
'/planes2/$planId/flujo': {
|
||||
id: '/planes2/$planId/flujo'
|
||||
path: '/flujo'
|
||||
fullPath: '/planes2/$planId/flujo'
|
||||
preLoaderRoute: typeof Planes2PlanIdFlujoRouteImport
|
||||
parentRoute: typeof Planes2PlanIdRouteRoute
|
||||
}
|
||||
'/planes2/$planId/documento': {
|
||||
id: '/planes2/$planId/documento'
|
||||
path: '/documento'
|
||||
fullPath: '/planes2/$planId/documento'
|
||||
preLoaderRoute: typeof Planes2PlanIdDocumentoRouteImport
|
||||
parentRoute: typeof Planes2PlanIdRouteRoute
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
interface Planes2PlanIdRouteRouteChildren {
|
||||
Planes2PlanIdDocumentoRoute: typeof Planes2PlanIdDocumentoRoute
|
||||
Planes2PlanIdFlujoRoute: typeof Planes2PlanIdFlujoRoute
|
||||
Planes2PlanIdHistorialRoute: typeof Planes2PlanIdHistorialRoute
|
||||
Planes2PlanIdIaplanRoute: typeof Planes2PlanIdIaplanRoute
|
||||
Planes2PlanIdMapaRoute: typeof Planes2PlanIdMapaRoute
|
||||
Planes2PlanIdMateriasRoute: typeof Planes2PlanIdMateriasRoute
|
||||
Planes2PlanIdIndexRoute: typeof Planes2PlanIdIndexRoute
|
||||
}
|
||||
|
||||
const Planes2PlanIdRouteRouteChildren: Planes2PlanIdRouteRouteChildren = {
|
||||
Planes2PlanIdDocumentoRoute: Planes2PlanIdDocumentoRoute,
|
||||
Planes2PlanIdFlujoRoute: Planes2PlanIdFlujoRoute,
|
||||
Planes2PlanIdHistorialRoute: Planes2PlanIdHistorialRoute,
|
||||
Planes2PlanIdIaplanRoute: Planes2PlanIdIaplanRoute,
|
||||
Planes2PlanIdMapaRoute: Planes2PlanIdMapaRoute,
|
||||
Planes2PlanIdMateriasRoute: Planes2PlanIdMateriasRoute,
|
||||
Planes2PlanIdIndexRoute: Planes2PlanIdIndexRoute,
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ export type Materia = {
|
||||
tipo: 'Obligatoria' | 'Optativa' | 'Especialidad';
|
||||
ciclo: number;
|
||||
linea: string;
|
||||
estado: string;
|
||||
};
|
||||
|
||||
interface MateriaCardProps {
|
||||
|
||||
131
src/routes/planes2/$planId/documento.tsx
Normal file
131
src/routes/planes2/$planId/documento.tsx
Normal file
@@ -0,0 +1,131 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
FileText,
|
||||
Download,
|
||||
RefreshCcw,
|
||||
ExternalLink,
|
||||
CheckCircle2,
|
||||
Clock,
|
||||
FileJson
|
||||
} from "lucide-react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
|
||||
export const Route = createFileRoute('/planes2/$planId/documento')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 bg-slate-50/30 min-h-screen">
|
||||
|
||||
{/* HEADER DE ACCIONES */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start md:items-center gap-4">
|
||||
<div>
|
||||
<h1 className="text-xl font-bold text-slate-800">Documento del Plan</h1>
|
||||
<p className="text-sm text-muted-foreground">Vista previa y descarga del documento oficial</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<RefreshCcw size={16} /> Regenerar
|
||||
</Button>
|
||||
<Button variant="outline" size="sm" className="gap-2">
|
||||
<Download size={16} /> Descargar Word
|
||||
</Button>
|
||||
<Button size="sm" className="gap-2 bg-teal-700 hover:bg-teal-800">
|
||||
<Download size={16} /> Descargar PDF
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TARJETAS DE ESTADO */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<StatusCard
|
||||
icon={<CheckCircle2 className="text-green-500" />}
|
||||
label="Estado"
|
||||
value="Generado"
|
||||
/>
|
||||
<StatusCard
|
||||
icon={<Clock className="text-blue-500" />}
|
||||
label="Última generación"
|
||||
value="28 Ene 2024, 11:30"
|
||||
/>
|
||||
<StatusCard
|
||||
icon={<FileJson className="text-orange-500" />}
|
||||
label="Versión"
|
||||
value="v1.2"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* CONTENEDOR DEL DOCUMENTO (Visor) */}
|
||||
<Card className="border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="bg-slate-100/50 p-2 border-b flex justify-between items-center px-4">
|
||||
<div className="flex items-center gap-2 text-xs text-slate-500 font-medium">
|
||||
<FileText size={14} />
|
||||
Plan_Estudios_ISC_2024.pdf
|
||||
</div>
|
||||
<Button variant="ghost" size="sm" className="text-xs gap-1 h-7">
|
||||
Abrir en nueva pestaña <ExternalLink size={12} />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<CardContent className="p-0 bg-slate-200/50 flex justify-center py-8 min-h-[800px]">
|
||||
{/* SIMULACIÓN DE HOJA DE PAPEL */}
|
||||
<div className="bg-white w-full max-w-[800px] shadow-2xl p-12 md:p-16 min-h-[1000px] border relative">
|
||||
|
||||
{/* Contenido del Plan */}
|
||||
<div className="text-center mb-12">
|
||||
<p className="text-xs uppercase tracking-widest text-slate-400 font-bold mb-1">Universidad Tecnológica</p>
|
||||
<h2 className="text-2xl font-bold text-slate-800">Plan de Estudios 2024</h2>
|
||||
<h3 className="text-lg text-teal-700 font-semibold">Ingeniería en Sistemas Computacionales</h3>
|
||||
<p className="text-xs text-slate-500 mt-1">Facultad de Ingeniería</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-8 text-slate-700">
|
||||
<section>
|
||||
<h4 className="font-bold text-sm mb-2">1. Objetivo General</h4>
|
||||
<p className="text-sm leading-relaxed text-justify">
|
||||
Formar profesionales altamente capacitados en el desarrollo de soluciones tecnológicas innovadoras, con sólidos conocimientos en programación, bases de datos, redes y seguridad informática.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="font-bold text-sm mb-2">2. Perfil de Ingreso</h4>
|
||||
<p className="text-sm leading-relaxed text-justify">
|
||||
Egresados de educación media superior con conocimientos básicos de matemáticas, razonamiento lógico y habilidades de comunicación. Interés por la tecnología y la resolución de problemas.
|
||||
</p>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h4 className="font-bold text-sm mb-2">3. Perfil de Egreso</h4>
|
||||
<p className="text-sm leading-relaxed text-justify">
|
||||
Profesional capaz de diseñar, desarrollar e implementar sistemas de software de calidad, administrar infraestructuras de red y liderar proyectos tecnológicos multidisciplinarios.
|
||||
</p>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Marca de agua o decoración lateral (opcional) */}
|
||||
<div className="absolute top-0 left-0 w-1 h-full bg-slate-100" />
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Componente pequeño para las tarjetas de estado superior
|
||||
function StatusCard({ icon, label, value }: { icon: React.ReactNode, label: string, value: string }) {
|
||||
return (
|
||||
<Card className="bg-white border-slate-200">
|
||||
<CardContent className="p-4 flex items-center gap-4">
|
||||
<div className="p-2 rounded-full bg-slate-50 border">
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-tight">{label}</p>
|
||||
<p className="text-sm font-semibold text-slate-700">{value}</p>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
134
src/routes/planes2/$planId/flujo.tsx
Normal file
134
src/routes/planes2/$planId/flujo.tsx
Normal file
@@ -0,0 +1,134 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { CheckCircle2, Circle, Clock } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
|
||||
import { Textarea } from "@/components/ui/textarea"
|
||||
|
||||
export const Route = createFileRoute('/planes2/$planId/flujo')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6">
|
||||
{/* Header Informativo (Opcional, si no viene del layout padre) */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold italic">Flujo de Aprobación</h1>
|
||||
<p className="text-sm text-muted-foreground">Gestiona el proceso de revisión y aprobación del plan</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
|
||||
|
||||
{/* LADO IZQUIERDO: Timeline del Flujo */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
|
||||
{/* Estado: Completado */}
|
||||
<div className="relative flex gap-4 pb-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="rounded-full bg-green-100 p-1 text-green-600">
|
||||
<CheckCircle2 className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-px flex-1 bg-green-200 mt-2" />
|
||||
</div>
|
||||
<Card className="flex-1">
|
||||
<CardHeader className="flex flex-row items-center justify-between py-3">
|
||||
<div>
|
||||
<CardTitle className="text-lg">Borrador</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">14 de enero de 2024</p>
|
||||
</div>
|
||||
<Badge variant="secondary" className="bg-green-100 text-green-700">Completado</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm border-t pt-3">
|
||||
<p className="font-semibold text-muted-foreground mb-2">Comentarios</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>Documento inicial creado</li>
|
||||
<li>Estructura base definida</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Estado: En Curso (Actual) */}
|
||||
<div className="relative flex gap-4 pb-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="rounded-full bg-blue-100 p-1 text-blue-600 ring-2 ring-blue-500 ring-offset-2">
|
||||
<Clock className="h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-px flex-1 bg-slate-200 mt-2" />
|
||||
</div>
|
||||
<Card className="flex-1 border-blue-500 bg-blue-50/10">
|
||||
<CardHeader className="flex flex-row items-center justify-between py-3">
|
||||
<div>
|
||||
<CardTitle className="text-lg text-blue-700">En Revisión</CardTitle>
|
||||
<p className="text-xs text-muted-foreground">19 de febrero de 2024</p>
|
||||
</div>
|
||||
<Badge variant="default" className="bg-blue-500">En curso</Badge>
|
||||
</CardHeader>
|
||||
<CardContent className="text-sm border-t border-blue-100 pt-3">
|
||||
<p className="font-semibold text-muted-foreground mb-2">Comentarios</p>
|
||||
<ul className="list-disc list-inside space-y-1 text-muted-foreground">
|
||||
<li>Revisión de objetivo general pendiente</li>
|
||||
<li>Mapa curricular aprobado preliminarmente</li>
|
||||
</ul>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* Estado: Pendiente */}
|
||||
<div className="relative flex gap-4 pb-4">
|
||||
<div className="flex flex-col items-center">
|
||||
<div className="rounded-full bg-slate-100 p-1 text-slate-400">
|
||||
<Circle className="h-6 w-6" />
|
||||
</div>
|
||||
</div>
|
||||
<Card className="flex-1 opacity-60 grayscale-[0.5]">
|
||||
<CardHeader className="flex flex-row items-center justify-between py-3">
|
||||
<CardTitle className="text-lg">Revisión Expertos</CardTitle>
|
||||
<Badge variant="outline">Pendiente</Badge>
|
||||
</CardHeader>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{/* LADO DERECHO: Formulario de Transición */}
|
||||
<div className="lg:col-span-1">
|
||||
<Card className="sticky top-6">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">Transición de Estado</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="flex items-center justify-between p-3 bg-slate-50 rounded-lg text-sm border">
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-muted-foreground">Estado actual</p>
|
||||
<p className="font-bold">En Revisión</p>
|
||||
</div>
|
||||
<div className="h-px flex-1 bg-slate-300 mx-4" />
|
||||
<div className="text-center">
|
||||
<p className="text-xs text-muted-foreground">Siguiente</p>
|
||||
<p className="font-bold text-primary">Revisión Expertos</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<label className="text-sm font-medium">Comentario de transición</label>
|
||||
<Textarea
|
||||
placeholder="Agrega un comentario para la transición..."
|
||||
className="min-h-[120px]"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Button className="w-full bg-teal-600 hover:bg-teal-700">
|
||||
Avanzar a Revisión Expertos
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
142
src/routes/planes2/$planId/historial.tsx
Normal file
142
src/routes/planes2/$planId/historial.tsx
Normal file
@@ -0,0 +1,142 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import {
|
||||
GitBranch,
|
||||
Edit3,
|
||||
PlusCircle,
|
||||
FileText,
|
||||
RefreshCw,
|
||||
User
|
||||
} from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Card, CardContent } from "@/components/ui/card"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
|
||||
export const Route = createFileRoute('/planes2/$planId/historial')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const historyEvents = [
|
||||
{
|
||||
id: 1,
|
||||
type: 'Cambio de estado',
|
||||
user: 'Dr. Juan Pérez',
|
||||
description: 'Plan pasado de Borrador a En Revisión',
|
||||
date: 'Hace 2 días',
|
||||
icon: <GitBranch className="h-4 w-4" />,
|
||||
details: { from: 'Borrador', to: 'En Revisión' }
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
type: 'Edición',
|
||||
user: 'Lic. María García',
|
||||
description: 'Actualizado perfil de egreso',
|
||||
date: 'Hace 3 días',
|
||||
icon: <Edit3 className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
type: 'Reorganización',
|
||||
user: 'Ing. Carlos López',
|
||||
description: 'Movida materia BD102 de ciclo 3 a ciclo 4',
|
||||
date: 'Hace 5 días',
|
||||
icon: <RefreshCw className="h-4 w-4" />,
|
||||
details: { from: 'Ciclo 3', to: 'Ciclo 4' }
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
type: 'Creación',
|
||||
user: 'Dr. Juan Pérez',
|
||||
description: 'Añadida nueva materia: Inteligencia Artificial',
|
||||
date: 'Hace 1 semana',
|
||||
icon: <PlusCircle className="h-4 w-4" />,
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
type: 'Documento',
|
||||
user: 'Lic. María García',
|
||||
description: 'Generado documento oficial v1.0',
|
||||
date: 'Hace 1 semana',
|
||||
icon: <FileText className="h-4 w-4" />,
|
||||
}
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="p-6 max-w-5xl mx-auto">
|
||||
<div className="mb-8">
|
||||
<h1 className="text-xl font-bold text-slate-800">Historial de Cambios</h1>
|
||||
<p className="text-sm text-muted-foreground">Registro de todas las modificaciones realizadas al plan</p>
|
||||
</div>
|
||||
|
||||
<div className="relative space-y-0">
|
||||
{/* Línea vertical de fondo */}
|
||||
<div className="absolute left-9 top-0 bottom-0 w-px bg-slate-200" />
|
||||
|
||||
{historyEvents.map((event) => (
|
||||
<div key={event.id} className="relative flex gap-6 pb-8 group">
|
||||
|
||||
{/* Indicador con Icono */}
|
||||
<div className="relative z-10 flex h-18 flex-col items-center">
|
||||
<div className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 border-white bg-slate-100 text-slate-600 shadow-sm group-hover:bg-teal-50 group-hover:text-teal-600 transition-colors">
|
||||
{event.icon}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tarjeta de Contenido */}
|
||||
<Card className="flex-1 shadow-none border-slate-200 hover:border-teal-200 transition-colors">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex flex-col md:flex-row md:items-center justify-between gap-2 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-bold text-slate-800 text-sm">{event.type}</span>
|
||||
<Badge variant="outline" className="text-[10px] font-normal py-0">
|
||||
{event.date}
|
||||
</Badge>
|
||||
</div>
|
||||
<div className="flex items-center gap-2 text-xs text-muted-foreground">
|
||||
<Avatar className="h-5 w-5 border">
|
||||
<AvatarFallback className="text-[8px] bg-slate-50"><User size={10}/></AvatarFallback>
|
||||
</Avatar>
|
||||
{event.user}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p className="text-sm text-slate-600 mb-3">{event.description}</p>
|
||||
|
||||
{/* Badges de transición (si existen) */}
|
||||
{event.details && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<Badge variant="secondary" className="bg-orange-50 text-orange-700 hover:bg-orange-50 border-orange-100 text-[10px]">
|
||||
{event.details.from}
|
||||
</Badge>
|
||||
<span className="text-slate-400 text-xs">→</span>
|
||||
<Badge variant="secondary" className="bg-green-50 text-green-700 hover:bg-green-50 border-green-100 text-[10px]">
|
||||
{event.details.to}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Evento inicial de creación */}
|
||||
<div className="relative flex gap-6 group">
|
||||
<div className="relative z-10 flex items-center">
|
||||
<div className="flex h-[42px] w-[42px] items-center justify-center rounded-full border-4 border-white bg-teal-600 text-white shadow-sm">
|
||||
<PlusCircle className="h-4 w-4" />
|
||||
</div>
|
||||
</div>
|
||||
<Card className="flex-1 bg-teal-50/30 border-teal-100 shadow-none">
|
||||
<CardContent className="p-4">
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<span className="font-bold text-teal-900 text-sm">Creación</span>
|
||||
<span className="text-[10px] text-teal-600 font-medium">14 Ene 2024</span>
|
||||
</div>
|
||||
<p className="text-sm text-teal-800/80">Plan de estudios creado</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
118
src/routes/planes2/$planId/iaplan.tsx
Normal file
118
src/routes/planes2/$planId/iaplan.tsx
Normal file
@@ -0,0 +1,118 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Sparkles, Send, Paperclip, Target, UserCheck, Lightbulb, FileText } from "lucide-react"
|
||||
import { useState } from 'react' // Importamos useState
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Input } from "@/components/ui/input"
|
||||
import { ScrollArea } from "@/components/ui/scroll-area"
|
||||
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
|
||||
|
||||
|
||||
export const Route = createFileRoute('/planes2/$planId/iaplan')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
// 1. Estado para el texto del input
|
||||
const [inputValue, setInputValue] = useState('')
|
||||
|
||||
// 2. Estado para la lista de mensajes (iniciamos con los de la imagen)
|
||||
const [messages, setMessages] = useState([
|
||||
{ id: 1, role: 'ai', text: 'Hola, soy tu asistente de IA para el diseño del plan de estudios...' },
|
||||
{ id: 2, role: 'user', text: 'jkasakj' },
|
||||
{ id: 3, role: 'ai', text: 'Entendido. Estoy procesando tu solicitud.' },
|
||||
])
|
||||
|
||||
// 3. Función para enviar el mensaje
|
||||
const handleSend = () => {
|
||||
if (!inputValue.trim()) return
|
||||
|
||||
// Agregamos el mensaje del usuario
|
||||
const newMessage = {
|
||||
id: Date.now(),
|
||||
role: 'user',
|
||||
text: inputValue
|
||||
}
|
||||
|
||||
setMessages([...messages, newMessage])
|
||||
setInputValue('') // Limpiamos el input
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-[calc(100vh-200px)] gap-6 p-4">
|
||||
<div className="flex flex-col flex-1 bg-slate-50/50 rounded-xl border relative overflow-hidden">
|
||||
|
||||
<ScrollArea className="flex-1 p-6">
|
||||
<div className="space-y-6 max-w-3xl mx-auto">
|
||||
{/* 4. Mapeamos los mensajes dinámicamente */}
|
||||
{messages.map((msg) => (
|
||||
<div key={msg.id} className={`flex ${msg.role === 'user' ? 'flex-row-reverse' : 'flex-row'} gap-3`}>
|
||||
{msg.role === 'ai' && (
|
||||
<Avatar className="h-8 w-8 border bg-teal-50">
|
||||
<AvatarFallback className="text-teal-600"><Sparkles size={16}/></AvatarFallback>
|
||||
</Avatar>
|
||||
)}
|
||||
|
||||
<div className={msg.role === 'ai' ? 'space-y-2' : ''}>
|
||||
{msg.role === 'ai' && <p className="text-xs font-bold text-teal-700 uppercase tracking-wider">Asistente IA</p>}
|
||||
<div className={`p-4 rounded-2xl text-sm shadow-sm ${
|
||||
msg.role === 'user'
|
||||
? 'bg-teal-600 text-white rounded-tr-none'
|
||||
: 'bg-white border text-slate-700 rounded-tl-none'
|
||||
}`}>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
{/* 5. Input vinculado al estado */}
|
||||
<div className="p-4 bg-white border-t">
|
||||
<div className="max-w-4xl mx-auto flex gap-2 items-center bg-slate-50 border rounded-lg px-3 py-1 shadow-sm focus-within:ring-1 focus-within:ring-teal-500 transition-all">
|
||||
<Input
|
||||
value={inputValue}
|
||||
onChange={(e) => setInputValue(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSend()} // Enviar con Enter
|
||||
className="border-none bg-transparent focus-visible:ring-0 text-sm"
|
||||
placeholder='Escribe tu solicitud... Usa ":" para mencionar campos'
|
||||
/>
|
||||
<Button variant="ghost" size="icon" className="text-slate-400">
|
||||
<Paperclip size={18} />
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSend}
|
||||
size="icon"
|
||||
className="bg-teal-600 hover:bg-teal-700 h-8 w-8"
|
||||
>
|
||||
<Send size={16} />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Panel lateral (se mantiene igual) */}
|
||||
<div className="w-72 space-y-4">
|
||||
<div className="flex items-center gap-2 text-orange-500 font-semibold text-sm mb-4">
|
||||
<Lightbulb size={18} />
|
||||
Acciones rápidas
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<ActionButton icon={<Target className="text-teal-500" size={18} />} text="Mejorar objetivo general" />
|
||||
<ActionButton icon={<UserCheck className="text-slate-500" size={18} />} text="Redactar perfil de egreso" />
|
||||
<ActionButton icon={<Lightbulb className="text-blue-500" size={18} />} text="Sugerir competencias" />
|
||||
<ActionButton icon={<FileText className="text-teal-500" size={18} />} text="Justificar pertinencia" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ActionButton({ icon, text }: { icon: React.ReactNode, text: string }) {
|
||||
return (
|
||||
<Button variant="outline" className="w-full justify-start gap-3 h-auto py-3 px-4 text-sm font-normal hover:bg-slate-50 border-slate-200 shadow-sm text-slate-700">
|
||||
{icon}
|
||||
{text}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@@ -26,7 +26,12 @@ function DatosGenerales() {
|
||||
)
|
||||
}
|
||||
|
||||
function Card({ title, children }) {
|
||||
interface CustomCardProps {
|
||||
title: string;
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
function Card({ title, children }: CustomCardProps) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-white p-4">
|
||||
<h3 className="font-semibold mb-2">{title}</h3>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { MateriaCard } from './MateriaCard';
|
||||
import type { Materia } from './MateriaCard'; // Agregamos 'type' aquí
|
||||
|
||||
export const Route = createFileRoute('/planes2/$planId/mapa')({
|
||||
component: MapaCurricular,
|
||||
@@ -9,12 +10,56 @@ const CICLOS = ["Ciclo 1", "Ciclo 2", "Ciclo 3", "Ciclo 4", "Ciclo 5", "Ciclo 6"
|
||||
const LINEAS = ["Formación Básica", "Ciencias de la Computación", "Desarrollo de Software", "Redes y Seguridad", "Gestión y Profesionalización"];
|
||||
|
||||
// Ejemplo de materia
|
||||
const MATERIAS = [
|
||||
{ nombre: "Cálculo Diferencial", linea: "Formación Básica", ciclo: 1, creditos: 8 },
|
||||
{ nombre: "Fundamentos de Programación", linea: "Ciencias de la Computación", ciclo: 1, creditos: 8 },
|
||||
{ nombre: "Fundamentos de Programación 2", linea: "Ciencias de la Computación", ciclo: 1, creditos: 8 },
|
||||
// ... más materias
|
||||
];
|
||||
const MATERIAS: Materia[] = [
|
||||
{
|
||||
id: "1",
|
||||
clave: 'MAT101',
|
||||
nombre: 'Cálculo Diferencial',
|
||||
creditos: 8,
|
||||
hd: 4,
|
||||
hi: 4,
|
||||
ciclo: 1,
|
||||
linea: 'Formación Básica',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Aprobada',
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
clave: 'FIS101',
|
||||
nombre: 'Física Mecánica',
|
||||
creditos: 6,
|
||||
hd: 3,
|
||||
hi: 3,
|
||||
ciclo: 1,
|
||||
linea: 'Formación Básica',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Aprobada',
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
clave: 'PRO101',
|
||||
nombre: 'Fundamentos de Programación',
|
||||
creditos: 8,
|
||||
hd: 4,
|
||||
hi: 4,
|
||||
ciclo: 1,
|
||||
linea: 'Ciencias de la Computación',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Revisada',
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
clave: 'EST101',
|
||||
nombre: 'Estructura de Datos',
|
||||
creditos: 6,
|
||||
hd: 3,
|
||||
hi: 3,
|
||||
ciclo: 2,
|
||||
linea: 'Ciencias de la Computación',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Borrador',
|
||||
},
|
||||
]
|
||||
|
||||
function MapaCurricular() {
|
||||
return (
|
||||
|
||||
219
src/routes/planes2/$planId/materias.tsx
Normal file
219
src/routes/planes2/$planId/materias.tsx
Normal file
@@ -0,0 +1,219 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { useState } from 'react'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from '@/components/ui/table'
|
||||
|
||||
export const Route = createFileRoute('/planes2/$planId/materias')({
|
||||
component: Materias,
|
||||
})
|
||||
|
||||
type Materia = {
|
||||
id: string;
|
||||
clave: string
|
||||
nombre: string
|
||||
creditos: number
|
||||
hd: number
|
||||
hi: number
|
||||
ciclo: string
|
||||
linea: string
|
||||
tipo: 'Obligatoria' | 'Optativa' | 'Troncal'
|
||||
estado: 'Aprobada' | 'Revisada' | 'Borrador'
|
||||
}
|
||||
|
||||
const MATERIAS: Materia[] = [
|
||||
{
|
||||
id: "1",
|
||||
clave: 'MAT101',
|
||||
nombre: 'Cálculo Diferencial',
|
||||
creditos: 8,
|
||||
hd: 4,
|
||||
hi: 4,
|
||||
ciclo: 'Ciclo 1',
|
||||
linea: 'Formación Básica',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Aprobada',
|
||||
},
|
||||
{
|
||||
id: "2",
|
||||
clave: 'FIS101',
|
||||
nombre: 'Física Mecánica',
|
||||
creditos: 6,
|
||||
hd: 3,
|
||||
hi: 3,
|
||||
ciclo: 'Ciclo 1',
|
||||
linea: 'Formación Básica',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Aprobada',
|
||||
},
|
||||
{
|
||||
id: "3",
|
||||
clave: 'PRO101',
|
||||
nombre: 'Fundamentos de Programación',
|
||||
creditos: 8,
|
||||
hd: 4,
|
||||
hi: 4,
|
||||
ciclo: 'Ciclo 1',
|
||||
linea: 'Ciencias de la Computación',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Revisada',
|
||||
},
|
||||
{
|
||||
id: "4",
|
||||
clave: 'EST101',
|
||||
nombre: 'Estructura de Datos',
|
||||
creditos: 6,
|
||||
hd: 3,
|
||||
hi: 3,
|
||||
ciclo: 'Ciclo 2',
|
||||
linea: 'Ciencias de la Computación',
|
||||
tipo: 'Obligatoria',
|
||||
estado: 'Borrador',
|
||||
},
|
||||
]
|
||||
|
||||
function Materias() {
|
||||
const [search, setSearch] = useState('')
|
||||
const [filtro, setFiltro] = useState<'Todas' | Materia['tipo']>('Todas')
|
||||
|
||||
const materiasFiltradas = MATERIAS.filter((m) => {
|
||||
const okFiltro = filtro === 'Todas' || m.tipo === filtro
|
||||
const okSearch =
|
||||
m.nombre.toLowerCase().includes(search.toLowerCase()) ||
|
||||
m.clave.toLowerCase().includes(search.toLowerCase())
|
||||
|
||||
return okFiltro && okSearch
|
||||
})
|
||||
|
||||
const totalCreditos = materiasFiltradas.reduce(
|
||||
(acc, m) => acc + m.creditos,
|
||||
0
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header */}
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold">Materias del Plan</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{materiasFiltradas.length} materias · {totalCreditos} créditos
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline">Clonar de mi Facultad</Button>
|
||||
<Button variant="outline">Clonar de otra Facultad</Button>
|
||||
<Button className="bg-emerald-700 hover:bg-emerald-800">
|
||||
+ Nueva Materia
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Buscador y filtros */}
|
||||
<div className="flex items-center gap-4">
|
||||
<Input
|
||||
placeholder="Buscar por nombre o clave..."
|
||||
value={search}
|
||||
onChange={(e) => setSearch(e.target.value)}
|
||||
className="w-64"
|
||||
/>
|
||||
|
||||
<div className="flex gap-2">
|
||||
{['Todas', 'Obligatoria', 'Optativa', 'Troncal'].map((t) => (
|
||||
<Button
|
||||
key={t}
|
||||
variant={filtro === t ? 'secondary' : 'ghost'}
|
||||
size="sm"
|
||||
onClick={() => setFiltro(t as any)}
|
||||
>
|
||||
{t === 'Obligatoria' ? 'Obligatorias' : t}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabla */}
|
||||
<div className="rounded-md border">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>Clave</TableHead>
|
||||
<TableHead>Nombre</TableHead>
|
||||
<TableHead className="text-center">Créditos</TableHead>
|
||||
<TableHead className="text-center">HD</TableHead>
|
||||
<TableHead className="text-center">HI</TableHead>
|
||||
<TableHead>Ciclo</TableHead>
|
||||
<TableHead>Línea</TableHead>
|
||||
<TableHead>Tipo</TableHead>
|
||||
<TableHead>Estado</TableHead>
|
||||
<TableHead className="text-center">Acciones</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
|
||||
<TableBody>
|
||||
{materiasFiltradas.map((m) => (
|
||||
<TableRow key={m.clave}>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{m.clave}
|
||||
</TableCell>
|
||||
<TableCell className="font-medium">{m.nombre}</TableCell>
|
||||
<TableCell className="text-center">{m.creditos}</TableCell>
|
||||
<TableCell className="text-center">{m.hd}</TableCell>
|
||||
<TableCell className="text-center">{m.hi}</TableCell>
|
||||
<TableCell>{m.ciclo}</TableCell>
|
||||
<TableCell>{m.linea}</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Badge variant="secondary">{m.tipo}</Badge>
|
||||
</TableCell>
|
||||
|
||||
<TableCell>
|
||||
<Badge
|
||||
variant="secondary"
|
||||
className={
|
||||
m.estado === 'Aprobada'
|
||||
? 'bg-emerald-100 text-emerald-700'
|
||||
: m.estado === 'Revisada'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-gray-100 text-gray-500'
|
||||
}
|
||||
>
|
||||
{m.estado}
|
||||
</Badge>
|
||||
|
||||
</TableCell>
|
||||
|
||||
<TableCell className="text-center">
|
||||
<Button variant="ghost" size="icon">
|
||||
✏️
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
|
||||
{materiasFiltradas.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell
|
||||
colSpan={10}
|
||||
className="text-center py-6 text-muted-foreground"
|
||||
>
|
||||
No se encontraron materias
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
import { createFileRoute, Outlet, Link } from '@tanstack/react-router'
|
||||
import { ChevronLeft, GraduationCap, Clock, Hash, CalendarDays, Rocket, BookOpen, CheckCircle2 } from "lucide-react"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
|
||||
export const Route = createFileRoute('/planes2/$planId')({
|
||||
component: PlanLayout,
|
||||
@@ -8,51 +10,109 @@ function PlanLayout() {
|
||||
const { planId } = Route.useParams()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Header del plan */}
|
||||
<div>
|
||||
<Link to="/planes2" className="text-sm text-gray-500">
|
||||
← Volver a planes
|
||||
</Link>
|
||||
|
||||
<h1 className="text-2xl font-bold mt-2">Plan de Estudios 2024</h1>
|
||||
<p className="text-gray-600">
|
||||
Ingeniería en Sistemas Computacionales
|
||||
</p>
|
||||
<div className="min-h-screen bg-white">
|
||||
{/* 1. Header Superior con Sombra (Volver a planes) */}
|
||||
<div className="border-b bg-white/50 backdrop-blur-sm sticky top-0 z-20 shadow-sm">
|
||||
<div className="px-6 py-2">
|
||||
<Link
|
||||
to="/planes2"
|
||||
className="flex items-center gap-1 text-xs text-gray-500 hover:text-gray-800 transition-colors w-fit"
|
||||
>
|
||||
<ChevronLeft size={14} /> Volver a planes
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<nav className="flex gap-6 border-b">
|
||||
<Tab to="/planes2/$planId" params={{ planId }}>
|
||||
Datos Generales
|
||||
</Tab>
|
||||
{/* 2. Contenido Principal con Padding */}
|
||||
<div className="p-8 max-w-[1600px] mx-auto space-y-8">
|
||||
|
||||
{/* Header del Plan y Badges */}
|
||||
<div className="flex flex-col md:flex-row justify-between items-start gap-4">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold tracking-tight text-slate-900">Plan de Estudios 2024</h1>
|
||||
<p className="text-lg text-slate-500 font-medium mt-1">
|
||||
Ingeniería en Sistemas Computacionales
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Badges de la derecha */}
|
||||
<div className="flex gap-2">
|
||||
<Badge variant="secondary" className="bg-blue-50 text-blue-700 border-blue-100 gap-1 px-3">
|
||||
<Rocket size={12} /> Ingeniería
|
||||
</Badge>
|
||||
<Badge variant="secondary" className="bg-orange-50 text-orange-700 border-orange-100 gap-1 px-3">
|
||||
<BookOpen size={12} /> Licenciatura
|
||||
</Badge>
|
||||
<Badge className="bg-teal-50 text-teal-700 border-teal-200 gap-1 px-3 hover:bg-teal-100">
|
||||
<CheckCircle2 size={12} /> En Revisión
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Tab to="/planes2/$planId/mapa" params={{ planId }}>
|
||||
Mapa Curricular
|
||||
</Tab>
|
||||
{/* 3. Cards de Información (Nivel, Duración, etc.) */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<InfoCard icon={<GraduationCap className="text-slate-400" />} label="Nivel" value="Superior" />
|
||||
<InfoCard icon={<Clock className="text-slate-400" />} label="Duración" value="9 Semestres" />
|
||||
<InfoCard icon={<Hash className="text-slate-400" />} label="Créditos" value="320" />
|
||||
<InfoCard icon={<CalendarDays className="text-slate-400" />} label="Creación" value="14 ene 2024" />
|
||||
</div>
|
||||
|
||||
<Tab to="/planes2/$planId/materias" params={{ planId }}>
|
||||
Materias
|
||||
</Tab>
|
||||
</nav>
|
||||
{/* 4. Navegación de Tabs */}
|
||||
<div className="border-b overflow-x-auto scrollbar-hide">
|
||||
<nav className="flex gap-8 min-w-max">
|
||||
<Tab to="/planes2/$planId" params={{ planId }}>Datos Generales</Tab>
|
||||
<Tab to="/planes2/$planId/mapa" params={{ planId }}>Mapa Curricular</Tab>
|
||||
<Tab to="/planes2/$planId/materias" params={{ planId }}>Materias</Tab>
|
||||
<Tab to="/planes2/$planId/flujo" params={{ planId }}>Flujo y Estados</Tab>
|
||||
<Tab to="/planes2/$planId/iaplan" params={{ planId }}>IA del Plan</Tab>
|
||||
<Tab to="/planes2/$planId/documento" params={{ planId }}>Documento</Tab>
|
||||
<Tab to="/planes2/$planId/historial" params={{ planId }}>Historial</Tab>
|
||||
</nav>
|
||||
</div>
|
||||
|
||||
{/* Aquí se renderiza cada tab */}
|
||||
<Outlet />
|
||||
{/* 5. Contenido del Tab */}
|
||||
<main className="pt-2 animate-in fade-in duration-500">
|
||||
<Outlet />
|
||||
</main>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Tab({ to, params, children }) {
|
||||
// Sub-componente para las tarjetas de información
|
||||
function InfoCard({ icon, label, value }: { icon: React.ReactNode, label: string, value: string }) {
|
||||
return (
|
||||
<div className="flex items-center gap-4 bg-slate-50/50 border border-slate-200/60 p-4 rounded-xl shadow-sm">
|
||||
<div className="p-2 bg-white rounded-lg border shadow-sm">
|
||||
{icon}
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-[10px] uppercase font-bold text-slate-400 tracking-wider leading-none mb-1">{label}</p>
|
||||
<p className="text-sm font-semibold text-slate-700">{value}</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function Tab({
|
||||
to,
|
||||
params,
|
||||
children
|
||||
}: {
|
||||
to: string;
|
||||
params?: any;
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return (
|
||||
<Link
|
||||
to={to}
|
||||
params={params}
|
||||
className="pb-2 border-b-2 border-transparent hover:border-teal-600"
|
||||
className="pb-3 text-sm font-medium text-slate-500 border-b-2 border-transparent hover:text-slate-800 transition-all"
|
||||
activeProps={{
|
||||
className: 'border-teal-600 text-teal-700 font-medium',
|
||||
className: 'border-teal-600 text-teal-700 font-bold',
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user