feat: activate AI usage
This commit is contained in:
@@ -0,0 +1,52 @@
|
|||||||
|
-- AI Usage Logs: Add prompt and response fields for detailed tracking
|
||||||
|
-- This allows storing the actual prompts and responses for debugging and analytics
|
||||||
|
|
||||||
|
-- Add new columns to ai_usage_logs
|
||||||
|
ALTER TABLE ai_usage_logs
|
||||||
|
ADD COLUMN IF NOT EXISTS prompt TEXT,
|
||||||
|
ADD COLUMN IF NOT EXISTS response TEXT;
|
||||||
|
|
||||||
|
-- Update log_ai_usage function to accept prompt and response
|
||||||
|
CREATE OR REPLACE FUNCTION log_ai_usage(
|
||||||
|
p_user_id UUID,
|
||||||
|
p_org_id UUID,
|
||||||
|
p_tokens INTEGER,
|
||||||
|
p_input_tokens INTEGER,
|
||||||
|
p_output_tokens INTEGER,
|
||||||
|
p_endpoint VARCHAR,
|
||||||
|
p_model VARCHAR,
|
||||||
|
p_request_type VARCHAR,
|
||||||
|
p_metadata JSONB,
|
||||||
|
p_prompt TEXT DEFAULT NULL,
|
||||||
|
p_response TEXT DEFAULT NULL
|
||||||
|
)
|
||||||
|
RETURNS UUID AS $$
|
||||||
|
DECLARE
|
||||||
|
v_log_id UUID;
|
||||||
|
v_cost NUMERIC(10, 6);
|
||||||
|
BEGIN
|
||||||
|
-- Calculate estimated cost (OpenAI-like pricing)
|
||||||
|
v_cost := (p_input_tokens::NUMERIC * 0.000001) + (p_output_tokens::NUMERIC * 0.000003);
|
||||||
|
|
||||||
|
INSERT INTO ai_usage_logs (
|
||||||
|
user_id, organization_id, tokens_used, input_tokens, output_tokens,
|
||||||
|
endpoint, model, request_type, request_metadata, estimated_cost_usd,
|
||||||
|
prompt, response
|
||||||
|
)
|
||||||
|
VALUES (
|
||||||
|
p_user_id, p_org_id, p_tokens, p_input_tokens, p_output_tokens,
|
||||||
|
p_endpoint, p_model, p_request_type, p_metadata, v_cost,
|
||||||
|
p_prompt, p_response
|
||||||
|
)
|
||||||
|
RETURNING id INTO v_log_id;
|
||||||
|
|
||||||
|
RETURN v_log_id;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
-- Add indexes for text search (optional, can be enabled if needed for analytics)
|
||||||
|
-- CREATE INDEX idx_ai_usage_prompt ON ai_usage_logs USING gin (to_tsvector('spanish', prompt));
|
||||||
|
-- CREATE INDEX idx_ai_usage_response ON ai_usage_logs USING gin (to_tsvector('spanish', response));
|
||||||
|
|
||||||
|
COMMENT ON COLUMN ai_usage_logs.prompt IS 'The actual prompt sent to the AI model';
|
||||||
|
COMMENT ON COLUMN ai_usage_logs.response IS 'The AI model response content';
|
||||||
@@ -1135,6 +1135,29 @@ pub async fn summarize_lesson(
|
|||||||
.unwrap_or("")
|
.unwrap_or("")
|
||||||
.trim();
|
.trim();
|
||||||
|
|
||||||
|
// Calculate and log token usage
|
||||||
|
let system_prompt = "You are an expert English Teacher. Summarize the following lesson content.";
|
||||||
|
let input_tokens = count_tokens(system_prompt) + count_tokens(&content_text);
|
||||||
|
let output_tokens = count_tokens(summary);
|
||||||
|
let total_tokens = input_tokens + output_tokens;
|
||||||
|
|
||||||
|
let _ = sqlx::query("SELECT log_ai_usage($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
|
||||||
|
.bind(claims.sub)
|
||||||
|
.bind(org_ctx.id)
|
||||||
|
.bind(total_tokens)
|
||||||
|
.bind(input_tokens)
|
||||||
|
.bind(output_tokens)
|
||||||
|
.bind("/lessons/summarize")
|
||||||
|
.bind(&model)
|
||||||
|
.bind("summary")
|
||||||
|
.bind(&json!({
|
||||||
|
"lesson_id": id,
|
||||||
|
}))
|
||||||
|
.bind(&format!("{} - {}", system_prompt, content_text)) // prompt
|
||||||
|
.bind(summary) // response
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
// 3. Update lesson
|
// 3. Update lesson
|
||||||
let updated_lesson =
|
let updated_lesson =
|
||||||
sqlx::query_as::<_, Lesson>("UPDATE lessons SET summary = $1 WHERE id = $2 RETURNING *")
|
sqlx::query_as::<_, Lesson>("UPDATE lessons SET summary = $1 WHERE id = $2 RETURNING *")
|
||||||
@@ -1274,8 +1297,8 @@ pub async fn generate_quiz(
|
|||||||
let output_tokens = count_tokens(&response_json.to_string());
|
let output_tokens = count_tokens(&response_json.to_string());
|
||||||
let total_tokens = input_tokens + output_tokens;
|
let total_tokens = input_tokens + output_tokens;
|
||||||
|
|
||||||
// Log AI usage
|
// Log AI usage with prompt and response
|
||||||
let _ = sqlx::query("SELECT log_ai_usage($1, $2, $3, $4, $5, $6, $7, $8, $9)")
|
let _ = sqlx::query("SELECT log_ai_usage($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
|
||||||
.bind(claims.sub)
|
.bind(claims.sub)
|
||||||
.bind(org_ctx.id)
|
.bind(org_ctx.id)
|
||||||
.bind(total_tokens)
|
.bind(total_tokens)
|
||||||
@@ -1288,6 +1311,8 @@ pub async fn generate_quiz(
|
|||||||
"lesson_id": id,
|
"lesson_id": id,
|
||||||
"quiz_type": quiz_req.quiz_type,
|
"quiz_type": quiz_req.quiz_type,
|
||||||
}))
|
}))
|
||||||
|
.bind(&system_prompt) // prompt
|
||||||
|
.bind(&response_json.to_string()) // response
|
||||||
.execute(&pool)
|
.execute(&pool)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -1804,7 +1829,7 @@ pub async fn generate_mermaid_diagram(
|
|||||||
let transcription_str = lesson.transcription.as_ref().and_then(|v| v.as_str());
|
let transcription_str = lesson.transcription.as_ref().and_then(|v| v.as_str());
|
||||||
let summary_str = lesson.summary.as_deref();
|
let summary_str = lesson.summary.as_deref();
|
||||||
let lesson_context = transcription_str.or(summary_str).unwrap_or("Conceptos generales de la lección.");
|
let lesson_context = transcription_str.or(summary_str).unwrap_or("Conceptos generales de la lección.");
|
||||||
let user_hint = payload.prompt_hint.unwrap_or_else(|| "Extrae los conceptos y flujos principales de la lección.".to_string());
|
let user_hint = payload.prompt_hint.clone().unwrap_or_else(|| "Extrae los conceptos y flujos principales de la lección.".to_string());
|
||||||
|
|
||||||
let system_prompt = format!(
|
let system_prompt = format!(
|
||||||
"Eres un experto arquitecto de información y especialista en diagramación usando Mermaid.js.\n\
|
"Eres un experto arquitecto de información y especialista en diagramación usando Mermaid.js.\n\
|
||||||
@@ -1861,6 +1886,29 @@ pub async fn generate_mermaid_diagram(
|
|||||||
.strip_prefix("```\n").unwrap_or(ai_response)
|
.strip_prefix("```\n").unwrap_or(ai_response)
|
||||||
.strip_suffix("```").unwrap_or(ai_response).trim();
|
.strip_suffix("```").unwrap_or(ai_response).trim();
|
||||||
|
|
||||||
|
// Calculate and log token usage
|
||||||
|
let input_tokens = count_tokens(&system_prompt) + count_tokens("Genera el código Mermaid directamente.");
|
||||||
|
let output_tokens = count_tokens(cleaned_response);
|
||||||
|
let total_tokens = input_tokens + output_tokens;
|
||||||
|
|
||||||
|
let _ = sqlx::query("SELECT log_ai_usage($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
|
||||||
|
.bind(_claims.sub)
|
||||||
|
.bind(org_ctx.id)
|
||||||
|
.bind(total_tokens)
|
||||||
|
.bind(input_tokens)
|
||||||
|
.bind(output_tokens)
|
||||||
|
.bind("/lessons/generate-mermaid")
|
||||||
|
.bind(&model)
|
||||||
|
.bind("diagram-generation")
|
||||||
|
.bind(&json!({
|
||||||
|
"lesson_id": lesson_id,
|
||||||
|
"hint": payload.prompt_hint,
|
||||||
|
}))
|
||||||
|
.bind(&system_prompt) // prompt
|
||||||
|
.bind(cleaned_response) // response
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(serde_json::json!({
|
Ok(Json(serde_json::json!({
|
||||||
"mermaid_code": cleaned_response,
|
"mermaid_code": cleaned_response,
|
||||||
})))
|
})))
|
||||||
@@ -2116,6 +2164,30 @@ pub async fn generate_hotspots(
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Calculate and log token usage
|
||||||
|
let full_prompt = format!("{} - {}", system_prompt, user_prompt);
|
||||||
|
let input_tokens = count_tokens(&full_prompt) + 500; // Estimate for image tokens
|
||||||
|
let output_tokens = count_tokens(&hotspots.to_string());
|
||||||
|
let total_tokens = input_tokens + output_tokens;
|
||||||
|
|
||||||
|
let _ = sqlx::query("SELECT log_ai_usage($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)")
|
||||||
|
.bind(_claims.sub)
|
||||||
|
.bind(org_ctx.id)
|
||||||
|
.bind(total_tokens)
|
||||||
|
.bind(input_tokens)
|
||||||
|
.bind(output_tokens)
|
||||||
|
.bind("/lessons/generate-hotspots")
|
||||||
|
.bind(&model)
|
||||||
|
.bind("hotspots-generation")
|
||||||
|
.bind(&json!({
|
||||||
|
"lesson_id": lesson_id,
|
||||||
|
"image_url": payload.image_url,
|
||||||
|
}))
|
||||||
|
.bind(&full_prompt) // prompt
|
||||||
|
.bind(&hotspots.to_string()) // response
|
||||||
|
.execute(&pool)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(hotspots))
|
Ok(Json(hotspots))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -135,9 +135,9 @@ fn calculate_course_type(plan_name: &str) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn calculate_course_type_from_duration(duracion: Option<i32>) -> String {
|
fn calculate_course_type_from_duration(duracion: Option<f64>) -> String {
|
||||||
match duracion {
|
match duracion {
|
||||||
Some(d) if d >= 70 => "intensive".to_string(), // 80h or more = intensive
|
Some(d) if d as i64 >= 70 => "intensive".to_string(), // 80h or more = intensive
|
||||||
_ => "regular".to_string(), // 40h or less = regular
|
_ => "regular".to_string(), // 40h or less = regular
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -396,7 +396,12 @@ pub async fn import_from_mysql(
|
|||||||
)
|
)
|
||||||
.fetch_all(&mysql_pool)
|
.fetch_all(&mysql_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch plans: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to fetch: {}", e);
|
||||||
|
tracing::error!("MySQL Error: {}", error_msg);
|
||||||
|
tracing::error!("Check column names in your MySQL database (plandeestudios, curso tables)");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
tracing::info!("Fetched {} study plans from MySQL", mysql_plans.len());
|
tracing::info!("Fetched {} study plans from MySQL", mysql_plans.len());
|
||||||
|
|
||||||
@@ -408,7 +413,7 @@ pub async fn import_from_mysql(
|
|||||||
c.NivelCurso AS nivel_curso,
|
c.NivelCurso AS nivel_curso,
|
||||||
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
||||||
pe.Nombre AS nombre_plan,
|
pe.Nombre AS nombre_plan,
|
||||||
CAST(c.Duracion AS SIGNED INTEGER) AS duracion
|
c.Duracion AS duracion
|
||||||
FROM curso c
|
FROM curso c
|
||||||
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
||||||
WHERE c.Activo = 1
|
WHERE c.Activo = 1
|
||||||
@@ -757,7 +762,7 @@ pub async fn list_mysql_courses(
|
|||||||
c.NivelCurso AS nivel_curso,
|
c.NivelCurso AS nivel_curso,
|
||||||
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
||||||
pe.Nombre AS nombre_plan,
|
pe.Nombre AS nombre_plan,
|
||||||
CAST(c.Duracion AS SIGNED INTEGER) AS duracion
|
c.Duracion AS duracion
|
||||||
FROM curso c
|
FROM curso c
|
||||||
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
||||||
WHERE c.Activo = 1
|
WHERE c.Activo = 1
|
||||||
@@ -783,8 +788,8 @@ pub async fn get_mysql_plans(
|
|||||||
let plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
let plans: Vec<MySqlPlanInfo> = sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
mysql_id as "idPlanDeEstudios",
|
mysql_id as id_plan_de_estudios,
|
||||||
name as "NombrePlan"
|
name as nombre_plan
|
||||||
FROM mysql_study_plans
|
FROM mysql_study_plans
|
||||||
WHERE organization_id = $1 AND is_active = true
|
WHERE organization_id = $1 AND is_active = true
|
||||||
ORDER BY name
|
ORDER BY name
|
||||||
@@ -808,12 +813,12 @@ pub async fn get_mysql_courses_by_plan(
|
|||||||
let courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
let courses: Vec<MySqlCourseInfo> = sqlx::query_as(
|
||||||
r#"
|
r#"
|
||||||
SELECT
|
SELECT
|
||||||
c.mysql_id as "idCursos",
|
c.mysql_id as id_cursos,
|
||||||
c.name as "NombreCurso",
|
c.name as nombre_curso,
|
||||||
c.level as "NivelCurso",
|
c.level as nivel_curso,
|
||||||
sp.mysql_id as "idPlanDeEstudios",
|
sp.mysql_id as id_plan_de_estudios,
|
||||||
sp.name as "NombrePlan",
|
sp.name as nombre_plan,
|
||||||
c.duracion as "Duracion"
|
c.duracion as duracion
|
||||||
FROM mysql_courses c
|
FROM mysql_courses c
|
||||||
JOIN mysql_study_plans sp ON c.study_plan_id = sp.id
|
JOIN mysql_study_plans sp ON c.study_plan_id = sp.id
|
||||||
WHERE c.organization_id = $1
|
WHERE c.organization_id = $1
|
||||||
@@ -838,34 +843,18 @@ pub struct MySqlCoursesFilters {
|
|||||||
|
|
||||||
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
|
#[derive(Debug, Clone, sqlx::FromRow, Serialize)]
|
||||||
pub struct MySqlPlanInfo {
|
pub struct MySqlPlanInfo {
|
||||||
#[sqlx(rename = "idPlanDeEstudios")]
|
|
||||||
#[serde(rename = "idPlanDeEstudios")]
|
|
||||||
pub id_plan_de_estudios: i32,
|
pub id_plan_de_estudios: i32,
|
||||||
#[sqlx(rename = "NombrePlan")]
|
|
||||||
#[serde(rename = "NombrePlan")]
|
|
||||||
pub nombre_plan: String,
|
pub nombre_plan: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
#[derive(Debug, Clone, Serialize, Deserialize, sqlx::FromRow)]
|
||||||
pub struct MySqlCourseInfo {
|
pub struct MySqlCourseInfo {
|
||||||
#[sqlx(rename = "idCursos")]
|
|
||||||
#[serde(rename = "idCursos")]
|
|
||||||
pub id_cursos: i32,
|
pub id_cursos: i32,
|
||||||
#[sqlx(rename = "NombreCurso")]
|
|
||||||
#[serde(rename = "NombreCurso")]
|
|
||||||
pub nombre_curso: String,
|
pub nombre_curso: String,
|
||||||
#[sqlx(rename = "NivelCurso")]
|
|
||||||
#[serde(rename = "NivelCurso", skip_serializing_if = "Option::is_none")]
|
|
||||||
pub nivel_curso: Option<i32>,
|
pub nivel_curso: Option<i32>,
|
||||||
#[sqlx(rename = "idPlanDeEstudios")]
|
|
||||||
#[serde(rename = "idPlanDeEstudios")]
|
|
||||||
pub id_plan_de_estudios: i32,
|
pub id_plan_de_estudios: i32,
|
||||||
#[sqlx(rename = "NombrePlan")]
|
|
||||||
#[serde(rename = "NombrePlan")]
|
|
||||||
pub nombre_plan: String,
|
pub nombre_plan: String,
|
||||||
#[sqlx(rename = "Duracion")]
|
pub duracion: Option<f64>, // Duration in hours (40=regular, 80=intensive) - MySQL float type
|
||||||
#[serde(rename = "Duracion", skip_serializing_if = "Option::is_none")]
|
|
||||||
pub duracion: Option<i32>, // Duration in hours (40=regular, 80=intensive)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
@@ -878,10 +867,19 @@ pub async fn import_all_from_mysql(
|
|||||||
Org(org_ctx): Org,
|
Org(org_ctx): Org,
|
||||||
claims: Claims,
|
claims: Claims,
|
||||||
State(pool): State<PgPool>,
|
State(pool): State<PgPool>,
|
||||||
Json(payload): Json<ImportAllFromMySQLPayload>,
|
body: String,
|
||||||
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
) -> Result<Json<serde_json::Value>, (StatusCode, String)> {
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
|
// Parse optional JSON body
|
||||||
|
let import_metadata_only = if body.trim().is_empty() {
|
||||||
|
false
|
||||||
|
} else {
|
||||||
|
serde_json::from_str::<ImportAllFromMySQLPayload>(&body)
|
||||||
|
.map(|p| p.import_metadata_only.unwrap_or(false))
|
||||||
|
.unwrap_or(false)
|
||||||
|
};
|
||||||
|
|
||||||
// Connect to MySQL
|
// Connect to MySQL
|
||||||
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
let mysql_url = std::env::var("MYSQL_DATABASE_URL")
|
||||||
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
.map_err(|_| (StatusCode::INTERNAL_SERVER_ERROR, "MYSQL_DATABASE_URL not configured".to_string()))?;
|
||||||
@@ -903,7 +901,12 @@ pub async fn import_all_from_mysql(
|
|||||||
)
|
)
|
||||||
.fetch_all(&mysql_pool)
|
.fetch_all(&mysql_pool)
|
||||||
.await
|
.await
|
||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to fetch plans: {}", e)))?;
|
.map_err(|e| {
|
||||||
|
let error_msg = format!("Failed to fetch: {}", e);
|
||||||
|
tracing::error!("MySQL Error: {}", error_msg);
|
||||||
|
tracing::error!("Check column names in your MySQL database (plandeestudios, curso tables)");
|
||||||
|
(StatusCode::INTERNAL_SERVER_ERROR, error_msg)
|
||||||
|
})?;
|
||||||
|
|
||||||
tracing::info!("Fetched {} study plans from MySQL", mysql_plans.len());
|
tracing::info!("Fetched {} study plans from MySQL", mysql_plans.len());
|
||||||
|
|
||||||
@@ -915,7 +918,7 @@ pub async fn import_all_from_mysql(
|
|||||||
c.NivelCurso AS nivel_curso,
|
c.NivelCurso AS nivel_curso,
|
||||||
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
||||||
pe.Nombre AS nombre_plan,
|
pe.Nombre AS nombre_plan,
|
||||||
CAST(c.Duracion AS SIGNED INTEGER) AS duracion
|
c.Duracion AS duracion
|
||||||
FROM curso c
|
FROM curso c
|
||||||
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
||||||
WHERE c.Activo = 1
|
WHERE c.Activo = 1
|
||||||
@@ -936,7 +939,7 @@ pub async fn import_all_from_mysql(
|
|||||||
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save courses/plans: {}", e)))?;
|
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to save courses/plans: {}", e)))?;
|
||||||
|
|
||||||
// If only metadata import is requested, return early
|
// If only metadata import is requested, return early
|
||||||
if payload.import_metadata_only.unwrap_or(false) {
|
if import_metadata_only {
|
||||||
return Ok(Json(json!({
|
return Ok(Json(json!({
|
||||||
"success": true,
|
"success": true,
|
||||||
"message": "Metadatos importados exitosamente",
|
"message": "Metadatos importados exitosamente",
|
||||||
@@ -1305,7 +1308,7 @@ pub async fn import_course_from_mysql(
|
|||||||
c.NivelCurso AS nivel_curso,
|
c.NivelCurso AS nivel_curso,
|
||||||
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
pe.idPlanDeEstudios AS id_plan_de_estudios,
|
||||||
pe.Nombre AS nombre_plan,
|
pe.Nombre AS nombre_plan,
|
||||||
CAST(c.Duracion AS SIGNED INTEGER) AS duracion
|
c.Duracion AS duracion
|
||||||
FROM curso c
|
FROM curso c
|
||||||
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
JOIN plandeestudios pe ON c.idPlanDeEstudios = pe.idPlanDeEstudios
|
||||||
WHERE c.idCursos = ? AND c.Activo = 1 AND pe.Activo = 1
|
WHERE c.idCursos = ? AND c.Activo = 1 AND pe.Activo = 1
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import React, { useState } from 'react';
|
import React from 'react';
|
||||||
import PageLayout from '@/components/PageLayout';
|
import PageLayout from '@/components/PageLayout';
|
||||||
import { TestTemplateManager, TestTemplateForm } from '@/components/TestTemplates';
|
import { AlertTriangle } from 'lucide-react';
|
||||||
//import { Plus } from 'lucide-react';
|
|
||||||
|
|
||||||
export default function TestTemplatesPage() {
|
export default function TestTemplatesPage() {
|
||||||
const [showCreateForm, setShowCreateForm] = useState(false);
|
|
||||||
const [refreshKey, setRefreshKey] = useState(0);
|
|
||||||
|
|
||||||
const handleSuccess = () => {
|
|
||||||
setShowCreateForm(false);
|
|
||||||
setRefreshKey(prev => prev + 1);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Plantillas de Pruebas">
|
<PageLayout title="Plantillas de Pruebas">
|
||||||
<div key={refreshKey}>
|
<div className="flex flex-col items-center justify-center p-8">
|
||||||
<TestTemplateManager
|
<div className="bg-yellow-50 border border-yellow-200 rounded-lg p-6 max-w-2xl">
|
||||||
onSelectTemplate={() => setShowCreateForm(true)}
|
<div className="flex items-center gap-3 mb-4">
|
||||||
onCreateTemplate={() => setShowCreateForm(true)}
|
<AlertTriangle className="w-8 h-8 text-yellow-600" />
|
||||||
/>
|
<h2 className="text-xl font-semibold text-yellow-800">
|
||||||
|
Funcionalidad Temporalmente Desactivada
|
||||||
|
</h2>
|
||||||
|
</div>
|
||||||
|
<p className="text-yellow-700 mb-4">
|
||||||
|
Las plantillas de pruebas están temporalmente desactivadas mientras se realizan mejoras en el sistema.
|
||||||
|
</p>
|
||||||
|
<p className="text-yellow-600 text-sm">
|
||||||
|
Esta funcionalidad estará disponible próximamente.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{showCreateForm && (
|
|
||||||
<TestTemplateForm
|
|
||||||
onSuccess={handleSuccess}
|
|
||||||
onCancel={() => setShowCreateForm(false)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</PageLayout>
|
</PageLayout>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user