Se fucionan rutas

This commit is contained in:
2026-01-06 14:44:39 -06:00
parent fa53ddfb0b
commit a87bcdc1b9
15 changed files with 523 additions and 347 deletions

View File

@@ -0,0 +1,137 @@
import * as Dialog from '@radix-ui/react-dialog';
import { Pencil, X, Info } from 'lucide-react';
export type Materia = {
id: string;
clave: string;
nombre: string;
creditos: number;
hd: number; // Horas Docente
hi: number; // Horas Independientes
tipo: 'Obligatoria' | 'Optativa' | 'Especialidad';
ciclo: number;
linea: string;
estado: string;
};
interface MateriaCardProps {
materia: Materia;
}
export function MateriaCard({ materia }: MateriaCardProps) {
return (
<Dialog.Root>
{/* Trigger: La tarjeta en sí misma */}
<Dialog.Trigger asChild>
<div className="group relative flex flex-col p-2 mb-2 rounded-lg border border-slate-200 bg-white hover:border-emerald-500 hover:shadow-md transition-all cursor-pointer select-none">
{/* Header de la tarjeta */}
<div className="flex justify-between items-start mb-1">
<span className="text-[9px] font-mono font-bold text-slate-400 uppercase">{materia.clave}</span>
<div className="flex gap-1">
<span className="px-1.5 py-0.5 rounded-full bg-emerald-100 text-emerald-700 text-[8px] font-bold uppercase">
{materia.tipo === 'Obligatoria' ? 'OB' : 'OP'}
</span>
</div>
</div>
{/* Nombre */}
<h4 className="text-[11px] font-semibold text-slate-800 leading-tight mb-2 min-h-[2rem]">
{materia.nombre}
</h4>
{/* Footer de la tarjeta (Créditos y Horas) */}
<div className="flex justify-between items-center text-[9px] text-slate-500 border-t pt-1 border-slate-50">
<span>{materia.creditos} cr</span>
<div className="flex gap-1">
<span>HD:{materia.hd}</span>
<span>HI:{materia.hi}</span>
</div>
</div>
{/* Overlay de Hover (Opcional: un iconito de editar) */}
<div className="absolute top-1 right-1 opacity-0 group-hover:opacity-100 transition-opacity">
<Pencil className="w-3 h-3 text-emerald-600" />
</div>
</div>
</Dialog.Trigger>
{/* Modal / Portal */}
<Dialog.Portal>
<Dialog.Overlay className="fixed inset-0 bg-black/40 backdrop-blur-sm z-50 animate-in fade-in" />
<Dialog.Content className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-full max-w-md bg-white rounded-xl shadow-2xl p-6 z-50 border border-slate-200 animate-in zoom-in-95">
<div className="flex justify-between items-center mb-6">
<Dialog.Title className="text-lg font-bold text-slate-800">Editar Materia</Dialog.Title>
<Dialog.Close className="text-slate-400 hover:text-slate-600 transition-colors">
<X className="w-5 h-5" />
</Dialog.Close>
</div>
<form className="space-y-4">
{/* Clave y Nombre */}
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Clave</label>
<input
defaultValue={materia.clave}
className="px-3 py-2 rounded-lg border border-slate-300 focus:ring-2 focus:ring-emerald-500 outline-none text-sm font-mono"
/>
</div>
<div className="col-span-2 flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Nombre</label>
<input
defaultValue={materia.nombre}
className="px-3 py-2 rounded-lg border border-slate-300 focus:ring-2 focus:ring-emerald-500 outline-none text-sm"
/>
</div>
</div>
{/* Créditos y Horas */}
<div className="grid grid-cols-3 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase italic">Créditos</label>
<input type="number" defaultValue={materia.creditos} className="px-3 py-2 rounded-lg border border-slate-300 text-sm" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase italic">HD (Hrs Docente)</label>
<input type="number" defaultValue={materia.hd} className="px-3 py-2 rounded-lg border border-slate-300 text-sm" />
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase italic">HI (Hrs Indep.)</label>
<input type="number" defaultValue={materia.hi} className="px-3 py-2 rounded-lg border border-slate-300 text-sm" />
</div>
</div>
{/* Ciclo y Línea */}
<div className="grid grid-cols-2 gap-4">
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Ciclo</label>
<select className="px-3 py-2 rounded-lg border border-slate-300 text-sm bg-white">
<option>Ciclo {materia.ciclo}</option>
</select>
</div>
<div className="flex flex-col gap-1.5">
<label className="text-xs font-bold text-slate-600 uppercase">Línea Curricular</label>
<select className="px-3 py-2 rounded-lg border border-slate-300 text-sm bg-white">
<option>{materia.linea}</option>
</select>
</div>
</div>
{/* Botones de acción */}
<div className="flex justify-end gap-3 pt-6">
<Dialog.Close className="px-4 py-2 rounded-lg text-sm font-semibold text-slate-600 hover:bg-slate-100 transition-colors">
Cancelar
</Dialog.Close>
<button
type="button"
className="px-6 py-2 rounded-lg text-sm font-semibold bg-emerald-700 text-white hover:bg-emerald-800 transition-colors shadow-sm"
>
Guardar
</button>
</div>
</form>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}

View File

@@ -0,0 +1,41 @@
import { createFileRoute } from '@tanstack/react-router'
export const Route = createFileRoute('/planes/$planId/_detalle/datos')({
component: DatosGenerales,
})
function DatosGenerales() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<Card title="Objetivo General">
Formar profesionales altamente capacitados...
</Card>
<Card title="Perfil de Ingreso">
Egresados de educación media superior...
</Card>
<Card title="Perfil de Egreso">
Profesional capaz de diseñar...
</Card>
<Card title="Competencias Genéricas">
Pensamiento crítico, comunicación efectiva...
</Card>
</div>
)
}
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>
<p className="text-sm text-gray-600">{children}</p>
</div>
)
}

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('/planes/$planId/_detalle/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('/planes/$planId/_detalle/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('/planes/$planId/_detalle/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('/planes/$planId/_detalle/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

@@ -0,0 +1,123 @@
import { createFileRoute } from '@tanstack/react-router'
import { MateriaCard } from './MateriaCard';
import type { Materia } from './MateriaCard'; // Agregamos 'type' aquí
export const Route = createFileRoute('/planes/$planId/_detalle/mapa')({
component: MapaCurricular,
})
const CICLOS = ["Ciclo 1", "Ciclo 2", "Ciclo 3", "Ciclo 4", "Ciclo 5", "Ciclo 6", "Ciclo 7", "Ciclo 8", "Ciclo 9"];
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: 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 (
<div className="p-4 overflow-x-auto">
<h2 className="text-xl font-semibold mb-6">Mapa Curricular</h2>
{/* Contenedor de la Grid */}
<div
className="grid min-w-[1200px] border-l border-t border-slate-200"
style={{
// 1 columna para nombres de líneas + 9 ciclos
gridTemplateColumns: '200px repeat(9, 1fr)',
}}
>
{/* Header: Espacio vacío + Ciclos */}
<div className="bg-slate-50 p-2 border-r border-b border-slate-200 font-medium text-sm text-slate-500">
Línea Curricular
</div>
{CICLOS.map((ciclo) => (
<div key={ciclo} className="bg-slate-50 p-2 border-r border-b border-slate-200 text-center font-medium text-sm text-slate-500">
{ciclo}
</div>
))}
{/* Filas por cada Línea Curricular */}
{LINEAS.map((linea) => (
<>
{/* Nombre de la línea (Primera columna) */}
<div className="bg-slate-50 p-3 border-r border-b border-slate-200 flex items-center text-xs font-bold uppercase text-slate-600">
{linea}
</div>
{/* Celdas para cada ciclo en esta línea */}
{[1, 2, 3, 4, 5, 6, 7, 8, 9].map((numCiclo) => (
<div
key={`${linea}-${numCiclo}`}
className="p-2 border-r border-b border-slate-100 min-h-[120px] bg-white/50"
>
{/* Filtrar materias que pertenecen a esta posición */}
{MATERIAS.filter(m => m.linea === linea && m.ciclo === numCiclo).map((materia) => (
<MateriaCard key={materia.id} materia={materia} />
))}
</div>
))}
</>
))}
</div>
{/* Sección de materias sin asignar (como en tu imagen) */}
<div className="mt-8">
<h3 className="text-sm font-bold text-slate-500 mb-3 uppercase tracking-wider">Materias sin asignar</h3>
<div className="flex gap-4">
<div className="p-3 border rounded-lg bg-slate-50 border-dashed border-slate-300 w-48 text-[10px]">
<div className="font-bold">Inglés Técnico</div>
<div className="text-slate-500">4 cr HD: 2 HI: 2</div>
</div>
</div>
</div>
</div>
)
}

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('/planes/$planId/_detalle/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

@@ -0,0 +1,119 @@
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('/planes/$planId/_detalle')({
component: RouteComponent,
})
function RouteComponent() {
const { planId } = Route.useParams()
return (
<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="/planes"
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>
{/* 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>
{/* 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>
{/* 4. Navegación de Tabs */}
<div className="border-b overflow-x-auto scrollbar-hide">
<nav className="flex gap-8 min-w-max">
<Tab to="/planes/$planId/datos" params={{ planId }}>Datos Generales</Tab>
<Tab to="/planes/$planId/mapa" params={{ planId }}>Mapa Curricular</Tab>
<Tab to="/planes/$planId/materias" params={{ planId }}>Materias</Tab>
<Tab to="/planes/$planId/flujo" params={{ planId }}>Flujo y Estados</Tab>
<Tab to="/planes/$planId/iaplan" params={{ planId }}>IA del Plan</Tab>
<Tab to="/planes/$planId/documento" params={{ planId }}>Documento</Tab>
<Tab to="/planes/$planId/historial" params={{ planId }}>Historial</Tab>
</nav>
</div>
{/* 5. Contenido del Tab */}
<main className="pt-2 animate-in fade-in duration-500">
<Outlet />
</main>
</div>
</div>
)
}
// 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-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-bold',
}}
>
{children}
</Link>
)
}