Se fucionan rutas
This commit is contained in:
137
src/routes/planes/$planId/_detalle/MateriaCard.tsx
Normal file
137
src/routes/planes/$planId/_detalle/MateriaCard.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
41
src/routes/planes/$planId/_detalle/datos.tsx
Normal file
41
src/routes/planes/$planId/_detalle/datos.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
131
src/routes/planes/$planId/_detalle/documento.tsx
Normal file
131
src/routes/planes/$planId/_detalle/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('/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>
|
||||
)
|
||||
}
|
||||
134
src/routes/planes/$planId/_detalle/flujo.tsx
Normal file
134
src/routes/planes/$planId/_detalle/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('/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>
|
||||
)
|
||||
}
|
||||
142
src/routes/planes/$planId/_detalle/historial.tsx
Normal file
142
src/routes/planes/$planId/_detalle/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('/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>
|
||||
)
|
||||
}
|
||||
118
src/routes/planes/$planId/_detalle/iaplan.tsx
Normal file
118
src/routes/planes/$planId/_detalle/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('/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>
|
||||
)
|
||||
}
|
||||
123
src/routes/planes/$planId/_detalle/mapa.tsx
Normal file
123
src/routes/planes/$planId/_detalle/mapa.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
219
src/routes/planes/$planId/_detalle/materias.tsx
Normal file
219
src/routes/planes/$planId/_detalle/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('/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>
|
||||
)
|
||||
}
|
||||
119
src/routes/planes/$planId/_detalle/route.tsx
Normal file
119
src/routes/planes/$planId/_detalle/route.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +1,18 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/planes/$planId/asignaturas/$asignaturaId')({
|
||||
export const Route = createFileRoute(
|
||||
'/planes/$planId/asignaturas/$asignaturaId'
|
||||
)({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
return <div>Hello "/asignaturas/$asignaturaId"!</div>
|
||||
const { planId, asignaturaId } = Route.useParams()
|
||||
|
||||
return (
|
||||
<div>
|
||||
Plan: {planId} <br />
|
||||
Asignatura: {asignaturaId}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
146
src/routes/planes/$planId/asignaturas/index.tsx
Normal file
146
src/routes/planes/$planId/asignaturas/index.tsx
Normal file
@@ -0,0 +1,146 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent } from '@/components/ui/card'
|
||||
import {
|
||||
BookOpen,
|
||||
Sparkles,
|
||||
FileText,
|
||||
Library,
|
||||
LayoutTemplate,
|
||||
History,
|
||||
ArrowRight,
|
||||
GraduationCap,
|
||||
} from 'lucide-react'
|
||||
|
||||
export const Route = createFileRoute(
|
||||
'/planes/$planId/asignaturas/'
|
||||
)({
|
||||
component: MateriasLandingPage,
|
||||
})
|
||||
|
||||
export default function MateriasLandingPage() {
|
||||
return (
|
||||
<div className="w-full">
|
||||
{/* ================= HERO ================= */}
|
||||
<section className="bg-gradient-to-b from-[#0b1d3a] to-[#0e2a5c] text-white">
|
||||
<div className="max-w-7xl mx-auto px-6 py-28">
|
||||
<div className="flex items-center gap-2 mb-6 text-sm text-blue-200">
|
||||
<GraduationCap className="w-5 h-5 text-yellow-400" />
|
||||
<span>SISTEMA DE GESTIÓN CURRICULAR</span>
|
||||
</div>
|
||||
|
||||
<h1 className="text-5xl font-bold mb-6">
|
||||
Universidad La Salle
|
||||
</h1>
|
||||
|
||||
<p className="max-w-xl text-lg text-blue-100 mb-10">
|
||||
Diseña, documenta y mejora programas de estudio con herramientas
|
||||
de inteligencia artificial integradas y cumplimiento normativo SEP.
|
||||
</p>
|
||||
|
||||
<Button
|
||||
size="lg"
|
||||
className="bg-yellow-400 text-black hover:bg-yellow-300 font-semibold"
|
||||
>
|
||||
Ver materia de ejemplo
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ================= FEATURES ================= */}
|
||||
<section className="bg-white py-24">
|
||||
<div className="max-w-7xl mx-auto px-6">
|
||||
<h2 className="text-center text-2xl font-semibold mb-14">
|
||||
Características principales
|
||||
</h2>
|
||||
|
||||
<div className="grid gap-8 md:grid-cols-3">
|
||||
<FeatureCard
|
||||
icon={<BookOpen />}
|
||||
title="Gestión de Materias"
|
||||
description="Edita datos generales, contenido temático y bibliografía con una interfaz intuitiva."
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={<Sparkles />}
|
||||
title="IA Integrada"
|
||||
description="Usa inteligencia artificial para mejorar objetivos, competencias y alinear con perfiles de egreso."
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={<FileText />}
|
||||
title="Documentos SEP"
|
||||
description="Genera automáticamente documentos oficiales para la Secretaría de Educación Pública."
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={<Library />}
|
||||
title="Biblioteca Digital"
|
||||
description="Busca y vincula recursos del repositorio de Biblioteca La Salle directamente."
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={<LayoutTemplate />}
|
||||
title="Plantillas Flexibles"
|
||||
description="Adapta la estructura de materias según plantillas SEP o institucionales."
|
||||
/>
|
||||
|
||||
<FeatureCard
|
||||
icon={<History />}
|
||||
title="Historial Completo"
|
||||
description="Rastrea todos los cambios con historial detallado por usuario y fecha."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* ================= CTA ================= */}
|
||||
<section className="bg-gray-50 py-20">
|
||||
<div className="max-w-3xl mx-auto text-center px-6">
|
||||
<h3 className="text-xl font-semibold mb-4">
|
||||
Explora la vista de detalle de materia
|
||||
</h3>
|
||||
|
||||
<p className="text-muted-foreground mb-8">
|
||||
Navega por las diferentes pestañas para ver cómo funciona el sistema
|
||||
de gestión curricular.
|
||||
</p>
|
||||
|
||||
<Button size="lg" className="bg-[#0e2a5c] hover:bg-[#0b1d3a]">
|
||||
Ir a Inteligencia Artificial Aplicada
|
||||
<ArrowRight className="ml-2 h-4 w-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
/* ================= FEATURE CARD ================= */
|
||||
|
||||
function FeatureCard({
|
||||
icon,
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
icon: React.ReactNode
|
||||
title: string
|
||||
description: string
|
||||
}) {
|
||||
return (
|
||||
<Card className="border border-gray-200 shadow-sm">
|
||||
<CardContent className="p-6 space-y-4">
|
||||
<div className="w-10 h-10 rounded-md bg-yellow-100 text-yellow-600 flex items-center justify-center">
|
||||
{icon}
|
||||
</div>
|
||||
|
||||
<h4 className="font-semibold">{title}</h4>
|
||||
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{description}
|
||||
</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
9
src/routes/planes/$planId/asignaturas/route.tsx
Normal file
9
src/routes/planes/$planId/asignaturas/route.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { createFileRoute, Outlet } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/planes/$planId/asignaturas')({
|
||||
component: AsignaturasLayout,
|
||||
})
|
||||
|
||||
function AsignaturasLayout() {
|
||||
return <Outlet />
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import { createFileRoute } from '@tanstack/react-router'
|
||||
|
||||
export const Route = createFileRoute('/planes/$planId/')({
|
||||
component: RouteComponent,
|
||||
})
|
||||
|
||||
function RouteComponent() {
|
||||
const { planId } = Route.useParams()
|
||||
return <div>Hello "/planes/{planId}"!</div>
|
||||
}
|
||||
Reference in New Issue
Block a user