Wizard listo para enviar información a ai-generate-plan
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
// scripts/update-types.ts
|
// scripts/update-types.ts
|
||||||
// Uso:
|
/* Uso:
|
||||||
// bun run scripts/update-types.ts
|
bun run scripts/update-types.ts
|
||||||
|
*/
|
||||||
import { $ } from "bun";
|
import { $ } from "bun";
|
||||||
|
|
||||||
console.log("🔄 Generando tipos de Supabase...");
|
console.log("🔄 Generando tipos de Supabase...");
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import { TemplateSelectorCard } from './TemplateSelectorCard'
|
import type {
|
||||||
|
EstructuraPlanRow,
|
||||||
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
|
FacultadRow,
|
||||||
import type { CARRERAS } from '@/features/planes/nuevo/catalogs'
|
NivelPlanEstudio,
|
||||||
|
TipoCiclo,
|
||||||
|
} from '@/data/types/domain'
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||||
|
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -13,31 +15,23 @@ import {
|
|||||||
SelectTrigger,
|
SelectTrigger,
|
||||||
SelectValue,
|
SelectValue,
|
||||||
} from '@/components/ui/select'
|
} from '@/components/ui/select'
|
||||||
import { Separator } from '@/components/ui/separator'
|
|
||||||
import { useCatalogosPlanes } from '@/data/hooks/usePlans'
|
import { useCatalogosPlanes } from '@/data/hooks/usePlans'
|
||||||
import {
|
import { NIVELES, TIPOS_CICLO } from '@/features/planes/nuevo/catalogs'
|
||||||
FACULTADES,
|
|
||||||
NIVELES,
|
|
||||||
TIPOS_CICLO,
|
|
||||||
PLANTILLAS_ANEXO_1,
|
|
||||||
PLANTILLAS_ANEXO_2,
|
|
||||||
} from '@/features/planes/nuevo/catalogs'
|
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
export function PasoBasicosForm({
|
export function PasoBasicosForm({
|
||||||
wizard,
|
wizard,
|
||||||
onChange,
|
onChange,
|
||||||
carrerasFiltradas,
|
|
||||||
}: {
|
}: {
|
||||||
wizard: NewPlanWizardState
|
wizard: NewPlanWizardState
|
||||||
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
|
||||||
carrerasFiltradas: typeof CARRERAS
|
|
||||||
}) {
|
}) {
|
||||||
const { data: catalogos } = useCatalogosPlanes()
|
const { data: catalogos } = useCatalogosPlanes()
|
||||||
|
|
||||||
// Preferir los catálogos remotos si están disponibles; si no, usar los locales
|
// Preferir los catálogos remotos si están disponibles; si no, usar los locales
|
||||||
const facultadesList = catalogos?.facultades ?? FACULTADES
|
const facultadesList = catalogos?.facultades ?? []
|
||||||
const rawCarreras = catalogos?.carreras ?? carrerasFiltradas
|
const rawCarreras = catalogos?.carreras ?? []
|
||||||
|
const estructurasPlanList = catalogos?.estructurasPlan ?? []
|
||||||
|
|
||||||
const filteredCarreras = rawCarreras.filter((c: any) => {
|
const filteredCarreras = rawCarreras.filter((c: any) => {
|
||||||
const facId = wizard.datosBasicos.facultadId
|
const facId = wizard.datosBasicos.facultadId
|
||||||
@@ -57,10 +51,15 @@ export function PasoBasicosForm({
|
|||||||
placeholder="Ej. Ingeniería en Sistemas (2026)"
|
placeholder="Ej. Ingeniería en Sistemas (2026)"
|
||||||
value={wizard.datosBasicos.nombrePlan}
|
value={wizard.datosBasicos.nombrePlan}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
...w,
|
(w): NewPlanWizardState => ({
|
||||||
datosBasicos: { ...w.datosBasicos, nombrePlan: e.target.value },
|
...w,
|
||||||
}))
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
nombrePlan: e.target.value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
/>
|
/>
|
||||||
@@ -71,14 +70,16 @@ export function PasoBasicosForm({
|
|||||||
<Select
|
<Select
|
||||||
value={wizard.datosBasicos.facultadId}
|
value={wizard.datosBasicos.facultadId}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
...w,
|
(w): NewPlanWizardState => ({
|
||||||
datosBasicos: {
|
...w,
|
||||||
...w.datosBasicos,
|
datosBasicos: {
|
||||||
facultadId: value,
|
...w.datosBasicos,
|
||||||
carreraId: '',
|
facultadId: value,
|
||||||
},
|
carreraId: '',
|
||||||
}))
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
@@ -93,7 +94,7 @@ export function PasoBasicosForm({
|
|||||||
<SelectValue placeholder="Ej. Facultad de Ingeniería" />
|
<SelectValue placeholder="Ej. Facultad de Ingeniería" />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{facultadesList.map((f: any) => (
|
{facultadesList.map((f: FacultadRow) => (
|
||||||
<SelectItem key={f.id} value={f.id}>
|
<SelectItem key={f.id} value={f.id}>
|
||||||
{f.nombre}
|
{f.nombre}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
@@ -107,10 +108,12 @@ export function PasoBasicosForm({
|
|||||||
<Select
|
<Select
|
||||||
value={wizard.datosBasicos.carreraId}
|
value={wizard.datosBasicos.carreraId}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
...w,
|
(w): NewPlanWizardState => ({
|
||||||
datosBasicos: { ...w.datosBasicos, carreraId: value },
|
...w,
|
||||||
}))
|
datosBasicos: { ...w.datosBasicos, carreraId: value },
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
disabled={!wizard.datosBasicos.facultadId}
|
disabled={!wizard.datosBasicos.facultadId}
|
||||||
>
|
>
|
||||||
@@ -174,13 +177,15 @@ export function PasoBasicosForm({
|
|||||||
<Select
|
<Select
|
||||||
value={wizard.datosBasicos.tipoCiclo}
|
value={wizard.datosBasicos.tipoCiclo}
|
||||||
onValueChange={(value: TipoCiclo) =>
|
onValueChange={(value: TipoCiclo) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
...w,
|
(w): NewPlanWizardState => ({
|
||||||
datosBasicos: {
|
...w,
|
||||||
...w.datosBasicos,
|
datosBasicos: {
|
||||||
tipoCiclo: value as any,
|
...w.datosBasicos,
|
||||||
},
|
tipoCiclo: value as any,
|
||||||
}))
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectTrigger
|
<SelectTrigger
|
||||||
@@ -212,22 +217,63 @@ export function PasoBasicosForm({
|
|||||||
min={1}
|
min={1}
|
||||||
value={wizard.datosBasicos.numCiclos ?? ''}
|
value={wizard.datosBasicos.numCiclos ?? ''}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
...w,
|
(w): NewPlanWizardState => ({
|
||||||
datosBasicos: {
|
...w,
|
||||||
...w.datosBasicos,
|
datosBasicos: {
|
||||||
// Keep undefined when the input is empty so the field stays optional
|
...w.datosBasicos,
|
||||||
numCiclos:
|
// Keep undefined when the input is empty so the field stays optional
|
||||||
e.target.value === '' ? undefined : Number(e.target.value),
|
numCiclos:
|
||||||
},
|
e.target.value === ''
|
||||||
}))
|
? undefined
|
||||||
|
: Number(e.target.value),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
|
||||||
placeholder="Ej. 8"
|
placeholder="Ej. 8"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="grid gap-1">
|
||||||
|
<Label htmlFor="estructuraPlan">Estructura de plan de estudios</Label>
|
||||||
|
<Select
|
||||||
|
value={wizard.datosBasicos.estructuraPlanId ?? ''}
|
||||||
|
onValueChange={(value: string) =>
|
||||||
|
onChange(
|
||||||
|
(w): NewPlanWizardState => ({
|
||||||
|
...w,
|
||||||
|
datosBasicos: {
|
||||||
|
...w.datosBasicos,
|
||||||
|
estructuraPlanId: value,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<SelectTrigger
|
||||||
|
id="tipoCiclo"
|
||||||
|
className={cn(
|
||||||
|
'w-full min-w-0 [&>span]:block! [&>span]:truncate!',
|
||||||
|
!wizard.datosBasicos.estructuraPlanId
|
||||||
|
? 'text-muted-foreground font-normal italic opacity-70' // Es Placeholder
|
||||||
|
: 'font-medium not-italic', // Tiene Valor (Medium)
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<SelectValue placeholder="Ej. Plan base SEP/ULSA (2026)" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{estructurasPlanList.map((t: EstructuraPlanRow) => (
|
||||||
|
<SelectItem key={t.id} value={t.id}>
|
||||||
|
{t.nombre}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<Separator className="my-3" />
|
{/* <Separator className="my-3" />
|
||||||
<div className="grid gap-4 sm:grid-cols-2">
|
<div className="grid gap-4 sm:grid-cols-2">
|
||||||
<TemplateSelectorCard
|
<TemplateSelectorCard
|
||||||
cardTitle="Plantilla de plan de estudios"
|
cardTitle="Plantilla de plan de estudios"
|
||||||
@@ -263,7 +309,7 @@ export function PasoBasicosForm({
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div> */}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,13 +2,13 @@ import { Upload, File, X, FileText } from 'lucide-react'
|
|||||||
import { useState, useCallback, useEffect, useRef } from 'react'
|
import { useState, useCallback, useEffect, useRef } from 'react'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||||
import { cn } from '@/lib/utils'
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
interface UploadedFile {
|
export interface UploadedFile {
|
||||||
id: string
|
id: string // Necesario para React (key)
|
||||||
name: string
|
file: File // La fuente de verdad (contiene name, size, type)
|
||||||
size: string
|
preview?: string // Opcional: si fueran imágenes
|
||||||
type: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
interface FileDropzoneProps {
|
interface FileDropzoneProps {
|
||||||
@@ -37,9 +37,7 @@ export function FileDropzone({
|
|||||||
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
typeof crypto !== 'undefined' && 'randomUUID' in crypto
|
||||||
? (crypto as any).randomUUID()
|
? (crypto as any).randomUUID()
|
||||||
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
: `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
name: file.name,
|
file,
|
||||||
size: formatFileSize(file.size),
|
|
||||||
type: file.name.split('.').pop() || 'file',
|
|
||||||
}))
|
}))
|
||||||
setFiles((prev) => {
|
setFiles((prev) => {
|
||||||
const room = Math.max(0, maxFiles - prev.length)
|
const room = Math.max(0, maxFiles - prev.length)
|
||||||
@@ -97,12 +95,6 @@ export function FileDropzone({
|
|||||||
if (onFilesChangeRef.current) onFilesChangeRef.current(files)
|
if (onFilesChangeRef.current) onFilesChangeRef.current(files)
|
||||||
}, [files])
|
}, [files])
|
||||||
|
|
||||||
const formatFileSize = (bytes: number): string => {
|
|
||||||
if (bytes < 1024) return bytes + ' B'
|
|
||||||
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
|
|
||||||
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFileIcon = (type: string) => {
|
const getFileIcon = (type: string) => {
|
||||||
switch (type.toLowerCase()) {
|
switch (type.toLowerCase()) {
|
||||||
case 'pdf':
|
case 'pdf':
|
||||||
@@ -170,23 +162,25 @@ export function FileDropzone({
|
|||||||
{/* Uploaded files list */}
|
{/* Uploaded files list */}
|
||||||
{files.length > 0 && (
|
{files.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
{files.map((file) => (
|
{files.map((item) => (
|
||||||
<div
|
<div
|
||||||
key={file.id}
|
key={item.id}
|
||||||
className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3"
|
className="bg-accent/50 border-border fade-in flex items-center gap-3 rounded-lg border p-3"
|
||||||
>
|
>
|
||||||
{getFileIcon(file.type)}
|
{getFileIcon(item.file.type)}
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
<p className="text-foreground truncate text-sm font-medium">
|
<p className="text-foreground truncate text-sm font-medium">
|
||||||
{file.name}
|
{item.file.name}
|
||||||
|
</p>
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{formatFileSize(item.file.size)}
|
||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground text-xs">{file.size}</p>
|
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="text-muted-foreground hover:text-destructive h-8 w-8"
|
className="text-muted-foreground hover:text-destructive h-8 w-8"
|
||||||
onClick={() => removeFile(file.id)}
|
onClick={() => removeFile(item.id)}
|
||||||
>
|
>
|
||||||
<X className="h-4 w-4" />
|
<X className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { FileDropzone } from './FileDropZone'
|
import { FileDropzone } from './FileDropZone'
|
||||||
import ReferenciasParaIA from './ReferenciasParaIA'
|
import ReferenciasParaIA from './ReferenciasParaIA'
|
||||||
|
|
||||||
|
import type { UploadedFile } from './FileDropZone'
|
||||||
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
|
||||||
|
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
@@ -116,14 +117,16 @@ export function PasoDetallesPanel({
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
onFilesChange={(files) =>
|
onFilesChange={(files: Array<UploadedFile>) =>
|
||||||
onChange((w) => ({
|
onChange(
|
||||||
...w,
|
(w): NewPlanWizardState => ({
|
||||||
iaConfig: {
|
...w,
|
||||||
...(w.iaConfig || ({} as any)),
|
iaConfig: {
|
||||||
archivosAdjuntos: files,
|
...(w.iaConfig || ({} as any)),
|
||||||
},
|
archivosAdjuntos: files,
|
||||||
}))
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import BarraBusqueda from '../../BarraBusqueda'
|
|||||||
|
|
||||||
import { FileDropzone } from './FileDropZone'
|
import { FileDropzone } from './FileDropZone'
|
||||||
|
|
||||||
|
import type { UploadedFile } from './FileDropZone'
|
||||||
|
|
||||||
import { Checkbox } from '@/components/ui/checkbox'
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
import { Label } from '@/components/ui/label'
|
import { Label } from '@/components/ui/label'
|
||||||
import {
|
import {
|
||||||
@@ -27,9 +29,7 @@ const ReferenciasParaIA = ({
|
|||||||
selectedRepositorioIds?: Array<string>
|
selectedRepositorioIds?: Array<string>
|
||||||
onToggleArchivo?: (id: string, checked: boolean) => void
|
onToggleArchivo?: (id: string, checked: boolean) => void
|
||||||
onToggleRepositorio?: (id: string, checked: boolean) => void
|
onToggleRepositorio?: (id: string, checked: boolean) => void
|
||||||
onFilesChange?: (
|
onFilesChange?: (files: Array<UploadedFile>) => void
|
||||||
files: Array<{ id: string; name: string; size: string; type: string }>,
|
|
||||||
) => void
|
|
||||||
}) => {
|
}) => {
|
||||||
const [busquedaArchivos, setBusquedaArchivos] = useState('')
|
const [busquedaArchivos, setBusquedaArchivos] = useState('')
|
||||||
const [busquedaRepositorios, setBusquedaRepositorios] = useState('')
|
const [busquedaRepositorios, setBusquedaRepositorios] = useState('')
|
||||||
|
|||||||
@@ -8,12 +8,11 @@ import {
|
|||||||
CardTitle,
|
CardTitle,
|
||||||
} from '@/components/ui/card'
|
} from '@/components/ui/card'
|
||||||
import {
|
import {
|
||||||
PLANTILLAS_ANEXO_1,
|
|
||||||
PLANTILLAS_ANEXO_2,
|
|
||||||
PLANES_EXISTENTES,
|
PLANES_EXISTENTES,
|
||||||
ARCHIVOS,
|
ARCHIVOS,
|
||||||
REPOSITORIOS,
|
REPOSITORIOS,
|
||||||
} from '@/features/planes/nuevo/catalogs'
|
} from '@/features/planes/nuevo/catalogs'
|
||||||
|
import { formatFileSize } from '@/features/planes/utils/format-file-size'
|
||||||
|
|
||||||
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
||||||
return (
|
return (
|
||||||
@@ -32,12 +31,6 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
const repositoriosRef =
|
const repositoriosRef =
|
||||||
wizard.iaConfig?.repositoriosReferencia ?? []
|
wizard.iaConfig?.repositoriosReferencia ?? []
|
||||||
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
const adjuntos = wizard.iaConfig?.archivosAdjuntos ?? []
|
||||||
const plantillaPlan = PLANTILLAS_ANEXO_1.find(
|
|
||||||
(x) => x.id === wizard.datosBasicos.plantillaPlanId,
|
|
||||||
)
|
|
||||||
const plantillaMapa = PLANTILLAS_ANEXO_2.find(
|
|
||||||
(x) => x.id === wizard.datosBasicos.plantillaMapaId,
|
|
||||||
)
|
|
||||||
const contenido = (
|
const contenido = (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
@@ -68,89 +61,56 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
{wizard.datosBasicos.tipoCiclo})
|
{wizard.datosBasicos.tipoCiclo})
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Plantilla plan:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{(plantillaPlan?.name ||
|
|
||||||
wizard.datosBasicos.plantillaPlanId ||
|
|
||||||
'—') +
|
|
||||||
' · ' +
|
|
||||||
(wizard.datosBasicos.plantillaPlanVersion || '—')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<span className="text-muted-foreground">
|
|
||||||
Mapa curricular:{' '}
|
|
||||||
</span>
|
|
||||||
<span className="font-medium">
|
|
||||||
{(plantillaMapa?.name ||
|
|
||||||
wizard.datosBasicos.plantillaMapaId ||
|
|
||||||
'—') +
|
|
||||||
' · ' +
|
|
||||||
(wizard.datosBasicos.plantillaMapaVersion || '—')}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<span className="text-muted-foreground">Modo: </span>
|
<span className="text-muted-foreground">Modo: </span>
|
||||||
<span className="font-medium">
|
<span className="font-medium">
|
||||||
{wizard.modoCreacion === 'MANUAL' && 'Manual'}
|
{wizard.tipoOrigen === 'MANUAL' && 'Manual'}
|
||||||
{wizard.modoCreacion === 'IA' && 'Generado con IA'}
|
{wizard.tipoOrigen === 'IA' && 'Generado con IA'}
|
||||||
{wizard.modoCreacion === 'CLONADO' &&
|
{wizard.tipoOrigen === 'CLONADO_INTERNO' &&
|
||||||
wizard.subModoClonado === 'INTERNO' &&
|
|
||||||
'Clonado desde plan del sistema'}
|
'Clonado desde plan del sistema'}
|
||||||
{wizard.modoCreacion === 'CLONADO' &&
|
{wizard.tipoOrigen === 'CLONADO_TRADICIONAL' &&
|
||||||
wizard.subModoClonado === 'TRADICIONAL' &&
|
|
||||||
'Importado desde documentos tradicionales'}
|
'Importado desde documentos tradicionales'}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
{wizard.modoCreacion === 'CLONADO' &&
|
{wizard.tipoOrigen === 'CLONADO_INTERNO' && (
|
||||||
wizard.subModoClonado === 'INTERNO' && (
|
<div className="mt-2">
|
||||||
<div className="mt-2">
|
<span className="text-muted-foreground">Plan origen: </span>
|
||||||
<span className="text-muted-foreground">
|
<span className="font-medium">
|
||||||
Plan origen:{' '}
|
{(() => {
|
||||||
</span>
|
const p = PLANES_EXISTENTES.find(
|
||||||
<span className="font-medium">
|
(x) => x.id === wizard.clonInterno?.planOrigenId,
|
||||||
{(() => {
|
)
|
||||||
const p = PLANES_EXISTENTES.find(
|
return (
|
||||||
(x) => x.id === wizard.clonInterno?.planOrigenId,
|
p?.nombre || wizard.clonInterno?.planOrigenId || '—'
|
||||||
)
|
)
|
||||||
return (
|
})()}
|
||||||
p?.nombre || wizard.clonInterno?.planOrigenId || '—'
|
</span>
|
||||||
)
|
</div>
|
||||||
})()}
|
)}
|
||||||
</span>
|
{wizard.tipoOrigen === 'CLONADO_TRADICIONAL' && (
|
||||||
</div>
|
<div className="mt-2">
|
||||||
)}
|
<div className="font-medium">Documentos adjuntos</div>
|
||||||
{wizard.modoCreacion === 'CLONADO' &&
|
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||||
wizard.subModoClonado === 'TRADICIONAL' && (
|
<li>
|
||||||
<div className="mt-2">
|
<span className="text-foreground">Word del plan:</span>{' '}
|
||||||
<div className="font-medium">Documentos adjuntos</div>
|
{wizard.clonTradicional?.archivoWordPlanId?.name || '—'}
|
||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="text-foreground">
|
<span className="text-foreground">
|
||||||
Word del plan:
|
Mapa curricular:
|
||||||
</span>{' '}
|
</span>{' '}
|
||||||
{wizard.clonTradicional?.archivoWordPlanId?.name ||
|
{wizard.clonTradicional?.archivoMapaExcelId?.name ||
|
||||||
'—'}
|
'—'}
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<span className="text-foreground">
|
<span className="text-foreground">Asignaturas:</span>{' '}
|
||||||
Mapa curricular:
|
{wizard.clonTradicional?.archivoAsignaturasExcelId
|
||||||
</span>{' '}
|
?.name || '—'}
|
||||||
{wizard.clonTradicional?.archivoMapaExcelId?.name ||
|
</li>
|
||||||
'—'}
|
</ul>
|
||||||
</li>
|
</div>
|
||||||
<li>
|
)}
|
||||||
<span className="text-foreground">Asignaturas:</span>{' '}
|
{wizard.tipoOrigen === 'IA' && (
|
||||||
{wizard.clonTradicional?.archivoAsignaturasExcelId
|
|
||||||
?.name || '—'}
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{wizard.modoCreacion === 'IA' && (
|
|
||||||
<div className="bg-muted/50 mt-2 rounded-md p-3">
|
<div className="bg-muted/50 mt-2 rounded-md p-3">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-muted-foreground">Enfoque: </span>
|
<span className="text-muted-foreground">Enfoque: </span>
|
||||||
@@ -208,8 +168,10 @@ export function PasoResumenCard({ wizard }: { wizard: NewPlanWizardState }) {
|
|||||||
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
<ul className="text-muted-foreground list-disc pl-5 text-xs">
|
||||||
{adjuntos.map((f) => (
|
{adjuntos.map((f) => (
|
||||||
<li key={f.id}>
|
<li key={f.id}>
|
||||||
<span className="text-foreground">{f.name}</span>{' '}
|
<span className="text-foreground">
|
||||||
<span>· {f.size}</span>
|
{f.file.name}
|
||||||
|
</span>{' '}
|
||||||
|
<span>· {formatFileSize(f.file.size)}</span>
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import type {
|
|||||||
TipoCiclo,
|
TipoCiclo,
|
||||||
UUID,
|
UUID,
|
||||||
} from "../types/domain";
|
} from "../types/domain";
|
||||||
|
import type { UploadedFile } from "@/components/planes/wizard/PasoDetallesPanel/FileDropZone";
|
||||||
|
|
||||||
const EDGE = {
|
const EDGE = {
|
||||||
plans_create_manual: "plans_create_manual",
|
plans_create_manual: "plans_create_manual",
|
||||||
@@ -220,6 +221,7 @@ export type AIGeneratePlanInput = {
|
|||||||
notasAdicionales?: string;
|
notasAdicionales?: string;
|
||||||
archivosReferencia?: Array<UUID>;
|
archivosReferencia?: Array<UUID>;
|
||||||
repositoriosIds?: Array<UUID>;
|
repositoriosIds?: Array<UUID>;
|
||||||
|
archivosAdjuntos: Array<UploadedFile>;
|
||||||
usarMCP?: boolean;
|
usarMCP?: boolean;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -339,15 +341,20 @@ export async function plans_get_document(
|
|||||||
export async function getCatalogos() {
|
export async function getCatalogos() {
|
||||||
const supabase = supabaseBrowser();
|
const supabase = supabaseBrowser();
|
||||||
|
|
||||||
const [facRes, carRes, estRes] = await Promise.all([
|
const [facultadesRes, carrerasRes, estadosRes, estructurasPlanRes] =
|
||||||
supabase.from("facultades").select("*").order("nombre"),
|
await Promise.all([
|
||||||
supabase.from("carreras").select("*").order("nombre"),
|
supabase.from("facultades").select("*").order("nombre"),
|
||||||
supabase.from("estados_plan").select("*").order("orden"),
|
supabase.from("carreras").select("*").order("nombre"),
|
||||||
]);
|
supabase.from("estados_plan").select("*").order("orden"),
|
||||||
|
supabase.from("estructuras_plan").select("*").order("creado_en", {
|
||||||
|
ascending: true,
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
facultades: facRes.data ?? [],
|
facultades: facultadesRes.data ?? [],
|
||||||
carreras: carRes.data ?? [],
|
carreras: carrerasRes.data ?? [],
|
||||||
estados: estRes.data ?? [],
|
estados: estadosRes.data ?? [],
|
||||||
|
estructurasPlan: estructurasPlanRes.data ?? [],
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createClient } from "@supabase/supabase-js";
|
|||||||
import { getEnv } from "./env";
|
import { getEnv } from "./env";
|
||||||
|
|
||||||
import type { SupabaseClient } from "@supabase/supabase-js";
|
import type { SupabaseClient } from "@supabase/supabase-js";
|
||||||
import type { Database } from "src/types/supabase.js";
|
import type { Database } from "src/types/supabase";
|
||||||
|
|
||||||
let _client: SupabaseClient<Database> | null = null;
|
let _client: SupabaseClient<Database> | null = null;
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,6 @@ export default function NuevoPlanModalContainer() {
|
|||||||
const {
|
const {
|
||||||
wizard,
|
wizard,
|
||||||
setWizard,
|
setWizard,
|
||||||
carrerasFiltradas,
|
|
||||||
canContinueDesdeModo,
|
canContinueDesdeModo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeDetalles,
|
canContinueDesdeDetalles,
|
||||||
@@ -125,7 +124,10 @@ export default function NuevoPlanModalContainer() {
|
|||||||
{({ methods }) => {
|
{({ methods }) => {
|
||||||
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
|
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
|
||||||
const totalSteps = Wizard.steps.length
|
const totalSteps = Wizard.steps.length
|
||||||
const nextStep = Wizard.steps[currentIndex]
|
const nextStep = Wizard.steps[currentIndex] ?? {
|
||||||
|
title: '',
|
||||||
|
description: '',
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -154,7 +156,6 @@ export default function NuevoPlanModalContainer() {
|
|||||||
<PasoBasicosForm
|
<PasoBasicosForm
|
||||||
wizard={wizard}
|
wizard={wizard}
|
||||||
onChange={setWizard}
|
onChange={setWizard}
|
||||||
carrerasFiltradas={carrerasFiltradas}
|
|
||||||
/>
|
/>
|
||||||
</Wizard.Stepper.Panel>
|
</Wizard.Stepper.Panel>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
import { useMemo, useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { CARRERAS } from "../catalogs";
|
|
||||||
|
|
||||||
import type { NewPlanWizardState, PlanPreview } from "../types";
|
import type { NewPlanWizardState, PlanPreview } from "../types";
|
||||||
import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
|
import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
|
||||||
@@ -16,10 +14,7 @@ export function useNuevoPlanWizard() {
|
|||||||
nivel: "",
|
nivel: "",
|
||||||
tipoCiclo: "",
|
tipoCiclo: "",
|
||||||
numCiclos: undefined,
|
numCiclos: undefined,
|
||||||
plantillaPlanId: "",
|
estructuraPlanId: null,
|
||||||
plantillaPlanVersion: "",
|
|
||||||
plantillaMapaId: "",
|
|
||||||
plantillaMapaVersion: "",
|
|
||||||
},
|
},
|
||||||
// datosBasicos: {
|
// datosBasicos: {
|
||||||
// nombrePlan: "Medicina",
|
// nombrePlan: "Medicina",
|
||||||
@@ -51,11 +46,6 @@ export function useNuevoPlanWizard() {
|
|||||||
errorMessage: null,
|
errorMessage: null,
|
||||||
});
|
});
|
||||||
|
|
||||||
const carrerasFiltradas = useMemo(() => {
|
|
||||||
const fac = wizard.datosBasicos.facultadId;
|
|
||||||
return fac ? CARRERAS.filter((c) => c.facultadId === fac) : CARRERAS;
|
|
||||||
}, [wizard.datosBasicos.facultadId]);
|
|
||||||
|
|
||||||
const canContinueDesdeModo = wizard.tipoOrigen === "MANUAL" ||
|
const canContinueDesdeModo = wizard.tipoOrigen === "MANUAL" ||
|
||||||
wizard.tipoOrigen === "IA" ||
|
wizard.tipoOrigen === "IA" ||
|
||||||
(wizard.tipoOrigen === "CLONADO_INTERNO" ||
|
(wizard.tipoOrigen === "CLONADO_INTERNO" ||
|
||||||
@@ -68,10 +58,7 @@ export function useNuevoPlanWizard() {
|
|||||||
(wizard.datosBasicos.numCiclos !== undefined &&
|
(wizard.datosBasicos.numCiclos !== undefined &&
|
||||||
wizard.datosBasicos.numCiclos > 0) &&
|
wizard.datosBasicos.numCiclos > 0) &&
|
||||||
// Requerir ambas plantillas (plan y mapa) con versión
|
// Requerir ambas plantillas (plan y mapa) con versión
|
||||||
!!wizard.datosBasicos.plantillaPlanId &&
|
!!wizard.datosBasicos.estructuraPlanId;
|
||||||
!!wizard.datosBasicos.plantillaPlanVersion &&
|
|
||||||
!!wizard.datosBasicos.plantillaMapaId &&
|
|
||||||
!!wizard.datosBasicos.plantillaMapaVersion;
|
|
||||||
|
|
||||||
const canContinueDesdeDetalles = (() => {
|
const canContinueDesdeDetalles = (() => {
|
||||||
if (wizard.tipoOrigen === "MANUAL") return true;
|
if (wizard.tipoOrigen === "MANUAL") return true;
|
||||||
@@ -130,7 +117,6 @@ export function useNuevoPlanWizard() {
|
|||||||
return {
|
return {
|
||||||
wizard,
|
wizard,
|
||||||
setWizard,
|
setWizard,
|
||||||
carrerasFiltradas,
|
|
||||||
canContinueDesdeModo,
|
canContinueDesdeModo,
|
||||||
canContinueDesdeBasicos,
|
canContinueDesdeBasicos,
|
||||||
canContinueDesdeDetalles,
|
canContinueDesdeDetalles,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import type { UploadedFile } from "@/components/planes/wizard/PasoDetallesPanel/FileDropZone";
|
||||||
import type {
|
import type {
|
||||||
NivelPlanEstudio,
|
NivelPlanEstudio,
|
||||||
TipoCiclo,
|
TipoCiclo,
|
||||||
@@ -24,10 +25,7 @@ export type NewPlanWizardState = {
|
|||||||
tipoCiclo: TipoCiclo | "";
|
tipoCiclo: TipoCiclo | "";
|
||||||
numCiclos: number | undefined;
|
numCiclos: number | undefined;
|
||||||
// Selección de plantillas (obligatorias)
|
// Selección de plantillas (obligatorias)
|
||||||
plantillaPlanId?: string;
|
estructuraPlanId: string | null;
|
||||||
plantillaPlanVersion?: string;
|
|
||||||
plantillaMapaId?: string;
|
|
||||||
plantillaMapaVersion?: string;
|
|
||||||
};
|
};
|
||||||
clonInterno?: { planOrigenId: string | null };
|
clonInterno?: { planOrigenId: string | null };
|
||||||
clonTradicional?: {
|
clonTradicional?: {
|
||||||
@@ -58,7 +56,7 @@ export type NewPlanWizardState = {
|
|||||||
archivosReferencia: Array<string>;
|
archivosReferencia: Array<string>;
|
||||||
repositoriosReferencia?: Array<string>;
|
repositoriosReferencia?: Array<string>;
|
||||||
archivosAdjuntos?: Array<
|
archivosAdjuntos?: Array<
|
||||||
{ id: string; name: string; size: string; type: string }
|
UploadedFile
|
||||||
>;
|
>;
|
||||||
};
|
};
|
||||||
resumen: { previewPlan?: PlanPreview };
|
resumen: { previewPlan?: PlanPreview };
|
||||||
|
|||||||
5
src/features/planes/utils/format-file-size.ts
Normal file
5
src/features/planes/utils/format-file-size.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
export const formatFileSize = (bytes: number): string => {
|
||||||
|
if (bytes < 1024) return bytes + " B";
|
||||||
|
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " KB";
|
||||||
|
return (bytes / (1024 * 1024)).toFixed(1) + " MB";
|
||||||
|
};
|
||||||
@@ -30,4 +30,29 @@ export const Route = createRootRouteWithContext<MyRouterContext>()({
|
|||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
|
||||||
|
errorComponent: ({ error, reset }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex min-h-[50vh] flex-col items-center justify-center space-y-4 p-6 text-center">
|
||||||
|
<h2 className="text-2xl font-bold text-red-600">
|
||||||
|
¡Ups! Algo salió mal
|
||||||
|
</h2>
|
||||||
|
<p className="max-w-md text-gray-600">
|
||||||
|
Ocurrió un error inesperado al cargar esta sección.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
{/* Opcional: Mostrar el detalle técnico en desarrollo */}
|
||||||
|
<pre className="max-w-full overflow-auto rounded border border-gray-300 bg-gray-100 p-4 text-left text-xs">
|
||||||
|
{error.message}
|
||||||
|
</pre>
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={reset}
|
||||||
|
className="rounded bg-blue-600 px-4 py-2 text-white transition-colors hover:bg-blue-700"
|
||||||
|
>
|
||||||
|
Intentar de nuevo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user