wip
This commit is contained in:
32
src/components/shadcn-studio/checkbox/checkbox-13.tsx
Normal file
32
src/components/shadcn-studio/checkbox/checkbox-13.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import { Checkbox } from '@/components/ui/checkbox'
|
||||||
|
import { Label } from '@/components/ui/label'
|
||||||
|
|
||||||
|
const CheckboxCardDemo = () => {
|
||||||
|
return (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label className="border-border hover:border-primary/30 hover:bg-accent/50 flex cursor-pointer items-center items-start gap-2 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">
|
||||||
|
<Checkbox
|
||||||
|
defaultChecked
|
||||||
|
className="data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700"
|
||||||
|
/>
|
||||||
|
<div className="grid gap-1.5 font-normal">
|
||||||
|
<p className="text-sm leading-none font-medium">Auto Start</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Starting with your OS.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
<Label className="hover:bg-accent/50 flex items-start gap-2 rounded-lg border p-3 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">
|
||||||
|
<Checkbox className="data-[state=checked]:border-blue-600 data-[state=checked]:bg-blue-600 data-[state=checked]:text-white dark:data-[state=checked]:border-blue-700 dark:data-[state=checked]:bg-blue-700" />
|
||||||
|
<div className="grid gap-1.5 font-normal">
|
||||||
|
<p className="text-sm leading-none font-medium">Auto update</p>
|
||||||
|
<p className="text-muted-foreground text-sm">
|
||||||
|
Download and install new version
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</Label>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default CheckboxCardDemo
|
||||||
76
src/components/shadcn-studio/tabs/tabs-03.tsx
Normal file
76
src/components/shadcn-studio/tabs/tabs-03.tsx
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
import { BookIcon, GiftIcon, HeartIcon } from 'lucide-react'
|
||||||
|
|
||||||
|
import CheckboxCardDemo from '../checkbox/checkbox-13'
|
||||||
|
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: 'Explore',
|
||||||
|
value: 'explore',
|
||||||
|
icon: BookIcon,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<CheckboxCardDemo />
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Favorites',
|
||||||
|
value: 'favorites',
|
||||||
|
icon: HeartIcon,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
All your{' '}
|
||||||
|
<span className="text-foreground font-semibold">favorites</span> are
|
||||||
|
saved here. Revisit articles, collections, and moments you love, any
|
||||||
|
time you want a little inspiration.
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Surprise',
|
||||||
|
value: 'surprise',
|
||||||
|
icon: GiftIcon,
|
||||||
|
content: (
|
||||||
|
<>
|
||||||
|
<span className="text-foreground font-semibold">Surprise!</span>{' '}
|
||||||
|
Here's something unexpected—a fun fact, a quirky tip, or a daily
|
||||||
|
challenge. Come back for a new surprise every day!
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const TabsWithIconDemo = () => {
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Tabs defaultValue="explore" className="gap-4">
|
||||||
|
<TabsList className="w-full">
|
||||||
|
{tabs.map(({ icon: Icon, name, value }) => (
|
||||||
|
<TabsTrigger
|
||||||
|
key={value}
|
||||||
|
value={value}
|
||||||
|
className="flex items-center gap-1 px-2.5 sm:px-3"
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
{name}
|
||||||
|
</TabsTrigger>
|
||||||
|
))}
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
{tabs.map((tab) => (
|
||||||
|
<TabsContent
|
||||||
|
key={tab.value}
|
||||||
|
value={tab.value}
|
||||||
|
className="animate-in fade-in duration-300 ease-out"
|
||||||
|
>
|
||||||
|
<p className="text-muted-foreground text-sm">{tab.content}</p>
|
||||||
|
</TabsContent>
|
||||||
|
))}
|
||||||
|
</Tabs>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export default TabsWithIconDemo
|
||||||
30
src/components/ui/checkbox.tsx
Normal file
30
src/components/ui/checkbox.tsx
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
|
||||||
|
import { CheckIcon } from "lucide-react"
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
|
function Checkbox({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof CheckboxPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<CheckboxPrimitive.Root
|
||||||
|
data-slot="checkbox"
|
||||||
|
className={cn(
|
||||||
|
"peer border-input dark:bg-input/30 data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:data-[state=checked]:bg-primary data-[state=checked]:border-primary focus-visible:border-ring focus-visible:ring-ring/50 aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive size-4 shrink-0 rounded-[4px] border shadow-xs transition-shadow outline-none focus-visible:ring-[3px] disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
>
|
||||||
|
<CheckboxPrimitive.Indicator
|
||||||
|
data-slot="checkbox-indicator"
|
||||||
|
className="grid place-content-center text-current transition-none"
|
||||||
|
>
|
||||||
|
<CheckIcon className="size-3.5" />
|
||||||
|
</CheckboxPrimitive.Indicator>
|
||||||
|
</CheckboxPrimitive.Root>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Checkbox }
|
||||||
64
src/components/ui/tabs.tsx
Normal file
64
src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
import * as TabsPrimitive from '@radix-ui/react-tabs'
|
||||||
|
import * as React from 'react'
|
||||||
|
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
|
||||||
|
function Tabs({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Root>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Root
|
||||||
|
data-slot="tabs"
|
||||||
|
className={cn('flex flex-col gap-2', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsList({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.List>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
data-slot="tabs-list"
|
||||||
|
className={cn(
|
||||||
|
'bg-muted text-muted-foreground inline-flex h-9 w-fit items-center justify-center rounded-lg p-[3px]',
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsTrigger({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
data-slot="tabs-trigger"
|
||||||
|
className={cn(
|
||||||
|
"data-[state=active]:bg-background dark:data-[state=active]:text-foreground focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 text-foreground dark:text-muted-foreground inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap transition-[color,box-shadow] focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:shadow-sm [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function TabsContent({
|
||||||
|
className,
|
||||||
|
...props
|
||||||
|
}: React.ComponentProps<typeof TabsPrimitive.Content>) {
|
||||||
|
return (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
data-slot="tabs-content"
|
||||||
|
className={cn('flex-1 outline-none', className)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
127
src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx
Normal file
127
src/features/asignaturas/nueva/NuevaAsignaturaModalContainer.tsx
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
|
||||||
|
import { useNuevaAsignaturaWizard } from './hooks/useNuevaAsignaturaWizard'
|
||||||
|
|
||||||
|
import { PasoBasicosForm } from '@/components/asignaturas/wizard/PasoBasicosForm'
|
||||||
|
import { PasoConfiguracionPanel } from '@/components/asignaturas/wizard/PasoConfiguracionPanel'
|
||||||
|
import { PasoMetodoCardGroup } from '@/components/asignaturas/wizard/PasoMetodoCardGroup'
|
||||||
|
import { PasoResumenCard } from '@/components/asignaturas/wizard/PasoResumenCard'
|
||||||
|
import { VistaSinPermisos } from '@/components/asignaturas/wizard/VistaSinPermisos'
|
||||||
|
import { WizardControls } from '@/components/asignaturas/wizard/WizardControls'
|
||||||
|
import { WizardHeader } from '@/components/asignaturas/wizard/WizardHeader'
|
||||||
|
import { defineStepper } from '@/components/stepper'
|
||||||
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
const Wizard = defineStepper(
|
||||||
|
{
|
||||||
|
id: 'metodo',
|
||||||
|
title: 'Método',
|
||||||
|
description: 'Manual, IA o Clonado',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'basicos',
|
||||||
|
title: 'Datos básicos',
|
||||||
|
description: 'Nombre y estructura',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'configuracion',
|
||||||
|
title: 'Configuración',
|
||||||
|
description: 'Detalles según modo',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'resumen',
|
||||||
|
title: 'Resumen',
|
||||||
|
description: 'Confirmar creación',
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
|
||||||
|
|
||||||
|
export function NuevaAsignaturaModalContainer({ planId }: { planId: string }) {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const role = auth_get_current_user_role()
|
||||||
|
|
||||||
|
const {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
canContinueDesdeMetodo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeConfig,
|
||||||
|
simularGeneracionIA,
|
||||||
|
crearAsignatura,
|
||||||
|
} = useNuevaAsignaturaWizard(planId)
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate({ to: `/planes/${planId}/asignaturas`, resetScroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
|
||||||
|
onInteractOutside={(e) => e.preventDefault()}
|
||||||
|
>
|
||||||
|
{role !== 'JEFE_CARRERA' ? (
|
||||||
|
<VistaSinPermisos onClose={handleClose} />
|
||||||
|
) : (
|
||||||
|
<Wizard.Stepper.Provider
|
||||||
|
initialStep={Wizard.utils.getFirst().id}
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
>
|
||||||
|
{({ methods }) => (
|
||||||
|
<>
|
||||||
|
<WizardHeader
|
||||||
|
title="Nueva Asignatura"
|
||||||
|
Wizard={Wizard}
|
||||||
|
methods={{ ...methods, onClose: handleClose }}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoMetodoCardGroup
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoBasicosForm wizard={wizard} onChange={setWizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoConfiguracionPanel
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
onGenerarIA={simularGeneracionIA}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoResumenCard wizard={wizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<WizardControls
|
||||||
|
Wizard={Wizard}
|
||||||
|
methods={methods}
|
||||||
|
wizard={wizard}
|
||||||
|
canContinueDesdeMetodo={canContinueDesdeMetodo}
|
||||||
|
canContinueDesdeBasicos={canContinueDesdeBasicos}
|
||||||
|
canContinueDesdeConfig={canContinueDesdeConfig}
|
||||||
|
onCreate={() => crearAsignatura(handleClose)}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Wizard.Stepper.Provider>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
56
src/features/asignaturas/nueva/catalogs.ts
Normal file
56
src/features/asignaturas/nueva/catalogs.ts
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
import type { TipoAsignatura } from "./types";
|
||||||
|
|
||||||
|
export const ESTRUCTURAS_SEP = [
|
||||||
|
{ id: "sep-lic-2025", label: "Licenciatura SEP v2025" },
|
||||||
|
{ id: "sep-pos-2023", label: "Posgrado SEP v2023" },
|
||||||
|
{ id: "ulsa-int-2024", label: "Estándar Interno ULSA 2024" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const TIPOS_MATERIA: Array<{ value: TipoAsignatura; label: string }> = [
|
||||||
|
{ value: "OBLIGATORIA", label: "Obligatoria" },
|
||||||
|
{ value: "OPTATIVA", label: "Optativa" },
|
||||||
|
{ value: "TRONCAL", label: "Troncal / Eje común" },
|
||||||
|
{ value: "OTRO", label: "Otro" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FACULTADES = [
|
||||||
|
{ id: "ing", nombre: "Facultad de Ingeniería" },
|
||||||
|
{ id: "med", nombre: "Facultad de Medicina" },
|
||||||
|
{ id: "neg", nombre: "Facultad de Negocios" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CARRERAS = [
|
||||||
|
{ id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
|
||||||
|
{ id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
|
||||||
|
{ id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
|
||||||
|
{ id: "act", nombre: "Actuaría", facultadId: "neg" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PLANES_MOCK = [
|
||||||
|
{ id: "p1", nombre: "Plan 2010 Sistemas", carreraId: "sis" },
|
||||||
|
{ id: "p2", nombre: "Plan 2016 Sistemas", carreraId: "sis" },
|
||||||
|
{ id: "p3", nombre: "Plan 2015 Industrial", carreraId: "ind" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const MATERIAS_MOCK = [
|
||||||
|
{
|
||||||
|
id: "m1",
|
||||||
|
nombre: "Programación Orientada a Objetos",
|
||||||
|
creditos: 8,
|
||||||
|
clave: "POO-101",
|
||||||
|
},
|
||||||
|
{ id: "m2", nombre: "Cálculo Diferencial", creditos: 6, clave: "MAT-101" },
|
||||||
|
{ id: "m3", nombre: "Ética Profesional", creditos: 4, clave: "HUM-302" },
|
||||||
|
{
|
||||||
|
id: "m4",
|
||||||
|
nombre: "Bases de Datos Avanzadas",
|
||||||
|
creditos: 8,
|
||||||
|
clave: "BD-201",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ARCHIVOS_SISTEMA_MOCK = [
|
||||||
|
{ id: "doc1", name: "Sílabo_Base_Ingenieria.pdf" },
|
||||||
|
{ id: "doc2", name: "Competencias_Egreso_2025.docx" },
|
||||||
|
{ id: "doc3", name: "Reglamento_Academico.pdf" },
|
||||||
|
];
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import { useState } from "react";
|
||||||
|
|
||||||
|
import type { AsignaturaPreview, NewSubjectWizardState } from "../types";
|
||||||
|
|
||||||
|
export function useNuevaAsignaturaWizard(planId: string) {
|
||||||
|
const [wizard, setWizard] = useState<NewSubjectWizardState>({
|
||||||
|
step: 1,
|
||||||
|
planId,
|
||||||
|
modoCreacion: null,
|
||||||
|
datosBasicos: {
|
||||||
|
nombre: "",
|
||||||
|
clave: "",
|
||||||
|
tipo: "OBLIGATORIA",
|
||||||
|
creditos: 0,
|
||||||
|
horasSemana: 0,
|
||||||
|
estructuraId: "",
|
||||||
|
},
|
||||||
|
clonInterno: {},
|
||||||
|
clonTradicional: {
|
||||||
|
archivoWordAsignaturaId: null,
|
||||||
|
archivosAdicionalesIds: [],
|
||||||
|
},
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoque: "",
|
||||||
|
notasAdicionales: "",
|
||||||
|
archivosExistentesIds: [],
|
||||||
|
},
|
||||||
|
resumen: {},
|
||||||
|
isLoading: false,
|
||||||
|
errorMessage: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const canContinueDesdeMetodo = wizard.modoCreacion === "MANUAL" ||
|
||||||
|
wizard.modoCreacion === "IA" ||
|
||||||
|
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
|
||||||
|
|
||||||
|
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombre &&
|
||||||
|
wizard.datosBasicos.creditos > 0 &&
|
||||||
|
!!wizard.datosBasicos.estructuraId;
|
||||||
|
|
||||||
|
const canContinueDesdeConfig = (() => {
|
||||||
|
if (wizard.modoCreacion === "MANUAL") return true;
|
||||||
|
if (wizard.modoCreacion === "IA") {
|
||||||
|
return !!wizard.iaConfig?.descripcionEnfoque;
|
||||||
|
}
|
||||||
|
if (wizard.modoCreacion === "CLONADO") {
|
||||||
|
if (wizard.subModoClonado === "INTERNO") {
|
||||||
|
return !!wizard.clonInterno?.asignaturaOrigenId;
|
||||||
|
}
|
||||||
|
if (wizard.subModoClonado === "TRADICIONAL") {
|
||||||
|
return !!wizard.clonTradicional?.archivoWordAsignaturaId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const simularGeneracionIA = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true }));
|
||||||
|
await new Promise((r) => setTimeout(r, 1500));
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
resumen: {
|
||||||
|
previewAsignatura: {
|
||||||
|
nombre: w.datosBasicos.nombre,
|
||||||
|
objetivo:
|
||||||
|
"Aplicar los fundamentos teóricos para la resolución de problemas...",
|
||||||
|
unidades: 5,
|
||||||
|
bibliografiaCount: 3,
|
||||||
|
} as AsignaturaPreview,
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
const crearAsignatura = async (onCreated: () => void) => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true }));
|
||||||
|
await new Promise((r) => setTimeout(r, 1000));
|
||||||
|
onCreated();
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
canContinueDesdeMetodo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeConfig,
|
||||||
|
simularGeneracionIA,
|
||||||
|
crearAsignatura,
|
||||||
|
};
|
||||||
|
}
|
||||||
45
src/features/asignaturas/nueva/types.ts
Normal file
45
src/features/asignaturas/nueva/types.ts
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
|
||||||
|
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
|
||||||
|
export type TipoAsignatura = "OBLIGATORIA" | "OPTATIVA" | "TRONCAL" | "OTRO";
|
||||||
|
|
||||||
|
export type AsignaturaPreview = {
|
||||||
|
nombre: string;
|
||||||
|
objetivo: string;
|
||||||
|
unidades: number;
|
||||||
|
bibliografiaCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewSubjectWizardState = {
|
||||||
|
step: 1 | 2 | 3 | 4;
|
||||||
|
planId: string;
|
||||||
|
modoCreacion: ModoCreacion | null;
|
||||||
|
subModoClonado?: SubModoClonado;
|
||||||
|
datosBasicos: {
|
||||||
|
nombre: string;
|
||||||
|
clave?: string;
|
||||||
|
tipo: TipoAsignatura;
|
||||||
|
creditos: number;
|
||||||
|
horasSemana?: number;
|
||||||
|
estructuraId: string;
|
||||||
|
};
|
||||||
|
clonInterno?: {
|
||||||
|
facultadId?: string;
|
||||||
|
carreraId?: string;
|
||||||
|
planOrigenId?: string;
|
||||||
|
asignaturaOrigenId?: string | null;
|
||||||
|
};
|
||||||
|
clonTradicional?: {
|
||||||
|
archivoWordAsignaturaId: string | null;
|
||||||
|
archivosAdicionalesIds: Array<string>;
|
||||||
|
};
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoque: string;
|
||||||
|
notasAdicionales: string;
|
||||||
|
archivosExistentesIds: Array<string>;
|
||||||
|
};
|
||||||
|
resumen: {
|
||||||
|
previewAsignatura?: AsignaturaPreview;
|
||||||
|
};
|
||||||
|
isLoading: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
};
|
||||||
205
src/features/planes/nuevo/NuevoPlanModalContainer.tsx
Normal file
205
src/features/planes/nuevo/NuevoPlanModalContainer.tsx
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
import { useNavigate } from '@tanstack/react-router'
|
||||||
|
import * as Icons from 'lucide-react'
|
||||||
|
|
||||||
|
import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard'
|
||||||
|
|
||||||
|
import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm'
|
||||||
|
import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel'
|
||||||
|
import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup'
|
||||||
|
import { PasoResumenCard } from '@/components/planes/wizard/PasoResumenCard'
|
||||||
|
import { WizardControls } from '@/components/planes/wizard/WizardControls'
|
||||||
|
import { WizardHeader } from '@/components/planes/wizard/WizardHeader'
|
||||||
|
import { defineStepper } from '@/components/stepper'
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardHeader,
|
||||||
|
CardTitle,
|
||||||
|
} from '@/components/ui/card'
|
||||||
|
import {
|
||||||
|
Dialog,
|
||||||
|
DialogContent,
|
||||||
|
DialogHeader,
|
||||||
|
DialogTitle,
|
||||||
|
} from '@/components/ui/dialog'
|
||||||
|
|
||||||
|
// Mock de permisos/rol
|
||||||
|
const auth_get_current_user_role = () => 'JEFE_CARRERA' as const
|
||||||
|
|
||||||
|
const Wizard = defineStepper(
|
||||||
|
{
|
||||||
|
id: 'modo',
|
||||||
|
title: 'Método',
|
||||||
|
description: 'Selecciona cómo crearás el plan',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'basicos',
|
||||||
|
title: 'Datos básicos',
|
||||||
|
description: 'Nombre, carrera, nivel y ciclos',
|
||||||
|
},
|
||||||
|
{ id: 'detalles', title: 'Detalles', description: 'IA, clonado o archivos' },
|
||||||
|
{ id: 'resumen', title: 'Resumen', description: 'Confirma y crea el plan' },
|
||||||
|
)
|
||||||
|
|
||||||
|
export default function NuevoPlanModalContainer() {
|
||||||
|
const navigate = useNavigate()
|
||||||
|
const role = auth_get_current_user_role()
|
||||||
|
|
||||||
|
const {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
carrerasFiltradas,
|
||||||
|
canContinueDesdeModo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeDetalles,
|
||||||
|
generarPreviewIA,
|
||||||
|
} = useNuevoPlanWizard()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
navigate({ to: '/planes', resetScroll: false })
|
||||||
|
}
|
||||||
|
|
||||||
|
const crearPlan = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }))
|
||||||
|
await new Promise((r) => setTimeout(r, 900))
|
||||||
|
const nuevoId = (() => {
|
||||||
|
if (wizard.modoCreacion === 'MANUAL') return 'plan_new_manual_001'
|
||||||
|
if (wizard.modoCreacion === 'IA') return 'plan_new_ai_001'
|
||||||
|
if (wizard.subModoClonado === 'INTERNO') return 'plan_new_clone_001'
|
||||||
|
return 'plan_new_import_001'
|
||||||
|
})()
|
||||||
|
navigate({ to: `/planes/${nuevoId}` })
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={true} onOpenChange={(open) => !open && handleClose()}>
|
||||||
|
<DialogContent
|
||||||
|
className="flex h-[90vh] w-[calc(100%-2rem)] flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl"
|
||||||
|
onInteractOutside={(e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{role !== 'JEFE_CARRERA' ? (
|
||||||
|
<>
|
||||||
|
<DialogHeader className="flex-none border-b p-6">
|
||||||
|
<DialogTitle>Nuevo plan de estudios</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex-1 p-6">
|
||||||
|
<Card className="border-destructive/40">
|
||||||
|
<CardHeader>
|
||||||
|
<CardTitle className="flex items-center gap-2">
|
||||||
|
<Icons.ShieldAlert className="text-destructive h-5 w-5" />
|
||||||
|
Sin permisos
|
||||||
|
</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
No tienes permisos para crear planes de estudio.
|
||||||
|
</CardDescription>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="flex justify-end">
|
||||||
|
<button
|
||||||
|
className="ring-offset-background focus:ring-ring data-[state=open]:bg-accent data-[state=open]:text-muted-foreground rounded-md border px-3 py-2 text-sm opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-none"
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Volver
|
||||||
|
</button>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<Wizard.Stepper.Provider
|
||||||
|
initialStep={Wizard.utils.getFirst().id}
|
||||||
|
className="flex h-full flex-col"
|
||||||
|
>
|
||||||
|
{({ methods }) => {
|
||||||
|
const currentIndex = Wizard.utils.getIndex(methods.current.id) + 1
|
||||||
|
const totalSteps = Wizard.steps.length
|
||||||
|
const nextStep = Wizard.steps[currentIndex]
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<WizardHeader
|
||||||
|
currentIndex={currentIndex}
|
||||||
|
totalSteps={totalSteps}
|
||||||
|
currentTitle={methods.current.title}
|
||||||
|
currentDescription={methods.current.description}
|
||||||
|
nextTitle={nextStep?.title}
|
||||||
|
onClose={handleClose}
|
||||||
|
Wizard={Wizard}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="flex-1 overflow-y-auto bg-gray-50/30 p-6">
|
||||||
|
<div className="mx-auto max-w-3xl">
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 0 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoModoCardGroup
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 1 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoBasicosForm
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
carrerasFiltradas={carrerasFiltradas}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 2 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoDetallesPanel
|
||||||
|
wizard={wizard}
|
||||||
|
onChange={setWizard}
|
||||||
|
onGenerarIA={generarPreviewIA}
|
||||||
|
isLoading={wizard.isLoading}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
{Wizard.utils.getIndex(methods.current.id) === 3 && (
|
||||||
|
<Wizard.Stepper.Panel>
|
||||||
|
<PasoResumenCard wizard={wizard} />
|
||||||
|
</Wizard.Stepper.Panel>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-none border-t bg-white p-6">
|
||||||
|
<Wizard.Stepper.Controls>
|
||||||
|
<WizardControls
|
||||||
|
errorMessage={wizard.errorMessage}
|
||||||
|
onPrev={() => methods.prev()}
|
||||||
|
onNext={() => methods.next()}
|
||||||
|
onCreate={crearPlan}
|
||||||
|
disablePrev={
|
||||||
|
Wizard.utils.getIndex(methods.current.id) === 0 ||
|
||||||
|
wizard.isLoading
|
||||||
|
}
|
||||||
|
disableNext={
|
||||||
|
wizard.isLoading ||
|
||||||
|
(Wizard.utils.getIndex(methods.current.id) === 0 &&
|
||||||
|
!canContinueDesdeModo) ||
|
||||||
|
(Wizard.utils.getIndex(methods.current.id) === 1 &&
|
||||||
|
!canContinueDesdeBasicos) ||
|
||||||
|
(Wizard.utils.getIndex(methods.current.id) === 2 &&
|
||||||
|
!canContinueDesdeDetalles)
|
||||||
|
}
|
||||||
|
disableCreate={wizard.isLoading}
|
||||||
|
isLastStep={
|
||||||
|
Wizard.utils.getIndex(methods.current.id) >=
|
||||||
|
Wizard.steps.length - 1
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Wizard.Stepper.Controls>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}}
|
||||||
|
</Wizard.Stepper.Provider>
|
||||||
|
)}
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
)
|
||||||
|
}
|
||||||
116
src/features/planes/nuevo/catalogs.ts
Normal file
116
src/features/planes/nuevo/catalogs.ts
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
import type { TipoCiclo } from "./types";
|
||||||
|
|
||||||
|
export const FACULTADES = [
|
||||||
|
{ id: "ing", nombre: "Facultad de Ingeniería" },
|
||||||
|
{
|
||||||
|
id: "med",
|
||||||
|
nombre: "Facultad de Medicina en medicina en medicina en medicina",
|
||||||
|
},
|
||||||
|
{ id: "neg", nombre: "Facultad de Negocios" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const CARRERAS = [
|
||||||
|
{ id: "sis", nombre: "Ing. en Sistemas", facultadId: "ing" },
|
||||||
|
{ id: "ind", nombre: "Ing. Industrial", facultadId: "ing" },
|
||||||
|
{ id: "medico", nombre: "Médico Cirujano", facultadId: "med" },
|
||||||
|
{ id: "act", nombre: "Actuaría", facultadId: "neg" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const NIVELES = [
|
||||||
|
"Licenciatura",
|
||||||
|
"Especialidad",
|
||||||
|
"Maestría",
|
||||||
|
"Doctorado",
|
||||||
|
];
|
||||||
|
export const TIPOS_CICLO: Array<{ value: TipoCiclo; label: string }> = [
|
||||||
|
{ value: "SEMESTRE", label: "Semestre" },
|
||||||
|
{ value: "CUATRIMESTRE", label: "Cuatrimestre" },
|
||||||
|
{ value: "TRIMESTRE", label: "Trimestre" },
|
||||||
|
];
|
||||||
|
|
||||||
|
export const PLANES_EXISTENTES = [
|
||||||
|
{
|
||||||
|
id: "plan-2021-sis",
|
||||||
|
nombre: "ISC 2021",
|
||||||
|
estado: "Aprobado",
|
||||||
|
anio: 2021,
|
||||||
|
facultadId: "ing",
|
||||||
|
carreraId: "sis",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plan-2020-ind",
|
||||||
|
nombre: "I. Industrial 2020",
|
||||||
|
estado: "Aprobado",
|
||||||
|
anio: 2020,
|
||||||
|
facultadId: "ing",
|
||||||
|
carreraId: "ind",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "plan-2019-med",
|
||||||
|
nombre: "Medicina 2019",
|
||||||
|
estado: "Vigente",
|
||||||
|
anio: 2019,
|
||||||
|
facultadId: "med",
|
||||||
|
carreraId: "medico",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const ARCHIVOS = [
|
||||||
|
{
|
||||||
|
id: "file-1",
|
||||||
|
nombre: "Sílabo POO 2023.docx",
|
||||||
|
tipo: "docx",
|
||||||
|
tamaño: "245 KB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file-2",
|
||||||
|
nombre: "Guía de prácticas BD.pdf",
|
||||||
|
tipo: "pdf",
|
||||||
|
tamaño: "1.2 MB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file-3",
|
||||||
|
nombre: "Rúbrica evaluación proyectos.xlsx",
|
||||||
|
tipo: "xlsx",
|
||||||
|
tamaño: "89 KB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file-4",
|
||||||
|
nombre: "Banco de reactivos IA.docx",
|
||||||
|
tipo: "docx",
|
||||||
|
tamaño: "567 KB",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "file-5",
|
||||||
|
nombre: "Material didáctico Web.pdf",
|
||||||
|
tipo: "pdf",
|
||||||
|
tamaño: "3.4 MB",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const REPOSITORIOS = [
|
||||||
|
{
|
||||||
|
id: "repo-1",
|
||||||
|
nombre: "Materiales ISC 2024",
|
||||||
|
descripcion: "Documentos de referencia para Ingeniería en Sistemas",
|
||||||
|
cantidadArchivos: 45,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "repo-2",
|
||||||
|
nombre: "Lineamientos SEP",
|
||||||
|
descripcion: "Documentos oficiales y normativas SEP actualizadas",
|
||||||
|
cantidadArchivos: 12,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "repo-3",
|
||||||
|
nombre: "Bibliografía Digital",
|
||||||
|
descripcion: "Recursos bibliográficos digitalizados",
|
||||||
|
cantidadArchivos: 128,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "repo-4",
|
||||||
|
nombre: "Plantillas Institucionales",
|
||||||
|
descripcion: "Formatos y plantillas oficiales ULSA",
|
||||||
|
cantidadArchivos: 23,
|
||||||
|
},
|
||||||
|
];
|
||||||
115
src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts
Normal file
115
src/features/planes/nuevo/hooks/useNuevoPlanWizard.ts
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
|
import { CARRERAS } from "../catalogs";
|
||||||
|
|
||||||
|
import type { NewPlanWizardState, PlanPreview, TipoCiclo } from "../types";
|
||||||
|
|
||||||
|
export function useNuevoPlanWizard() {
|
||||||
|
const [wizard, setWizard] = useState<NewPlanWizardState>({
|
||||||
|
step: 1,
|
||||||
|
modoCreacion: null,
|
||||||
|
datosBasicos: {
|
||||||
|
nombrePlan: "",
|
||||||
|
carreraId: "",
|
||||||
|
facultadId: "",
|
||||||
|
nivel: "",
|
||||||
|
tipoCiclo: "",
|
||||||
|
numCiclos: undefined,
|
||||||
|
},
|
||||||
|
clonInterno: { planOrigenId: null },
|
||||||
|
clonTradicional: {
|
||||||
|
archivoWordPlanId: null,
|
||||||
|
archivoMapaExcelId: null,
|
||||||
|
archivoAsignaturasExcelId: null,
|
||||||
|
},
|
||||||
|
iaConfig: {
|
||||||
|
descripcionEnfoque: "",
|
||||||
|
poblacionObjetivo: "",
|
||||||
|
notasAdicionales: "",
|
||||||
|
archivosReferencia: [],
|
||||||
|
},
|
||||||
|
resumen: {},
|
||||||
|
isLoading: false,
|
||||||
|
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.modoCreacion === "MANUAL" ||
|
||||||
|
wizard.modoCreacion === "IA" ||
|
||||||
|
(wizard.modoCreacion === "CLONADO" && !!wizard.subModoClonado);
|
||||||
|
|
||||||
|
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombrePlan &&
|
||||||
|
!!wizard.datosBasicos.carreraId &&
|
||||||
|
!!wizard.datosBasicos.facultadId &&
|
||||||
|
!!wizard.datosBasicos.nivel &&
|
||||||
|
(wizard.datosBasicos.numCiclos !== undefined &&
|
||||||
|
wizard.datosBasicos.numCiclos > 0);
|
||||||
|
|
||||||
|
const canContinueDesdeDetalles = (() => {
|
||||||
|
if (wizard.modoCreacion === "MANUAL") return true;
|
||||||
|
if (wizard.modoCreacion === "IA") {
|
||||||
|
return !!wizard.iaConfig?.descripcionEnfoque;
|
||||||
|
}
|
||||||
|
if (wizard.modoCreacion === "CLONADO") {
|
||||||
|
if (wizard.subModoClonado === "INTERNO") {
|
||||||
|
return !!wizard.clonInterno?.planOrigenId;
|
||||||
|
}
|
||||||
|
if (wizard.subModoClonado === "TRADICIONAL") {
|
||||||
|
const t = wizard.clonTradicional;
|
||||||
|
if (!t) return false;
|
||||||
|
const tieneWord = !!t.archivoWordPlanId;
|
||||||
|
const tieneAlMenosUnExcel = !!t.archivoMapaExcelId ||
|
||||||
|
!!t.archivoAsignaturasExcelId;
|
||||||
|
return tieneWord && tieneAlMenosUnExcel;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
})();
|
||||||
|
|
||||||
|
const generarPreviewIA = async () => {
|
||||||
|
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }));
|
||||||
|
await new Promise((r) => setTimeout(r, 800));
|
||||||
|
// Ensure preview has the stricter types required by `PlanPreview`.
|
||||||
|
let tipoCicloSafe: TipoCiclo;
|
||||||
|
if (wizard.datosBasicos.tipoCiclo === "") {
|
||||||
|
tipoCicloSafe = "SEMESTRE";
|
||||||
|
} else {
|
||||||
|
tipoCicloSafe = wizard.datosBasicos.tipoCiclo;
|
||||||
|
}
|
||||||
|
const numCiclosSafe: number =
|
||||||
|
typeof wizard.datosBasicos.numCiclos === "number"
|
||||||
|
? wizard.datosBasicos.numCiclos
|
||||||
|
: 1;
|
||||||
|
|
||||||
|
const preview: PlanPreview = {
|
||||||
|
nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre",
|
||||||
|
nivel: wizard.datosBasicos.nivel || "Licenciatura",
|
||||||
|
tipoCiclo: tipoCicloSafe,
|
||||||
|
numCiclos: numCiclosSafe,
|
||||||
|
numAsignaturasAprox: numCiclosSafe * 6,
|
||||||
|
secciones: [
|
||||||
|
{ id: "obj", titulo: "Objetivos", resumen: "Borrador de objetivos…" },
|
||||||
|
{ id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
setWizard((w) => ({
|
||||||
|
...w,
|
||||||
|
isLoading: false,
|
||||||
|
resumen: { previewPlan: preview },
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
wizard,
|
||||||
|
setWizard,
|
||||||
|
carrerasFiltradas,
|
||||||
|
canContinueDesdeModo,
|
||||||
|
canContinueDesdeBasicos,
|
||||||
|
canContinueDesdeDetalles,
|
||||||
|
generarPreviewIA,
|
||||||
|
};
|
||||||
|
}
|
||||||
41
src/features/planes/nuevo/types.ts
Normal file
41
src/features/planes/nuevo/types.ts
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE";
|
||||||
|
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
|
||||||
|
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
|
||||||
|
|
||||||
|
export type PlanPreview = {
|
||||||
|
nombrePlan: string;
|
||||||
|
nivel: string;
|
||||||
|
tipoCiclo: TipoCiclo;
|
||||||
|
numCiclos: number;
|
||||||
|
numAsignaturasAprox?: number;
|
||||||
|
secciones?: Array<{ id: string; titulo: string; resumen: string }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NewPlanWizardState = {
|
||||||
|
step: 1 | 2 | 3 | 4;
|
||||||
|
modoCreacion: ModoCreacion | null;
|
||||||
|
subModoClonado?: SubModoClonado;
|
||||||
|
datosBasicos: {
|
||||||
|
nombrePlan: string;
|
||||||
|
carreraId: string;
|
||||||
|
facultadId: string;
|
||||||
|
nivel: string;
|
||||||
|
tipoCiclo: TipoCiclo | "";
|
||||||
|
numCiclos: number | undefined;
|
||||||
|
};
|
||||||
|
clonInterno?: { planOrigenId: string | null };
|
||||||
|
clonTradicional?: {
|
||||||
|
archivoWordPlanId: string | null;
|
||||||
|
archivoMapaExcelId: string | null;
|
||||||
|
archivoAsignaturasExcelId: string | null;
|
||||||
|
};
|
||||||
|
iaConfig?: {
|
||||||
|
descripcionEnfoque: string;
|
||||||
|
poblacionObjetivo: string;
|
||||||
|
notasAdicionales: string;
|
||||||
|
archivosReferencia: Array<string>;
|
||||||
|
};
|
||||||
|
resumen: { previewPlan?: PlanPreview };
|
||||||
|
isLoading: boolean;
|
||||||
|
errorMessage: string | null;
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user