Barra de busqueda para filtrar referencias para la IA, cambios a FileDropZone
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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") {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user