2 Commits

Author SHA1 Message Date
9fd816bfa1 Actualizar esta sección de seriación fix #195 2026-03-20 16:09:39 -06:00
658b2e245c Merge pull request 'Que no haga scroll fix #193' (#199) from issue/193-que-no-haga-scroll into main
All checks were successful
Deploy to Azure Static Web Apps / build-and-deploy (push) Successful in 1m20s
Reviewed-on: #199
2026-03-19 20:20:45 +00:00
2 changed files with 155 additions and 105 deletions

View File

@@ -7,6 +7,13 @@ import type { AsignaturaDetail } from '@/data'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select'
import { Textarea } from '@/components/ui/textarea' import { Textarea } from '@/components/ui/textarea'
import { import {
Tooltip, Tooltip,
@@ -14,6 +21,7 @@ import {
TooltipProvider, TooltipProvider,
TooltipTrigger, TooltipTrigger,
} from '@/components/ui/tooltip' } from '@/components/ui/tooltip'
import { usePlanAsignaturas } from '@/data'
import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects' import { useSubject, useUpdateAsignatura } from '@/data/hooks/useSubjects'
export interface BibliografiaEntry { export interface BibliografiaEntry {
@@ -59,8 +67,12 @@ export default function AsignaturaDetailPage() {
const { asignaturaId } = useParams({ const { asignaturaId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId', from: '/planes/$planId/asignaturas/$asignaturaId',
}) })
const { planId } = useParams({
from: '/planes/$planId/asignaturas/$asignaturaId',
})
const { data: asignaturaApi } = useSubject(asignaturaId) const { data: asignaturaApi } = useSubject(asignaturaId)
const { data: asignaturasApi, isLoading: loadingAsig } =
usePlanAsignaturas(planId)
const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null) const [asignatura, setAsignatura] = useState<AsignaturaDetail | null>(null)
const updateAsignatura = useUpdateAsignatura() const updateAsignatura = useUpdateAsignatura()
@@ -81,16 +93,54 @@ export default function AsignaturaDetailPage() {
}, },
}) })
} }
const asignaturaSeriada = useMemo(() => {
if (!asignaturaApi?.prerrequisito_asignatura_id || !asignaturasApi)
return null
return asignaturasApi.find(
(asig) => asig.id === asignaturaApi.prerrequisito_asignatura_id,
)
}, [asignaturaApi, asignaturasApi])
const requisitosFormateados = useMemo(() => {
if (!asignaturaSeriada) return []
return [
{
type: 'Pre-requisito',
code: asignaturaSeriada.codigo,
name: asignaturaSeriada.nombre,
id: asignaturaSeriada.id, // Guardamos el ID para el select
},
]
}, [asignaturaSeriada])
const handleUpdatePrerrequisito = (newId: string | null) => {
updateAsignatura.mutate({
asignaturaId,
patch: {
prerrequisito_asignatura_id: newId,
},
})
}
/* ---------- sincronizar API ---------- */ /* ---------- sincronizar API ---------- */
useEffect(() => { useEffect(() => {
if (asignaturaApi) setAsignatura(asignaturaApi) console.log(requisitosFormateados)
}, [asignaturaApi])
return <DatosGenerales onPersistDato={handlePersistDatoGeneral} /> if (asignaturaApi) setAsignatura(asignaturaApi)
}, [asignaturaApi, requisitosFormateados])
return (
<DatosGenerales
pre={requisitosFormateados}
availableSubjects={asignaturasApi}
onPersistDato={handlePersistDatoGeneral}
/>
)
} }
function DatosGenerales({ function DatosGenerales({
onPersistDato, onPersistDato,
pre,
availableSubjects,
}: { }: {
onPersistDato: (clave: string, value: string) => void onPersistDato: (clave: string, value: string) => void
}) { }) {
@@ -265,18 +315,19 @@ function DatosGenerales({
<InfoCard <InfoCard
title="Requisitos y Seriación" title="Requisitos y Seriación"
type="requirements" type="requirements"
initialContent={[ initialContent={pre}
{ // Pasamos las materias del plan para el Select (excluyendo la actual)
type: 'Pre-requisito', availableSubjects={
code: 'PA-301', availableSubjects?.filter((a) => a.id !== asignaturaId) || []
name: 'Programación Avanzada', }
onPersist={({ value }) => {
updateAsignatura.mutate({
asignaturaId,
patch: {
prerrequisito_asignatura_id: value, // value ya viene como ID o null desde handleSave
}, },
{ })
type: 'Co-requisito', }}
code: 'MAT-201',
name: 'Matemáticas Discretas',
},
]}
/> />
{/* Tarjeta de Evaluación */} {/* Tarjeta de Evaluación */}
@@ -316,6 +367,7 @@ interface InfoCardProps {
containerRef?: React.RefObject<HTMLDivElement | null> containerRef?: React.RefObject<HTMLDivElement | null>
forceEditToken?: number forceEditToken?: number
highlightToken?: number highlightToken?: number
availableSubjects?: any
} }
function InfoCard({ function InfoCard({
@@ -332,6 +384,7 @@ function InfoCard({
containerRef, containerRef,
forceEditToken, forceEditToken,
highlightToken, highlightToken,
availableSubjects,
}: InfoCardProps) { }: InfoCardProps) {
const [isEditing, setIsEditing] = useState(false) const [isEditing, setIsEditing] = useState(false)
const [isHighlighted, setIsHighlighted] = useState(false) const [isHighlighted, setIsHighlighted] = useState(false)
@@ -349,7 +402,8 @@ function InfoCard({
useEffect(() => { useEffect(() => {
setData(initialContent) setData(initialContent)
setTempText(initialContent) setTempText(initialContent)
console.log(data)
console.log(initialContent)
if (type === 'evaluation') { if (type === 'evaluation') {
const raw = Array.isArray(initialContent) ? initialContent : [] const raw = Array.isArray(initialContent) ? initialContent : []
const rows: Array<CriterioEvaluacionRowDraft> = raw const rows: Array<CriterioEvaluacionRowDraft> = raw
@@ -392,6 +446,8 @@ function InfoCard({
const handleSave = () => { const handleSave = () => {
console.log('clave, valor:', clave, String(tempText ?? '')) console.log('clave, valor:', clave, String(tempText ?? ''))
console.log(clave)
console.log(tempText)
if (type === 'evaluation') { if (type === 'evaluation') {
const cleaned: Array<CriterioEvaluacionRow> = [] const cleaned: Array<CriterioEvaluacionRow> = []
@@ -422,6 +478,25 @@ function InfoCard({
void onPersist?.({ type, clave, value: cleaned }) void onPersist?.({ type, clave, value: cleaned })
return return
} }
if (type === 'requirements') {
console.log('entre aqui ')
// Si tempText es un array y tiene elementos, tomamos el ID del primero
// Si es "none" o está vacío, mandamos null (para limpiar la seriación)
const prerequisiteId =
Array.isArray(tempText) && tempText.length > 0 ? tempText[0].id : null
setData(tempText) // Actualiza la vista local
setIsEditing(false)
// Mandamos el ID específico a la base de datos
void onPersist?.({
type,
clave: 'prerrequisito_asignatura_id', // Forzamos la columna correcta
value: prerequisiteId,
})
return
}
setData(tempText) setData(tempText)
setIsEditing(false) setIsEditing(false)
@@ -541,7 +616,52 @@ function InfoCard({
<CardContent className="pt-4"> <CardContent className="pt-4">
{isEditing ? ( {isEditing ? (
<div className="space-y-3"> <div className="space-y-3">
{type === 'evaluation' ? ( {/* Condicionales de edición según el tipo */}
{type === 'requirements' ? (
<div className="space-y-3">
<label className="text-xs font-medium text-slate-500">
Materia de Seriación
</label>
<Select
value={tempText?.[0]?.id || 'none'}
onValueChange={(val) => {
const selected = availableSubjects?.find(
(s) => s.id === val,
)
if (val === 'none' || !selected) {
console.log('guardando')
setTempText([])
} else {
console.log('hola')
setTempText([
{
id: selected.id,
type: 'Pre-requisito',
code: selected.codigo,
name: selected.nombre,
},
])
}
}}
>
<SelectTrigger>
<SelectValue placeholder="Selecciona una materia" />
</SelectTrigger>
<SelectContent>
<SelectItem value="none">
Ninguna (Sin seriación)
</SelectItem>
{availableSubjects?.map((asig) => (
<SelectItem key={asig.id} value={asig.id}>
{asig.codigo} - {asig.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
) : type === 'evaluation' ? (
<div className="space-y-3"> <div className="space-y-3">
<div className="space-y-2"> <div className="space-y-2">
{evalRows.map((row) => ( {evalRows.map((row) => (
@@ -563,85 +683,36 @@ function InfoCard({
) )
}} }}
/> />
<Input <Input
value={row.porcentaje} value={row.porcentaje}
placeholder="%" placeholder="%"
type="number" type="number"
min={1}
max={100}
step={1}
inputMode="numeric"
onChange={(e) => { onChange={(e) => {
const raw = e.target.value const raw = e.target.value
// Solo permitir '' o dígitos
if (raw !== '' && !/^\d+$/.test(raw)) return if (raw !== '' && !/^\d+$/.test(raw)) return
if (raw === '') {
setEvalRows((prev) =>
prev.map((r) =>
r.id === row.id
? {
id: r.id,
criterio: r.criterio,
porcentaje: '',
}
: r,
),
)
return
}
const n = Number(raw)
if (!Number.isFinite(n)) return
const porcentaje = Math.trunc(n)
if (porcentaje < 1 || porcentaje > 100) return
// No permitir suma > 100
setEvalRows((prev) => { setEvalRows((prev) => {
const next = prev.map((r) => const next = prev.map((r) =>
r.id === row.id r.id === row.id ? { ...r, porcentaje: raw } : r,
? { )
id: r.id, const total = next.reduce(
criterio: r.criterio, (acc, r) => acc + (Number(r.porcentaje) || 0),
porcentaje: raw, 0,
}
: r,
) )
const total = next.reduce((acc, r) => {
const v = String(r.porcentaje).trim()
if (!v) return acc
const nn = Number(v)
if (!Number.isFinite(nn)) return acc
const vv = Math.trunc(nn)
if (vv < 1 || vv > 100) return acc
return acc + vv
}, 0)
return total > 100 ? prev : next return total > 100 ? prev : next
}) })
}} }}
/> />
<div className="text-sm text-slate-600">%</div>
<div
className="flex w-[1ch] items-center justify-center text-sm text-slate-600"
aria-hidden
>
%
</div>
<Button <Button
variant="ghost" variant="ghost"
size="icon" size="icon"
className="h-8 w-8 text-red-600 hover:bg-red-50" className="h-8 w-8 text-red-600 hover:bg-red-50"
onClick={() => { onClick={() =>
setEvalRows((prev) => setEvalRows((prev) =>
prev.filter((r) => r.id !== row.id), prev.filter((r) => r.id !== row.id),
) )
}} }
aria-label="Quitar renglón"
title="Quitar"
> >
<Minus className="h-4 w-4" /> <Minus className="h-4 w-4" />
</Button> </Button>
@@ -651,22 +722,15 @@ function InfoCard({
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span <span
className={ className={`text-sm ${evaluationTotal === 100 ? 'text-muted-foreground' : 'text-destructive font-semibold'}`}
'text-sm ' +
(evaluationTotal === 100
? 'text-muted-foreground'
: 'text-destructive font-semibold')
}
> >
Total: {evaluationTotal}/100 Total: {evaluationTotal}/100
</span> </span>
<Button <Button
variant="ghost" variant="ghost"
size="sm" size="sm"
className="text-emerald-700 hover:bg-emerald-50" className="text-emerald-700 hover:bg-emerald-50"
onClick={() => { onClick={() =>
// Agregar una fila vacía (siempre permitido)
setEvalRows((prev) => [ setEvalRows((prev) => [
...prev, ...prev,
{ {
@@ -675,7 +739,7 @@ function InfoCard({
porcentaje: '', porcentaje: '',
}, },
]) ])
}} }
> >
<Plus className="mr-2 h-4 w-4" /> Agregar renglón <Plus className="mr-2 h-4 w-4" /> Agregar renglón
</Button> </Button>
@@ -689,28 +753,15 @@ function InfoCard({
className="min-h-30 text-sm leading-relaxed" className="min-h-30 text-sm leading-relaxed"
/> />
)} )}
{/* Botones de acción comunes */}
<div className="flex justify-end gap-2"> <div className="flex justify-end gap-2">
<Button <Button
size="sm" size="sm"
variant="ghost" variant="ghost"
onClick={() => { onClick={() => {
setIsEditing(false) setIsEditing(false)
if (type === 'evaluation') { // Lógica de reset si es necesario...
const raw = Array.isArray(data) ? data : []
setEvalRows(
raw.map((r: CriterioEvaluacionRow) => ({
id: crypto.randomUUID(),
criterio:
typeof r.criterio === 'string' ? r.criterio : '',
porcentaje:
typeof r.porcentaje === 'number'
? String(Math.trunc(r.porcentaje))
: typeof r.porcentaje === 'string'
? String(Math.trunc(Number(r.porcentaje)))
: '',
})),
)
}
}} }}
> >
Cancelar Cancelar
@@ -726,6 +777,7 @@ function InfoCard({
</div> </div>
</div> </div>
) : ( ) : (
/* Modo Visualización */
<div className="text-sm leading-relaxed text-slate-600"> <div className="text-sm leading-relaxed text-slate-600">
{type === 'text' && {type === 'text' &&
(data ? ( (data ? (
@@ -734,9 +786,7 @@ function InfoCard({
<p className="text-slate-400 italic">Sin información.</p> <p className="text-slate-400 italic">Sin información.</p>
))} ))}
{type === 'requirements' && <RequirementsView items={data} />} {type === 'requirements' && <RequirementsView items={data} />}
{type === 'evaluation' && ( {type === 'evaluation' && <EvaluationView items={data} />}
<EvaluationView items={data as Array<CriterioEvaluacionRow>} />
)}
</div> </div>
)} )}
</CardContent> </CardContent>

View File

@@ -191,7 +191,7 @@ export async function subjects_get(subjectId: UUID): Promise<AsignaturaDetail> {
.from('asignaturas') .from('asignaturas')
.select( .select(
` `
id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion, id,plan_estudio_id,estructura_id,codigo,nombre,tipo,creditos,numero_ciclo,linea_plan_id,orden_celda,estado,datos,contenido_tematico,horas_academicas,horas_independientes,asignatura_hash,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,criterios_de_evaluacion,prerrequisito_asignatura_id,
planes_estudio( planes_estudio(
id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en, id,carrera_id,estructura_id,nombre,nivel,tipo_ciclo,numero_ciclos,datos,estado_actual_id,activo,tipo_origen,meta_origen,creado_por,actualizado_por,creado_en,actualizado_en,
carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono)) carreras(id,facultad_id,nombre,nombre_corto,clave_sep,activa, facultades(id,nombre,nombre_corto,color,icono))