Wizard listo para enviar información a ai-generate-plan

This commit is contained in:
2026-01-15 15:54:36 -06:00
parent b08d58e262
commit 9aad9aed00
14 changed files with 1199 additions and 1142 deletions

View File

@@ -1,6 +1,7 @@
// scripts/update-types.ts // scripts/update-types.ts
// Uso: /* Uso:
// bun run scripts/update-types.ts bun run scripts/update-types.ts
*/
import { $ } from "bun"; import { $ } from "bun";
console.log("🔄 Generando tipos de Supabase..."); console.log("🔄 Generando tipos de Supabase...");

View File

@@ -1,7 +1,9 @@
import { TemplateSelectorCard } from './TemplateSelectorCard' import type {
EstructuraPlanRow,
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain' FacultadRow,
import type { CARRERAS } from '@/features/planes/nuevo/catalogs' NivelPlanEstudio,
TipoCiclo,
} from '@/data/types/domain'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types' import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
@@ -13,31 +15,23 @@ import {
SelectTrigger, SelectTrigger,
SelectValue, SelectValue,
} from '@/components/ui/select' } from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { useCatalogosPlanes } from '@/data/hooks/usePlans' import { useCatalogosPlanes } from '@/data/hooks/usePlans'
import { import { NIVELES, TIPOS_CICLO } from '@/features/planes/nuevo/catalogs'
FACULTADES,
NIVELES,
TIPOS_CICLO,
PLANTILLAS_ANEXO_1,
PLANTILLAS_ANEXO_2,
} from '@/features/planes/nuevo/catalogs'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
export function PasoBasicosForm({ export function PasoBasicosForm({
wizard, wizard,
onChange, onChange,
carrerasFiltradas,
}: { }: {
wizard: NewPlanWizardState wizard: NewPlanWizardState
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>> onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
carrerasFiltradas: typeof CARRERAS
}) { }) {
const { data: catalogos } = useCatalogosPlanes() const { data: catalogos } = useCatalogosPlanes()
// Preferir los catálogos remotos si están disponibles; si no, usar los locales // Preferir los catálogos remotos si están disponibles; si no, usar los locales
const facultadesList = catalogos?.facultades ?? FACULTADES const facultadesList = catalogos?.facultades ?? []
const rawCarreras = catalogos?.carreras ?? carrerasFiltradas const rawCarreras = catalogos?.carreras ?? []
const estructurasPlanList = catalogos?.estructurasPlan ?? []
const filteredCarreras = rawCarreras.filter((c: any) => { const filteredCarreras = rawCarreras.filter((c: any) => {
const facId = wizard.datosBasicos.facultadId const facId = wizard.datosBasicos.facultadId
@@ -57,10 +51,15 @@ export function PasoBasicosForm({
placeholder="Ej. Ingeniería en Sistemas (2026)" placeholder="Ej. Ingeniería en Sistemas (2026)"
value={wizard.datosBasicos.nombrePlan} value={wizard.datosBasicos.nombrePlan}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange((w) => ({ onChange(
...w, (w): NewPlanWizardState => ({
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value }, ...w,
})) datosBasicos: {
...w.datosBasicos,
nombrePlan: e.target.value,
},
}),
)
} }
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic" className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
/> />
@@ -71,14 +70,16 @@ export function PasoBasicosForm({
<Select <Select
value={wizard.datosBasicos.facultadId} value={wizard.datosBasicos.facultadId}
onValueChange={(value) => onValueChange={(value) =>
onChange((w) => ({ onChange(
...w, (w): NewPlanWizardState => ({
datosBasicos: { ...w,
...w.datosBasicos, datosBasicos: {
facultadId: value, ...w.datosBasicos,
carreraId: '', facultadId: value,
}, carreraId: '',
})) },
}),
)
} }
> >
<SelectTrigger <SelectTrigger
@@ -93,7 +94,7 @@ export function PasoBasicosForm({
<SelectValue placeholder="Ej. Facultad de Ingeniería" /> <SelectValue placeholder="Ej. Facultad de Ingeniería" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
{facultadesList.map((f: any) => ( {facultadesList.map((f: FacultadRow) => (
<SelectItem key={f.id} value={f.id}> <SelectItem key={f.id} value={f.id}>
{f.nombre} {f.nombre}
</SelectItem> </SelectItem>
@@ -107,10 +108,12 @@ export function PasoBasicosForm({
<Select <Select
value={wizard.datosBasicos.carreraId} value={wizard.datosBasicos.carreraId}
onValueChange={(value) => onValueChange={(value) =>
onChange((w) => ({ onChange(
...w, (w): NewPlanWizardState => ({
datosBasicos: { ...w.datosBasicos, carreraId: value }, ...w,
})) datosBasicos: { ...w.datosBasicos, carreraId: value },
}),
)
} }
disabled={!wizard.datosBasicos.facultadId} disabled={!wizard.datosBasicos.facultadId}
> >
@@ -174,13 +177,15 @@ export function PasoBasicosForm({
<Select <Select
value={wizard.datosBasicos.tipoCiclo} value={wizard.datosBasicos.tipoCiclo}
onValueChange={(value: TipoCiclo) => onValueChange={(value: TipoCiclo) =>
onChange((w) => ({ onChange(
...w, (w): NewPlanWizardState => ({
datosBasicos: { ...w,
...w.datosBasicos, datosBasicos: {
tipoCiclo: value as any, ...w.datosBasicos,
}, tipoCiclo: value as any,
})) },
}),
)
} }
> >
<SelectTrigger <SelectTrigger
@@ -212,22 +217,63 @@ export function PasoBasicosForm({
min={1} min={1}
value={wizard.datosBasicos.numCiclos ?? ''} value={wizard.datosBasicos.numCiclos ?? ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange((w) => ({ onChange(
...w, (w): NewPlanWizardState => ({
datosBasicos: { ...w,
...w.datosBasicos, datosBasicos: {
// Keep undefined when the input is empty so the field stays optional ...w.datosBasicos,
numCiclos: // Keep undefined when the input is empty so the field stays optional
e.target.value === '' ? undefined : Number(e.target.value), numCiclos:
}, e.target.value === ''
})) ? undefined
: Number(e.target.value),
},
}),
)
} }
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic" className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
placeholder="Ej. 8" placeholder="Ej. 8"
/> />
</div> </div>
<div className="grid gap-1">
<Label htmlFor="estructuraPlan">Estructura de plan de estudios</Label>
<Select
value={wizard.datosBasicos.estructuraPlanId ?? ''}
onValueChange={(value: string) =>
onChange(
(w): NewPlanWizardState => ({
...w,
datosBasicos: {
...w.datosBasicos,
estructuraPlanId: value,
},
}),
)
}
>
<SelectTrigger
id="tipoCiclo"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.estructuraPlanId
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}
>
<SelectValue placeholder="Ej. Plan base SEP/ULSA (2026)" />
</SelectTrigger>
<SelectContent>
{estructurasPlanList.map((t: EstructuraPlanRow) => (
<SelectItem key={t.id} value={t.id}>
{t.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
</div> </div>
<Separator className="my-3" /> {/* <Separator className="my-3" />
<div className="grid gap-4 sm:grid-cols-2"> <div className="grid gap-4 sm:grid-cols-2">
<TemplateSelectorCard <TemplateSelectorCard
cardTitle="Plantilla de plan de estudios" cardTitle="Plantilla de plan de estudios"
@@ -263,7 +309,7 @@ export function PasoBasicosForm({
})) }))
} }
/> />
</div> </div> */}
</div> </div>
) )
} }

View File

@@ -2,13 +2,13 @@ import { Upload, File, X, FileText } from 'lucide-react'
import { useState, useCallback, useEffect, useRef } from 'react' import { useState, useCallback, useEffect, useRef } from 'react'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { formatFileSize } from '@/features/planes/utils/format-file-size'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
interface UploadedFile { export interface UploadedFile {
id: string id: string // Necesario para React (key)
name: string file: File // La fuente de verdad (contiene name, size, type)
size: string preview?: string // Opcional: si fueran imágenes
type: string
} }
interface FileDropzoneProps { interface FileDropzoneProps {
@@ -37,9 +37,7 @@ export function FileDropzone({
typeof crypto !== 'undefined' && 'randomUUID' in crypto typeof crypto !== 'undefined' && 'randomUUID' in crypto
? (crypto as any).randomUUID() ? (crypto as any).randomUUID()
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, : `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: file.name, file,
size: formatFileSize(file.size),
type: file.name.split('.').pop() || 'file',
})) }))
setFiles((prev) => { setFiles((prev) => {
const room = Math.max(0, maxFiles - prev.length) const room = Math.max(0, maxFiles - prev.length)
@@ -97,12 +95,6 @@ export function FileDropzone({
if (onFilesChangeRef.current) onFilesChangeRef.current(files) if (onFilesChangeRef.current) onFilesChangeRef.current(files)
}, [files]) }, [files])
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
const getFileIcon = (type: string) => { const getFileIcon = (type: string) => {
switch (type.toLowerCase()) { switch (type.toLowerCase()) {
case 'pdf': case 'pdf':
@@ -170,23 +162,25 @@ export function FileDropzone({
{/* Uploaded files list */} {/* Uploaded files list */}
{files.length > 0 && ( {files.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{files.map((file) => ( {files.map((item) => (
<div <div
key={file.id} key={item.id}
className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3" className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3"
> >
{getFileIcon(file.type)} {getFileIcon(item.file.type)}
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="text-foreground truncate text-sm font-medium"> <p className="text-foreground truncate text-sm font-medium">
{file.name} {item.file.name}
</p>
<p className="text-muted-foreground text-xs">
{formatFileSize(item.file.size)}
</p> </p>
<p className="text-muted-foreground text-xs">{file.size}</p>
</div> </div>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="text-muted-foreground hover:text-destructive h-8 w-8" className="text-muted-foreground hover:text-destructive h-8 w-8"
onClick={() => removeFile(file.id)} onClick={() => removeFile(item.id)}
> >
<X className="h-4 w-4" /> <X className="h-4 w-4" />
</Button> </Button>

View File

@@ -1,6 +1,7 @@
import { FileDropzone } from './FileDropZone' import { FileDropzone } from './FileDropZone'
import ReferenciasParaIA from './ReferenciasParaIA' import ReferenciasParaIA from './ReferenciasParaIA'
import type { UploadedFile } from './FileDropZone'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types' import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
@@ -116,14 +117,16 @@ export function PasoDetallesPanel({
} }
}) })
} }
onFilesChange={(files) => onFilesChange={(files: Array<UploadedFile>) =>
onChange((w) => ({ onChange(
...w, (w): NewPlanWizardState => ({
iaConfig: { ...w,
...(w.iaConfig || ({} as any)), iaConfig: {
archivosAdjuntos: files, ...(w.iaConfig || ({} as any)),
}, archivosAdjuntos: files,
})) },
}),
)
} }
/> />
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">

View File

@@ -5,6 +5,8 @@ import BarraBusqueda from '../../BarraBusqueda'
import { FileDropzone } from './FileDropZone' import { FileDropzone } from './FileDropZone'
import type { UploadedFile } from './FileDropZone'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { import {
@@ -27,9 +29,7 @@ const ReferenciasParaIA = ({
selectedRepositorioIds?: Array<string> selectedRepositorioIds?: Array<string>
onToggleArchivo?: (id: string, checked: boolean) => void onToggleArchivo?: (id: string, checked: boolean) => void
onToggleRepositorio?: (id: string, checked: boolean) => void onToggleRepositorio?: (id: string, checked: boolean) => void
onFilesChange?: ( onFilesChange?: (files: Array<UploadedFile>) => void
files: Array<{ id: string; name: string; size: string; type: string }>,
) => void
}) => { }) => {
const [busquedaArchivos, setBusquedaArchivos] = useState('') const [busquedaArchivos, setBusquedaArchivos] = useState('')
const [busquedaRepositorios, setBusquedaRepositorios] = useState('') const [busquedaRepositorios, setBusquedaRepositorios] = useState('')

View File

@@ -8,12 +8,11 @@ import {
CardTitle, CardTitle,
} from '@/components/ui/card' } from '@/components/ui/card'
import { import {
PLANTILLAS_ANEXO_1,
PLANTILLAS_ANEXO_2,
PLANES_EXISTENTES, PLANES_EXISTENTES,
ARCHIVOS, ARCHIVOS,
REPOSITORIOS, REPOSITORIOS,
} from '@/features/planes/nuevo/catalogs' } from '@/features/planes/nuevo/catalogs'
import { formatFileSize } from '@/features/planes/utils/format-file-size'
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) { export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
return ( return (
@@ -32,12 +31,6 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
const repositoriosRef = const repositoriosRef =
wizard.iaConfig?.repositoriosReferencia ?? [] wizard.iaConfig?.repositoriosReferencia ?? []
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? [] const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
const plantillaPlan = PLANTILLAS_ANEXO_1.find(
(x) => x.id === wizard.datosBasicos.plantillaPlanId,
)
const plantillaMapa = PLANTILLAS_ANEXO_2.find(
(x) => x.id === wizard.datosBasicos.plantillaMapaId,
)
const contenido = ( const contenido = (
<> <>
<div> <div>
@@ -68,89 +61,56 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
{wizard.datosBasicos.tipoCiclo}) {wizard.datosBasicos.tipoCiclo})
</span> </span>
</div> </div>
<div className="mt-2">
<span className="text-muted-foreground">
Plantilla plan:{' '}
</span>
<span className="font-medium">
{(plantillaPlan?.name ||
wizard.datosBasicos.plantillaPlanId ||
'—') +
' · ' +
(wizard.datosBasicos.plantillaPlanVersion || '—')}
</span>
</div>
<div>
<span className="text-muted-foreground">
Mapa curricular:{' '}
</span>
<span className="font-medium">
{(plantillaMapa?.name ||
wizard.datosBasicos.plantillaMapaId ||
'—') +
' · ' +
(wizard.datosBasicos.plantillaMapaVersion || '—')}
</span>
</div>
<div className="mt-2"> <div className="mt-2">
<span className="text-muted-foreground">Modo: </span> <span className="text-muted-foreground">Modo: </span>
<span className="font-medium"> <span className="font-medium">
{wizard.modoCreacion === 'MANUAL' && 'Manual'} {wizard.tipoOrigen === 'MANUAL' && 'Manual'}
{wizard.modoCreacion === 'IA' && 'Generado con IA'} {wizard.tipoOrigen === 'IA' && 'Generado con IA'}
{wizard.modoCreacion === 'CLONADO' && {wizard.tipoOrigen === 'CLONADO_INTERNO' &&
wizard.subModoClonado === 'INTERNO' &&
'Clonado desde plan del sistema'} 'Clonado desde plan del sistema'}
{wizard.modoCreacion === 'CLONADO' && {wizard.tipoOrigen === 'CLONADO_TRADICIONAL' &&
wizard.subModoClonado === 'TRADICIONAL' &&
'Importado desde documentos tradicionales'} 'Importado desde documentos tradicionales'}
</span> </span>
</div> </div>
{wizard.modoCreacion === 'CLONADO' && {wizard.tipoOrigen === 'CLONADO_INTERNO' && (
wizard.subModoClonado === 'INTERNO' && ( <div className="mt-2">
<div className="mt-2"> <span className="text-muted-foreground">Plan origen: </span>
<span className="text-muted-foreground"> <span className="font-medium">
Plan origen:{' '} {(() => {
</span> const p = PLANES_EXISTENTES.find(
<span className="font-medium"> (x) => x.id === wizard.clonInterno?.planOrigenId,
{(() => { )
const p = PLANES_EXISTENTES.find( return (
(x) => x.id === wizard.clonInterno?.planOrigenId, p?.nombre || wizard.clonInterno?.planOrigenId || '—'
) )
return ( })()}
p?.nombre || wizard.clonInterno?.planOrigenId || '—' </span>
) </div>
})()} )}
</span> {wizard.tipoOrigen === 'CLONADO_TRADICIONAL' && (
</div> <div className="mt-2">
)} <div className="font-medium">Documentos adjuntos</div>
{wizard.modoCreacion === 'CLONADO' && <ul className="text-muted-foreground list-disc pl-5 text-xs">
wizard.subModoClonado === 'TRADICIONAL' && ( <li>
<div className="mt-2"> <span className="text-foreground">Word del plan:</span>{' '}
<div className="font-medium">Documentos adjuntos</div> {wizard.clonTradicional?.archivoWordPlanId?.name || '—'}
<ul className="text-muted-foreground list-disc pl-5 text-xs"> </li>
<li> <li>
<span className="text-foreground"> <span className="text-foreground">
Word del plan: Mapa curricular:
</span>{' '} </span>{' '}
{wizard.clonTradicional?.archivoWordPlanId?.name || {wizard.clonTradicional?.archivoMapaExcelId?.name ||
'—'} '—'}
</li> </li>
<li> <li>
<span className="text-foreground"> <span className="text-foreground">Asignaturas:</span>{' '}
Mapa curricular: {wizard.clonTradicional?.archivoAsignaturasExcelId
</span>{' '} ?.name || ''}
{wizard.clonTradicional?.archivoMapaExcelId?.name || </li>
'—'} </ul>
</li> </div>
<li> )}
<span className="text-foreground">Asignaturas:</span>{' '} {wizard.tipoOrigen === 'IA' && (
{wizard.clonTradicional?.archivoAsignaturasExcelId
?.name || '—'}
</li>
</ul>
</div>
)}
{wizard.modoCreacion === 'IA' && (
<div className="bg-muted/50 mt-2 rounded-md p-3"> <div className="bg-muted/50 mt-2 rounded-md p-3">
<div> <div>
<span className="text-muted-foreground">Enfoque: </span> <span className="text-muted-foreground">Enfoque: </span>
@@ -208,8 +168,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
<ul className="text-muted-foreground list-disc pl-5 text-xs"> <ul className="text-muted-foreground list-disc pl-5 text-xs">
{adjuntos.map((f) => ( {adjuntos.map((f) => (
<li key={f.id}> <li key={f.id}>
<span className="text-foreground">{f.name}</span>{' '} <span className="text-foreground">
<span>· {f.size}</span> {f.file.name}
</span>{' '}
<span>· {formatFileSize(f.file.size)}</span>
</li> </li>
))} ))}
</ul> </ul>

View File

@@ -14,6 +14,7 @@ import type {
TipoCiclo, TipoCiclo,
UUID, UUID,
} from "../types/domain"; } from "../types/domain";
import type { UploadedFile } from "@/components/planes/wizard/PasoDetallesPanel/FileDropZone";
const EDGE = { const EDGE = {
plans_create_manual: "plans_create_manual", plans_create_manual: "plans_create_manual",
@@ -220,6 +221,7 @@ export type AIGeneratePlanInput = {
notasAdicionales?: string; notasAdicionales?: string;
archivosReferencia?: Array<UUID>; archivosReferencia?: Array<UUID>;
repositoriosIds?: Array<UUID>; repositoriosIds?: Array<UUID>;
archivosAdjuntos: Array<UploadedFile>;
usarMCP?: boolean; usarMCP?: boolean;
}; };
}; };
@@ -339,15 +341,20 @@ export async function plans_get_document(
export async function getCatalogos() { export async function getCatalogos() {
const supabase = supabaseBrowser(); const supabase = supabaseBrowser();
const [facRes, carRes, estRes] = await Promise.all([ const [facultadesRes, carrerasRes, estadosRes, estructurasPlanRes] =
supabase.from("facultades").select("*").order("nombre"), await Promise.all([
supabase.from("carreras").select("*").order("nombre"), supabase.from("facultades").select("*").order("nombre"),
supabase.from("estados_plan").select("*").order("orden"), supabase.from("carreras").select("*").order("nombre"),
]); supabase.from("estados_plan").select("*").order("orden"),
supabase.from("estructuras_plan").select("*").order("creado_en", {
ascending: true,
}),
]);
return { return {
facultades: facRes.data ?? [], facultades: facultadesRes.data ?? [],
carreras: carRes.data ?? [], carreras: carrerasRes.data ?? [],
estados: estRes.data ?? [], estados: estadosRes.data ?? [],
estructurasPlan: estructurasPlanRes.data ?? [],
}; };
} }

View File

@@ -3,7 +3,7 @@ import { createClient } from "@supabase/supabase-js";
import { getEnv } from "./env"; import { getEnv } from "./env";
import type { SupabaseClient } from "@supabase/supabase-js"; import type { SupabaseClient } from "@supabase/supabase-js";
import type { Database } from "src/types/supabase.js"; import type { Database } from "src/types/supabase";
let _client: SupabaseClient<Database> | null = null; let _client: SupabaseClient<Database> | null = null;

View File

@@ -51,7 +51,6 @@ export default function NuevoPlanModalContainer() {
const { const {
wizard, wizard,
setWizard, setWizard,
carrerasFiltradas,
canContinueDesdeModo, canContinueDesdeModo,
canContinueDesdeBasicos, canContinueDesdeBasicos,
canContinueDesdeDetalles, canContinueDesdeDetalles,
@@ -125,7 +124,10 @@ export default function NuevoPlanModalContainer() {
{({ methods }) => { {({ methods }) => {
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1 const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
const totalSteps = Wizard.steps.length const totalSteps = Wizard.steps.length
const nextStep = Wizard.steps[currentIndex] const nextStep = Wizard.steps[currentIndex] ?? {
title: '',
description: '',
}
return ( return (
<> <>
@@ -154,7 +156,6 @@ export default function NuevoPlanModalContainer() {
<PasoBasicosForm <PasoBasicosForm
wizard={wizard} wizard={wizard}
onChange={setWizard} onChange={setWizard}
carrerasFiltradas={carrerasFiltradas}
/> />
</Wizard.Stepper.Panel> </Wizard.Stepper.Panel>
)} )}

View File

@@ -1,6 +1,4 @@
import { useMemo, useState } from "react"; import { useState } from "react";
import { CARRERAS } from "../catalogs";
import type { NewPlanWizardState, PlanPreview } from "../types"; import type { NewPlanWizardState, PlanPreview } from "../types";
import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain"; import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
@@ -16,10 +14,7 @@ export function useNuevoPlanWizard() {
nivel: "", nivel: "",
tipoCiclo: "", tipoCiclo: "",
numCiclos: undefined, numCiclos: undefined,
plantillaPlanId: "", estructuraPlanId: null,
plantillaPlanVersion: "",
plantillaMapaId: "",
plantillaMapaVersion: "",
}, },
// datosBasicos: { // datosBasicos: {
// nombrePlan: "Medicina", // nombrePlan: "Medicina",
@@ -51,11 +46,6 @@ export function useNuevoPlanWizard() {
errorMessage: null, errorMessage: null,
}); });
const carrerasFiltradas = useMemo(() => {
const fac = wizard.datosBasicos.facultadId;
return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS;
}, [wizard.datosBasicos.facultadId]);
const canContinueDesdeModo = wizard.tipoOrigen === "MANUAL" || const canContinueDesdeModo = wizard.tipoOrigen === "MANUAL" ||
wizard.tipoOrigen === "IA" || wizard.tipoOrigen === "IA" ||
(wizard.tipoOrigen === "CLONADO_INTERNO" || (wizard.tipoOrigen === "CLONADO_INTERNO" ||
@@ -68,10 +58,7 @@ export function useNuevoPlanWizard() {
(wizard.datosBasicos.numCiclos !== undefined && (wizard.datosBasicos.numCiclos !== undefined &&
wizard.datosBasicos.numCiclos > 0) && wizard.datosBasicos.numCiclos > 0) &&
// Requerir ambas plantillas (plan y mapa) con versión // Requerir ambas plantillas (plan y mapa) con versión
!!wizard.datosBasicos.plantillaPlanId && !!wizard.datosBasicos.estructuraPlanId;
!!wizard.datosBasicos.plantillaPlanVersion &&
!!wizard.datosBasicos.plantillaMapaId &&
!!wizard.datosBasicos.plantillaMapaVersion;
const canContinueDesdeDetalles = (() => { const canContinueDesdeDetalles = (() => {
if (wizard.tipoOrigen === "MANUAL") return true; if (wizard.tipoOrigen === "MANUAL") return true;
@@ -130,7 +117,6 @@ export function useNuevoPlanWizard() {
return { return {
wizard, wizard,
setWizard, setWizard,
carrerasFiltradas,
canContinueDesdeModo, canContinueDesdeModo,
canContinueDesdeBasicos, canContinueDesdeBasicos,
canContinueDesdeDetalles, canContinueDesdeDetalles,

View File

@@ -1,3 +1,4 @@
import type { UploadedFile } from "@/components/planes/wizard/PasoDetallesPanel/FileDropZone";
import type { import type {
NivelPlanEstudio, NivelPlanEstudio,
TipoCiclo, TipoCiclo,
@@ -24,10 +25,7 @@ export type NewPlanWizardState = {
tipoCiclo: TipoCiclo | ""; tipoCiclo: TipoCiclo | "";
numCiclos: number | undefined; numCiclos: number | undefined;
// Selección de plantillas (obligatorias) // Selección de plantillas (obligatorias)
plantillaPlanId?: string; estructuraPlanId: string | null;
plantillaPlanVersion?: string;
plantillaMapaId?: string;
plantillaMapaVersion?: string;
}; };
clonInterno?: { planOrigenId: string | null }; clonInterno?: { planOrigenId: string | null };
clonTradicional?: { clonTradicional?: {
@@ -58,7 +56,7 @@ export type NewPlanWizardState = {
archivosReferencia: Array<string>; archivosReferencia: Array<string>;
repositoriosReferencia?: Array<string>; repositoriosReferencia?: Array<string>;
archivosAdjuntos?: Array< archivosAdjuntos?: Array<
{ id: string; name: string; size: string; type: string } UploadedFile
>; >;
}; };
resumen: { previewPlan?: PlanPreview }; resumen: { previewPlan?: PlanPreview };

View File

@@ -0,0 +1,5 @@
export const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + " B";
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
};

View File

@@ -30,4 +30,29 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
/> />
</> </>
), ),
errorComponent: ({ error, reset }) => {
return (
<div className="flex min-h-[50vh] flex-col items-center justify-center space-y-4 p-6 text-center">
<h2 className="text-2xl font-bold text-red-600">
¡Ups! Algo salió mal
</h2>
<p className="max-w-md text-gray-600">
Ocurrió un error inesperado al cargar esta sección.
</p>
{/* Opcional: Mostrar el detalle técnico en desarrollo */}
<pre className="max-w-full overflow-auto rounded border border-gray-300 bg-gray-100 p-4 text-left text-xs">
{error.message}
</pre>
<button
onClick={reset}
className="rounded bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700"
>
Intentar de nuevo
</button>
</div>
)
},
}) })

File diff suppressed because it is too large Load Diff