feat: Mejorar la gestión de plantillas de prueba y agregar validaciones para la composición de preguntas
This commit is contained in:
@@ -19,7 +19,7 @@ interface Question {
|
||||
question_order: number;
|
||||
question_type: QuestionType;
|
||||
question_text: string;
|
||||
options?: string[];
|
||||
options?: unknown;
|
||||
correct_answer?: unknown;
|
||||
explanation?: string;
|
||||
points: number;
|
||||
@@ -27,11 +27,12 @@ interface Question {
|
||||
}
|
||||
|
||||
interface TestTemplateFormProps {
|
||||
templateId?: string;
|
||||
onSuccess?: () => void;
|
||||
onCancel?: () => void;
|
||||
}
|
||||
|
||||
export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFormProps) {
|
||||
export default function TestTemplateForm({ templateId, onSuccess, onCancel }: TestTemplateFormProps) {
|
||||
const [formData, setFormData] = useState<CreateTestTemplatePayload>({
|
||||
name: '',
|
||||
description: '',
|
||||
@@ -62,6 +63,7 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
const [selectedCourseId, setSelectedCourseId] = useState<number | ''>('');
|
||||
const [loadingPlans, setLoadingPlans] = useState(false);
|
||||
const [loadingCourses, setLoadingCourses] = useState(false);
|
||||
const isEditing = Boolean(templateId);
|
||||
|
||||
// Load MySQL plans on mount
|
||||
useEffect(() => {
|
||||
@@ -100,6 +102,86 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
loadCourses();
|
||||
}, [selectedPlanId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!templateId) return;
|
||||
|
||||
const loadTemplateForEdit = async () => {
|
||||
try {
|
||||
setSaving(true);
|
||||
const data = await cmsApi.getTestTemplate(templateId);
|
||||
|
||||
setFormData({
|
||||
name: data.template.name,
|
||||
description: data.template.description || '',
|
||||
mysql_course_id: data.template.mysql_course_id,
|
||||
level: data.template.level,
|
||||
course_type: data.template.course_type,
|
||||
test_type: data.template.test_type,
|
||||
duration_minutes: data.template.duration_minutes,
|
||||
passing_score: data.template.passing_score,
|
||||
total_points: data.template.total_points,
|
||||
instructions: data.template.instructions || '',
|
||||
template_data: data.template.template_data || { sections: [], questions: [] },
|
||||
tags: data.template.tags || [],
|
||||
});
|
||||
|
||||
setSections(
|
||||
(data.sections || []).map((section) => ({
|
||||
id: section.id,
|
||||
title: section.title,
|
||||
description: section.description || '',
|
||||
section_order: section.section_order,
|
||||
points: section.points,
|
||||
instructions: section.instructions || '',
|
||||
}))
|
||||
);
|
||||
|
||||
setQuestions(
|
||||
(data.questions || []).map((question) => {
|
||||
const allowedTypes: QuestionType[] = [
|
||||
'multiple-choice',
|
||||
'true-false',
|
||||
'short-answer',
|
||||
'essay',
|
||||
'matching',
|
||||
'ordering',
|
||||
'fill-in-the-blanks',
|
||||
'audio-response',
|
||||
];
|
||||
const normalizedType = allowedTypes.includes(question.question_type)
|
||||
? question.question_type
|
||||
: 'multiple-choice';
|
||||
|
||||
return {
|
||||
id: question.id,
|
||||
section_id: question.section_id,
|
||||
question_order: question.question_order,
|
||||
question_type: normalizedType,
|
||||
question_text: question.question_text,
|
||||
options: question.options,
|
||||
correct_answer: question.correct_answer,
|
||||
explanation: question.explanation || '',
|
||||
points: question.points,
|
||||
metadata: question.metadata,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
if (data.template.mysql_course_id) {
|
||||
setSelectedCourseId(data.template.mysql_course_id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to load template for edit:', error);
|
||||
alert('No se pudo cargar la plantilla para editar');
|
||||
onCancel?.();
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
loadTemplateForEdit();
|
||||
}, [templateId, onCancel]);
|
||||
|
||||
// Handle course selection - store mysql_course_id (preferred approach)
|
||||
const handleCourseSelect = (courseId: number | '') => {
|
||||
setSelectedCourseId(courseId);
|
||||
@@ -123,6 +205,19 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
return;
|
||||
}
|
||||
|
||||
// Business rules by test type:
|
||||
// - CA must have at least 4 questions.
|
||||
// - MWT/MOT/FOT/FWT must have exactly 1 question.
|
||||
if (formData.test_type === 'CA' && questions.length < 4) {
|
||||
alert('Las plantillas CA deben tener minimo 4 preguntas.');
|
||||
return;
|
||||
}
|
||||
|
||||
if (formData.test_type !== 'CA' && questions.length !== 1) {
|
||||
alert('Las plantillas MWT, MOT, FOT y FWT deben tener exactamente 1 pregunta.');
|
||||
return;
|
||||
}
|
||||
|
||||
// Validate: either mysql_course_id OR level+course_type must be provided
|
||||
if (!formData.mysql_course_id && (!formData.level || !formData.course_type)) {
|
||||
alert('Debes seleccionar un curso de MySQL o especificar nivel y tipo de curso manualmente');
|
||||
@@ -132,24 +227,58 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
try {
|
||||
setSaving(true);
|
||||
|
||||
// Primero crear la plantilla
|
||||
const template = await cmsApi.createTestTemplate(formData);
|
||||
|
||||
// Luego agregar secciones
|
||||
let targetTemplateId = templateId;
|
||||
|
||||
if (isEditing && templateId) {
|
||||
await cmsApi.updateTestTemplate(templateId, {
|
||||
name: formData.name,
|
||||
description: formData.description,
|
||||
mysql_course_id: formData.mysql_course_id,
|
||||
level: formData.level,
|
||||
course_type: formData.course_type,
|
||||
test_type: formData.test_type,
|
||||
duration_minutes: formData.duration_minutes,
|
||||
passing_score: formData.passing_score,
|
||||
total_points: formData.total_points,
|
||||
instructions: formData.instructions,
|
||||
template_data: formData.template_data,
|
||||
tags: formData.tags,
|
||||
});
|
||||
|
||||
const existingData = await cmsApi.getTestTemplate(templateId);
|
||||
|
||||
for (const existingQuestion of existingData.questions) {
|
||||
await cmsApi.deleteTemplateQuestion(templateId, existingQuestion.id);
|
||||
}
|
||||
|
||||
for (const existingSection of existingData.sections) {
|
||||
await cmsApi.deleteTemplateSection(templateId, existingSection.id);
|
||||
}
|
||||
} else {
|
||||
const template = await cmsApi.createTestTemplate(formData);
|
||||
targetTemplateId = template.id;
|
||||
}
|
||||
|
||||
if (!targetTemplateId) {
|
||||
throw new Error('No se pudo determinar la plantilla a guardar');
|
||||
}
|
||||
|
||||
const sectionIdMap = new Map<string, string>();
|
||||
|
||||
for (const section of sections) {
|
||||
await cmsApi.createTemplateSection(template.id, {
|
||||
const createdSection = await cmsApi.createTemplateSection(targetTemplateId, {
|
||||
title: section.title,
|
||||
description: section.description,
|
||||
section_order: section.section_order,
|
||||
points: section.points,
|
||||
instructions: section.instructions,
|
||||
});
|
||||
sectionIdMap.set(section.id, createdSection.id);
|
||||
}
|
||||
|
||||
// Finalmente agregar preguntas
|
||||
|
||||
for (const question of questions) {
|
||||
await cmsApi.createTemplateQuestion(template.id, {
|
||||
section_id: question.section_id,
|
||||
await cmsApi.createTemplateQuestion(targetTemplateId, {
|
||||
section_id: question.section_id ? sectionIdMap.get(question.section_id) : undefined,
|
||||
question_order: question.question_order,
|
||||
question_type: question.question_type,
|
||||
question_text: question.question_text,
|
||||
@@ -160,8 +289,8 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
metadata: question.metadata,
|
||||
});
|
||||
}
|
||||
|
||||
alert('Plantilla creada exitosamente');
|
||||
|
||||
alert(isEditing ? 'Plantilla actualizada exitosamente' : 'Plantilla creada exitosamente');
|
||||
onSuccess?.();
|
||||
} catch (error) {
|
||||
console.error('Failed to create template:', error);
|
||||
@@ -319,6 +448,8 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
'essay': 'Ensayo',
|
||||
'matching': 'Emparejamiento',
|
||||
'ordering': 'Ordenar',
|
||||
'fill-in-the-blanks': 'Completar espacios',
|
||||
'audio-response': 'Respuesta de audio',
|
||||
};
|
||||
return labels[type] || type;
|
||||
};
|
||||
@@ -329,8 +460,12 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between p-6 border-b border-gray-200 sticky top-0 bg-white z-10">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-gray-900">Nueva Plantilla de Prueba</h2>
|
||||
<p className="text-sm text-gray-500">Crea preguntas y secciones para tu evaluación</p>
|
||||
<h2 className="text-xl font-bold text-gray-900">
|
||||
{isEditing ? 'Editar Plantilla de Prueba' : 'Nueva Plantilla de Prueba'}
|
||||
</h2>
|
||||
<p className="text-sm text-gray-500">
|
||||
{isEditing ? 'Modifica preguntas y secciones de tu evaluación' : 'Crea preguntas y secciones para tu evaluación'}
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={onCancel}
|
||||
@@ -763,7 +898,24 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
</label>
|
||||
<select
|
||||
value={question.question_type}
|
||||
onChange={(e) => handleUpdateQuestion(question.id, { question_type: e.target.value as QuestionType })}
|
||||
onChange={(e) => {
|
||||
const nextType = e.target.value as QuestionType;
|
||||
const updates: Partial<Question> = { question_type: nextType };
|
||||
|
||||
if (nextType === 'true-false') {
|
||||
updates.options = ['Verdadero', 'Falso'];
|
||||
updates.correct_answer =
|
||||
typeof question.correct_answer === 'number' ? question.correct_answer : 0;
|
||||
} else if (
|
||||
nextType === 'multiple-choice' &&
|
||||
!Array.isArray(question.options)
|
||||
) {
|
||||
updates.options = ['Opción 1', 'Opción 2', 'Opción 3', 'Opción 4'];
|
||||
updates.correct_answer = 0;
|
||||
}
|
||||
|
||||
handleUpdateQuestion(question.id, updates);
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="multiple-choice">Opción Múltiple</option>
|
||||
@@ -772,6 +924,8 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
<option value="essay">Ensayo</option>
|
||||
<option value="matching">Emparejamiento</option>
|
||||
<option value="ordering">Ordenar</option>
|
||||
<option value="fill-in-the-blanks">Completar espacios</option>
|
||||
<option value="audio-response">Respuesta de audio</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
@@ -810,7 +964,7 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
<label className="block text-sm font-medium text-gray-700">
|
||||
Opciones (marca la correcta)
|
||||
</label>
|
||||
{question.options?.map((option, oIdx) => (
|
||||
{(Array.isArray(question.options) ? question.options : []).map((option, oIdx) => (
|
||||
<div key={oIdx} className="flex items-center gap-2">
|
||||
<input
|
||||
type="radio"
|
||||
@@ -821,11 +975,13 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
value={option}
|
||||
value={String(option ?? '')}
|
||||
onChange={(e) => {
|
||||
const newOptions = [...(question.options || [])];
|
||||
newOptions[oIdx] = e.target.value;
|
||||
handleUpdateQuestion(question.id, { options: newOptions });
|
||||
const currentOptions = Array.isArray(question.options)
|
||||
? [...question.options]
|
||||
: [];
|
||||
currentOptions[oIdx] = e.target.value;
|
||||
handleUpdateQuestion(question.id, { options: currentOptions });
|
||||
}}
|
||||
className="flex-1 px-3 py-2 border border-gray-300 rounded-lg text-sm"
|
||||
placeholder={`Opción ${oIdx + 1}`}
|
||||
@@ -833,7 +989,10 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
const newOptions = question.options?.filter((_, idx) => idx !== oIdx);
|
||||
const currentOptions = Array.isArray(question.options)
|
||||
? question.options
|
||||
: [];
|
||||
const newOptions = currentOptions.filter((_, idx) => idx !== oIdx);
|
||||
handleUpdateQuestion(question.id, { options: newOptions });
|
||||
}}
|
||||
className="p-2 text-red-600 hover:bg-red-50 rounded"
|
||||
@@ -844,9 +1003,17 @@ export default function TestTemplateForm({ onSuccess, onCancel }: TestTemplateFo
|
||||
))}
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => handleUpdateQuestion(question.id, {
|
||||
options: [...(question.options || []), `Opción ${(question.options?.length || 0) + 1}`]
|
||||
})}
|
||||
onClick={() => {
|
||||
const currentOptions = Array.isArray(question.options)
|
||||
? question.options
|
||||
: [];
|
||||
handleUpdateQuestion(question.id, {
|
||||
options: [
|
||||
...currentOptions,
|
||||
`Opción ${currentOptions.length + 1}`,
|
||||
],
|
||||
});
|
||||
}}
|
||||
className="text-sm text-blue-600 hover:text-blue-700 flex items-center gap-1"
|
||||
>
|
||||
<Plus className="w-3 h-3" />
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import {
|
||||
cmsApi,
|
||||
questionBankApi,
|
||||
@@ -8,6 +8,8 @@ import {
|
||||
TestTemplateFilters,
|
||||
TestType,
|
||||
Course,
|
||||
Module,
|
||||
Lesson,
|
||||
MySqlPlan,
|
||||
MySqlCourse,
|
||||
} from '@/lib/api';
|
||||
@@ -26,7 +28,6 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
const [filters, setFilters] = useState<TestTemplateFilters>({});
|
||||
const [showFilters, setShowFilters] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState('');
|
||||
|
||||
const [filterPlans, setFilterPlans] = useState<MySqlPlan[]>([]);
|
||||
const [filterCourses, setFilterCourses] = useState<MySqlCourse[]>([]);
|
||||
const [filterPlanId, setFilterPlanId] = useState<number | ''>('');
|
||||
@@ -35,25 +36,18 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
const [showApplyModal, setShowApplyModal] = useState(false);
|
||||
const [selectedTemplate, setSelectedTemplate] = useState<TestTemplate | null>(null);
|
||||
const [courses, setCourses] = useState<Course[]>([]);
|
||||
|
||||
const [applyPlans, setApplyPlans] = useState<MySqlPlan[]>([]);
|
||||
const [applyCourses, setApplyCourses] = useState<MySqlCourse[]>([]);
|
||||
const [applyPlanId, setApplyPlanId] = useState<number | ''>('');
|
||||
const [applyCourseId, setApplyCourseId] = useState<number | ''>('');
|
||||
const [applyCoursesError, setApplyCoursesError] = useState('');
|
||||
const [selectedTargetCourseId, setSelectedTargetCourseId] = useState<string>('');
|
||||
|
||||
const [selectedLesson, setSelectedLesson] = useState<string>('');
|
||||
const [applying, setApplying] = useState(false);
|
||||
|
||||
const resetApplyState = () => {
|
||||
setSelectedTemplate(null);
|
||||
setApplyPlanId('');
|
||||
setApplyCourseId('');
|
||||
setApplyCoursesError('');
|
||||
setSelectedTargetCourseId('');
|
||||
setSelectedLesson('');
|
||||
};
|
||||
|
||||
const loadTemplates = async () => {
|
||||
const loadTemplates = useCallback(async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const data = await cmsApi.listTestTemplates(filters);
|
||||
@@ -63,88 +57,31 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, [filters]);
|
||||
|
||||
useEffect(() => {
|
||||
loadTemplates();
|
||||
}, [loadTemplates]);
|
||||
|
||||
useEffect(() => {
|
||||
questionBankApi.getMySQLPlans().then(setFilterPlans).catch(() => {});
|
||||
questionBankApi.getMySQLPlans().then(setApplyPlans).catch(() => {});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!filterPlanId) {
|
||||
setFilterCourses([]);
|
||||
setFilterCourseId('');
|
||||
setFilters((f) => ({ ...f, mysql_course_id: undefined }));
|
||||
setFilters((prev) => ({ ...prev, mysql_course_id: undefined }));
|
||||
return;
|
||||
}
|
||||
questionBankApi.getMySQLCoursesByPlan(filterPlanId).then(setFilterCourses).catch(() => {});
|
||||
}, [filterPlanId]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!applyPlanId) {
|
||||
setApplyCourses([]);
|
||||
setApplyCourseId('');
|
||||
setApplyCoursesError('');
|
||||
return;
|
||||
}
|
||||
questionBankApi
|
||||
.getMySQLCoursesByPlan(applyPlanId)
|
||||
.then((courses) => {
|
||||
setApplyCourses(courses);
|
||||
setApplyCoursesError('');
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to load MySQL courses by plan:', error);
|
||||
setApplyCourses([]);
|
||||
setApplyCoursesError('No se pudieron cargar los cursos para este plan.');
|
||||
.getMySQLCoursesByPlan(filterPlanId)
|
||||
.then(setFilterCourses)
|
||||
.catch(() => {
|
||||
setFilterCourses([]);
|
||||
});
|
||||
}, [applyPlanId]);
|
||||
|
||||
const getCourseTypeFromPlan = (planName: string): 'intensive' | 'regular' => {
|
||||
const planLower = (planName || '').toLowerCase();
|
||||
return planLower.includes('intensive') || planLower.includes('intensivo') ? 'intensive' : 'regular';
|
||||
};
|
||||
|
||||
const getCourseLevelFromMysql = (nivelCurso?: number, planNombre?: string): string => {
|
||||
if (typeof nivelCurso === 'number') {
|
||||
if (nivelCurso >= 1 && nivelCurso <= 2) return 'beginner';
|
||||
if (nivelCurso >= 3 && nivelCurso <= 4) return 'beginner_1';
|
||||
if (nivelCurso >= 5 && nivelCurso <= 6) return 'beginner_2';
|
||||
if (nivelCurso >= 7 && nivelCurso <= 8) return 'intermediate';
|
||||
if (nivelCurso >= 9 && nivelCurso <= 10) return 'intermediate_1';
|
||||
if (nivelCurso >= 11 && nivelCurso <= 12) return 'intermediate_2';
|
||||
return 'advanced';
|
||||
}
|
||||
|
||||
const planLower = (planNombre || '').toLowerCase();
|
||||
if (planLower.includes('basic') || planLower.includes('beginner')) return 'beginner';
|
||||
if (planLower.includes('intermediate') || planLower.includes('intermedio')) return 'intermediate';
|
||||
return 'advanced';
|
||||
};
|
||||
|
||||
const matchingPlatformCourses = useMemo(() => {
|
||||
if (!applyCourseId) return [] as Course[];
|
||||
const mysqlCourse = applyCourses.find((c) => c.idCursos === applyCourseId);
|
||||
if (!mysqlCourse) return [] as Course[];
|
||||
|
||||
const expectedLevel = getCourseLevelFromMysql(mysqlCourse.NivelCurso, mysqlCourse.NombrePlan);
|
||||
const expectedType = getCourseTypeFromPlan(mysqlCourse.NombrePlan);
|
||||
|
||||
return courses.filter((course) => {
|
||||
const courseLevel = String((course as { level?: string }).level || '').toLowerCase();
|
||||
const courseType = String((course as { course_type?: string }).course_type || '').toLowerCase();
|
||||
return courseLevel === expectedLevel && courseType === expectedType;
|
||||
});
|
||||
}, [applyCourseId, applyCourses, courses]);
|
||||
|
||||
const selectedPlatformCourseId = useMemo(() => {
|
||||
if (matchingPlatformCourses.length === 0) return '';
|
||||
return matchingPlatformCourses[0].id;
|
||||
}, [matchingPlatformCourses]);
|
||||
}, [filterPlanId]);
|
||||
|
||||
const filteredTemplates = useMemo(() => {
|
||||
if (!searchTerm.trim()) return templates;
|
||||
@@ -172,10 +109,8 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
const handleApplyTemplate = async (template: TestTemplate) => {
|
||||
setSelectedTemplate(template);
|
||||
setShowApplyModal(true);
|
||||
setApplyPlanId('');
|
||||
setApplyCourseId('');
|
||||
setSelectedTargetCourseId('');
|
||||
setSelectedLesson('');
|
||||
setApplyCoursesError('');
|
||||
|
||||
cmsApi.getCourses()
|
||||
.then(setCourses)
|
||||
@@ -183,8 +118,8 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
};
|
||||
|
||||
const handleApplyToLesson = async () => {
|
||||
if (!selectedTemplate || !selectedPlatformCourseId || !selectedLesson) {
|
||||
alert('Selecciona plan, curso y lección');
|
||||
if (!selectedTemplate || !selectedTargetCourseId || !selectedLesson) {
|
||||
alert('Selecciona curso y lección');
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -461,65 +396,27 @@ export default function TestTemplateManager({ onSelectTemplate, onCreateTemplate
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">1. Programa de Estudios</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">1. Curso OpenCCB</label>
|
||||
<select
|
||||
value={applyPlanId}
|
||||
value={selectedTargetCourseId}
|
||||
onChange={(e) => {
|
||||
setApplyPlanId(e.target.value ? Number(e.target.value) : '');
|
||||
setApplyCourseId('');
|
||||
setSelectedTargetCourseId(e.target.value);
|
||||
setSelectedLesson('');
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Selecciona un programa...</option>
|
||||
{applyPlans.map((p) => (
|
||||
<option key={p.idPlanDeEstudios} value={p.idPlanDeEstudios}>{p.NombrePlan}</option>
|
||||
<option value="">Selecciona un curso...</option>
|
||||
{courses.map((course) => (
|
||||
<option key={course.id} value={course.id}>{course.title}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{applyPlanId && (
|
||||
{selectedTargetCourseId && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">2. Curso</label>
|
||||
<select
|
||||
value={applyCourseId}
|
||||
onChange={(e) => {
|
||||
setApplyCourseId(e.target.value ? Number(e.target.value) : '');
|
||||
setSelectedLesson('');
|
||||
}}
|
||||
className="w-full px-3 py-2 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500"
|
||||
>
|
||||
<option value="">Selecciona un curso...</option>
|
||||
{applyCourses.map((c) => (
|
||||
<option key={c.idCursos} value={c.idCursos}>{c.NombreCurso}</option>
|
||||
))}
|
||||
</select>
|
||||
{applyCoursesError && (
|
||||
<p className="text-xs text-red-600 mt-1">{applyCoursesError}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{applyCourseId && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">3. Curso destino detectado</label>
|
||||
{matchingPlatformCourses.length > 0 ? (
|
||||
<div className="w-full px-3 py-2 border border-gray-300 rounded-lg bg-gray-50 text-sm text-gray-700">
|
||||
{matchingPlatformCourses[0].title}
|
||||
</div>
|
||||
) : (
|
||||
<div className="w-full px-3 py-2 border border-red-200 rounded-lg bg-red-50 text-sm text-red-700">
|
||||
No se encontró un curso de plataforma compatible para este curso SAM.
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedPlatformCourseId && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">4. Lección</label>
|
||||
<label className="block text-sm font-medium text-gray-700 mb-1">2. Lección</label>
|
||||
<LessonSelector
|
||||
courseId={selectedPlatformCourseId}
|
||||
courseId={selectedTargetCourseId}
|
||||
selectedLesson={selectedLesson}
|
||||
onSelect={setSelectedLesson}
|
||||
/>
|
||||
@@ -573,7 +470,7 @@ function LessonSelector({
|
||||
selectedLesson: string;
|
||||
onSelect: (lessonId: string) => void;
|
||||
}) {
|
||||
const [modules, setModules] = useState<any[]>([]);
|
||||
const [modules, setModules] = useState<Module[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -610,7 +507,7 @@ function LessonSelector({
|
||||
<option value="">Selecciona una lección...</option>
|
||||
{modules.map((mod) => (
|
||||
<optgroup key={mod.id} label={mod.title}>
|
||||
{(mod.lessons || []).map((lesson: any) => (
|
||||
{(mod.lessons || []).map((lesson: Lesson) => (
|
||||
<option key={lesson.id} value={lesson.id}>
|
||||
{lesson.title}
|
||||
</option>
|
||||
|
||||
Reference in New Issue
Block a user