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_1,
|
||||||
PLANTILLAS_ANEXO_2,
|
PLANTILLAS_ANEXO_2,
|
||||||
} from '@/features/planes/nuevo/catalogs'
|
} from '@/features/planes/nuevo/catalogs'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function PasoBasicosForm({
|
export function PasoBasicosForm({
|
||||||
wizard,
|
wizard,
|
||||||
@@ -34,7 +35,9 @@ export function PasoBasicosForm({
|
|||||||
<div className="flex flex-col gap-2">
|
<div className="flex flex-col gap-2">
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<div className="grid gap-1 sm:col-span-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
|
<Input
|
||||||
id="nombrePlan"
|
id="nombrePlan"
|
||||||
placeholder="Ej. Ingeniería en Sistemas 2026"
|
placeholder="Ej. Ingeniería en Sistemas 2026"
|
||||||
@@ -45,6 +48,7 @@ export function PasoBasicosForm({
|
|||||||
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
|
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -65,9 +69,14 @@ export function PasoBasicosForm({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id="facultad"
|
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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{FACULTADES.map((f) => (
|
{FACULTADES.map((f) => (
|
||||||
@@ -93,9 +102,14 @@ export function PasoBasicosForm({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id="carrera"
|
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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{carrerasFiltradas.map((c) => (
|
{carrerasFiltradas.map((c) => (
|
||||||
@@ -120,9 +134,14 @@ export function PasoBasicosForm({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id="nivel"
|
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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{NIVELES.map((n) => (
|
{NIVELES.map((n) => (
|
||||||
@@ -150,9 +169,14 @@ export function PasoBasicosForm({
|
|||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
id="tipoCiclo"
|
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>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{TIPOS_CICLO.map((t) => (
|
{TIPOS_CICLO.map((t) => (
|
||||||
@@ -170,17 +194,20 @@ export function PasoBasicosForm({
|
|||||||
id="numCiclos"
|
id="numCiclos"
|
||||||
type="number"
|
type="number"
|
||||||
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,
|
...w,
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
...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>
|
||||||
</div>
|
</div>
|
||||||
@@ -190,11 +217,35 @@ export function PasoBasicosForm({
|
|||||||
cardTitle="Plantilla de plan de estudios"
|
cardTitle="Plantilla de plan de estudios"
|
||||||
cardDescription="Selecciona el Word para tu nuevo plan."
|
cardDescription="Selecciona el Word para tu nuevo plan."
|
||||||
templatesData={PLANTILLAS_ANEXO_1}
|
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
|
<TemplateSelectorCard
|
||||||
cardTitle="Mapa curricular"
|
cardTitle="Plantilla de mapa curricular"
|
||||||
cardDescription="Selecciona el Excel para tu mapa curricular."
|
cardDescription="Selecciona el Excel para tu mapa curricular."
|
||||||
templatesData={PLANTILLAS_ANEXO_2}
|
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>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from 'react'
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -46,32 +46,49 @@ interface Props {
|
|||||||
cardTitle?: string
|
cardTitle?: string
|
||||||
cardDescription?: string
|
cardDescription?: string
|
||||||
templatesData?: Array<TemplateData>
|
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({
|
export function TemplateSelectorCard({
|
||||||
cardTitle = 'Configuración del Documento',
|
cardTitle = 'Configuración del Documento',
|
||||||
cardDescription = 'Selecciona la base para tu nuevo plan.',
|
cardDescription = 'Selecciona la base para tu nuevo plan.',
|
||||||
templatesData = DEFAULT_TEMPLATES_DATA,
|
templatesData = DEFAULT_TEMPLATES_DATA,
|
||||||
|
selectedTemplateId,
|
||||||
|
selectedVersion,
|
||||||
|
onChange,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const [selectedTemplate, setSelectedTemplate] = useState<string>('')
|
const [internalTemplate, setInternalTemplate] = useState<string>('')
|
||||||
const [selectedVersion, setSelectedVersion] = useState<string>('')
|
const [internalVersion, setInternalVersion] = useState<string>('')
|
||||||
|
|
||||||
|
const selectedTemplate = selectedTemplateId ?? internalTemplate
|
||||||
|
const version = selectedVersion ?? internalVersion
|
||||||
|
|
||||||
// Buscamos las versiones de la plantilla seleccionada
|
// Buscamos las versiones de la plantilla seleccionada
|
||||||
const currentTemplateData = templatesData.find(
|
const currentTemplateData = useMemo(
|
||||||
(t) => t.id === selectedTemplate,
|
() => templatesData.find((t) => t.id === selectedTemplate),
|
||||||
|
[templatesData, selectedTemplate],
|
||||||
)
|
)
|
||||||
const availableVersions = currentTemplateData?.versions || []
|
const availableVersions = currentTemplateData?.versions || []
|
||||||
|
|
||||||
const handleTemplateChange = (value: string) => {
|
const handleTemplateChange = (value: string) => {
|
||||||
setSelectedTemplate(value)
|
|
||||||
// Buscamos los datos de esta plantilla
|
|
||||||
const template = templatesData.find((t) => t.id === value)
|
const template = templatesData.find((t) => t.id === value)
|
||||||
|
const firstVersion = template?.versions?.[0] ?? ''
|
||||||
// Si tiene versiones, seleccionamos la primera automáticamente
|
if (onChange) {
|
||||||
if (template && template.versions.length > 0) {
|
onChange({ templateId: value, version: firstVersion })
|
||||||
setSelectedVersion(template.versions[0])
|
|
||||||
} else {
|
} 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>
|
</div>
|
||||||
|
|
||||||
<Select
|
<Select
|
||||||
value={selectedVersion}
|
value={version}
|
||||||
onValueChange={setSelectedVersion}
|
onValueChange={handleVersionChange}
|
||||||
disabled={!selectedTemplate}
|
disabled={!selectedTemplate}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ interface FileDropzoneProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function FileDropzone({
|
export function FileDropzone({
|
||||||
// onFilesChange,
|
onFilesChange,
|
||||||
acceptedTypes = '.doc,.docx,.pdf',
|
acceptedTypes = '.doc,.docx,.pdf',
|
||||||
maxFiles = 5,
|
maxFiles = 5,
|
||||||
title = 'Arrastra archivos aquí',
|
title = 'Arrastra archivos aquí',
|
||||||
@@ -39,16 +39,12 @@ export function FileDropzone({
|
|||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const handleDrop = useCallback(
|
const handleDrop = useCallback((e: React.DragEvent) => {
|
||||||
(e: React.DragEvent) => {
|
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
setIsDragging(false)
|
setIsDragging(false)
|
||||||
|
|
||||||
const droppedFiles = Array.from(e.dataTransfer.files)
|
const droppedFiles = Array.from(e.dataTransfer.files)
|
||||||
addFiles(droppedFiles)
|
addFiles(droppedFiles)
|
||||||
},
|
}, [])
|
||||||
[files],
|
|
||||||
)
|
|
||||||
|
|
||||||
const handleFileInput = useCallback(
|
const handleFileInput = useCallback(
|
||||||
(e: React.ChangeEvent<HTMLInputElement>) => {
|
(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
@@ -57,29 +53,40 @@ export function FileDropzone({
|
|||||||
addFiles(selectedFiles)
|
addFiles(selectedFiles)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[files],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
const addFiles = (newFiles: Array<File>) => {
|
const addFiles = useCallback(
|
||||||
const uploadedFiles: Array<UploadedFile> = newFiles
|
(newFiles: Array<File>) => {
|
||||||
.slice(0, maxFiles - files.length)
|
const toUpload: Array<UploadedFile> = newFiles.map((file) => ({
|
||||||
.map((file) => ({
|
id:
|
||||||
id: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||||
|
? (crypto as any).randomUUID()
|
||||||
|
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
name: file.name,
|
name: file.name,
|
||||||
size: formatFileSize(file.size),
|
size: formatFileSize(file.size),
|
||||||
type: file.name.split('.').pop() || 'file',
|
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)
|
const removeFile = useCallback(
|
||||||
setFiles(updatedFiles)
|
(fileId: string) => {
|
||||||
// onFilesChange(updatedFiles)
|
setFiles((prev) => {
|
||||||
}
|
const next = prev.filter((f) => f.id !== fileId)
|
||||||
|
if (onFilesChange) onFilesChange(next)
|
||||||
const removeFile = (fileId: string) => {
|
return next
|
||||||
const updatedFiles = files.filter((f) => f.id !== fileId)
|
})
|
||||||
setFiles(updatedFiles)
|
},
|
||||||
// onFilesChange(updatedFiles)
|
[onFilesChange],
|
||||||
}
|
)
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
const formatFileSize = (bytes: number): string => {
|
||||||
if (bytes < 1024) return bytes + ' B'
|
if (bytes < 1024) return bytes + ' B'
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { FileDropzone } from './FileDropZone'
|
||||||
import ReferenciasParaIA from './ReferenciasParaIA'
|
import ReferenciasParaIA from './ReferenciasParaIA'
|
||||||
|
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||||
@@ -82,7 +83,17 @@ export function PasoDetallesPanel({
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ReferenciasParaIA />
|
<ReferenciasParaIA
|
||||||
|
onFilesChange={(files) =>
|
||||||
|
onChange((w) => ({
|
||||||
|
...w,
|
||||||
|
iaConfig: {
|
||||||
|
...(w.iaConfig || ({} as any)),
|
||||||
|
archivosAdjuntos: files,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div className="text-muted-foreground text-sm">
|
<div className="text-muted-foreground text-sm">
|
||||||
Opcional: se pueden adjuntar recursos IA más adelante.
|
Opcional: se pueden adjuntar recursos IA más adelante.
|
||||||
@@ -229,10 +240,10 @@ export function PasoDetallesPanel({
|
|||||||
wizard.subModoClonado === 'TRADICIONAL'
|
wizard.subModoClonado === 'TRADICIONAL'
|
||||||
) {
|
) {
|
||||||
return (
|
return (
|
||||||
<div className="grid gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div>
|
<div className="flex flex-col gap-1">
|
||||||
<Label htmlFor="word">Word del plan (obligatorio)</Label>
|
<Label htmlFor="word">Word del plan (obligatorio)</Label>
|
||||||
<input
|
{/* <input
|
||||||
id="word"
|
id="word"
|
||||||
type="file"
|
type="file"
|
||||||
accept=".doc,.docx"
|
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>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import { FileText, FolderOpen, Upload } from 'lucide-react'
|
import { FileText, FolderOpen, Upload } from 'lucide-react'
|
||||||
|
import { useMemo, useState } from 'react'
|
||||||
|
|
||||||
|
import BarraBusqueda from '../../BarraBusqueda'
|
||||||
|
|
||||||
import { FileDropzone } from './FileDropZone'
|
import { FileDropzone } from './FileDropZone'
|
||||||
|
|
||||||
@@ -13,6 +16,40 @@ import {
|
|||||||
} from '@/components/ui/motion-tabs'
|
} from '@/components/ui/motion-tabs'
|
||||||
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
|
import { ARCHIVOS, REPOSITORIOS } from '@/features/planes/nuevo/catalogs'
|
||||||
|
|
||||||
|
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 = [
|
const tabs = [
|
||||||
{
|
{
|
||||||
name: 'Archivos existentes',
|
name: 'Archivos existentes',
|
||||||
@@ -22,11 +59,18 @@ const tabs = [
|
|||||||
icon: FileText,
|
icon: FileText,
|
||||||
|
|
||||||
content: (
|
content: (
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col">
|
||||||
{ARCHIVOS.map((archivo) => (
|
<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
|
<Label
|
||||||
key={archivo.id}
|
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" />
|
<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}
|
{archivo.nombre}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p className="text-muted-foreground text-xs">{archivo.tamaño}</p>
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{archivo.tamaño}
|
||||||
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Label>
|
</Label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -53,11 +100,18 @@ const tabs = [
|
|||||||
icon: FolderOpen,
|
icon: FolderOpen,
|
||||||
|
|
||||||
content: (
|
content: (
|
||||||
<div className="flex flex-col gap-0.5">
|
<div className="flex flex-col">
|
||||||
{REPOSITORIOS.map((repositorio) => (
|
<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
|
<Label
|
||||||
key={repositorio.id}
|
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" />
|
<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>
|
</Label>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|
||||||
@@ -88,9 +143,7 @@ const tabs = [
|
|||||||
content: (
|
content: (
|
||||||
<div>
|
<div>
|
||||||
<FileDropzone
|
<FileDropzone
|
||||||
// onFilesChange={(files) =>
|
onFilesChange={onFilesChange}
|
||||||
// handleChange("archivosAdhocIds", files.map((f) => f.id))
|
|
||||||
// }
|
|
||||||
title="Sube archivos de referencia"
|
title="Sube archivos de referencia"
|
||||||
description="Documentos que serán usados como contexto para la generación"
|
description="Documentos que serán usados como contexto para la generación"
|
||||||
/>
|
/>
|
||||||
@@ -99,7 +152,6 @@ const tabs = [
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const ReferenciasParaIA = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="flex w-full flex-col gap-1">
|
<div className="flex w-full flex-col gap-1">
|
||||||
<Label>Referencias para la IA</Label>
|
<Label>Referencias para la IA</Label>
|
||||||
|
|||||||
@@ -7,6 +7,10 @@ import {
|
|||||||
CardHeader,
|
CardHeader,
|
||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
PLANTILLAS_ANEXO_1,
|
||||||
|
PLANTILLAS_ANEXO_2,
|
||||||
|
} from '@/features/planes/nuevo/catalogs'
|
||||||
|
|
||||||
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||||
const modo = wizard.modoCreacion
|
const modo = wizard.modoCreacion
|
||||||
@@ -46,6 +50,35 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
{wizard.datosBasicos.numCiclos} ({wizard.datosBasicos.tipoCiclo})
|
{wizard.datosBasicos.numCiclos} ({wizard.datosBasicos.tipoCiclo})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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">
|
<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">
|
||||||
@@ -59,6 +92,57 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
'Importado desde documentos tradicionales'}
|
'Importado desde documentos tradicionales'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</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 && (
|
{wizard.resumen.previewPlan && (
|
||||||
<div className="bg-muted mt-2 rounded-md p-3">
|
<div className="bg-muted mt-2 rounded-md p-3">
|
||||||
<div className="font-medium">Preview IA</div>
|
<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 = [
|
export const PLANTILLAS_ANEXO_1 = [
|
||||||
{
|
{
|
||||||
id: "sep-2025",
|
id: "sep-2025",
|
||||||
|
|||||||
@@ -8,6 +8,18 @@ export function useNuevoPlanWizard() {
|
|||||||
const [wizard, setWizard] = useState<NewPlanWizardState>({
|
const [wizard, setWizard] = useState<NewPlanWizardState>({
|
||||||
step: 1,
|
step: 1,
|
||||||
modoCreacion: null,
|
modoCreacion: null,
|
||||||
|
// datosBasicos: {
|
||||||
|
// nombrePlan: "",
|
||||||
|
// carreraId: "",
|
||||||
|
// facultadId: "",
|
||||||
|
// nivel: "",
|
||||||
|
// tipoCiclo: "",
|
||||||
|
// numCiclos: undefined,
|
||||||
|
// plantillaPlanId: "",
|
||||||
|
// plantillaPlanVersion: "",
|
||||||
|
// plantillaMapaId: "",
|
||||||
|
// plantillaMapaVersion: "",
|
||||||
|
// },
|
||||||
datosBasicos: {
|
datosBasicos: {
|
||||||
nombrePlan: "Medicina",
|
nombrePlan: "Medicina",
|
||||||
carreraId: "medico",
|
carreraId: "medico",
|
||||||
@@ -15,6 +27,10 @@ export function useNuevoPlanWizard() {
|
|||||||
nivel: "Licenciatura",
|
nivel: "Licenciatura",
|
||||||
tipoCiclo: "SEMESTRE",
|
tipoCiclo: "SEMESTRE",
|
||||||
numCiclos: 8,
|
numCiclos: 8,
|
||||||
|
plantillaPlanId: "sep-2025",
|
||||||
|
plantillaPlanVersion: "v2025.2 (Vigente)",
|
||||||
|
plantillaMapaId: "sep-2017-xlsx",
|
||||||
|
plantillaMapaVersion: "v2017.0",
|
||||||
},
|
},
|
||||||
clonInterno: { planOrigenId: null },
|
clonInterno: { planOrigenId: null },
|
||||||
clonTradicional: {
|
clonTradicional: {
|
||||||
@@ -27,6 +43,8 @@ export function useNuevoPlanWizard() {
|
|||||||
poblacionObjetivo: "",
|
poblacionObjetivo: "",
|
||||||
notasAdicionales: "",
|
notasAdicionales: "",
|
||||||
archivosReferencia: [],
|
archivosReferencia: [],
|
||||||
|
repositoriosReferencia: [],
|
||||||
|
archivosAdjuntos: [],
|
||||||
},
|
},
|
||||||
resumen: {},
|
resumen: {},
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -47,12 +65,19 @@ export function useNuevoPlanWizard() {
|
|||||||
!!wizard.datosBasicos.facultadId &&
|
!!wizard.datosBasicos.facultadId &&
|
||||||
!!wizard.datosBasicos.nivel &&
|
!!wizard.datosBasicos.nivel &&
|
||||||
(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
|
||||||
|
!!wizard.datosBasicos.plantillaPlanId &&
|
||||||
|
!!wizard.datosBasicos.plantillaPlanVersion &&
|
||||||
|
!!wizard.datosBasicos.plantillaMapaId &&
|
||||||
|
!!wizard.datosBasicos.plantillaMapaVersion;
|
||||||
|
|
||||||
const canContinueDesdeDetalles = (() => {
|
const canContinueDesdeDetalles = (() => {
|
||||||
if (wizard.modoCreacion === "MANUAL") return true;
|
if (wizard.modoCreacion === "MANUAL") return true;
|
||||||
if (wizard.modoCreacion === "IA") {
|
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.modoCreacion === "CLONADO") {
|
||||||
if (wizard.subModoClonado === "INTERNO") {
|
if (wizard.subModoClonado === "INTERNO") {
|
||||||
|
|||||||
@@ -22,11 +22,20 @@ export type NewPlanWizardState = {
|
|||||||
nivel: string;
|
nivel: string;
|
||||||
tipoCiclo: TipoCiclo | "";
|
tipoCiclo: TipoCiclo | "";
|
||||||
numCiclos: number | undefined;
|
numCiclos: number | undefined;
|
||||||
|
// Selección de plantillas (obligatorias)
|
||||||
|
plantillaPlanId?: string;
|
||||||
|
plantillaPlanVersion?: string;
|
||||||
|
plantillaMapaId?: string;
|
||||||
|
plantillaMapaVersion?: string;
|
||||||
};
|
};
|
||||||
clonInterno?: { planOrigenId: string | null };
|
clonInterno?: { planOrigenId: string | null };
|
||||||
clonTradicional?: {
|
clonTradicional?: {
|
||||||
archivoWordPlanId: string | null;
|
archivoWordPlanId:
|
||||||
archivoMapaExcelId: string | null;
|
| { id: string; name: string; size: string; type: string }
|
||||||
|
| null;
|
||||||
|
archivoMapaExcelId:
|
||||||
|
| string
|
||||||
|
| null;
|
||||||
archivoAsignaturasExcelId: string | null;
|
archivoAsignaturasExcelId: string | null;
|
||||||
};
|
};
|
||||||
iaConfig?: {
|
iaConfig?: {
|
||||||
@@ -34,6 +43,10 @@ export type NewPlanWizardState = {
|
|||||||
poblacionObjetivo: string;
|
poblacionObjetivo: string;
|
||||||
notasAdicionales: string;
|
notasAdicionales: string;
|
||||||
archivosReferencia: Array<string>;
|
archivosReferencia: Array<string>;
|
||||||
|
repositoriosReferencia?: Array<string>;
|
||||||
|
archivosAdjuntos?: Array<
|
||||||
|
{ id: string; name: string; size: string; type: string }
|
||||||
|
>;
|
||||||
};
|
};
|
||||||
resumen: { previewPlan?: PlanPreview };
|
resumen: { previewPlan?: PlanPreview };
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
|
|||||||
@@ -183,10 +183,19 @@ function RouteComponent() {
|
|||||||
|
|
||||||
// Filtrado de planes
|
// Filtrado de planes
|
||||||
const filteredPlans = useMemo(() => {
|
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) => {
|
return planes.filter((p) => {
|
||||||
const matchName = term
|
const matchName = term
|
||||||
? p.nombrePrograma.toLowerCase().includes(term)
|
? // Limpiamos también el nombre del programa antes de comparar
|
||||||
|
cleanText(p.nombrePrograma).includes(term)
|
||||||
: true
|
: true
|
||||||
const matchFac =
|
const matchFac =
|
||||||
facultadSel === 'todas' ? true : p.facultadId === facultadSel
|
facultadSel === 'todas' ? true : p.facultadId === facultadSel
|
||||||
|
|||||||
Reference in New Issue
Block a user