Merge branch 'main' into issue/80-deshacerse-de-todos-estos-query-params-de-la-url

This commit is contained in:
2026-02-06 22:01:11 +00:00
7 changed files with 141 additions and 94 deletions

View File

@@ -27,14 +27,25 @@ export function PasoBasicosForm({
const [creditosInput, setCreditosInput] = useState<string>(() => { const [creditosInput, setCreditosInput] = useState<string>(() => {
const c = Number(wizard.datosBasicos.creditos ?? 0) const c = Number(wizard.datosBasicos.creditos ?? 0)
return c > 0 ? c.toFixed(2) : '' let newC = c
console.log('antes', newC)
if (Number.isFinite(c) && c > 999) {
newC = 999
}
console.log('desp', newC)
return newC > 0 ? newC.toFixed(2) : ''
}) })
const [creditosFocused, setCreditosFocused] = useState(false) const [creditosFocused, setCreditosFocused] = useState(false)
useEffect(() => { useEffect(() => {
if (creditosFocused) return if (creditosFocused) return
const c = Number(wizard.datosBasicos.creditos ?? 0) const c = Number(wizard.datosBasicos.creditos ?? 0)
setCreditosInput(c > 0 ? c.toFixed(2) : '') let newC = c
if (Number.isFinite(c) && c > 999) {
newC = 999
}
setCreditosInput(newC > 0 ? newC.toFixed(2) : '')
}, [wizard.datosBasicos.creditos, creditosFocused]) }, [wizard.datosBasicos.creditos, creditosFocused])
return ( return (
@@ -44,6 +55,7 @@ export function PasoBasicosForm({
<Input <Input
id="nombre" id="nombre"
placeholder="Ej. Matemáticas Discretas" placeholder="Ej. Matemáticas Discretas"
maxLength={200}
value={wizard.datosBasicos.nombre} value={wizard.datosBasicos.nombre}
onChange={(e) => onChange={(e) =>
onChange( onChange(
@@ -67,6 +79,7 @@ export function PasoBasicosForm({
<Input <Input
id="codigo" id="codigo"
placeholder="Ej. MAT-101" placeholder="Ej. MAT-101"
maxLength={200}
value={wizard.datosBasicos.codigo || ''} value={wizard.datosBasicos.codigo || ''}
onChange={(e) => onChange={(e) =>
onChange( onChange(
@@ -123,6 +136,7 @@ export function PasoBasicosForm({
id="creditos" id="creditos"
type="text" type="text"
inputMode="decimal" inputMode="decimal"
maxLength={6}
pattern="^\\d*(?:[.,]\\d{0,2})?$" pattern="^\\d*(?:[.,]\\d{0,2})?$"
value={creditosInput} value={creditosInput}
onKeyDown={(e) => { onKeyDown={(e) => {
@@ -144,7 +158,7 @@ export function PasoBasicosForm({
} }
const normalized = raw.replace(',', '.') const normalized = raw.replace(',', '.')
const asNumber = Number.parseFloat(normalized) let asNumber = Number.parseFloat(normalized)
if (!Number.isFinite(asNumber) || asNumber <= 0) { if (!Number.isFinite(asNumber) || asNumber <= 0) {
setCreditosInput('') setCreditosInput('')
onChange((w) => ({ onChange((w) => ({
@@ -154,6 +168,9 @@ export function PasoBasicosForm({
return return
} }
// Cap to 999
if (asNumber > 999) asNumber = 999
const fixed = asNumber.toFixed(2) const fixed = asNumber.toFixed(2)
setCreditosInput(fixed) setCreditosInput(fixed)
onChange((w) => ({ onChange((w) => ({
@@ -174,6 +191,22 @@ export function PasoBasicosForm({
if (!/^\d*(?:[.,]\d{0,2})?$/.test(nextRaw)) return if (!/^\d*(?:[.,]\d{0,2})?$/.test(nextRaw)) return
// If typed number exceeds 999, cap it immediately (prevents entering >999)
const asNumberRaw = Number.parseFloat(nextRaw.replace(',', '.'))
if (Number.isFinite(asNumberRaw) && asNumberRaw > 999) {
// show capped value to the user
const cappedStr = '999.00'
setCreditosInput(cappedStr)
onChange((w) => ({
...w,
datosBasicos: {
...w.datosBasicos,
creditos: 999,
},
}))
return
}
setCreditosInput(nextRaw) setCreditosInput(nextRaw)
const asNumber = Number.parseFloat(nextRaw.replace(',', '.')) const asNumber = Number.parseFloat(nextRaw.replace(',', '.'))
@@ -191,94 +224,6 @@ export function PasoBasicosForm({
/> />
</div> </div>
<div className="grid gap-1">
<Label htmlFor="horasAcademicas">
Horas Académicas
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
(Opcional)
</span>
</Label>
<Input
id="horasAcademicas"
type="number"
min={1}
step={1}
inputMode="numeric"
pattern="[0-9]*"
value={wizard.datosBasicos.horasAcademicas ?? ''}
onKeyDown={(e) => {
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
e.preventDefault()
}
}}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(
(w): NewSubjectWizardState => ({
...w,
datosBasicos: {
...w.datosBasicos,
horasAcademicas: (() => {
const raw = e.target.value
if (raw === '') return null
const asNumber = Number(raw)
if (Number.isNaN(asNumber)) return null
// Coerce to positive integer (natural numbers without zero)
const n = Math.floor(Math.abs(asNumber))
return n >= 1 ? n : 1
})(),
},
}),
)
}
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
placeholder="Ej. 48"
/>
</div>
<div className="grid gap-1">
<Label htmlFor="horasIndependientes">
Horas Independientes
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
(Opcional)
</span>
</Label>
<Input
id="horasIndependientes"
type="number"
min={1}
step={1}
inputMode="numeric"
pattern="[0-9]*"
value={wizard.datosBasicos.horasIndependientes ?? ''}
onKeyDown={(e) => {
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
e.preventDefault()
}
}}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(
(w): NewSubjectWizardState => ({
...w,
datosBasicos: {
...w.datosBasicos,
horasIndependientes: (() => {
const raw = e.target.value
if (raw === '') return null
const asNumber = Number(raw)
if (Number.isNaN(asNumber)) return null
// Coerce to positive integer (natural numbers without zero)
const n = Math.floor(Math.abs(asNumber))
return n >= 1 ? n : 1
})(),
},
}),
)
}
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
placeholder="Ej. 24"
/>
</div>
<div className="grid gap-1"> <div className="grid gap-1">
<Label htmlFor="estructura">Estructura de la asignatura</Label> <Label htmlFor="estructura">Estructura de la asignatura</Label>
<Select <Select
@@ -314,6 +259,98 @@ export function PasoBasicosForm({
Define los campos requeridos (ej. Objetivos, Temario, Evaluación). Define los campos requeridos (ej. Objetivos, Temario, Evaluación).
</p> </p>
</div> </div>
<div className="grid gap-1">
<Label htmlFor="horasAcademicas">
Horas Académicas
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
(Opcional)
</span>
</Label>
<Input
id="horasAcademicas"
type="number"
min={1}
max={999}
step={1}
inputMode="numeric"
pattern="[0-9]*"
value={wizard.datosBasicos.horasAcademicas ?? ''}
onKeyDown={(e) => {
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
e.preventDefault()
}
}}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(
(w): NewSubjectWizardState => ({
...w,
datosBasicos: {
...w.datosBasicos,
horasAcademicas: (() => {
const raw = e.target.value
if (raw === '') return null
const asNumber = Number(raw)
if (Number.isNaN(asNumber)) return null
// Coerce to positive integer (natural numbers without zero)
const n = Math.floor(Math.abs(asNumber))
const capped = Math.min(n >= 1 ? n : 1, 999)
return capped
})(),
},
}),
)
}
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
placeholder="Ej. 48"
/>
</div>
<div className="grid gap-1">
<Label htmlFor="horasIndependientes">
Horas Independientes
<span className="text-xs font-normal text-gray-500 dark:text-gray-400">
(Opcional)
</span>
</Label>
<Input
id="horasIndependientes"
type="number"
min={1}
max={999}
step={1}
inputMode="numeric"
pattern="[0-9]*"
value={wizard.datosBasicos.horasIndependientes ?? ''}
onKeyDown={(e) => {
if (['.', ',', '-', 'e', 'E', '+'].includes(e.key)) {
e.preventDefault()
}
}}
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange(
(w): NewSubjectWizardState => ({
...w,
datosBasicos: {
...w.datosBasicos,
horasIndependientes: (() => {
const raw = e.target.value
if (raw === '') return null
const asNumber = Number(raw)
if (Number.isNaN(asNumber)) return null
// Coerce to positive integer (natural numbers without zero)
const n = Math.floor(Math.abs(asNumber))
const capped = Math.min(n >= 1 ? n : 1, 999)
return capped
})(),
},
}),
)
}
className="placeholder:text-muted-foreground/70 font-medium not-italic placeholder:font-normal placeholder:italic"
placeholder="Ej. 24"
/>
</div>
</div> </div>
) )
} }

View File

@@ -56,6 +56,7 @@ export function PasoDetallesPanel({
<Label>Descripción del enfoque académico</Label> <Label>Descripción del enfoque académico</Label>
<Textarea <Textarea
placeholder="Describe el enfoque, alcance y público objetivo. Ej.: Teórica-práctica enfocada en patrones de diseño, con proyectos semanales." placeholder="Describe el enfoque, alcance y público objetivo. Ej.: Teórica-práctica enfocada en patrones de diseño, con proyectos semanales."
maxLength={7000}
value={wizard.iaConfig?.descripcionEnfoqueAcademico} value={wizard.iaConfig?.descripcionEnfoqueAcademico}
onChange={(e) => onChange={(e) =>
onChange( onChange(
@@ -80,6 +81,7 @@ export function PasoDetallesPanel({
</Label> </Label>
<Textarea <Textarea
placeholder="Opcional: restricciones y preferencias. Ej.: incluye bibliografía en español, evita contenido avanzado, prioriza evaluación por proyectos." placeholder="Opcional: restricciones y preferencias. Ej.: incluye bibliografía en español, evita contenido avanzado, prioriza evaluación por proyectos."
maxLength={7000}
value={wizard.iaConfig?.instruccionesAdicionalesIA} value={wizard.iaConfig?.instruccionesAdicionalesIA}
onChange={(e) => onChange={(e) =>
onChange( onChange(

View File

@@ -50,6 +50,7 @@ export function PasoBasicosForm({
id="nombrePlan" id="nombrePlan"
placeholder="Ej. Ingeniería en Sistemas (2026)" placeholder="Ej. Ingeniería en Sistemas (2026)"
value={wizard.datosBasicos.nombrePlan} value={wizard.datosBasicos.nombrePlan}
maxLength={200}
onChange={(e: React.ChangeEvent<HTMLInputElement>) => onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
onChange( onChange(
(w): NewPlanWizardState => ({ (w): NewPlanWizardState => ({
@@ -228,6 +229,7 @@ export function PasoBasicosForm({
id="numCiclos" id="numCiclos"
type="number" type="number"
min={1} min={1}
max={99}
step={1} step={1}
inputMode="numeric" inputMode="numeric"
pattern="[0-9]*" pattern="[0-9]*"
@@ -251,7 +253,8 @@ export function PasoBasicosForm({
if (Number.isNaN(asNumber)) return null if (Number.isNaN(asNumber)) return null
// Coerce to positive integer (natural numbers without zero) // Coerce to positive integer (natural numbers without zero)
const n = Math.floor(Math.abs(asNumber)) const n = Math.floor(Math.abs(asNumber))
return n >= 1 ? n : 1 const capped = Math.min(n >= 1 ? n : 1, 99)
return capped
})(), })(),
}, },
}), }),

View File

@@ -49,6 +49,7 @@ export function PasoDetallesPanel({
id="desc" id="desc"
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none" className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
placeholder="Describe el enfoque del programa…" placeholder="Describe el enfoque del programa…"
maxLength={7000}
value={wizard.iaConfig?.descripcionEnfoqueAcademico || ''} value={wizard.iaConfig?.descripcionEnfoqueAcademico || ''}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange((w) => ({ onChange((w) => ({
@@ -73,6 +74,7 @@ export function PasoDetallesPanel({
id="notas" id="notas"
className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none" className="bg-background text-foreground ring-offset-background focus-visible:ring-ring min-h-24 w-full rounded-md border px-3 py-2 text-sm shadow-sm focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
placeholder="Lineamientos institucionales, restricciones, etc." placeholder="Lineamientos institucionales, restricciones, etc."
maxLength={7000}
value={wizard.iaConfig?.instruccionesAdicionalesIA || ''} value={wizard.iaConfig?.instruccionesAdicionalesIA || ''}
onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) => onChange={(e: React.ChangeEvent<HTMLTextAreaElement>) =>
onChange((w) => ({ onChange((w) => ({

View File

@@ -79,7 +79,7 @@ export async function plans_list(
`, `,
{ count: 'exact' }, { count: 'exact' },
) )
.order('actualizado_en', { ascending: false }) .order('creado_en', { ascending: false })
// 2. Aplicamos filtros dinámicos // 2. Aplicamos filtros dinámicos

View File

@@ -93,6 +93,7 @@ export type Database = {
asignatura_hash: string | null asignatura_hash: string | null
codigo: string | null codigo: string | null
contenido_tematico: Json contenido_tematico: Json
conversation_id: string | null
creado_en: string creado_en: string
creado_por: string | null creado_por: string | null
creditos: number creditos: number
@@ -116,6 +117,7 @@ export type Database = {
asignatura_hash?: string | null asignatura_hash?: string | null
codigo?: string | null codigo?: string | null
contenido_tematico?: Json contenido_tematico?: Json
conversation_id?: string | null
creado_en?: string creado_en?: string
creado_por?: string | null creado_por?: string | null
creditos: number creditos: number
@@ -139,6 +141,7 @@ export type Database = {
asignatura_hash?: string | null asignatura_hash?: string | null
codigo?: string | null codigo?: string | null
contenido_tematico?: Json contenido_tematico?: Json
conversation_id?: string | null
creado_en?: string creado_en?: string
creado_por?: string | null creado_por?: string | null
creditos?: number creditos?: number

View File

@@ -1 +1 @@
v2.67.1 v2.75.0