diff --git a/src/components/planes/wizard/PasoBasicosForm/PasoBasicosForm.tsx b/src/components/planes/wizard/PasoBasicosForm/PasoBasicosForm.tsx index 2a1fdd8..bdc6b97 100644 --- a/src/components/planes/wizard/PasoBasicosForm/PasoBasicosForm.tsx +++ b/src/components/planes/wizard/PasoBasicosForm/PasoBasicosForm.tsx @@ -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({
- +
@@ -65,9 +69,14 @@ export function PasoBasicosForm({ > span]:block! [&>span]:truncate!', + !wizard.datosBasicos.facultadId + ? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder + : 'font-medium not-italic', // Tiene Valor (Medium) + )} > - + {FACULTADES.map((f) => ( @@ -93,9 +102,14 @@ export function PasoBasicosForm({ > span]:block! [&>span]:truncate!', + !wizard.datosBasicos.carreraId + ? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder + : 'font-medium not-italic', // Tiene Valor (Medium) + )} > - + {carrerasFiltradas.map((c) => ( @@ -120,9 +134,14 @@ export function PasoBasicosForm({ > span]:block! [&>span]:truncate!', + !wizard.datosBasicos.nivel + ? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder + : 'font-medium not-italic', // Tiene Valor (Medium) + )} > - + {NIVELES.map((n) => ( @@ -150,9 +169,14 @@ export function PasoBasicosForm({ > span]:block! [&>span]:truncate!', + !wizard.datosBasicos.tipoCiclo + ? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder + : 'font-medium not-italic', // Tiene Valor (Medium) + )} > - + {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) => 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" />
@@ -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, + }, + })) + } /> + onChange((w) => ({ + ...w, + datosBasicos: { + ...w.datosBasicos, + plantillaMapaId: templateId, + plantillaMapaVersion: version, + }, + })) + } /> diff --git a/src/components/planes/wizard/PasoBasicosForm/TemplateSelectorCard.tsx b/src/components/planes/wizard/PasoBasicosForm/TemplateSelectorCard.tsx index e7510f1..73b234a 100644 --- a/src/components/planes/wizard/PasoBasicosForm/TemplateSelectorCard.tsx +++ b/src/components/planes/wizard/PasoBasicosForm/TemplateSelectorCard.tsx @@ -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 + // 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('') - const [selectedVersion, setSelectedVersion] = useState('') + const [internalTemplate, setInternalTemplate] = useState('') + const [internalVersion, setInternalVersion] = useState('') + + 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({ */} + + { + onChange((w) => ({ + ...w, + clonTradicional: { + ...(w.clonTradicional || ({} as any)), + archivoWordPlanId: file, + }, + })) + }} />
diff --git a/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx b/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx index c0a7a63..af04fc0 100644 --- a/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx +++ b/src/components/planes/wizard/PasoDetallesPanel/ReferenciasParaIA.tsx @@ -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,93 +16,142 @@ import { } from '@/components/ui/motion-tabs' import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs' -const tabs = [ - { - name: 'Archivos existentes', +const ReferenciasParaIA = ({ + onFilesChange, +}: { + onFilesChange?: ( + files: Array<{ id: string; name: string; size: string; type: string }>, + ) => void +}) => { + const [busquedaArchivos, setBusquedaArchivos] = useState('') + const [busquedaRepositorios, setBusquedaRepositorios] = useState('') - value: 'archivos-existentes', + 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 + } - icon: FileText, + // Filtrado de archivos y de repositorios + const archivosFiltrados = useMemo(() => { + // Función helper para limpiar texto (quita acentos y hace minúsculas) - content: ( -
- {ARCHIVOS.map((archivo) => ( - - ))} -
- ), - }, + value: 'archivos-existentes', - { - name: 'Repositorios', + icon: FileText, - value: 'repositorios', + content: ( +
+ +
+ {archivosFiltrados.map((archivo) => ( +
- - ))} -
- ), - }, + { + name: 'Repositorios', - { - name: 'Subir archivos', + value: 'repositorios', - value: 'subir-archivos', + icon: FolderOpen, - icon: Upload, + content: ( +
+ +
+ {repositoriosFiltrados.map((repositorio) => ( + + ))} +
+
+ ), + }, + + { + name: 'Subir archivos', + + value: 'subir-archivos', + + icon: Upload, + + content: ( +
+ +
+ ), + }, + ] -const ReferenciasParaIA = () => { return (
diff --git a/src/components/planes/wizard/PasoResumenCard.tsx b/src/components/planes/wizard/PasoResumenCard.tsx index 35e255d..31a4d45 100644 --- a/src/components/planes/wizard/PasoResumenCard.tsx +++ b/src/components/planes/wizard/PasoResumenCard.tsx @@ -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})
+ {/* Plantillas seleccionadas */} +
+ Plantilla plan: + + {(() => { + 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}` + })()} + +
+
+ Mapa curricular: + + {(() => { + 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}` + })()} + +
Modo: @@ -59,6 +92,57 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) { 'Importado desde documentos tradicionales'}
+ {modo === 'IA' && ( +
+
+ Enfoque: + + {wizard.iaConfig?.descripcionEnfoque || '—'} + +
+
+ Notas: + + {wizard.iaConfig?.notasAdicionales || '—'} + +
+ {!!(wizard.iaConfig?.archivosReferencia?.length || 0) && ( +
+ Archivos existentes:{' '} + {wizard.iaConfig?.archivosReferencia?.length} +
+ )} + {!!(wizard.iaConfig?.repositoriosReferencia?.length || 0) && ( +
+ Repositorios:{' '} + {wizard.iaConfig?.repositoriosReferencia?.length} +
+ )} + {!!(wizard.iaConfig?.archivosAdjuntos?.length || 0) && ( +
+
Adjuntos
+
    + {wizard.iaConfig?.archivosAdjuntos?.map((f) => ( +
  • + {f.name}{' '} + · {f.size} +
  • + ))} +
+
+ )} +
+ )} + {modo === 'CLONADO' && sub === 'TRADICIONAL' && ( +
+ + Archivo Word del plan:{' '} + + + {wizard.clonTradicional?.archivoWordPlanId?.name || '—'} + +
+ )} {wizard.resumen.previewPlan && (
Preview IA
diff --git a/src/features/planes/nuevo/catalogs.ts b/src/features/planes/nuevo/catalogs.ts index b9bffa8..805193e 100644 --- a/src/features/planes/nuevo/catalogs.ts +++ b/src/features/planes/nuevo/catalogs.ts @@ -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", diff --git a/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts index 5bc8ece..8809d74 100644 --- a/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts +++ b/src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts @@ -8,6 +8,18 @@ export function useNuevoPlanWizard() { const [wizard, setWizard] = useState({ 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") { diff --git a/src/features/planes/nuevo/types.ts b/src/features/planes/nuevo/types.ts index 49c08b1..fae8153 100644 --- a/src/features/planes/nuevo/types.ts +++ b/src/features/planes/nuevo/types.ts @@ -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; + repositoriosReferencia?: Array; + archivosAdjuntos?: Array< + { id: string; name: string; size: string; type: string } + >; }; resumen: { previewPlan?: PlanPreview }; isLoading: boolean; diff --git a/src/routes/planes/_lista/route.tsx b/src/routes/planes/_lista/route.tsx index 3a21a99..eddb20a 100644 --- a/src/routes/planes/_lista/route.tsx +++ b/src/routes/planes/_lista/route.tsx @@ -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