Se termina seccion de detalle de plan

This commit is contained in:
Robert
2025-12-30 19:23:36 -06:00
parent 13f7816786
commit ad1c889f49
18 changed files with 9052 additions and 41 deletions

View File

@@ -9,6 +9,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",
@@ -244,6 +245,8 @@
"@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" } }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
@@ -256,6 +259,8 @@
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
@@ -276,6 +281,8 @@
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.4", "", { "dependencies": { "@radix-ui/react-slot": "1.2.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-separator": ["@radix-ui/react-separator@1.1.8", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.4" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.4", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA=="],
@@ -1316,6 +1323,10 @@
"@radix-ui/react-portal/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-scroll-area/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-scroll-area/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-tooltip/@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-tooltip/@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
@@ -1394,6 +1405,8 @@
"@radix-ui/react-portal/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-scroll-area/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-visually-hidden/@radix-ui/react-primitive/@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@tanstack/devtools/@tanstack/devtools-client/@tanstack/devtools-event-client": ["@tanstack/devtools-event-client@0.3.5", "", {}, "sha512-RL1f5ZlfZMpghrCIdzl6mLOFLTuhqmPNblZgBaeKfdtk5rfbjykurv+VfYydOFXj0vxVIoA2d/zT7xfD7Ph8fw=="],

7849
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -21,6 +21,7 @@
"@radix-ui/react-dialog": "^1.1.15",
"@radix-ui/react-label": "^2.1.8",
"@radix-ui/react-popover": "^1.1.15",
"@radix-ui/react-scroll-area": "^1.2.10",
"@radix-ui/react-separator": "^1.1.8",
"@radix-ui/react-slot": "^1.2.4",
"@radix-ui/react-tooltip": "^1.2.8",

View File

@@ -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"

View File

@@ -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"

View 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
View 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,
}

View 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 }

View File

@@ -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,
}

View File

@@ -10,6 +10,7 @@ export type Materia = {
tipo: 'Obligatoria' | 'Optativa' | 'Especialidad';
ciclo: number;
linea: string;
estado: string;
};
interface MateriaCardProps {

View 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>
)
}

View 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>
)
}

View 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>
)
}

View 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>
)
}

View File

@@ -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>

View File

@@ -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 (

View 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>
)
}

View File

@@ -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>
)
}
}