Actualizar esta sección de seriación fix #195
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
Reference in New Issue
Block a user