Barra de busqueda para filtrar referencias para la IA, cambios a FileDropZone

This commit is contained in:
2026-01-08 13:41:37 -06:00
parent edae79c255
commit cddc676f7d
10 changed files with 417 additions and 148 deletions

View File

@@ -20,6 +20,7 @@ import {
PLANTILLAS_ANEXO_1,
PLANTILLAS_ANEXO_2,
} from '@/features/planes/nuevo/catalogs'
import { cn } from '@/lib/utils'
export function PasoBasicosForm({
wizard,
@@ -34,7 +35,9 @@ export function PasoBasicosForm({
<div className="flex flex-col gap-2">
<div className="grid gap-4 sm:grid-cols-2">
<div className="grid gap-1 sm:col-span-2">
<Label htmlFor="nombrePlan">Nombre del plan</Label>
<Label htmlFor="nombrePlan">
Nombre del plan <span className="text-destructive">*</span>
</Label>
<Input
id="nombrePlan"
placeholder="Ej. Ingeniería en Sistemas 2026"
@@ -45,6 +48,7 @@ export function PasoBasicosForm({
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
}))
}
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
/>
</div>
@@ -65,9 +69,14 @@ export function PasoBasicosForm({
>
<SelectTrigger
id="facultad"
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.facultadId
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}
>
<SelectValue placeholder="Selecciona facultad…" />
<SelectValue placeholder="Ej. Facultad de Ingeniería" />
</SelectTrigger>
<SelectContent>
{FACULTADES.map((f) => (
@@ -93,9 +102,14 @@ export function PasoBasicosForm({
>
<SelectTrigger
id="carrera"
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.carreraId
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}
>
<SelectValue placeholder="Selecciona carrera…" />
<SelectValue placeholder="Ej. Ingeniería en Cibernética y Sistemas Computacionales" />
</SelectTrigger>
<SelectContent>
{carrerasFiltradas.map((c) => (
@@ -120,9 +134,14 @@ export function PasoBasicosForm({
>
<SelectTrigger
id="nivel"
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.nivel
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}
>
<SelectValue placeholder="Selecciona nivel…" />
<SelectValue placeholder="Ej. Licenciatura" />
</SelectTrigger>
<SelectContent>
{NIVELES.map((n) => (
@@ -150,9 +169,14 @@ export function PasoBasicosForm({
>
<SelectTrigger
id="tipoCiclo"
className="w-full min-w-0 [&>span]:block! [&>span]:truncate!"
className={cn(
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
!wizard.datosBasicos.tipoCiclo
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
: 'font-medium not-italic', // Tiene Valor (Medium)
)}
>
<SelectValue placeholder="Selecciona tipo de ciclo…" />
<SelectValue placeholder="Ej. Semestre" />
</SelectTrigger>
<SelectContent>
{TIPOS_CICLO.map((t) => (
@@ -170,17 +194,20 @@ export function PasoBasicosForm({
id="numCiclos"
type="number"
min={1}
value={wizard.datosBasicos.numCiclos}
value={wizard.datosBasicos.numCiclos ?? ''}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange((w) => ({
...w,
datosBasicos: {
...w.datosBasicos,
numCiclos: Number(e.target.value || 1),
// Keep undefined when the input is empty so the field stays optional
numCiclos:
e.target.value === '' ? undefined : Number(e.target.value),
},
}))
}
placeholder="1"
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
placeholder="Ej. 8"
/>
</div>
</div>
@@ -190,11 +217,35 @@ export function PasoBasicosForm({
cardTitle="Plantilla de plan de estudios"
cardDescription="Selecciona el Word para tu nuevo plan."
templatesData={PLANTILLAS_ANEXO_1}
selectedTemplateId={wizard.datosBasicos.plantillaPlanId || ''}
selectedVersion={wizard.datosBasicos.plantillaPlanVersion || ''}
onChange={({ templateId, version }) =>
onChange((w) => ({
...w,
datosBasicos: {
...w.datosBasicos,
plantillaPlanId: templateId,
plantillaPlanVersion: version,
},
}))
}
/>
<TemplateSelectorCard
cardTitle="Mapa curricular"
cardTitle="Plantilla de mapa curricular"
cardDescription="Selecciona el Excel para tu mapa curricular."
templatesData={PLANTILLAS_ANEXO_2}
selectedTemplateId={wizard.datosBasicos.plantillaMapaId || ''}
selectedVersion={wizard.datosBasicos.plantillaMapaVersion || ''}
onChange={({ templateId, version }) =>
onChange((w) => ({
...w,
datosBasicos: {
...w.datosBasicos,
plantillaMapaId: templateId,
plantillaMapaVersion: version,
},
}))
}
/>
</div>
</div>

View File

@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useMemo, useState } from 'react'
import {
Card,
@@ -46,32 +46,49 @@ interface Props {
cardTitle?: string
cardDescription?: string
templatesData?: Array<TemplateData>
// Controlled selection (optional). If not provided, component manages its own state
selectedTemplateId?: string
selectedVersion?: string
onChange?: (sel: { templateId: string; version: string }) => void
}
export function TemplateSelectorCard({
cardTitle = 'Configuración del Documento',
cardDescription = 'Selecciona la base para tu nuevo plan.',
templatesData = DEFAULT_TEMPLATES_DATA,
selectedTemplateId,
selectedVersion,
onChange,
}: Props) {
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
const [selectedVersion, setSelectedVersion] = useState<string>('')
const [internalTemplate, setInternalTemplate] = useState<string>('')
const [internalVersion, setInternalVersion] = useState<string>('')
const selectedTemplate = selectedTemplateId ?? internalTemplate
const version = selectedVersion ?? internalVersion
// Buscamos las versiones de la plantilla seleccionada
const currentTemplateData = templatesData.find(
(t) => t.id === selectedTemplate,
const currentTemplateData = useMemo(
() => templatesData.find((t) => t.id === selectedTemplate),
[templatesData, selectedTemplate],
)
const availableVersions = currentTemplateData?.versions || []
const handleTemplateChange = (value: string) => {
setSelectedTemplate(value)
// Buscamos los datos de esta plantilla
const template = templatesData.find((t) => t.id === value)
// Si tiene versiones, seleccionamos la primera automáticamente
if (template && template.versions.length > 0) {
setSelectedVersion(template.versions[0])
const firstVersion = template?.versions?.[0] ?? ''
if (onChange) {
onChange({ templateId: value, version: firstVersion })
} else {
setSelectedVersion('')
setInternalTemplate(value)
setInternalVersion(firstVersion)
}
}
const handleVersionChange = (value: string) => {
if (onChange) {
onChange({ templateId: selectedTemplate, version: value })
} else {
setInternalVersion(value)
}
}
@@ -125,8 +142,8 @@ export function TemplateSelectorCard({
</div>
<Select
value={selectedVersion}
onValueChange={setSelectedVersion}
value={version}
onValueChange={handleVersionChange}
disabled={!selectedTemplate}
>
<SelectTrigger

View File

@@ -20,7 +20,7 @@ interface FileDropzoneProps {
}
export function FileDropzone({
// onFilesChange,
onFilesChange,
acceptedTypes = '.doc,.docx,.pdf',
maxFiles = 5,
title = 'Arrastra archivos aquí',
@@ -39,16 +39,12 @@ export function FileDropzone({
setIsDragging(false)
}, [])
const handleDrop = useCallback(
(e: React.DragEvent) => {
const handleDrop = useCallback((e: React.DragEvent) => {
e.preventDefault()
setIsDragging(false)
const droppedFiles = Array.from(e.dataTransfer.files)
addFiles(droppedFiles)
},
[files],
)
}, [])
const handleFileInput = useCallback(
(e: React.ChangeEvent<HTMLInputElement>) => {
@@ -57,29 +53,40 @@ export function FileDropzone({
addFiles(selectedFiles)
}
},
[files],
[],
)
const addFiles = (newFiles: Array<File>) => {
const uploadedFiles: Array<UploadedFile> = newFiles
.slice(0, maxFiles - files.length)
.map((file) => ({
id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
const addFiles = useCallback(
(newFiles: Array<File>) => {
const toUpload: Array<UploadedFile> = newFiles.map((file) => ({
id:
typeof crypto !== 'undefined' && 'randomUUID' in crypto
? (crypto as any).randomUUID()
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
name: file.name,
size: formatFileSize(file.size),
type: file.name.split('.').pop() || 'file',
}))
setFiles((prev) => {
const room = Math.max(0, maxFiles - prev.length)
const next = [...prev, ...toUpload.slice(0, room)].slice(0, maxFiles)
if (onFilesChange) onFilesChange(next)
return next
})
},
[maxFiles, onFilesChange],
)
const updatedFiles = [...files, ...uploadedFiles].slice(0, maxFiles)
setFiles(updatedFiles)
// onFilesChange(updatedFiles)
}
const removeFile = (fileId: string) => {
const updatedFiles = files.filter((f) => f.id !== fileId)
setFiles(updatedFiles)
// onFilesChange(updatedFiles)
}
const removeFile = useCallback(
(fileId: string) => {
setFiles((prev) => {
const next = prev.filter((f) => f.id !== fileId)
if (onFilesChange) onFilesChange(next)
return next
})
},
[onFilesChange],
)
const formatFileSize = (bytes: number): string => {
if (bytes < 1024) return bytes + ' B'

View File

@@ -1,3 +1,4 @@
import { FileDropzone } from './FileDropZone'
import ReferenciasParaIA from './ReferenciasParaIA'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
@@ -82,7 +83,17 @@ export function PasoDetallesPanel({
}
/>
</div>
<ReferenciasParaIA />
<ReferenciasParaIA
onFilesChange={(files) =>
onChange((w) => ({
...w,
iaConfig: {
...(w.iaConfig || ({} as any)),
archivosAdjuntos: files,
},
}))
}
/>
<div className="flex items-center justify-between">
<div className="text-muted-foreground text-sm">
Opcional: se pueden adjuntar recursos IA más adelante.
@@ -229,10 +240,10 @@ export function PasoDetallesPanel({
wizard.subModoClonado === 'TRADICIONAL'
) {
return (
<div className="grid gap-4">
<div>
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
<Label htmlFor="word">Word del plan (obligatorio)</Label>
<input
{/* <input
id="word"
type="file"
accept=".doc,.docx"
@@ -248,6 +259,20 @@ export function PasoDetallesPanel({
},
}))
}
/> */}
<FileDropzone
acceptedTypes=".doc,.docx"
maxFiles={1}
onFilesChange={(file) => {
onChange((w) => ({
...w,
clonTradicional: {
...(w.clonTradicional || ({} as any)),
archivoWordPlanId: file,
},
}))
}}
/>
</div>
<div>

View File

@@ -1,4 +1,7 @@
import { FileText, FolderOpen, Upload } from 'lucide-react'
import { useMemo, useState } from 'react'
import BarraBusqueda from '../../BarraBusqueda'
import { FileDropzone } from './FileDropZone'
@@ -13,7 +16,41 @@ import {
} from '@/components/ui/motion-tabs'
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
const tabs = [
const ReferenciasParaIA = ({
onFilesChange,
}: {
onFilesChange?: (
files: Array<{ id: string; name: string; size: string; type: string }>,
) => void
}) => {
const [busquedaArchivos, setBusquedaArchivos] = useState('')
const [busquedaRepositorios, setBusquedaRepositorios] = useState('')
const cleanText = (text: string) => {
return text
.normalize('NFD') // Descompone "á" en "a" + "´"
.replace(/[\u0300-\u036f]/g, '') // Elimina los símbolos diacríticos
.toLowerCase() // Convierte a minúsculas
}
// Filtrado de archivos y de repositorios
const archivosFiltrados = useMemo(() => {
// Función helper para limpiar texto (quita acentos y hace minúsculas)
const term = cleanText(busquedaArchivos)
return ARCHIVOS.filter((archivo) =>
cleanText(archivo.nombre).includes(term),
)
}, [busquedaArchivos])
const repositoriosFiltrados = useMemo(() => {
const term = cleanText(busquedaRepositorios)
return REPOSITORIOS.filter((repositorio) =>
cleanText(repositorio.nombre).includes(term),
)
}, [busquedaRepositorios])
const tabs = [
{
name: 'Archivos existentes',
@@ -22,11 +59,18 @@ const tabs = [
icon: FileText,
content: (
<div className="flex flex-col gap-0.5">
{ARCHIVOS.map((archivo) => (
<div className="flex flex-col">
<BarraBusqueda
value={busquedaArchivos}
onChange={setBusquedaArchivos}
placeholder="Buscar archivo existente..."
className="m-1 mb-1.5"
/>
<div className="flex h-72 flex-col gap-0.5 overflow-y-auto">
{archivosFiltrados.map((archivo) => (
<Label
key={archivo.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950"
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
@@ -37,11 +81,14 @@ const tabs = [
{archivo.nombre}
</p>
<p className="text-muted-foreground text-xs">{archivo.tamaño}</p>
<p className="text-muted-foreground text-xs">
{archivo.tamaño}
</p>
</div>
</Label>
))}
</div>
</div>
),
},
@@ -53,11 +100,18 @@ const tabs = [
icon: FolderOpen,
content: (
<div className="flex flex-col gap-0.5">
{REPOSITORIOS.map((repositorio) => (
<div className="flex flex-col">
<BarraBusqueda
value={busquedaRepositorios}
onChange={setBusquedaRepositorios}
placeholder="Buscar repositorio..."
className="m-1 mb-1.5"
/>
<div className="flex h-72 flex-col gap-0.5 overflow-y-auto">
{repositoriosFiltrados.map((repositorio) => (
<Label
key={repositorio.id}
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-[[aria-checked=true]]:border-blue-600 has-[[aria-checked=true]]:bg-blue-50 dark:has-[[aria-checked=true]]:border-blue-900 dark:has-[[aria-checked=true]]:bg-blue-950"
className="border-border hover:border-primary/30 hover:bg-accent/50 m-0.5 flex cursor-pointer items-center gap-3 rounded-lg border p-3 transition-colors has-aria-checked:border-blue-600 has-aria-checked:bg-blue-50 dark:has-aria-checked:border-blue-900 dark:has-aria-checked:bg-blue-950"
>
<Checkbox className="peer border-primary ring-offset-background data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground focus-visible:ring-ring h-5 w-5 shrink-0 rounded-sm border focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none disabled:cursor-not-allowed disabled:opacity-50" />
@@ -75,6 +129,7 @@ const tabs = [
</Label>
))}
</div>
</div>
),
},
@@ -88,18 +143,15 @@ const tabs = [
content: (
<div>
<FileDropzone
// onFilesChange={(files) =>
// handleChange("archivosAdhocIds", files.map((f) => f.id))
// }
onFilesChange={onFilesChange}
title="Sube archivos de referencia"
description="Documentos que serán usados como contexto para la generación"
/>
</div>
),
},
]
]
const ReferenciasParaIA = () => {
return (
<div className="flex w-full flex-col gap-1">
<Label>Referencias para la IA</Label>

View File

@@ -7,6 +7,10 @@ import {
CardHeader,
CardTitle,
} from '@/components/ui/card'
import {
PLANTILLAS_ANEXO_1,
PLANTILLAS_ANEXO_2,
} from '@/features/planes/nuevo/catalogs'
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
const modo = wizard.modoCreacion
@@ -46,6 +50,35 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
{wizard.datosBasicos.numCiclos} ({wizard.datosBasicos.tipoCiclo})
</span>
</div>
{/* Plantillas seleccionadas */}
<div className="mt-2">
<span className="text-muted-foreground">Plantilla plan: </span>
<span className="font-medium">
{(() => {
const t = PLANTILLAS_ANEXO_1.find(
(x) => x.id === wizard.datosBasicos.plantillaPlanId,
)
const name =
t?.name || wizard.datosBasicos.plantillaPlanId || '—'
const ver = wizard.datosBasicos.plantillaPlanVersion || '—'
return `${name} · ${ver}`
})()}
</span>
</div>
<div>
<span className="text-muted-foreground">Mapa curricular: </span>
<span className="font-medium">
{(() => {
const t = PLANTILLAS_ANEXO_2.find(
(x) => x.id === wizard.datosBasicos.plantillaMapaId,
)
const name =
t?.name || wizard.datosBasicos.plantillaMapaId || '—'
const ver = wizard.datosBasicos.plantillaMapaVersion || '—'
return `${name} · ${ver}`
})()}
</span>
</div>
<div className="mt-2">
<span className="text-muted-foreground">Modo: </span>
<span className="font-medium">
@@ -59,6 +92,57 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
'Importado desde documentos tradicionales'}
</span>
</div>
{modo === 'IA' && (
<div className="bg-muted/50 mt-2 rounded-md p-3">
<div>
<span className="text-muted-foreground">Enfoque: </span>
<span className="font-medium">
{wizard.iaConfig?.descripcionEnfoque || '—'}
</span>
</div>
<div>
<span className="text-muted-foreground">Notas: </span>
<span className="font-medium">
{wizard.iaConfig?.notasAdicionales || '—'}
</span>
</div>
{!!(wizard.iaConfig?.archivosReferencia?.length || 0) && (
<div className="text-muted-foreground text-xs">
Archivos existentes:{' '}
{wizard.iaConfig?.archivosReferencia?.length}
</div>
)}
{!!(wizard.iaConfig?.repositoriosReferencia?.length || 0) && (
<div className="text-muted-foreground text-xs">
Repositorios:{' '}
{wizard.iaConfig?.repositoriosReferencia?.length}
</div>
)}
{!!(wizard.iaConfig?.archivosAdjuntos?.length || 0) && (
<div className="mt-2">
<div className="font-medium">Adjuntos</div>
<ul className="text-muted-foreground list-disc pl-5 text-xs">
{wizard.iaConfig?.archivosAdjuntos?.map((f) => (
<li key={f.id}>
<span className="text-foreground">{f.name}</span>{' '}
<span>· {f.size}</span>
</li>
))}
</ul>
</div>
)}
</div>
)}
{modo === 'CLONADO' && sub === 'TRADICIONAL' && (
<div className="mt-2">
<span className="text-muted-foreground">
Archivo Word del plan:{' '}
</span>
<span className="font-medium">
{wizard.clonTradicional?.archivoWordPlanId?.name || '—'}
</span>
</div>
)}
{wizard.resumen.previewPlan && (
<div className="bg-muted mt-2 rounded-md p-3">
<div className="font-medium">Preview IA</div>

View File

@@ -115,20 +115,6 @@ export const REPOSITORIOS = [
},
];
export const ESTRUCTURAS_PLAN_ESTUDIO = [
{
id: "estruc-1",
nombre: "Estructura RVOE 2017.docx",
versiones: ["v1.0", "v1.1", "v2.0"],
},
{
id: "estruc-2",
nombre: "Estructura RVOE 2026.docx",
versiones: ["v1.0", "v1.1"],
},
{ id: "estruc-3", nombre: "Estructura ULSA 2022.docx", versiones: ["v1.0"] },
];
export const PLANTILLAS_ANEXO_1 = [
{
id: "sep-2025",

View File

@@ -8,6 +8,18 @@ export function useNuevoPlanWizard() {
const [wizard, setWizard] = useState<NewPlanWizardState>({
step: 1,
modoCreacion: null,
// datosBasicos: {
// nombrePlan: "",
// carreraId: "",
// facultadId: "",
// nivel: "",
// tipoCiclo: "",
// numCiclos: undefined,
// plantillaPlanId: "",
// plantillaPlanVersion: "",
// plantillaMapaId: "",
// plantillaMapaVersion: "",
// },
datosBasicos: {
nombrePlan: "Medicina",
carreraId: "medico",
@@ -15,6 +27,10 @@ export function useNuevoPlanWizard() {
nivel: "Licenciatura",
tipoCiclo: "SEMESTRE",
numCiclos: 8,
plantillaPlanId: "sep-2025",
plantillaPlanVersion: "v2025.2 (Vigente)",
plantillaMapaId: "sep-2017-xlsx",
plantillaMapaVersion: "v2017.0",
},
clonInterno: { planOrigenId: null },
clonTradicional: {
@@ -27,6 +43,8 @@ export function useNuevoPlanWizard() {
poblacionObjetivo: "",
notasAdicionales: "",
archivosReferencia: [],
repositoriosReferencia: [],
archivosAdjuntos: [],
},
resumen: {},
isLoading: false,
@@ -47,12 +65,19 @@ export function useNuevoPlanWizard() {
!!wizard.datosBasicos.facultadId &&
!!wizard.datosBasicos.nivel &&
(wizard.datosBasicos.numCiclos !== undefined &&
wizard.datosBasicos.numCiclos > 0);
wizard.datosBasicos.numCiclos > 0) &&
// Requerir ambas plantillas (plan y mapa) con versión
!!wizard.datosBasicos.plantillaPlanId &&
!!wizard.datosBasicos.plantillaPlanVersion &&
!!wizard.datosBasicos.plantillaMapaId &&
!!wizard.datosBasicos.plantillaMapaVersion;
const canContinueDesdeDetalles = (() => {
if (wizard.modoCreacion === "MANUAL") return true;
if (wizard.modoCreacion === "IA") {
return !!wizard.iaConfig?.descripcionEnfoque;
// Requerimos descripción del enfoque y notas adicionales
return !!wizard.iaConfig?.descripcionEnfoque &&
!!wizard.iaConfig?.notasAdicionales;
}
if (wizard.modoCreacion === "CLONADO") {
if (wizard.subModoClonado === "INTERNO") {

View File

@@ -22,11 +22,20 @@ export type NewPlanWizardState = {
nivel: string;
tipoCiclo: TipoCiclo | "";
numCiclos: number | undefined;
// Selección de plantillas (obligatorias)
plantillaPlanId?: string;
plantillaPlanVersion?: string;
plantillaMapaId?: string;
plantillaMapaVersion?: string;
};
clonInterno?: { planOrigenId: string | null };
clonTradicional?: {
archivoWordPlanId: string | null;
archivoMapaExcelId: string | null;
archivoWordPlanId:
| { id: string; name: string; size: string; type: string }
| null;
archivoMapaExcelId:
| string
| null;
archivoAsignaturasExcelId: string | null;
};
iaConfig?: {
@@ -34,6 +43,10 @@ export type NewPlanWizardState = {
poblacionObjetivo: string;
notasAdicionales: string;
archivosReferencia: Array<string>;
repositoriosReferencia?: Array<string>;
archivosAdjuntos?: Array<
{ id: string; name: string; size: string; type: string }
>;
};
resumen: { previewPlan?: PlanPreview };
isLoading: boolean;

View File

@@ -183,10 +183,19 @@ function RouteComponent() {
// Filtrado de planes
const filteredPlans = useMemo(() => {
const term = search.trim().toLowerCase()
// Función helper para limpiar texto (quita acentos y hace minúsculas)
const cleanText = (text: string) => {
return text
.normalize('NFD') // Descompone "á" en "a" + "´"
.replace(/[\u0300-\u036f]/g, '') // Elimina los símbolos diacríticos
.toLowerCase() // Convierte a minúsculas
}
// Limpiamos el término de búsqueda una sola vez antes de filtrar
const term = cleanText(search.trim())
return planes.filter((p) => {
const matchName = term
? p.nombrePrograma.toLowerCase().includes(term)
? // Limpiamos también el nombre del programa antes de comparar
cleanText(p.nombrePrograma).includes(term)
: true
const matchFac =
facultadSel === 'todas' ? true : p.facultadId === facultadSel