This commit is contained in:
2026-01-13 14:30:57 -06:00
parent 55c37b83b4
commit b08d58e262
12 changed files with 178 additions and 107 deletions

View File

@@ -45,6 +45,7 @@
"@tanstack/eslint-config": "^0.3.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/bun": "^1.3.6",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",
@@ -503,6 +504,8 @@
"@types/babel__traverse": ["@types/babel__traverse@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.2" } }, "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q=="],
"@types/bun": ["@types/bun@1.3.6", "", { "dependencies": { "bun-types": "1.3.6" } }, "sha512-uWCv6FO/8LcpREhenN1d1b6fcspAB+cefwD7uti8C8VffIv0Um08TKMn98FynpTiU38+y2dUO55T11NgDt8VAA=="],
"@types/chai": ["@types/chai@5.2.3", "", { "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" } }, "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA=="],
"@types/deep-eql": ["@types/deep-eql@4.0.2", "", {}, "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw=="],
@@ -663,6 +666,8 @@
"browserslist": ["browserslist@4.28.1", "", { "dependencies": { "baseline-browser-mapping": "^2.9.0", "caniuse-lite": "^1.0.30001759", "electron-to-chromium": "^1.5.263", "node-releases": "^2.0.27", "update-browserslist-db": "^1.2.0" }, "bin": { "browserslist": "cli.js" } }, "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA=="],
"bun-types": ["bun-types@1.3.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-OlFwHcnNV99r//9v5IIOgQ9Uk37gZqrNMCcqEaExdkVq3Avwqok1bJFmvGMCkCE0FqzdY8VMOZpfpR3lwI+CsQ=="],
"cac": ["cac@6.7.14", "", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
"call-bind": ["call-bind@1.0.8", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", "get-intrinsic": "^1.2.4", "set-function-length": "^1.2.2" } }, "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww=="],

View File

@@ -58,6 +58,7 @@
"@tanstack/eslint-config": "^0.3.0",
"@testing-library/dom": "^10.4.0",
"@testing-library/react": "^16.2.0",
"@types/bun": "^1.3.6",
"@types/node": "^22.10.2",
"@types/react": "^19.2.0",
"@types/react-dom": "^19.2.0",

18
scripts/update-types.ts Normal file
View File

@@ -0,0 +1,18 @@
// scripts/update-types.ts
// Uso:
// bun run scripts/update-types.ts
import { $ } from "bun";
console.log("🔄 Generando tipos de Supabase...");
try {
// Ejecutamos el comando y capturamos la salida como texto
const output = await $`supabase gen types typescript --linked`.text();
// Escribimos el archivo directamente con Bun (garantiza UTF-8)
await Bun.write("src/types/supabase.ts", output);
console.log("✅ Tipos actualizados correctamente con acentos.");
} catch (error) {
console.error("❌ Error generando tipos:", error);
}

View File

@@ -1,5 +1,6 @@
import { TemplateSelectorCard } from './TemplateSelectorCard'
import type { NivelPlanEstudio, TipoCiclo } from '@/data/types/domain'
import type { CARRERAS } from '@/features/planes/nuevo/catalogs'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
@@ -13,6 +14,7 @@ import {
SelectValue,
} from '@/components/ui/select'
import { Separator } from '@/components/ui/separator'
import { useCatalogosPlanes } from '@/data/hooks/usePlans'
import {
FACULTADES,
NIVELES,
@@ -31,6 +33,18 @@ export function PasoBasicosForm({
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
carrerasFiltradas: typeof CARRERAS
}) {
const { data: catalogos } = useCatalogosPlanes()
// Preferir los catálogos remotos si están disponibles; si no, usar los locales
const facultadesList = catalogos?.facultades ?? FACULTADES
const rawCarreras = catalogos?.carreras ?? carrerasFiltradas
const filteredCarreras = rawCarreras.filter((c: any) => {
const facId = wizard.datosBasicos.facultadId
if (!facId) return true
// soportar ambos shapes: `facultad_id` (BD) o `facultadId` (local)
return c.facultad_id ? c.facultad_id === facId : c.facultadId === facId
})
return (
<div className="flex flex-col gap-2">
<div className="grid gap-4 sm:grid-cols-2">
@@ -40,7 +54,7 @@ export function PasoBasicosForm({
</Label>
<Input
id="nombrePlan"
placeholder="Ej. Ingeniería en Sistemas 2026"
placeholder="Ej. Ingeniería en Sistemas (2026)"
value={wizard.datosBasicos.nombrePlan}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange((w) => ({
@@ -79,7 +93,7 @@ export function PasoBasicosForm({
<SelectValue placeholder="Ej. Facultad de Ingeniería" />
</SelectTrigger>
<SelectContent>
{FACULTADES.map((f) => (
{facultadesList.map((f: any) => (
<SelectItem key={f.id} value={f.id}>
{f.nombre}
</SelectItem>
@@ -112,7 +126,7 @@ export function PasoBasicosForm({
<SelectValue placeholder="Ej. Ingeniería en Cibernética y Sistemas Computacionales" />
</SelectTrigger>
<SelectContent>
{carrerasFiltradas.map((c) => (
{filteredCarreras.map((c: any) => (
<SelectItem key={c.id} value={c.id}>
{c.nombre}
</SelectItem>
@@ -125,11 +139,13 @@ export function PasoBasicosForm({
<Label htmlFor="nivel">Nivel</Label>
<Select
value={wizard.datosBasicos.nivel}
onValueChange={(value) =>
onChange((w) => ({
onValueChange={(value: NivelPlanEstudio) =>
onChange(
(w): NewPlanWizardState => ({
...w,
datosBasicos: { ...w.datosBasicos, nivel: value },
}))
}),
)
}
>
<SelectTrigger
@@ -157,7 +173,7 @@ export function PasoBasicosForm({
<Label htmlFor="tipoCiclo">Tipo de ciclo</Label>
<Select
value={wizard.datosBasicos.tipoCiclo}
onValueChange={(value) =>
onValueChange={(value: TipoCiclo) =>
onChange((w) => ({
...w,
datosBasicos: {
@@ -180,8 +196,8 @@ export function PasoBasicosForm({
</SelectTrigger>
<SelectContent>
{TIPOS_CICLO.map((t) => (
<SelectItem key={t.value} value={t.value}>
{t.label}
<SelectItem key={t} value={t}>
{t}
</SelectItem>
))}
</SelectContent>

View File

@@ -30,7 +30,7 @@ export function PasoDetallesPanel({
onGenerarIA: () => void
isLoading: boolean
}) {
if (wizard.modoCreacion === 'MANUAL') {
if (wizard.tipoOrigen === 'MANUAL') {
return (
<Card>
<CardHeader>
@@ -43,7 +43,7 @@ export function PasoDetallesPanel({
)
}
if (wizard.modoCreacion === 'IA') {
if (wizard.tipoOrigen === 'IA') {
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">
@@ -162,10 +162,7 @@ export function PasoDetallesPanel({
)
}
if (
wizard.modoCreacion === 'CLONADO' &&
wizard.subModoClonado === 'INTERNO'
) {
if (wizard.tipoOrigen === 'CLONADO_INTERNO') {
return (
<div className="grid gap-4">
<div className="grid gap-3 sm:grid-cols-3">
@@ -269,10 +266,7 @@ export function PasoDetallesPanel({
)
}
if (
wizard.modoCreacion === 'CLONADO' &&
wizard.subModoClonado === 'TRADICIONAL'
) {
if (wizard.tipoOrigen === 'CLONADO_TRADICIONAL') {
return (
<div className="flex flex-col gap-4">
<div className="flex flex-col gap-1">

View File

@@ -1,10 +1,7 @@
import * as Icons from 'lucide-react'
import type {
NewPlanWizardState,
ModoCreacion,
SubModoClonado,
} from '@/features/planes/nuevo/types'
import type { TipoOrigen } from '@/data/types/domain'
import type { NewPlanWizardState } from '@/features/planes/nuevo/types'
import {
Card,
@@ -21,8 +18,7 @@ export function PasoModoCardGroup({
wizard: NewPlanWizardState
onChange: React.Dispatch<React.SetStateAction<NewPlanWizardState>>
}) {
const isSelected = (m: ModoCreacion) => wizard.modoCreacion === m
const isSubSelected = (s: SubModoClonado) => wizard.subModoClonado === s
const isSelected = (m: TipoOrigen) => wizard.tipoOrigen === m
const handleKeyActivate = (e: React.KeyboardEvent, cb: () => void) => {
const key = e.key
if (
@@ -41,19 +37,21 @@ export function PasoModoCardGroup({
<Card
className={isSelected('MANUAL') ? 'ring-ring ring-2' : ''}
onClick={() =>
onChange((w) => ({
onChange(
(w): NewPlanWizardState => ({
...w,
modoCreacion: 'MANUAL',
subModoClonado: undefined,
}))
tipoOrigen: 'MANUAL',
}),
)
}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({
onChange(
(w): NewPlanWizardState => ({
...w,
modoCreacion: 'MANUAL',
subModoClonado: undefined,
})),
tipoOrigen: 'MANUAL',
}),
),
)
}
role="button"
@@ -70,19 +68,21 @@ export function PasoModoCardGroup({
<Card
className={isSelected('IA') ? 'ring-ring ring-2' : ''}
onClick={() =>
onChange((w) => ({
onChange(
(w): NewPlanWizardState => ({
...w,
modoCreacion: 'IA',
subModoClonado: undefined,
}))
tipoOrigen: 'IA',
}),
)
}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({
onChange(
(w): NewPlanWizardState => ({
...w,
modoCreacion: 'IA',
subModoClonado: undefined,
})),
tipoOrigen: 'IA',
}),
),
)
}
role="button"
@@ -99,11 +99,13 @@ export function PasoModoCardGroup({
</Card>
<Card
className={isSelected('CLONADO') ? 'ring-ring ring-2' : ''}
onClick={() => onChange((w) => ({ ...w, modoCreacion: 'CLONADO' }))}
className={isSelected('OTRO') ? 'ring-ring ring-2' : ''}
onClick={() =>
onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' }))
}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({ ...w, modoCreacion: 'CLONADO' })),
onChange((w): NewPlanWizardState => ({ ...w, tipoOrigen: 'OTRO' })),
)
}
role="button"
@@ -115,22 +117,34 @@ export function PasoModoCardGroup({
</CardTitle>
<CardDescription>Desde un plan existente o archivos.</CardDescription>
</CardHeader>
{wizard.modoCreacion === 'CLONADO' && (
{(wizard.tipoOrigen === 'OTRO' ||
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL') && (
<CardContent className="flex flex-col gap-3">
<div
role="button"
tabIndex={0}
onClick={(e) => {
e.stopPropagation()
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' }))
onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_INTERNO',
}),
)
}}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({ ...w, subModoClonado: 'INTERNO' })),
onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_INTERNO',
}),
),
)
}
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
isSubSelected('INTERNO')
isSelected('CLONADO_INTERNO')
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
: 'border-border text-muted-foreground'
} `}
@@ -144,15 +158,25 @@ export function PasoModoCardGroup({
tabIndex={0}
onClick={(e) => {
e.stopPropagation()
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' }))
onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_TRADICIONAL',
}),
)
}}
onKeyDown={(e: React.KeyboardEvent) =>
handleKeyActivate(e, () =>
onChange((w) => ({ ...w, subModoClonado: 'TRADICIONAL' })),
onChange(
(w): NewPlanWizardState => ({
...w,
tipoOrigen: 'CLONADO_TRADICIONAL',
}),
),
)
}
className={`hover:border-primary/50 hover:bg-accent flex cursor-pointer flex-row items-center justify-center gap-2 rounded-lg border p-4 text-center transition-all sm:flex-col ${
isSubSelected('TRADICIONAL')
isSelected('CLONADO_TRADICIONAL')
? 'border-primary bg-primary/5 ring-primary text-primary ring-1'
: 'border-border text-muted-foreground'
} `}

View File

@@ -1,4 +1,4 @@
import type { Database, Enums, Tables } from "../../types/supabase";
import type { Enums, Tables } from "../../types/supabase";
export type UUID = string;
@@ -52,14 +52,14 @@ export type PlanDatosSep = {
};
export type PlanEstudioWithRel =
& Database["public"]["Tables"]["planes_estudio"]["Row"]
& Tables<"planes_estudio">
& {
carreras:
| Database["public"]["Tables"]["carreras"]["Row"] & {
facultades: Database["public"]["Tables"]["facultades"]["Row"] | null;
| Tables<"carreras"> & {
facultades: Tables<"facultades"> | null;
}
| null;
estados_plan: Database["public"]["Tables"]["estados_plan"]["Row"] | null;
estados_plan: Tables<"estados_plan"> | null;
};
export type Paged<T> = { data: Array<T>; count: number | null };

View File

@@ -3,6 +3,8 @@ import * as Icons from 'lucide-react'
import { useNuevoPlanWizard } from './hooks/useNuevoPlanWizard'
import type { NewPlanWizardState } from './types'
import { PasoBasicosForm } from '@/components/planes/wizard/PasoBasicosForm/PasoBasicosForm'
import { PasoDetallesPanel } from '@/components/planes/wizard/PasoDetallesPanel/PasoDetallesPanel'
import { PasoModoCardGroup } from '@/components/planes/wizard/PasoModoCardGroup'
@@ -61,12 +63,20 @@ export default function NuevoPlanModalContainer() {
}
const crearPlan = async () => {
setWizard((w) => ({ ...w, isLoading: true, errorMessage: null }))
setWizard((w: NewPlanWizardState) => ({
...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'
if (wizard.tipoOrigen === 'MANUAL') return 'plan_new_manual_001'
if (wizard.tipoOrigen === 'IA') return 'plan_new_ai_001'
if (
wizard.tipoOrigen === 'CLONADO_INTERNO' ||
wizard.tipoOrigen === 'CLONADO_TRADICIONAL'
)
return 'plan_new_clone_001'
return 'plan_new_import_001'
})()
navigate({ to: `/planes/${nuevoId}` })
@@ -124,7 +134,7 @@ export default function NuevoPlanModalContainer() {
totalSteps={totalSteps}
currentTitle={methods.current.title}
currentDescription={methods.current.description}
nextTitle={nextStep?.title}
nextTitle={nextStep.title}
onClose={handleClose}
Wizard={Wizard}
/>

View File

@@ -1,4 +1,4 @@
import type { TipoCiclo } from "./types";
import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
export const FACULTADES = [
{ id: "ing", nombre: "Facultad de Ingeniería" },
@@ -16,16 +16,20 @@ export const CARRERAS = [
{ id: "act", nombre: "Actuaría", facultadId: "neg" },
];
export const NIVELES = [
export const NIVELES: Array<NivelPlanEstudio> = [
"Licenciatura",
"Especialidad",
"Maestría",
"Doctorado",
"Especialidad",
"Diplomado",
"Otro",
];
export const TIPOS_CICLO: Array<{ value: TipoCiclo; label: string }> = [
{ value: "SEMESTRE", label: "Semestre" },
{ value: "CUATRIMESTRE", label: "Cuatrimestre" },
{ value: "TRIMESTRE", label: "Trimestre" },
export const TIPOS_CICLO: Array<TipoCiclo> = [
"Semestre",
"Cuatrimestre",
"Trimestre",
"Otro",
];
export const PLANES_EXISTENTES = [

View File

@@ -2,12 +2,13 @@ import { useMemo, useState } from "react";
import { CARRERAS } from "../catalogs";
import type { NewPlanWizardState, PlanPreview, TipoCiclo } from "../types";
import type { NewPlanWizardState, PlanPreview } from "../types";
import type { NivelPlanEstudio, TipoCiclo } from "@/data/types/domain";
export function useNuevoPlanWizard() {
const [wizard, setWizard] = useState<NewPlanWizardState>({
step: 1,
modoCreacion: null,
tipoOrigen: null,
datosBasicos: {
nombrePlan: "",
carreraId: "",
@@ -40,7 +41,6 @@ export function useNuevoPlanWizard() {
},
iaConfig: {
descripcionEnfoque: "",
poblacionObjetivo: "",
notasAdicionales: "",
archivosReferencia: [],
repositoriosReferencia: [],
@@ -56,9 +56,10 @@ export function useNuevoPlanWizard() {
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 canContinueDesdeModo = wizard.tipoOrigen === "MANUAL" ||
wizard.tipoOrigen === "IA" ||
(wizard.tipoOrigen === "CLONADO_INTERNO" ||
wizard.tipoOrigen === "CLONADO_TRADICIONAL");
const canContinueDesdeBasicos = !!wizard.datosBasicos.nombrePlan &&
!!wizard.datosBasicos.carreraId &&
@@ -73,17 +74,16 @@ export function useNuevoPlanWizard() {
!!wizard.datosBasicos.plantillaMapaVersion;
const canContinueDesdeDetalles = (() => {
if (wizard.modoCreacion === "MANUAL") return true;
if (wizard.modoCreacion === "IA") {
if (wizard.tipoOrigen === "MANUAL") return true;
if (wizard.tipoOrigen === "IA") {
// Requerimos descripción del enfoque y notas adicionales
return !!wizard.iaConfig?.descripcionEnfoque &&
!!wizard.iaConfig?.notasAdicionales;
!!wizard.iaConfig.notasAdicionales;
}
if (wizard.modoCreacion === "CLONADO") {
if (wizard.subModoClonado === "INTERNO") {
if (wizard.tipoOrigen === "CLONADO_INTERNO") {
return !!wizard.clonInterno?.planOrigenId;
}
if (wizard.subModoClonado === "TRADICIONAL") {
if (wizard.tipoOrigen === "CLONADO_TRADICIONAL") {
const t = wizard.clonTradicional;
if (!t) return false;
const tieneWord = !!t.archivoWordPlanId;
@@ -91,7 +91,6 @@ export function useNuevoPlanWizard() {
!!t.archivoAsignaturasExcelId;
return tieneWord && tieneAlMenosUnExcel;
}
}
return false;
})();
@@ -101,7 +100,7 @@ export function useNuevoPlanWizard() {
// Ensure preview has the stricter types required by `PlanPreview`.
let tipoCicloSafe: TipoCiclo;
if (wizard.datosBasicos.tipoCiclo === "") {
tipoCicloSafe = "SEMESTRE";
tipoCicloSafe = "Semestre";
} else {
tipoCicloSafe = wizard.datosBasicos.tipoCiclo;
}
@@ -112,7 +111,7 @@ export function useNuevoPlanWizard() {
const preview: PlanPreview = {
nombrePlan: wizard.datosBasicos.nombrePlan || "Plan sin nombre",
nivel: wizard.datosBasicos.nivel || "Licenciatura",
nivel: wizard.datosBasicos.nivel as NivelPlanEstudio,
tipoCiclo: tipoCicloSafe,
numCiclos: numCiclosSafe,
numAsignaturasAprox: numCiclosSafe * 6,
@@ -121,7 +120,7 @@ export function useNuevoPlanWizard() {
{ id: "perfil", titulo: "Perfil de egreso", resumen: "Borrador…" },
],
};
setWizard((w) => ({
setWizard((w: NewPlanWizardState) => ({
...w,
isLoading: false,
resumen: { previewPlan: preview },

View File

@@ -1,10 +1,12 @@
export type TipoCiclo = "SEMESTRE" | "CUATRIMESTRE" | "TRIMESTRE";
export type ModoCreacion = "MANUAL" | "IA" | "CLONADO";
export type SubModoClonado = "INTERNO" | "TRADICIONAL";
import type {
NivelPlanEstudio,
TipoCiclo,
TipoOrigen,
} from "@/data/types/domain";
export type PlanPreview = {
nombrePlan: string;
nivel: string;
nivel: NivelPlanEstudio;
tipoCiclo: TipoCiclo;
numCiclos: number;
numAsignaturasAprox?: number;
@@ -13,13 +15,12 @@ export type PlanPreview = {
export type NewPlanWizardState = {
step: 1 | 2 | 3 | 4;
modoCreacion: ModoCreacion | null;
subModoClonado?: SubModoClonado;
tipoOrigen: TipoOrigen | null;
datosBasicos: {
nombrePlan: string;
carreraId: string;
facultadId: string;
nivel: string;
nivel: NivelPlanEstudio | "";
tipoCiclo: TipoCiclo | "";
numCiclos: number | undefined;
// Selección de plantillas (obligatorias)
@@ -53,7 +54,6 @@ export type NewPlanWizardState = {
};
iaConfig?: {
descripcionEnfoque: string;
poblacionObjetivo: string;
notasAdicionales: string;
archivosReferencia: Array<string>;
repositoriosReferencia?: Array<string>;

Binary file not shown.