From 0b7539583b5978bf893495ce146306a3232dcda5 Mon Sep 17 00:00:00 2001 From: "andres.sanjuan" Date: Tue, 23 Dec 2025 09:20:07 -0600 Subject: [PATCH] Initial commit --- category_courses/README.md | 321 ++++++ category_courses/amd/build/colorpicker.min.js | 3 + .../amd/build/colorpicker.min.js.map | 1 + .../amd/build/contrast_helper.min.js | 3 + .../amd/build/contrast_helper.min.js.map | 1 + category_courses/amd/build/dashboard.min.js | 3 + .../amd/build/dashboard.min.js.map | 1 + .../amd/build/hierarchy_navigation.min.js | 3 + .../amd/build/hierarchy_navigation.min.js.map | 1 + .../amd/build/manage_images.min.js | 3 + .../amd/build/manage_images.min.js.map | 1 + .../amd/build/rating_system.min.js | 10 + .../amd/build/rating_system.min.js.map | 1 + category_courses/amd/src/colorpicker.js | 73 ++ category_courses/amd/src/contrast_helper.js | 139 +++ category_courses/amd/src/dashboard.js | 79 ++ .../amd/src/hierarchy_navigation.js | 351 +++++++ category_courses/amd/src/manage_images.js | 37 + category_courses/amd/src/rating_system.js | 349 +++++++ category_courses/block_category_courses.php | 153 +++ category_courses/category_image_form.php | 73 ++ .../classes/external/get_category_data.php | 176 ++++ .../classes/external/rating_service.php | 167 +++ .../classes/external/save_image.php | 59 ++ .../hook_callbacks/output_callbacks.php | 46 + category_courses/classes/observer.php | 14 + category_courses/classes/output/main.php | 883 ++++++++++++++++ category_courses/classes/privacy/provider.php | 24 + category_courses/classes/rating_manager.php | 167 +++ category_courses/db/access.php | 39 + category_courses/db/caches.php | 13 + category_courses/db/events.php | 9 + category_courses/db/hooks.php | 32 + category_courses/db/install.php | 7 + category_courses/db/install.xml | 61 ++ category_courses/db/services.php | 60 ++ category_courses/db/upgrade.php | 90 ++ category_courses/edit_form.php | 43 + category_courses/edit_image.php | 160 +++ .../lang/en/block_category_courses.php | 182 ++++ .../lang/es_mx/block_category_courses.php | 182 ++++ .../lang/fil/block_category_courses.php | 182 ++++ .../lang/id/block_category_courses.php | 182 ++++ .../lang/vi/block_category_courses.php | 182 ++++ category_courses/lib.php | 40 + category_courses/manage_images.php | 156 +++ category_courses/settings.php | 97 ++ category_courses/styles.css | 983 ++++++++++++++++++ .../templates/breadcrumbs.mustache | 44 + .../templates/category_card.mustache | 76 ++ .../category_card_hierarchical.mustache | 106 ++ .../templates/comments_modal.mustache | 85 ++ .../templates/course_card.mustache | 101 ++ category_courses/templates/main.mustache | 44 + .../templates/progress_bar.mustache | 25 + .../templates/view_courses.mustache | 78 ++ category_courses/version.php | 31 + category_courses/view_courses.php | 124 +++ 58 files changed, 6556 insertions(+) create mode 100644 category_courses/README.md create mode 100644 category_courses/amd/build/colorpicker.min.js create mode 100644 category_courses/amd/build/colorpicker.min.js.map create mode 100644 category_courses/amd/build/contrast_helper.min.js create mode 100644 category_courses/amd/build/contrast_helper.min.js.map create mode 100644 category_courses/amd/build/dashboard.min.js create mode 100644 category_courses/amd/build/dashboard.min.js.map create mode 100644 category_courses/amd/build/hierarchy_navigation.min.js create mode 100644 category_courses/amd/build/hierarchy_navigation.min.js.map create mode 100644 category_courses/amd/build/manage_images.min.js create mode 100644 category_courses/amd/build/manage_images.min.js.map create mode 100644 category_courses/amd/build/rating_system.min.js create mode 100644 category_courses/amd/build/rating_system.min.js.map create mode 100644 category_courses/amd/src/colorpicker.js create mode 100644 category_courses/amd/src/contrast_helper.js create mode 100644 category_courses/amd/src/dashboard.js create mode 100644 category_courses/amd/src/hierarchy_navigation.js create mode 100644 category_courses/amd/src/manage_images.js create mode 100644 category_courses/amd/src/rating_system.js create mode 100644 category_courses/block_category_courses.php create mode 100644 category_courses/category_image_form.php create mode 100644 category_courses/classes/external/get_category_data.php create mode 100644 category_courses/classes/external/rating_service.php create mode 100644 category_courses/classes/external/save_image.php create mode 100644 category_courses/classes/hook_callbacks/output_callbacks.php create mode 100644 category_courses/classes/observer.php create mode 100644 category_courses/classes/output/main.php create mode 100644 category_courses/classes/privacy/provider.php create mode 100644 category_courses/classes/rating_manager.php create mode 100644 category_courses/db/access.php create mode 100644 category_courses/db/caches.php create mode 100644 category_courses/db/events.php create mode 100644 category_courses/db/hooks.php create mode 100644 category_courses/db/install.php create mode 100644 category_courses/db/install.xml create mode 100644 category_courses/db/services.php create mode 100644 category_courses/db/upgrade.php create mode 100644 category_courses/edit_form.php create mode 100644 category_courses/edit_image.php create mode 100644 category_courses/lang/en/block_category_courses.php create mode 100644 category_courses/lang/es_mx/block_category_courses.php create mode 100644 category_courses/lang/fil/block_category_courses.php create mode 100644 category_courses/lang/id/block_category_courses.php create mode 100644 category_courses/lang/vi/block_category_courses.php create mode 100644 category_courses/lib.php create mode 100644 category_courses/manage_images.php create mode 100644 category_courses/settings.php create mode 100644 category_courses/styles.css create mode 100644 category_courses/templates/breadcrumbs.mustache create mode 100644 category_courses/templates/category_card.mustache create mode 100644 category_courses/templates/category_card_hierarchical.mustache create mode 100644 category_courses/templates/comments_modal.mustache create mode 100644 category_courses/templates/course_card.mustache create mode 100644 category_courses/templates/main.mustache create mode 100644 category_courses/templates/progress_bar.mustache create mode 100644 category_courses/templates/view_courses.mustache create mode 100644 category_courses/version.php create mode 100644 category_courses/view_courses.php diff --git a/category_courses/README.md b/category_courses/README.md new file mode 100644 index 0000000..587e68a --- /dev/null +++ b/category_courses/README.md @@ -0,0 +1,321 @@ +# Manual de Usuario - Category Cards Plugin + +## 📋 Descripción General + +El plugin **Category Cards** transforma la visualización de categorías de cursos en Moodle con un sistema de **navegación jerárquica** que permite explorar categorías y cursos de forma intuitiva. Muestra tarjetas elegantes con imágenes personalizadas, colores únicos y información de progreso, adaptándose al tipo de usuario. + +## 🎯 Características Principales + +- **Navegación jerárquica** con breadcrumbs y exploración por niveles +- **Tarjetas visuales** con imágenes personalizadas o iniciales generadas automáticamente +- **Colores personalizables** por categoría con contraste automático de texto +- **Grid responsive** que se adapta automáticamente (1-6 cards por fila) +- **Configuraciones granulares** para categorías y cursos por separado +- **Gestión de imágenes** integrada con vista previa en tiempo real +- **Barras de progreso nativas** de Moodle para consistencia visual +- **Indicadores de visibilidad** para cursos ocultos (solo admins) +- **Soporte multiidioma** (Español e Inglés) + +--- + +## 🚀 Instalación + +### Requisitos del Sistema +- **Moodle 4.1+** o superior +- **PHP 7.4+** o superior +- **FontAwesome** (incluido en la mayoría de temas de Moodle) +- Permisos de administrador del sitio + +### Pasos de Instalación +1. Descargar el plugin desde el repositorio +2. Extraer en `/blocks/category_courses/` +3. Ir a **Administración del sitio > Notificaciones** +4. Seguir el proceso de instalación automática +5. El plugin creará automáticamente las tablas necesarias + +--- + +## ⚙️ Configuración Global + +### Acceso a Configuración +**Ruta:** `Administración del sitio > Plugins > Bloques > Category Cards` + +### Opciones Disponibles + +#### 🎨 Configuración de Colores *(Nuevas características)* +- **Color del Botón** + - Color personalizado para botones "EXPLORAR" en tarjetas de categoría + - Formato hexadecimal (ej: #951313) o nombres CSS + - Si se deja vacío, usa el color primario del tema + - Aplicación global en todo el sitio + +- **Color Inicial del Progreso** + - Color de inicio para el degradado de barras de progreso + - Por defecto: #4285f4 (azul Google) + - Formato hexadecimal requerido + +- **Color Final del Progreso** + - Color final para el degradado de barras de progreso + - Por defecto: #34a853 (verde Google) + - Crea degradado suave con el color inicial + +#### 📊 Configuración de Categorías *(Todas activadas por defecto)* +- **Mostrar progreso en categorías** + - Muestra barra de progreso con cursos completados + - Usa componente estándar de Moodle para consistencia visual + +- **Mostrar descripción de categorías** + - Muestra descripción de la categoría (truncada automáticamente) + - Se extrae de la descripción configurada en Moodle + +- **Mostrar contador de cursos** + - Chip con formato "X / Y cursos" + - Para administradores: X = completados, Y = total en categoría + - Para usuarios: X = completados, Y = inscritos + +#### 📚 Configuración de Cursos *(Todas activadas por defecto)* +- **Mostrar progreso en cursos** + - Barras de progreso individuales por curso + - Solo para usuarios inscritos + +- **Mostrar descripción de cursos** + - Resumen/descripción del curso + - Extraído del campo summary del curso + +- **Mostrar nombre corto de cursos** + - Código o nombre corto del curso + - Útil para identificación rápida + +#### 🔄 Opciones de Ordenamiento +- **Por defecto** *(Seleccionado)*: Mantiene orden original de Moodle +- **Alfabético**: A-Z por nombre de categoría +- **Por cantidad de cursos**: Mayor a menor número de cursos +- **Por progreso**: Mayor a menor porcentaje completado + +--- + +## 🎨 Gestión de Imágenes y Colores + +### Acceso a Gestión Visual +**Ruta:** `Administración del sitio > Plugins > Bloques > Category Cards > Manage Category Images` + +### Especificaciones de Imagen + +#### 📸 Tamaños Recomendados +- **Resolución óptima:** 550x168px (ratio 3.27:1) +- **Resolución alternativa:** 800x244px (ratio 3.28:1) +- **Resolución mínima:** 300x180px +- **Tamaño de archivo:** Máximo 2MB +- **Formatos soportados:** JPG, PNG, GIF, WebP + +#### 🎨 Personalización de Colores +- **Color picker visual** integrado para selección intuitiva de colores +- **Código hexadecimal** manual (#RRGGBB) con sincronización automática +- **Contraste automático** de texto (blanco/negro según luminancia) +- **Colores por defecto** generados automáticamente basados en el nombre +- **Vista previa en tiempo real** de los cambios aplicados +- **Selector de color nativo** del navegador con campo de texto sincronizado +- **Colores globales** para botones y barras de progreso configurables desde administración +- **Degradados personalizables** en barras de progreso con dos colores + +#### 🔄 Sistema de Imágenes +1. **Imagen personalizada:** Subida por administrador en gestión de imágenes +2. **Iniciales automáticas:** Generadas con color personalizado cuando no hay imagen + +--- + +## 🏗️ Configuración del Bloque + +### Agregar el Bloque +1. Activar **modo de edición** en la página deseada +2. Hacer clic en **"Agregar un bloque"** +3. Seleccionar **"Category Cards"** +4. El bloque aparecerá en la región seleccionada + +### Configuración Individual del Bloque + +#### Opciones de Categorías +- **Título personalizado:** Personaliza el título del bloque +- **Mostrar progreso:** Override de configuración global para categorías +- **Mostrar descripción:** Override de configuración global para categorías +- **Mostrar contador:** Override de configuración global para categorías + +#### Opciones de Cursos +- **Mostrar progreso de cursos:** Override de configuración global +- **Mostrar descripción de cursos:** Override de configuración global +- **Mostrar nombre corto:** Override de configuración global + +#### Configuración General +- **Orden de clasificación:** Override de configuración global + +--- + +## 👥 Comportamiento por Tipo de Usuario + +### 👨💼 Administradores +- **Navegación:** Acceso a todas las categorías del sistema +- **Visualización de cursos:** Todos los cursos (visibles y ocultos) +- **Indicadores especiales:** Badges y overlays para cursos ocultos +- **Progreso:** Solo de cursos donde están inscritos +- **Contador:** "Completados / Total en categoría" +- **Acceso:** Página de gestión de imágenes y colores + +### 👨🏫 Usuarios Regulares +- **Navegación:** Solo categorías donde tienen cursos inscritos y visibles +- **Visualización de cursos:** Solo cursos inscritos y visibles +- **Progreso:** De todos sus cursos inscritos +- **Contador:** "Completados / Inscritos" +- **Acceso:** Solo navegación y visualización + +--- + +## 🎯 Características Técnicas + +### 📱 Diseño Responsive +- **Grid automático:** `repeat(auto-fill, minmax(300px, 350px))` +- **Adaptación inteligente:** Se ajusta automáticamente al espacio disponible +- **Tablet (768px):** `minmax(250px, 350px)` para mejor aprovechamiento +- **Móvil (480px):** Una columna completa con gap optimizado + +### 🧭 Navegación Jerárquica +- **Breadcrumbs:** Navegación clara con botones clicables +- **Exploración por niveles:** Categorías → Subcategorías → Cursos +- **Botón EXPLORAR:** Con icono FontAwesome `fa-folder-open` +- **Vista combinada:** Subcategorías y cursos directos en el mismo nivel + +### 🎨 Elementos Visuales +- **Barras de progreso:** Template personalizado con degradado configurable +- **Botones personalizables:** Color global configurable para botones "EXPLORAR" +- **Tarjetas:** CSS Grid responsive con flexbox interno +- **Colores del tema:** Usa variables CSS del tema activo con overrides personalizables +- **Indicadores de visibilidad:** Badges rojos para cursos ocultos (admins) +- **Ícono de progreso:** FontAwesome check-circle con color sincronizado + +### 🌐 Internacionalización +- **Idiomas soportados:** Español (es_mx) e Inglés (en) +- **Strings localizados:** Todos los textos usan sistema de idiomas de Moodle +- **Extensible:** Fácil agregar nuevos idiomas + +--- + +## 🔧 Permisos y Capacidades + +### Capacidades del Plugin + +#### `block/category_courses:view` +- **Descripción:** Ver el contenido del bloque +- **Por defecto:** Todos los usuarios autenticados + +#### `block/category_courses:addinstance` +- **Descripción:** Agregar nueva instancia del bloque +- **Por defecto:** Profesores editores y administradores + +#### `block/category_courses:myaddinstance` +- **Descripción:** Agregar bloque al Dashboard personal +- **Por defecto:** Todos los usuarios + +#### `block/category_courses:manage` +- **Descripción:** Gestionar imágenes y colores de categorías +- **Por defecto:** Solo administradores + +--- + +## 📈 Mejores Prácticas + +### 🎨 Diseño Visual +- **Imágenes:** Usar 550x168px para resultados óptimos +- **Colores:** Mantener paleta coherente por área de conocimiento +- **Contraste:** El sistema calcula automáticamente el color de texto +- **Consistencia:** Usar estilo visual uniforme en todas las categorías + +### ⚡ Rendimiento +- **Límites:** Máximo 12 categorías por bloque recomendado +- **Imágenes:** Optimizar antes de subir (550x168px ideal) +- **Ubicación:** Colocar estratégicamente en Dashboard o páginas principales + +### 👥 Experiencia de Usuario +- **Navegación:** Botón INGRESAR para acceso directo +- **Información:** Progreso visual claro y contador de cursos +- **Responsive:** Funciona perfectamente en móviles +- **Accesibilidad:** Textos alternativos y contraste automático + +--- + +## 📞 Información Técnica + +### Versión del Plugin +- **Versión actual:** 3.3.1 +- **Compatibilidad:** Moodle 4.1+ +- **Última actualización:** Enero 2025 +- **Licencia:** GPL v3 + +### Nuevas Características v3.3.1 +- **Navegación jerárquica** completa con breadcrumbs +- **Configuraciones separadas** para categorías y cursos +- **Grid responsive mejorado** con auto-fill y minmax +- **Gestión de imágenes restaurada** con vista previa +- **Color picker avanzado** con selector visual y campo de texto sincronizado +- **Colores de botones configurables** a nivel global +- **Barras de progreso personalizables** con degradado configurable +- **Integración con tema** usando variables CSS +- **Indicadores de visibilidad** para cursos ocultos +- **Soporte multiidioma completo** (Español e Inglés) +- **Prevención de solicitudes concurrentes** con AbortController + +### Archivos Principales +- **Configuración:** `/blocks/category_courses/settings.php` +- **Templates:** `/blocks/category_courses/templates/` +- **Idiomas:** `/blocks/category_courses/lang/` +- **Estilos:** `/blocks/category_courses/styles.css` + +--- + +--- + +## 🚀 Guía de Navegación + +### Flujo de Navegación +1. **Nivel raíz:** Muestra categorías principales donde el usuario tiene acceso +2. **Nivel categoría:** Muestra subcategorías + cursos directos de esa categoría +3. **Breadcrumbs:** Permite volver a cualquier nivel anterior +4. **Exploración:** Botón "EXPLORAR" para navegar hacia subcategorías + +### Casos de Uso +- **Dashboard principal:** Navegación desde categorías raíz +- **Páginas de curso:** Exploración de categorías relacionadas +- **Administración:** Gestión visual de toda la estructura de categorías + +--- + +--- + +## 🎨 Configuración Avanzada de Colores + +### Jerarquía de Colores +1. **Configuración global** (Administración del sitio) +2. **Colores del tema** (fallback automático) +3. **Valores por defecto** del plugin + +### Colores Configurables + +#### Botones de Categoría +- **Campo:** Color del Botón +- **Aplicación:** Todos los botones "EXPLORAR" del sitio +- **Fallback:** Color primario del tema activo +- **Formato:** Hexadecimal (#RRGGBB) o nombres CSS + +#### Barras de Progreso +- **Campos:** Color Inicial + Color Final del Progreso +- **Aplicación:** Degradado en todas las barras de progreso +- **Incluye:** Ícono de check y texto de porcentaje +- **Formato:** Solo hexadecimal (#RRGGBB) +- **Por defecto:** Azul a verde (estilo Google) + +### Sincronización de Colores +- **Carga inicial:** Colores aplicados desde PHP +- **Navegación dinámica:** Colores aplicados vía AJAX +- **Consistencia:** Mismos colores en toda la experiencia + +--- + +*Este manual cubre todas las funcionalidades de la versión 3.3.1 del plugin Category Cards con navegación jerárquica y colores personalizables. El sistema está optimizado para proporcionar una experiencia de navegación intuitiva y visualmente atractiva con total control sobre la apariencia.* \ No newline at end of file diff --git a/category_courses/amd/build/colorpicker.min.js b/category_courses/amd/build/colorpicker.min.js new file mode 100644 index 0000000..d405712 --- /dev/null +++ b/category_courses/amd/build/colorpicker.min.js @@ -0,0 +1,3 @@ +define("block_category_courses/colorpicker",["exports","jquery"],(function(_exports,_jquery){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};_exports.init=()=>{(0,_jquery.default)(document).ready((function(){const initColorPicker=function(){(0,_jquery.default)('input[name="categorycolor"], input[name*="categorycolor"], input[name="s_block_category_courses_buttoncolor"], input[id*="buttoncolor"], input[name="s_block_category_courses_progresscolor1"], input[name="s_block_category_courses_progresscolor2"]').each((function(){if((0,_jquery.default)(this).next(".color-picker-wrapper").length)return;const $input=(0,_jquery.default)(this),$wrapper=(0,_jquery.default)('
'),$colorInput=(0,_jquery.default)('');let defaultColor="#667eea";$input.attr("name").includes("buttoncolor")&&(defaultColor="#007bff"),$input.attr("name").includes("progresscolor1")&&(defaultColor="#4285f4"),$input.attr("name").includes("progresscolor2")&&(defaultColor="#34a853"),$colorInput.val($input.val()||defaultColor),$colorInput.on("change",(function(){$input.val((0,_jquery.default)(this).val())})),$input.on("change",(function(){const value=(0,_jquery.default)(this).val();/^#[0-9A-F]{6}$/i.test(value)&&$colorInput.val(value)})),$wrapper.append($colorInput),$input.after($wrapper),$input.css("width","100px"),$colorInput.css({width:"40px",height:"30px",border:"1px solid #ccc","border-radius":"4px","margin-left":"10px",cursor:"pointer"})}))};initColorPicker(),setTimeout(initColorPicker,500),setTimeout(initColorPicker,1e3)}))}})); + +//# sourceMappingURL=colorpicker.min.js.map \ No newline at end of file diff --git a/category_courses/amd/build/colorpicker.min.js.map b/category_courses/amd/build/colorpicker.min.js.map new file mode 100644 index 0000000..86eb153 --- /dev/null +++ b/category_courses/amd/build/colorpicker.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"colorpicker.min.js","sources":["../src/colorpicker.js"],"sourcesContent":["// This file is part of Moodle: https://moodle.org/\n//\n// @module block_category_courses/colorpicker\nimport $ from 'jquery';\n/**\n * Initialize color picker for color fields\n * Works with global plugin settings and category management forms\n */\nexport const init = () => {\n // Wait for DOM to be ready\n $(document).ready(function() {\n // Add color picker to color fields - only global settings and category management\n const selector =\n 'input[name=\"categorycolor\"], input[name*=\"categorycolor\"], ' +\n 'input[name=\"s_block_category_courses_buttoncolor\"], input[id*=\"buttoncolor\"], ' +\n 'input[name=\"s_block_category_courses_progresscolor1\"], input[name=\"s_block_category_courses_progresscolor2\"]';\n const initColorPicker = function() {\n $(selector).each(function() {\n if ($(this).next('.color-picker-wrapper').length) {\n return; // Already initialized\n }\n const $input = $(this);\n // Create color picker wrapper\n const $wrapper = $('
');\n const $colorInput = $('');\n // Set initial value - use different defaults based on field type\n const isButtonColor = $input.attr('name').includes('buttoncolor');\n const isProgressColor1 = $input.attr('name').includes('progresscolor1');\n const isProgressColor2 = $input.attr('name').includes('progresscolor2');\n let defaultColor = '#667eea';\n if (isButtonColor) {\n defaultColor = '#007bff';\n }\n if (isProgressColor1) {\n defaultColor = '#4285f4';\n }\n if (isProgressColor2) {\n defaultColor = '#34a853';\n }\n $colorInput.val($input.val() || defaultColor);\n // Update text input when color changes\n $colorInput.on('change', function() {\n $input.val($(this).val());\n });\n // Update color picker when text input changes\n $input.on('change', function() {\n const value = $(this).val();\n if (/^#[0-9A-F]{6}$/i.test(value)) {\n $colorInput.val(value);\n }\n });\n // Insert color picker after text input\n $wrapper.append($colorInput);\n $input.after($wrapper);\n // Add some styling\n $input.css('width', '100px');\n $colorInput.css({\n width: '40px',\n height: '30px',\n border: '1px solid #ccc',\n 'border-radius': '4px',\n 'margin-left': '10px',\n cursor: 'pointer',\n });\n });\n };\n // Initialize immediately\n initColorPicker();\n // Also try after a delay for dynamically loaded content\n setTimeout(initColorPicker, 500);\n setTimeout(initColorPicker, 1000);\n });\n};\n"],"names":["document","ready","initColorPicker","each","this","next","length","$input","$wrapper","$colorInput","defaultColor","attr","includes","val","on","value","test","append","after","css","width","height","border","cursor","setTimeout"],"mappings":"wPAQoB,yBAEdA,UAAUC,OAAM,iBAMRC,gBAAkB,+BAHpB,yPAIYC,MAAK,eACT,mBAAEC,MAAMC,KAAK,yBAAyBC,oBAGpCC,QAAS,mBAAEH,MAEXI,UAAW,mBAAE,4CACbC,aAAc,mBAAE,uDAKlBC,aAAe,UAHGH,OAAOI,KAAK,QAAQC,SAAS,iBAK/CF,aAAe,WAJMH,OAAOI,KAAK,QAAQC,SAAS,oBAOlDF,aAAe,WANMH,OAAOI,KAAK,QAAQC,SAAS,oBASlDF,aAAe,WAEnBD,YAAYI,IAAIN,OAAOM,OAASH,cAEhCD,YAAYK,GAAG,UAAU,WACrBP,OAAOM,KAAI,mBAAET,MAAMS,UAGvBN,OAAOO,GAAG,UAAU,iBACVC,OAAQ,mBAAEX,MAAMS,MAClB,kBAAkBG,KAAKD,QACvBN,YAAYI,IAAIE,UAIxBP,SAASS,OAAOR,aAChBF,OAAOW,MAAMV,UAEbD,OAAOY,IAAI,QAAS,SACpBV,YAAYU,IAAI,CACZC,MAAO,OACPC,OAAQ,OACRC,OAAQ,iCACS,oBACF,OACfC,OAAQ,gBAKpBrB,kBAEAsB,WAAWtB,gBAAiB,KAC5BsB,WAAWtB,gBAAiB"} \ No newline at end of file diff --git a/category_courses/amd/build/contrast_helper.min.js b/category_courses/amd/build/contrast_helper.min.js new file mode 100644 index 0000000..7db8153 --- /dev/null +++ b/category_courses/amd/build/contrast_helper.min.js @@ -0,0 +1,3 @@ +define("block_category_courses/contrast_helper",["exports","jquery"],(function(_exports,_jquery){var obj;Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_jquery=(obj=_jquery)&&obj.__esModule?obj:{default:obj};const getLuminance=color=>{const hex=color.replace("#",""),sRGB=[parseInt(hex.substr(0,2),16)/255,parseInt(hex.substr(2,2),16)/255,parseInt(hex.substr(4,2),16)/255].map((c=>c<=.03928?c/12.92:Math.pow((c+.055)/1.055,2.4)));return.2126*sRGB[0]+.7152*sRGB[1]+.0722*sRGB[2]},getContrastRatio=(color1,color2)=>{const lum1=getLuminance(color1),lum2=getLuminance(color2);return(Math.max(lum1,lum2)+.05)/(Math.min(lum1,lum2)+.05)},getOptimalTextColor=backgroundColor=>getContrastRatio(backgroundColor,"#ffffff")>getContrastRatio(backgroundColor,"#000000")?"#ffffff":"#000000",adjustProgressContainerColors=()=>{(0,_jquery.default)(".category-card").each((function(){const $card=(0,_jquery.default)(this),bgColor=$card.css("background-color");if(!bgColor||"transparent"===bgColor)return;let hexColor=bgColor;if(bgColor.startsWith("rgb")){hexColor="#"+bgColor.match(/\d+/g).map((x=>{const hex=parseInt(x).toString(16);return 1===hex.length?"0"+hex:hex})).join("")}const optimalTextColor=getOptimalTextColor(hexColor),isDarkBg="#ffffff"===optimalTextColor;$card.attr("data-dark-bg",isDarkBg),$card.find(".progress-label, .progress-info span").css("color",optimalTextColor),$card.find(".course-info .course-count").css("color",optimalTextColor);const progressBg=isDarkBg?"rgba(255,255,255,0.2)":"rgba(0,0,0,0.2)";$card.find(".progress-bar").css("background-color",progressBg)})),(0,_jquery.default)(".course-card .custom-progress-container").each((function(){const $container=(0,_jquery.default)(this),bgColor=$container.closest(".course-card").css("background-color");if(!bgColor||"transparent"===bgColor)return;let hexColor=bgColor;if(bgColor.startsWith("rgb")){hexColor="#"+bgColor.match(/\d+/g).map((x=>{const hex=parseInt(x).toString(16);return 1===hex.length?"0"+hex:hex})).join("")}const optimalTextColor=getOptimalTextColor(hexColor);$container.find("span, i").css("color",optimalTextColor);const progressBg="#ffffff"===optimalTextColor?"rgba(255,255,255,0.2)":"rgba(0,0,0,0.2)";$container.next(".progress").css("background-color",progressBg)}))};_exports.init=()=>{(0,_jquery.default)(document).ready((()=>{adjustProgressContainerColors();new MutationObserver((()=>{adjustProgressContainerColors()})).observe(document.body,{childList:!0,subtree:!0}),setTimeout(adjustProgressContainerColors,500),setTimeout(adjustProgressContainerColors,1e3)}))}})); + +//# sourceMappingURL=contrast_helper.min.js.map \ No newline at end of file diff --git a/category_courses/amd/build/contrast_helper.min.js.map b/category_courses/amd/build/contrast_helper.min.js.map new file mode 100644 index 0000000..a54c1b1 --- /dev/null +++ b/category_courses/amd/build/contrast_helper.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"contrast_helper.min.js","sources":["../src/contrast_helper.js"],"sourcesContent":["// This file is part of Moodle: https://moodle.org/\n//\n// @module block_category_courses/contrast_helper\n\nimport $ from 'jquery';\n\n/**\n * Calculate luminance of a color\n * @param {string} color - Hex color string\n * @returns {number} Luminance value\n */\nconst getLuminance = (color) => {\n const hex = color.replace('#', '');\n const r = parseInt(hex.substr(0, 2), 16) / 255;\n const g = parseInt(hex.substr(2, 2), 16) / 255;\n const b = parseInt(hex.substr(4, 2), 16) / 255;\n\n const sRGB = [r, g, b].map(c => {\n return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);\n });\n\n return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2];\n};\n\n/**\n * Calculate contrast ratio between two colors\n * @param {string} color1 - First color\n * @param {string} color2 - Second color\n * @returns {number} Contrast ratio\n */\nconst getContrastRatio = (color1, color2) => {\n const lum1 = getLuminance(color1);\n const lum2 = getLuminance(color2);\n const brightest = Math.max(lum1, lum2);\n const darkest = Math.min(lum1, lum2);\n return (brightest + 0.05) / (darkest + 0.05);\n};\n\n/**\n * Get optimal text color for background\n * @param {string} backgroundColor - Background color\n * @returns {string} Optimal text color (white or black)\n */\nconst getOptimalTextColor = (backgroundColor) => {\n const whiteContrast = getContrastRatio(backgroundColor, '#ffffff');\n const blackContrast = getContrastRatio(backgroundColor, '#000000');\n return whiteContrast > blackContrast ? '#ffffff' : '#000000';\n};\n\n/**\n * Adjust progress container colors for better contrast\n */\nconst adjustProgressContainerColors = () => {\n $('.category-card').each(function() {\n const $card = $(this);\n const bgColor = $card.css('background-color');\n\n if (!bgColor || bgColor === 'transparent') {\n return;\n }\n\n // Convert RGB to hex if needed\n let hexColor = bgColor;\n if (bgColor.startsWith('rgb')) {\n const rgb = bgColor.match(/\\d+/g);\n hexColor = '#' + rgb.map(x => {\n const hex = parseInt(x).toString(16);\n return hex.length === 1 ? '0' + hex : hex;\n }).join('');\n }\n\n const optimalTextColor = getOptimalTextColor(hexColor);\n const isDarkBg = optimalTextColor === '#ffffff';\n\n // Set data attribute for CSS targeting\n $card.attr('data-dark-bg', isDarkBg);\n\n // Apply optimal colors to progress elements\n $card.find('.progress-label, .progress-info span').css('color', optimalTextColor);\n $card.find('.course-info .course-count').css('color', optimalTextColor);\n\n // Adjust progress bar background for better visibility\n const progressBg = isDarkBg ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)';\n $card.find('.progress-bar').css('background-color', progressBg);\n });\n\n // Also handle course cards\n $('.course-card .custom-progress-container').each(function() {\n const $container = $(this);\n const $card = $container.closest('.course-card');\n const bgColor = $card.css('background-color');\n\n if (!bgColor || bgColor === 'transparent') {\n return;\n }\n\n let hexColor = bgColor;\n if (bgColor.startsWith('rgb')) {\n const rgb = bgColor.match(/\\d+/g);\n hexColor = '#' + rgb.map(x => {\n const hex = parseInt(x).toString(16);\n return hex.length === 1 ? '0' + hex : hex;\n }).join('');\n }\n\n const optimalTextColor = getOptimalTextColor(hexColor);\n\n // Apply optimal colors to progress text and icon\n $container.find('span, i').css('color', optimalTextColor);\n\n // Adjust progress bar background\n const progressBg = optimalTextColor === '#ffffff' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)';\n $container.next('.progress').css('background-color', progressBg);\n });\n};\n\n/**\n * Initialize contrast helper\n */\nexport const init = () => {\n $(document).ready(() => {\n // Initial adjustment\n adjustProgressContainerColors();\n\n // Re-adjust when content changes (for dynamic loading)\n const observer = new MutationObserver(() => {\n adjustProgressContainerColors();\n });\n\n observer.observe(document.body, {\n childList: true,\n subtree: true\n });\n\n // Also adjust after a delay to catch any late-loading content\n setTimeout(adjustProgressContainerColors, 500);\n setTimeout(adjustProgressContainerColors, 1000);\n });\n};\n"],"names":["getLuminance","color","hex","replace","sRGB","parseInt","substr","map","c","Math","pow","getContrastRatio","color1","color2","lum1","lum2","max","min","getOptimalTextColor","backgroundColor","adjustProgressContainerColors","each","$card","this","bgColor","css","hexColor","startsWith","match","x","toString","length","join","optimalTextColor","isDarkBg","attr","find","progressBg","$container","closest","next","document","ready","MutationObserver","observe","body","childList","subtree","setTimeout"],"mappings":"oPAWMA,aAAgBC,cACZC,IAAMD,MAAME,QAAQ,IAAK,IAKzBC,KAAO,CAJHC,SAASH,IAAII,OAAO,EAAG,GAAI,IAAM,IACjCD,SAASH,IAAII,OAAO,EAAG,GAAI,IAAM,IACjCD,SAASH,IAAII,OAAO,EAAG,GAAI,IAAM,KAEpBC,KAAIC,GAChBA,GAAK,OAAUA,EAAI,MAAQC,KAAKC,KAAKF,EAAI,MAAS,MAAO,aAG7D,MAASJ,KAAK,GAAK,MAASA,KAAK,GAAK,MAASA,KAAK,IASzDO,iBAAmB,CAACC,OAAQC,gBACxBC,KAAOd,aAAaY,QACpBG,KAAOf,aAAaa,eACRJ,KAAKO,IAAIF,KAAMC,MAEb,MADJN,KAAKQ,IAAIH,KAAMC,MACQ,MAQrCG,oBAAuBC,iBACHR,iBAAiBQ,gBAAiB,WAClCR,iBAAiBQ,gBAAiB,WACjB,UAAY,UAMjDC,8BAAgC,yBAChC,kBAAkBC,MAAK,iBACfC,OAAQ,mBAAEC,MACVC,QAAUF,MAAMG,IAAI,wBAErBD,SAAuB,gBAAZA,mBAKZE,SAAWF,WACXA,QAAQG,WAAW,OAAQ,CAE3BD,SAAW,IADCF,QAAQI,MAAM,QACLrB,KAAIsB,UACf3B,IAAMG,SAASwB,GAAGC,SAAS,WACX,IAAf5B,IAAI6B,OAAe,IAAM7B,IAAMA,OACvC8B,KAAK,UAGNC,iBAAmBf,oBAAoBQ,UACvCQ,SAAgC,YAArBD,iBAGjBX,MAAMa,KAAK,eAAgBD,UAG3BZ,MAAMc,KAAK,wCAAwCX,IAAI,QAASQ,kBAChEX,MAAMc,KAAK,8BAA8BX,IAAI,QAASQ,wBAGhDI,WAAaH,SAAW,wBAA0B,kBACxDZ,MAAMc,KAAK,iBAAiBX,IAAI,mBAAoBY,mCAItD,2CAA2ChB,MAAK,iBACxCiB,YAAa,mBAAEf,MAEfC,QADQc,WAAWC,QAAQ,gBACXd,IAAI,wBAErBD,SAAuB,gBAAZA,mBAIZE,SAAWF,WACXA,QAAQG,WAAW,OAAQ,CAE3BD,SAAW,IADCF,QAAQI,MAAM,QACLrB,KAAIsB,UACf3B,IAAMG,SAASwB,GAAGC,SAAS,WACX,IAAf5B,IAAI6B,OAAe,IAAM7B,IAAMA,OACvC8B,KAAK,UAGNC,iBAAmBf,oBAAoBQ,UAG7CY,WAAWF,KAAK,WAAWX,IAAI,QAASQ,wBAGlCI,WAAkC,YAArBJ,iBAAiC,wBAA0B,kBAC9EK,WAAWE,KAAK,aAAaf,IAAI,mBAAoBY,8BAOzC,yBACdI,UAAUC,OAAM,KAEdtB,gCAGiB,IAAIuB,kBAAiB,KAClCvB,mCAGKwB,QAAQH,SAASI,KAAM,CAC5BC,WAAW,EACXC,SAAS,IAIbC,WAAW5B,8BAA+B,KAC1C4B,WAAW5B,8BAA+B"} \ No newline at end of file diff --git a/category_courses/amd/build/dashboard.min.js b/category_courses/amd/build/dashboard.min.js new file mode 100644 index 0000000..3fd7849 --- /dev/null +++ b/category_courses/amd/build/dashboard.min.js @@ -0,0 +1,3 @@ +define("block_category_courses/dashboard",["exports"],(function(_exports){function handleCardClick(e){if(e.target.closest("a, button, input, select, textarea"))return;const card=this,categoryId=card.dataset.categoryId,clickBehavior=card.dataset.clickBehavior||"category",linkElement=card.querySelector(".card-link"),url=linkElement?linkElement.dataset.url:null;if("courses"===clickBehavior){e.preventDefault();const form=document.createElement("form");form.method="POST",form.action=M.cfg.wwwroot+"/blocks/category_courses/view_courses.php";const cat=document.createElement("input");return cat.type="hidden",cat.name="categoryid",cat.value=categoryId,form.appendChild(cat),document.body.appendChild(form),void form.submit()}url&&(e.preventDefault(),card.classList.add("card-clicked"),setTimeout((()=>{window.location.href=url}),150))}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0;_exports.init=()=>{document.querySelectorAll(".category-card").forEach((card=>{card.setAttribute("role","button"),card.setAttribute("tabindex","0"),card.addEventListener("click",handleCardClick),card.addEventListener("keydown",(e=>{"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),handleCardClick.call(card,e))}))}))}})); + +//# sourceMappingURL=dashboard.min.js.map \ No newline at end of file diff --git a/category_courses/amd/build/dashboard.min.js.map b/category_courses/amd/build/dashboard.min.js.map new file mode 100644 index 0000000..38f0053 --- /dev/null +++ b/category_courses/amd/build/dashboard.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"dashboard.min.js","sources":["../src/dashboard.js"],"sourcesContent":["// This file is part of Moodle: https://moodle.org/\n// @module block_category_courses/dashboard\n\n/**\n * Maneja el clic (o enter/espacio) en la tarjeta de categoría.\n * Envía el categoryid por POST hacia view_courses.php\n * @param {Event} e - El evento de clic o teclado\n */\nfunction handleCardClick(e) {\n // No navegar si se hace clic en un elemento interactivo interno\n if (e.target.closest('a, button, input, select, textarea')) {\n return;\n }\n\n const card = this;\n const categoryId = card.dataset.categoryId;\n const clickBehavior = card.dataset.clickBehavior || 'category';\n const linkElement = card.querySelector('.card-link');\n const url = linkElement ? linkElement.dataset.url : null;\n\n if (clickBehavior === 'courses') {\n e.preventDefault();\n\n // --- Crear formulario invisible para POST ---\n const form = document.createElement('form');\n form.method = 'POST';\n form.action = M.cfg.wwwroot + '/blocks/category_courses/view_courses.php';\n\n const cat = document.createElement('input');\n cat.type = 'hidden';\n cat.name = 'categoryid';\n cat.value = categoryId;\n form.appendChild(cat);\n\n // Opcional: añadir sesskey si tu página valida tokens\n // const sk = document.createElement('input');\n // sk.type = 'hidden';\n // sk.name = 'sesskey';\n // sk.value = M.cfg.sesskey;\n // form.appendChild(sk);\n\n document.body.appendChild(form);\n form.submit();\n return;\n }\n\n // Comportamiento normal (navegación con URL)\n if (url) {\n e.preventDefault();\n card.classList.add('card-clicked');\n setTimeout(() => {\n window.location.href = url;\n }, 150);\n }\n}\n\n/**\n * Inicializa la funcionalidad de tarjetas de categorías.\n */\nexport const init = () => {\n const cards = document.querySelectorAll('.category-card');\n\n cards.forEach((card) => {\n // Accesibilidad\n card.setAttribute('role', 'button');\n card.setAttribute('tabindex', '0');\n\n // Click handler\n card.addEventListener('click', handleCardClick);\n\n // Teclado (Enter o espacio)\n card.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n e.preventDefault();\n handleCardClick.call(card, e);\n }\n });\n });\n};\n"],"names":["handleCardClick","e","target","closest","card","this","categoryId","dataset","clickBehavior","linkElement","querySelector","url","preventDefault","form","document","createElement","method","action","M","cfg","wwwroot","cat","type","name","value","appendChild","body","submit","classList","add","setTimeout","window","location","href","querySelectorAll","forEach","setAttribute","addEventListener","key","call"],"mappings":"mFAQSA,gBAAgBC,MAEjBA,EAAEC,OAAOC,QAAQ,mDAIfC,KAAOC,KACPC,WAAaF,KAAKG,QAAQD,WAC1BE,cAAgBJ,KAAKG,QAAQC,eAAiB,WAC9CC,YAAcL,KAAKM,cAAc,cACjCC,IAAMF,YAAcA,YAAYF,QAAQI,IAAM,QAE9B,YAAlBH,cAA6B,CAC7BP,EAAEW,uBAGIC,KAAOC,SAASC,cAAc,QACpCF,KAAKG,OAAS,OACdH,KAAKI,OAASC,EAAEC,IAAIC,QAAU,kDAExBC,IAAMP,SAASC,cAAc,gBACnCM,IAAIC,KAAO,SACXD,IAAIE,KAAO,aACXF,IAAIG,MAAQlB,WACZO,KAAKY,YAAYJ,KASjBP,SAASY,KAAKD,YAAYZ,WAC1BA,KAAKc,SAKLhB,MACAV,EAAEW,iBACFR,KAAKwB,UAAUC,IAAI,gBACnBC,YAAW,KACPC,OAAOC,SAASC,KAAOtB,MACxB,iGAOS,KACFG,SAASoB,iBAAiB,kBAElCC,SAAS/B,OAEXA,KAAKgC,aAAa,OAAQ,UAC1BhC,KAAKgC,aAAa,WAAY,KAG9BhC,KAAKiC,iBAAiB,QAASrC,iBAG/BI,KAAKiC,iBAAiB,WAAYpC,IAChB,UAAVA,EAAEqC,KAA6B,MAAVrC,EAAEqC,MACvBrC,EAAEW,iBACFZ,gBAAgBuC,KAAKnC,KAAMH"} \ No newline at end of file diff --git a/category_courses/amd/build/hierarchy_navigation.min.js b/category_courses/amd/build/hierarchy_navigation.min.js new file mode 100644 index 0000000..6220d5d --- /dev/null +++ b/category_courses/amd/build/hierarchy_navigation.min.js @@ -0,0 +1,3 @@ +define("block_category_courses/hierarchy_navigation",["exports","core/ajax","core/notification","core/templates"],(function(_exports,_ajax,_notification,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates);class HierarchyNavigation{constructor(){this.currentCategoryId=0,this.navigationHistory=[],this.container=null,this.contentContainer=null,this.isNavigating=!1}init(){this.container=document.querySelector(".hierarchy-navigation"),this.container&&(this.currentCategoryId=parseInt(this.container.dataset.currentCategory,10)||0,this.contentContainer=this.container.querySelector(".navigation-content"),this.setupEventListeners(),this.setupHistoryNavigation(),this.checkUrlCategory())}setupEventListeners(){this.container.addEventListener("click",(e=>{const categoryCard=e.target.closest('[data-action="navigate-category"]'),breadcrumbLink=e.target.closest('[data-action="navigate-breadcrumb"]'),categoryBtn=e.target.closest(".category-navigate-btn");if(categoryCard||categoryBtn){e.preventDefault();const categoryId=categoryCard?categoryCard.dataset.categoryId:categoryBtn.dataset.categoryId;if(!categoryId||isNaN(categoryId))return;categoryCard&&categoryCard.classList.add("navigating"),this.navigateToCategory(parseInt(categoryId,10))}if(breadcrumbLink){e.preventDefault();const categoryId=breadcrumbLink.dataset.categoryid;if(!categoryId||isNaN(categoryId))return;this.navigateToCategory(parseInt(categoryId,10))}})),this.container.addEventListener("keydown",(e=>{if("Enter"===e.key||" "===e.key){const categoryCard=e.target.closest('[data-action="navigate-category"]');if(categoryCard){e.preventDefault(),categoryCard.classList.add("navigating");const categoryId=categoryCard.dataset.categoryId;if(!categoryId||isNaN(categoryId))return;this.navigateToCategory(parseInt(categoryId,10))}}}))}async navigateToCategory(categoryId){let updateHistory=!(arguments.length>1&&void 0!==arguments[1])||arguments[1];if(!this.isNavigating&&(0===categoryId||categoryId!==this.currentCategoryId))try{this.isNavigating=!0,this.showLoading(),this.scrollToBlockStart();const data=await this.getCategoryData(categoryId);if(await this.updateContent(data),this.currentCategoryId=categoryId,updateHistory){const url=new URL(window.location);url.searchParams.set("categoryid",categoryId),history.pushState({categoryId:categoryId},"",url)}}catch(error){this.hideLoading(),this.isNavigating=!1,_notification.default.exception(error)}}getCategoryData(categoryId){return _ajax.default.call([{methodname:"block_category_courses_get_category_data",args:{categoryid:categoryId}}])[0]}async updateContent(data){if(data.breadcrumbs&&data.breadcrumbs.length>0){const breadcrumbsHtml=await _templates.default.render("block_category_courses/breadcrumbs",{breadcrumbs:data.breadcrumbs}),breadcrumbsContainer=this.container.querySelector(".breadcrumb-nav");if(breadcrumbsContainer){const tempDiv=document.createElement("div");tempDiv.innerHTML=breadcrumbsHtml,breadcrumbsContainer.replaceWith(tempDiv.firstElementChild)}else{const tempDiv=document.createElement("div");tempDiv.innerHTML=breadcrumbsHtml,this.container.insertBefore(tempDiv.firstElementChild,this.container.firstChild)}}const contentContainer=this.container.querySelector(".navigation-content");if(contentContainer){contentContainer.style.opacity="0",await new Promise((resolve=>setTimeout(resolve,150))),contentContainer.innerHTML="";try{if(data.hascategories&&data.categories.length>0){const categoriesContainer=document.createElement("div");categoriesContainer.className="level-content level-categories",categoriesContainer.setAttribute("data-level",data.currentcategoryid);const cardsContainer=document.createElement("div");cardsContainer.className="category-cards-container";const categoryPromises=data.categories.map((category=>(category.config=data.config,_templates.default.render("block_category_courses/category_card_hierarchical",category))));(await Promise.all(categoryPromises)).forEach((html=>{const tempDiv=document.createElement("div");tempDiv.innerHTML=html,cardsContainer.appendChild(tempDiv.firstElementChild)})),categoriesContainer.appendChild(cardsContainer),contentContainer.appendChild(categoriesContainer)}if(data.hascourses&&data.courses.length>0){const coursesContainer=document.createElement("div");coursesContainer.className="level-content level-courses",coursesContainer.setAttribute("data-level",data.currentcategoryid);const coursesGrid=document.createElement("div");coursesGrid.className="category-courses-grid";const coursePromises=data.courses.map((course=>_templates.default.render("block_category_courses/course_card",course)));(await Promise.all(coursePromises)).forEach((html=>{const tempDiv=document.createElement("div");tempDiv.innerHTML=html,coursesGrid.appendChild(tempDiv.firstElementChild)})),coursesContainer.appendChild(coursesGrid),contentContainer.appendChild(coursesContainer)}if(!data.hascategories&&!data.hascourses){const noContentDiv=document.createElement("div");noContentDiv.className="no-content",noContentDiv.textContent=data.nocontent||"",contentContainer.appendChild(noContentDiv)}contentContainer.style.opacity="1",this.initializeRatingSystem(),window.require&&require(["block_category_courses/rating_system"],(function(ratingSystem){ratingSystem.init()}))}catch(error){throw contentContainer.style.opacity="1",error}finally{this.hideLoading(),this.isNavigating=!1}}}showLoading(){this.contentContainer&&this.contentContainer.classList.add("loading"),this.container.classList.add("navigating")}hideLoading(){this.contentContainer&&this.contentContainer.classList.remove("loading"),this.container.classList.remove("navigating")}setupHistoryNavigation(){window.addEventListener("popstate",(event=>{event.state&&void 0!==event.state.categoryId&&this.navigateToCategory(event.state.categoryId,!1)}));const url=new URL(window.location),initialCategoryId=url.searchParams.get("categoryid")||this.currentCategoryId;history.replaceState({categoryId:parseInt(initialCategoryId,10)},"",url)}checkUrlCategory(){const url=new URL(window.location),urlCategoryId=parseInt(url.searchParams.get("categoryid"),10);urlCategoryId&&urlCategoryId!==this.currentCategoryId&&this.navigateToCategory(urlCategoryId,!1)}scrollToBlockStart(){const blockSection=document.body.querySelector("section.block_category_courses");if(blockSection){const parentElement=blockSection.parentElement;parentElement&&parentElement.scrollIntoView({behavior:"smooth",block:"start"})}}initializeRatingSystem(){this.container.querySelectorAll(".rating-stars").forEach((container=>{const userRating=parseInt(container.dataset.userRating)||0,stars=container.querySelectorAll(".rating-star");stars.forEach((star=>{star.classList.remove("selected","hover")})),stars.forEach(((star,index)=>{index{(new HierarchyNavigation).init()}})); + +//# sourceMappingURL=hierarchy_navigation.min.js.map \ No newline at end of file diff --git a/category_courses/amd/build/hierarchy_navigation.min.js.map b/category_courses/amd/build/hierarchy_navigation.min.js.map new file mode 100644 index 0000000..01aa36c --- /dev/null +++ b/category_courses/amd/build/hierarchy_navigation.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"hierarchy_navigation.min.js","sources":["../src/hierarchy_navigation.js"],"sourcesContent":["// This file is part of Moodle: https://moodle.org/\n// @module block_category_courses/hierarchy_navigation\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\n\n/**\n * Hierarchical category navigation\n */\nclass HierarchyNavigation {\n constructor() {\n this.currentCategoryId = 0;\n this.navigationHistory = [];\n this.container = null;\n this.contentContainer = null;\n this.isNavigating = false;\n }\n\n /**\n * Initializes the hierarchical navigation\n */\n init() {\n this.container = document.querySelector('.hierarchy-navigation');\n if (!this.container) {\n return;\n }\n this.currentCategoryId = parseInt(this.container.dataset.currentCategory, 10) || 0;\n this.contentContainer = this.container.querySelector('.navigation-content');\n this.setupEventListeners();\n this.setupHistoryNavigation();\n this.checkUrlCategory();\n }\n\n /**\n * Sets up event listeners\n */\n setupEventListeners() {\n // Event delegation para tarjetas de categoría\n this.container.addEventListener('click', (e) => {\n const categoryCard = e.target.closest('[data-action=\"navigate-category\"]');\n const breadcrumbLink = e.target.closest('[data-action=\"navigate-breadcrumb\"]');\n const categoryBtn = e.target.closest('.category-navigate-btn');\n\n if (categoryCard || categoryBtn) {\n e.preventDefault();\n const categoryId = categoryCard ? categoryCard.dataset.categoryId : categoryBtn.dataset.categoryId;\n if (!categoryId || isNaN(categoryId)) {\n return;\n }\n // Agregar feedback visual\n if (categoryCard) {\n categoryCard.classList.add('navigating');\n }\n this.navigateToCategory(parseInt(categoryId, 10));\n }\n\n if (breadcrumbLink) {\n e.preventDefault();\n const categoryId = breadcrumbLink.dataset.categoryid;\n if (!categoryId || isNaN(categoryId)) {\n return;\n }\n this.navigateToCategory(parseInt(categoryId, 10));\n }\n });\n\n // Soporte para teclado\n this.container.addEventListener('keydown', (e) => {\n if (e.key === 'Enter' || e.key === ' ') {\n const categoryCard = e.target.closest('[data-action=\"navigate-category\"]');\n if (categoryCard) {\n e.preventDefault();\n // Agregar feedback visual\n categoryCard.classList.add('navigating');\n const categoryId = categoryCard.dataset.categoryId;\n if (!categoryId || isNaN(categoryId)) {\n return;\n }\n this.navigateToCategory(parseInt(categoryId, 10));\n }\n }\n });\n }\n\n /**\n * Navigates to a specific category\n * @param {number} categoryId - Category ID\n * @param {boolean} updateHistory - Whether to update browser history\n */\n async navigateToCategory(categoryId, updateHistory = true) {\n // Bloquear completamente si ya está navegando\n if (this.isNavigating) {\n return;\n }\n\n // Para navegación a raíz (categoryId = 0), siempre permitir navegación\n if (categoryId !== 0 && categoryId === this.currentCategoryId) {\n return;\n }\n\n try {\n this.isNavigating = true;\n this.showLoading();\n // Scroll al inicio del bloque después de cargar el contenido\n this.scrollToBlockStart();\n // Obtener datos de la categoría\n const data = await this.getCategoryData(categoryId);\n // Actualizar contenido\n await this.updateContent(data);\n\n // Actualizar estado\n this.currentCategoryId = categoryId;\n\n // Actualizar historial del navegador\n if (updateHistory) {\n const url = new URL(window.location);\n url.searchParams.set('categoryid', categoryId);\n history.pushState({categoryId}, '', url);\n }\n } catch (error) {\n // En caso de error, liberar el bloqueo\n this.hideLoading();\n this.isNavigating = false;\n Notification.exception(error);\n }\n }\n\n /**\n * Gets category data via AJAX\n * @param {number} categoryId - Category ID\n * @returns {Promise} Category data\n */\n getCategoryData(categoryId) {\n return Ajax.call([\n {\n methodname: 'block_category_courses_get_category_data',\n args: {categoryid: categoryId},\n },\n ])[0];\n }\n\n /**\n * Updates the navigation content\n * @param {Object} data - Category data\n */\n async updateContent(data) {\n // Actualizar breadcrumbs\n if (data.breadcrumbs && data.breadcrumbs.length > 0) {\n const breadcrumbsHtml = await Templates.render('block_category_courses/breadcrumbs', {breadcrumbs: data.breadcrumbs});\n const breadcrumbsContainer = this.container.querySelector('.breadcrumb-nav');\n if (breadcrumbsContainer) {\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = breadcrumbsHtml;\n breadcrumbsContainer.replaceWith(tempDiv.firstElementChild);\n } else {\n // Insertar breadcrumbs al inicio si no existen\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = breadcrumbsHtml;\n this.container.insertBefore(tempDiv.firstElementChild, this.container.firstChild);\n }\n }\n\n const contentContainer = this.container.querySelector('.navigation-content');\n if (!contentContainer) {\n return;\n }\n\n // Limpiar contenido anterior con transición suave\n contentContainer.style.opacity = '0';\n\n // Esperar a que termine la transición antes de limpiar\n await new Promise((resolve) => setTimeout(resolve, 150));\n\n contentContainer.innerHTML = '';\n\n try {\n // Renderizar categorías si las hay\n if (data.hascategories && data.categories.length > 0) {\n const categoriesContainer = document.createElement('div');\n categoriesContainer.className = 'level-content level-categories';\n categoriesContainer.setAttribute('data-level', data.currentcategoryid);\n const cardsContainer = document.createElement('div');\n cardsContainer.className = 'category-cards-container';\n\n // Renderizar categorías concurrentemente para mejor rendimiento\n const categoryPromises = data.categories.map((category) => {\n category.config = data.config;\n return Templates.render('block_category_courses/category_card_hierarchical', category);\n });\n const categoryHtmls = await Promise.all(categoryPromises);\n\n categoryHtmls.forEach((html) => {\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = html;\n cardsContainer.appendChild(tempDiv.firstElementChild);\n });\n categoriesContainer.appendChild(cardsContainer);\n contentContainer.appendChild(categoriesContainer);\n }\n\n // Renderizar cursos si los hay\n if (data.hascourses && data.courses.length > 0) {\n const coursesContainer = document.createElement('div');\n coursesContainer.className = 'level-content level-courses';\n coursesContainer.setAttribute('data-level', data.currentcategoryid);\n const coursesGrid = document.createElement('div');\n coursesGrid.className = 'category-courses-grid';\n\n // Renderizar cursos concurrentemente para mejor rendimiento\n const coursePromises = data.courses.map((course) =>\n Templates.render('block_category_courses/course_card', course)\n );\n const courseHtmls = await Promise.all(coursePromises);\n\n courseHtmls.forEach((html) => {\n const tempDiv = document.createElement('div');\n tempDiv.innerHTML = html;\n coursesGrid.appendChild(tempDiv.firstElementChild);\n });\n coursesContainer.appendChild(coursesGrid);\n contentContainer.appendChild(coursesContainer);\n }\n\n // Mensaje si no hay contenido\n if (!data.hascategories && !data.hascourses) {\n const noContentDiv = document.createElement('div');\n noContentDiv.className = 'no-content';\n noContentDiv.textContent = data.nocontent || '';\n contentContainer.appendChild(noContentDiv);\n }\n\n // Restaurar opacidad con transición suave\n contentContainer.style.opacity = '1';\n\n // Initialize rating system for new course cards\n this.initializeRatingSystem();\n\n // Re-initialize the main rating system for new elements\n if (window.require) {\n require(['block_category_courses/rating_system'], function(ratingSystem) {\n ratingSystem.init();\n });\n }\n } catch (error) {\n // Si hay error durante la actualización, restaurar opacidad\n contentContainer.style.opacity = '1';\n throw error;\n } finally {\n // Liberar bloqueo solo después de que TODO el renderizado esté completo\n this.hideLoading();\n this.isNavigating = false;\n }\n }\n\n /**\n * Shows loading indicator\n */\n showLoading() {\n if (this.contentContainer) {\n this.contentContainer.classList.add('loading');\n }\n // Agregar clase al contenedor principal para deshabilitar interacciones\n this.container.classList.add('navigating');\n }\n\n /**\n * Hides loading indicator\n */\n hideLoading() {\n if (this.contentContainer) {\n this.contentContainer.classList.remove('loading');\n }\n // Remover clase del contenedor principal\n this.container.classList.remove('navigating');\n }\n /**\n * Sets up browser history navigation\n */\n setupHistoryNavigation() {\n // Escuchar eventos de navegación del navegador\n window.addEventListener('popstate', (event) => {\n if (event.state && event.state.categoryId !== undefined) {\n this.navigateToCategory(event.state.categoryId, false);\n }\n });\n\n // Establecer estado inicial\n const url = new URL(window.location);\n const initialCategoryId = url.searchParams.get('categoryid') || this.currentCategoryId;\n history.replaceState({categoryId: parseInt(initialCategoryId, 10)}, '', url);\n }\n\n /**\n * Checks URL for category parameter and navigates if needed\n */\n checkUrlCategory() {\n const url = new URL(window.location);\n const urlCategoryId = parseInt(url.searchParams.get('categoryid'), 10);\n\n if (urlCategoryId && urlCategoryId !== this.currentCategoryId) {\n this.navigateToCategory(urlCategoryId, false);\n }\n }\n\n /**\n * Scrolls to the beginning of the block after content loads\n */\n scrollToBlockStart() {\n const blockSection = document.body.querySelector('section.block_category_courses');\n if (blockSection) {\n const parentElement = blockSection.parentElement;\n if (parentElement) {\n parentElement.scrollIntoView({\n behavior: 'smooth',\n block: 'start'\n });\n }\n }\n }\n\n /**\n * Initialize rating system for newly rendered course cards\n */\n initializeRatingSystem() {\n // Initialize user ratings display\n this.container.querySelectorAll('.rating-stars').forEach((container) => {\n const userRating = parseInt(container.dataset.userRating) || 0;\n const stars = container.querySelectorAll('.rating-star');\n\n // Clear existing classes first\n stars.forEach((star) => {\n star.classList.remove('selected', 'hover');\n });\n\n // Apply selected class based on user rating\n stars.forEach((star, index) => {\n if (index < userRating) {\n star.classList.add('selected');\n }\n });\n });\n }\n}\n\n/**\n * Initializes the hierarchical navigation\n */\nexport const init = () => {\n const navigation = new HierarchyNavigation();\n navigation.init();\n};\n"],"names":["HierarchyNavigation","constructor","currentCategoryId","navigationHistory","container","contentContainer","isNavigating","init","document","querySelector","this","parseInt","dataset","currentCategory","setupEventListeners","setupHistoryNavigation","checkUrlCategory","addEventListener","e","categoryCard","target","closest","breadcrumbLink","categoryBtn","preventDefault","categoryId","isNaN","classList","add","navigateToCategory","categoryid","key","updateHistory","showLoading","scrollToBlockStart","data","getCategoryData","updateContent","url","URL","window","location","searchParams","set","history","pushState","error","hideLoading","exception","Ajax","call","methodname","args","breadcrumbs","length","breadcrumbsHtml","Templates","render","breadcrumbsContainer","tempDiv","createElement","innerHTML","replaceWith","firstElementChild","insertBefore","firstChild","style","opacity","Promise","resolve","setTimeout","hascategories","categories","categoriesContainer","className","setAttribute","currentcategoryid","cardsContainer","categoryPromises","map","category","config","all","forEach","html","appendChild","hascourses","courses","coursesContainer","coursesGrid","coursePromises","course","noContentDiv","textContent","nocontent","initializeRatingSystem","require","ratingSystem","remove","event","state","undefined","initialCategoryId","get","replaceState","urlCategoryId","blockSection","body","parentElement","scrollIntoView","behavior","block","querySelectorAll","userRating","stars","star","index"],"mappings":"gdASMA,oBACFC,mBACSC,kBAAoB,OACpBC,kBAAoB,QACpBC,UAAY,UACZC,iBAAmB,UACnBC,cAAe,EAMxBC,YACSH,UAAYI,SAASC,cAAc,yBACnCC,KAAKN,iBAGLF,kBAAoBS,SAASD,KAAKN,UAAUQ,QAAQC,gBAAiB,KAAO,OAC5ER,iBAAmBK,KAAKN,UAAUK,cAAc,4BAChDK,2BACAC,8BACAC,oBAMTF,2BAESV,UAAUa,iBAAiB,SAAUC,UAChCC,aAAeD,EAAEE,OAAOC,QAAQ,qCAChCC,eAAiBJ,EAAEE,OAAOC,QAAQ,uCAClCE,YAAcL,EAAEE,OAAOC,QAAQ,6BAEjCF,cAAgBI,YAAa,CAC7BL,EAAEM,uBACIC,WAAaN,aAAeA,aAAaP,QAAQa,WAAaF,YAAYX,QAAQa,eACnFA,YAAcC,MAAMD,mBAIrBN,cACAA,aAAaQ,UAAUC,IAAI,mBAE1BC,mBAAmBlB,SAASc,WAAY,QAG7CH,eAAgB,CAChBJ,EAAEM,uBACIC,WAAaH,eAAeV,QAAQkB,eACrCL,YAAcC,MAAMD,wBAGpBI,mBAAmBlB,SAASc,WAAY,cAKhDrB,UAAUa,iBAAiB,WAAYC,OAC1B,UAAVA,EAAEa,KAA6B,MAAVb,EAAEa,IAAa,OAC9BZ,aAAeD,EAAEE,OAAOC,QAAQ,wCAClCF,aAAc,CACdD,EAAEM,iBAEFL,aAAaQ,UAAUC,IAAI,oBACrBH,WAAaN,aAAaP,QAAQa,eACnCA,YAAcC,MAAMD,wBAGpBI,mBAAmBlB,SAASc,WAAY,mCAWpCA,gBAAYO,6EAE7BtB,KAAKJ,eAKU,IAAfmB,YAAoBA,aAAef,KAAKR,4BAKnCI,cAAe,OACf2B,mBAEAC,2BAECC,WAAazB,KAAK0B,gBAAgBX,qBAElCf,KAAK2B,cAAcF,WAGpBjC,kBAAoBuB,WAGrBO,cAAe,OACTM,IAAM,IAAIC,IAAIC,OAAOC,UAC3BH,IAAII,aAAaC,IAAI,aAAclB,YACnCmB,QAAQC,UAAU,CAACpB,WAAAA,YAAa,GAAIa,MAE1C,MAAOQ,YAEAC,mBACAzC,cAAe,wBACP0C,UAAUF,QAS/BV,gBAAgBX,mBACLwB,cAAKC,KAAK,CACb,CACIC,WAAY,2CACZC,KAAM,CAACtB,WAAYL,eAExB,uBAOaU,SAEZA,KAAKkB,aAAelB,KAAKkB,YAAYC,OAAS,EAAG,OAC3CC,sBAAwBC,mBAAUC,OAAO,qCAAsC,CAACJ,YAAalB,KAAKkB,cAClGK,qBAAuBhD,KAAKN,UAAUK,cAAc,sBACtDiD,qBAAsB,OAChBC,QAAUnD,SAASoD,cAAc,OACvCD,QAAQE,UAAYN,gBACpBG,qBAAqBI,YAAYH,QAAQI,uBACtC,OAEGJ,QAAUnD,SAASoD,cAAc,OACvCD,QAAQE,UAAYN,qBACfnD,UAAU4D,aAAaL,QAAQI,kBAAmBrD,KAAKN,UAAU6D,mBAIxE5D,iBAAmBK,KAAKN,UAAUK,cAAc,0BACjDJ,kBAKLA,iBAAiB6D,MAAMC,QAAU,UAG3B,IAAIC,SAASC,SAAYC,WAAWD,QAAS,OAEnDhE,iBAAiBwD,UAAY,UAIrB1B,KAAKoC,eAAiBpC,KAAKqC,WAAWlB,OAAS,EAAG,OAC5CmB,oBAAsBjE,SAASoD,cAAc,OACnDa,oBAAoBC,UAAY,iCAChCD,oBAAoBE,aAAa,aAAcxC,KAAKyC,yBAC9CC,eAAiBrE,SAASoD,cAAc,OAC9CiB,eAAeH,UAAY,iCAGrBI,iBAAmB3C,KAAKqC,WAAWO,KAAKC,WAC1CA,SAASC,OAAS9C,KAAK8C,OAChBzB,mBAAUC,OAAO,oDAAqDuB,oBAErDZ,QAAQc,IAAIJ,mBAE1BK,SAASC,aACbzB,QAAUnD,SAASoD,cAAc,OACvCD,QAAQE,UAAYuB,KACpBP,eAAeQ,YAAY1B,QAAQI,sBAEvCU,oBAAoBY,YAAYR,gBAChCxE,iBAAiBgF,YAAYZ,wBAI7BtC,KAAKmD,YAAcnD,KAAKoD,QAAQjC,OAAS,EAAG,OACtCkC,iBAAmBhF,SAASoD,cAAc,OAChD4B,iBAAiBd,UAAY,8BAC7Bc,iBAAiBb,aAAa,aAAcxC,KAAKyC,yBAC3Ca,YAAcjF,SAASoD,cAAc,OAC3C6B,YAAYf,UAAY,8BAGlBgB,eAAiBvD,KAAKoD,QAAQR,KAAKY,QACrCnC,mBAAUC,OAAO,qCAAsCkC,iBAEjCvB,QAAQc,IAAIQ,iBAE1BP,SAASC,aACXzB,QAAUnD,SAASoD,cAAc,OACvCD,QAAQE,UAAYuB,KACpBK,YAAYJ,YAAY1B,QAAQI,sBAEpCyB,iBAAiBH,YAAYI,aAC7BpF,iBAAiBgF,YAAYG,sBAI5BrD,KAAKoC,gBAAkBpC,KAAKmD,WAAY,OACnCM,aAAepF,SAASoD,cAAc,OAC5CgC,aAAalB,UAAY,aACzBkB,aAAaC,YAAc1D,KAAK2D,WAAa,GAC7CzF,iBAAiBgF,YAAYO,cAIjCvF,iBAAiB6D,MAAMC,QAAU,SAG5B4B,yBAGDvD,OAAOwD,SACPA,QAAQ,CAAC,yCAAyC,SAASC,cACvDA,aAAa1F,UAGvB,MAAOuC,aAELzC,iBAAiB6D,MAAMC,QAAU,IAC3BrB,mBAGDC,mBACAzC,cAAe,IAO5B2B,cACQvB,KAAKL,uBACAA,iBAAiBsB,UAAUC,IAAI,gBAGnCxB,UAAUuB,UAAUC,IAAI,cAMjCmB,cACQrC,KAAKL,uBACAA,iBAAiBsB,UAAUuE,OAAO,gBAGtC9F,UAAUuB,UAAUuE,OAAO,cAKpCnF,yBAEIyB,OAAOvB,iBAAiB,YAAakF,QAC7BA,MAAMC,YAAoCC,IAA3BF,MAAMC,MAAM3E,iBACtBI,mBAAmBsE,MAAMC,MAAM3E,YAAY,YAKlDa,IAAM,IAAIC,IAAIC,OAAOC,UACrB6D,kBAAoBhE,IAAII,aAAa6D,IAAI,eAAiB7F,KAAKR,kBACrE0C,QAAQ4D,aAAa,CAAC/E,WAAYd,SAAS2F,kBAAmB,KAAM,GAAIhE,KAM5EtB,yBACUsB,IAAM,IAAIC,IAAIC,OAAOC,UACrBgE,cAAgB9F,SAAS2B,IAAII,aAAa6D,IAAI,cAAe,IAE/DE,eAAiBA,gBAAkB/F,KAAKR,wBACnC2B,mBAAmB4E,eAAe,GAO/CvE,2BACUwE,aAAelG,SAASmG,KAAKlG,cAAc,qCACzCiG,aAAc,OACRE,cAAgBF,aAAaE,cAC/BA,eACAA,cAAcC,eAAe,CACzBC,SAAU,SACVC,MAAO,WAS3BhB,8BAES3F,UAAU4G,iBAAiB,iBAAiB7B,SAAS/E,kBAChD6G,WAAatG,SAASP,UAAUQ,QAAQqG,aAAe,EACvDC,MAAQ9G,UAAU4G,iBAAiB,gBAGzCE,MAAM/B,SAASgC,OACXA,KAAKxF,UAAUuE,OAAO,WAAY,YAItCgB,MAAM/B,SAAQ,CAACgC,KAAMC,SACbA,MAAQH,YACRE,KAAKxF,UAAUC,IAAI,iCAUnB,MACG,IAAI5B,qBACZO"} \ No newline at end of file diff --git a/category_courses/amd/build/manage_images.min.js b/category_courses/amd/build/manage_images.min.js new file mode 100644 index 0000000..dec13d6 --- /dev/null +++ b/category_courses/amd/build/manage_images.min.js @@ -0,0 +1,3 @@ +define("block_category_courses/manage_images",["exports","core/ajax","core/notification","jquery"],(function(_exports,_ajax,_notification,_jquery){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}}Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_notification=_interopRequireDefault(_notification),_jquery=_interopRequireDefault(_jquery);_exports.init=()=>{(0,_jquery.default)(".save-category").on("click",(function(){const $item=(0,_jquery.default)(this).closest(".category-item"),categoryid=(0,_jquery.default)(this).data("categoryid"),imageurl=$item.find(".image-url").val(),bgcolor=$item.find(".bg-color").val();_ajax.default.call([{methodname:"block_category_courses_save_image",args:{categoryid:categoryid,imageurl:imageurl,bgcolor:bgcolor}}])[0].done((function(response){response.success&&_notification.default.addNotification({message:M.util.get_string("categoryupdated","block_category_courses"),type:"success"})})).fail((function(error){_notification.default.exception(error)}))}))}})); + +//# sourceMappingURL=manage_images.min.js.map \ No newline at end of file diff --git a/category_courses/amd/build/manage_images.min.js.map b/category_courses/amd/build/manage_images.min.js.map new file mode 100644 index 0000000..df56010 --- /dev/null +++ b/category_courses/amd/build/manage_images.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"manage_images.min.js","sources":["../src/manage_images.js"],"sourcesContent":["// This file is part of Moodle: https://moodle.org/\n//\n// @module block_category_courses/manage_images\n\nimport Ajax from 'core/ajax';\nimport Notification from 'core/notification';\nimport $ from 'jquery';\n\nexport const init = () => {\n $('.save-category').on('click', function() {\n const $item = $(this).closest('.category-item');\n const categoryid = $(this).data('categoryid');\n const imageurl = $item.find('.image-url').val();\n const bgcolor = $item.find('.bg-color').val();\n Ajax.call([\n {\n methodname: 'block_category_courses_save_image',\n args: {\n categoryid: categoryid,\n imageurl: imageurl,\n bgcolor: bgcolor,\n },\n },\n ])[0]\n .done(function(response) {\n if (response.success) {\n Notification.addNotification({\n message: M.util.get_string('categoryupdated', 'block_category_courses'),\n type: 'success',\n });\n }\n })\n .fail(function(error) {\n Notification.exception(error);\n });\n });\n};\n"],"names":["on","$item","this","closest","categoryid","data","imageurl","find","val","bgcolor","call","methodname","args","done","response","success","addNotification","message","M","util","get_string","type","fail","error","exception"],"mappings":"gcAQoB,yBAChB,kBAAkBA,GAAG,SAAS,iBACxBC,OAAQ,mBAAEC,MAAMC,QAAQ,kBACxBC,YAAa,mBAAEF,MAAMG,KAAK,cAC1BC,SAAWL,MAAMM,KAAK,cAAcC,MACpCC,QAAUR,MAAMM,KAAK,aAAaC,oBACnCE,KAAK,CACR,CACEC,WAAY,oCACZC,KAAM,CACJR,WAAYA,WACZE,SAAUA,SACVG,QAASA,YAGZ,GACAI,MAAK,SAASC,UACTA,SAASC,+BACEC,gBAAgB,CAC3BC,QAASC,EAAEC,KAAKC,WAAW,kBAAmB,0BAC9CC,KAAM,eAIXC,MAAK,SAASC,6BACAC,UAAUD"} \ No newline at end of file diff --git a/category_courses/amd/build/rating_system.min.js b/category_courses/amd/build/rating_system.min.js new file mode 100644 index 0000000..3c02082 --- /dev/null +++ b/category_courses/amd/build/rating_system.min.js @@ -0,0 +1,10 @@ +define("block_category_courses/rating_system",["exports","core/ajax","core/modal_events","core/modal_factory","core/notification","core/templates"],(function(_exports,_ajax,_modal_events,_modal_factory,_notification,_templates){function _interopRequireDefault(obj){return obj&&obj.__esModule?obj:{default:obj}} +/** + * Rating system for category courses. + * + * @module block_category_courses/rating_system + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */Object.defineProperty(_exports,"__esModule",{value:!0}),_exports.init=void 0,_ajax=_interopRequireDefault(_ajax),_modal_events=_interopRequireDefault(_modal_events),_modal_factory=_interopRequireDefault(_modal_factory),_notification=_interopRequireDefault(_notification),_templates=_interopRequireDefault(_templates);_exports.init=()=>{initStarRatings(),initCommentButtons(),initializeUserRatings()};const initializeUserRatings=()=>{document.querySelectorAll(".rating-stars").forEach((container=>{const userRating=parseInt(container.dataset.userRating)||0;container.querySelectorAll(".rating-star").forEach(((star,index)=>{index{starRatingsInitialized||(starRatingsInitialized=!0,document.addEventListener("click",(e=>{e.target.matches(".rating-star")&&e.target.closest(".course-rating-section")&&(e.stopPropagation(),handleStarClick(e.target))})),document.addEventListener("mouseover",(e=>{e.target.matches(".rating-star")&&e.target.closest(".course-rating-section")&&highlightStars(e.target)})),document.addEventListener("mouseout",(e=>{e.target.matches(".rating-stars")&&e.target.closest(".course-rating-section")&&resetStarHighlight(e.target)})))},handleStarClick=star=>{const rating=parseInt(star.dataset.rating),courseid=parseInt(star.closest(".course-card").dataset.courseid);setRating(courseid,rating)},highlightStars=star=>{const rating=parseInt(star.dataset.rating);star.closest(".rating-stars").querySelectorAll(".rating-star").forEach(((s,index)=>{index{container.querySelectorAll(".rating-star").forEach((s=>s.classList.remove("hover")))},setRating=(courseid,rating)=>{_ajax.default.call([{methodname:"block_category_courses_set_rating",args:{courseid:courseid,rating:rating}}])[0].then((result=>(updateRatingDisplay(courseid,result.average_rating,result.total_ratings,rating),_notification.default.addNotification({message:M.util.get_string("ratingadded","block_category_courses"),type:"success"}),result))).catch(_notification.default.exception)},updateRatingDisplay=(courseid,average,total,userRating)=>{const card=document.querySelector('[data-courseid="'.concat(courseid,'"]'));if(!card)return;const avgElement=card.querySelector(".average-rating"),totalElement=card.querySelector(".total-ratings"),stars=card.querySelectorAll(".rating-star"),starsContainer=card.querySelector(".rating-stars");avgElement&&(avgElement.textContent=parseFloat(average).toFixed(1)),totalElement&&(totalElement.textContent="(".concat(total,")")),starsContainer&&(starsContainer.dataset.userRating=userRating),stars.forEach(((star,index)=>{star.classList.remove("selected","hover"),index{commentButtonsInitialized||(commentButtonsInitialized=!0,document.addEventListener("click",(e=>{if((e.target.matches(".comments-btn")||e.target.closest(".comments-btn"))&&e.target.closest(".course-rating-section")){e.stopPropagation();const btn=e.target.closest(".comments-btn"),courseid=parseInt(btn.closest(".course-card").dataset.courseid);openCommentsModal(courseid)}})))},openCommentsModal=courseid=>{_modal_factory.default.create({type:_modal_factory.default.types.SAVE_CANCEL,title:M.util.get_string("comments","block_category_courses"),body:'
',large:!0}).then((modal=>(loadCommentsContent(modal,courseid),modal.getRoot().on(_modal_events.default.save,(()=>{submitComment(modal,courseid)})),modal.show(),modal))).catch(_notification.default.exception)},loadCommentsContent=(modal,courseid)=>{const card=document.querySelector('[data-courseid="'.concat(courseid,'"]')),userRating=card&&parseInt(card.querySelector(".rating-stars").dataset.userRating)||0;_ajax.default.call([{methodname:"block_category_courses_get_comments",args:{courseid:courseid}}])[0].then((comments=>_templates.default.render("block_category_courses/comments_modal",{courseid:courseid,comments:comments,userRating:userRating}))).then((html=>(modal.setBody(html),initModalRatingStars(modal,userRating),html))).catch(_notification.default.exception)},submitComment=(modal,courseid)=>{const form=modal.getRoot().find("form")[0],formData=new FormData(form),rating=formData.get("rating"),comment=formData.get("comment");_ajax.default.call([{methodname:"block_category_courses_set_rating",args:{courseid:courseid,rating:parseInt(rating),comment:comment}}])[0].then((result=>(updateRatingDisplay(courseid,result.average_rating,result.total_ratings,rating),updateCommentsCount(courseid),modal.destroy(),result))).catch(_notification.default.exception)},updateCommentsCount=courseid=>{_ajax.default.call([{methodname:"block_category_courses_get_comments",args:{courseid:courseid,limit:1}}])[0].then((comments=>{const countElement=document.querySelector('[data-courseid="'.concat(courseid,'"]')).querySelector(".comments-count");return countElement&&(countElement.textContent=comments.length),comments})).catch(_notification.default.exception)},initModalRatingStars=function(modal){let userRating=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0;const modalRoot=modal.getRoot()[0];modalRoot.querySelectorAll(".rating-display").forEach((display=>{const rating=parseInt(display.dataset.rating)||0;display.querySelectorAll(".star-filled").forEach(((star,index)=>{index{index{if(e.target.matches(".rating-star-form")){const rating=parseInt(e.target.dataset.rating),container=e.target.closest(".rating-stars-form"),stars=container.querySelectorAll(".rating-star-form"),hiddenInput=container.nextElementSibling;stars.forEach(((star,index)=>{index{if(e.target.matches(".rating-star-form")){const rating=parseInt(e.target.dataset.rating);e.target.closest(".rating-stars-form").querySelectorAll(".rating-star-form").forEach(((star,index)=>{star.style.color=index{if(e.target.matches(".rating-stars-form")){e.target.querySelectorAll(".rating-star-form").forEach((star=>{star.classList.contains("selected")?star.style.color="#ffc107":star.style.color="#ddd"}))}}))}})); + +//# sourceMappingURL=rating_system.min.js.map \ No newline at end of file diff --git a/category_courses/amd/build/rating_system.min.js.map b/category_courses/amd/build/rating_system.min.js.map new file mode 100644 index 0000000..3a11579 --- /dev/null +++ b/category_courses/amd/build/rating_system.min.js.map @@ -0,0 +1 @@ +{"version":3,"file":"rating_system.min.js","sources":["../src/rating_system.js"],"sourcesContent":["// This file is part of Moodle - http://moodle.org/\n//\n// Moodle is free software: you can redistribute it and/or modify\n// it under the terms of the GNU General Public License as published by\n// the Free Software Foundation, either version 3 of the License, or\n// (at your option) any later version.\n//\n// Moodle is distributed in the hope that it will be useful,\n// but WITHOUT ANY WARRANTY; without even the implied warranty of\n// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the\n// GNU General Public License for more details.\n//\n// You should have received a copy of the GNU General Public License\n// along with Moodle. If not, see .\n\n/**\n * Rating system for category courses.\n *\n * @module block_category_courses/rating_system\n * @copyright 2025 Tu Nombre\n * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later\n */\n\nimport Ajax from 'core/ajax';\nimport ModalEvents from 'core/modal_events';\nimport ModalFactory from 'core/modal_factory';\nimport Notification from 'core/notification';\nimport Templates from 'core/templates';\n\nexport const init = () => {\n initStarRatings();\n initCommentButtons();\n initializeUserRatings();\n};\n\nconst initializeUserRatings = () => {\n document.querySelectorAll('.rating-stars').forEach((container) => {\n const userRating = parseInt(container.dataset.userRating) || 0;\n const stars = container.querySelectorAll('.rating-star');\n\n stars.forEach((star, index) => {\n if (index < userRating) {\n star.classList.add('selected');\n }\n });\n });\n};\n\nlet starRatingsInitialized = false;\n\nconst initStarRatings = () => {\n if (starRatingsInitialized) {\n return;\n }\n starRatingsInitialized = true;\n document.addEventListener('click', (e) => {\n if (e.target.matches('.rating-star') && e.target.closest('.course-rating-section')) {\n e.stopPropagation();\n handleStarClick(e.target);\n }\n });\n\n document.addEventListener('mouseover', (e) => {\n if (e.target.matches('.rating-star') && e.target.closest('.course-rating-section')) {\n highlightStars(e.target);\n }\n });\n\n document.addEventListener('mouseout', (e) => {\n if (e.target.matches('.rating-stars') && e.target.closest('.course-rating-section')) {\n resetStarHighlight(e.target);\n }\n });\n};\n\nconst handleStarClick = (star) => {\n const rating = parseInt(star.dataset.rating);\n const courseid = parseInt(star.closest('.course-card').dataset.courseid);\n\n setRating(courseid, rating);\n};\n\nconst highlightStars = (star) => {\n const rating = parseInt(star.dataset.rating);\n const container = star.closest('.rating-stars');\n const stars = container.querySelectorAll('.rating-star');\n\n stars.forEach((s, index) => {\n if (index < rating) {\n s.classList.add('hover');\n } else {\n s.classList.remove('hover');\n }\n });\n};\n\nconst resetStarHighlight = (container) => {\n const stars = container.querySelectorAll('.rating-star');\n stars.forEach((s) => s.classList.remove('hover'));\n};\n\nconst setRating = (courseid, rating) => {\n Ajax.call([\n {\n methodname: 'block_category_courses_set_rating',\n args: {\n courseid: courseid,\n rating: rating,\n },\n },\n ])[0]\n .then((result) => {\n updateRatingDisplay(courseid, result.average_rating, result.total_ratings, rating);\n Notification.addNotification({\n message: M.util.get_string('ratingadded', 'block_category_courses'),\n type: 'success',\n });\n return result;\n })\n .catch(Notification.exception);\n};\n\nconst updateRatingDisplay = (courseid, average, total, userRating) => {\n const card = document.querySelector(`[data-courseid=\"${courseid}\"]`);\n if (!card) {\n return;\n }\n\n const avgElement = card.querySelector('.average-rating');\n const totalElement = card.querySelector('.total-ratings');\n const stars = card.querySelectorAll('.rating-star');\n const starsContainer = card.querySelector('.rating-stars');\n\n // Update average and total\n if (avgElement) {\n avgElement.textContent = parseFloat(average).toFixed(1);\n }\n if (totalElement) {\n totalElement.textContent = `(${total})`;\n }\n\n // Update user's stars and container data\n if (starsContainer) {\n starsContainer.dataset.userRating = userRating;\n }\n\n stars.forEach((star, index) => {\n star.classList.remove('selected', 'hover');\n if (index < userRating) {\n star.classList.add('selected');\n }\n });\n};\n\nlet commentButtonsInitialized = false;\n\nconst initCommentButtons = () => {\n if (commentButtonsInitialized) {\n return;\n }\n commentButtonsInitialized = true;\n document.addEventListener('click', (e) => {\n const isCommentsBtn = e.target.matches('.comments-btn') || e.target.closest('.comments-btn');\n if (isCommentsBtn && e.target.closest('.course-rating-section')) {\n e.stopPropagation();\n const btn = e.target.closest('.comments-btn');\n const courseid = parseInt(btn.closest('.course-card').dataset.courseid);\n openCommentsModal(courseid);\n }\n });\n};\n\nconst openCommentsModal = (courseid) => {\n ModalFactory.create({\n type: ModalFactory.types.SAVE_CANCEL,\n title: M.util.get_string('comments', 'block_category_courses'),\n body: '
',\n large: true,\n })\n .then((modal) => {\n loadCommentsContent(modal, courseid);\n\n modal.getRoot().on(ModalEvents.save, () => {\n submitComment(modal, courseid);\n });\n\n modal.show();\n return modal;\n })\n .catch(Notification.exception);\n};\n\nconst loadCommentsContent = (modal, courseid) => {\n const card = document.querySelector(`[data-courseid=\"${courseid}\"]`);\n const userRating = card ? parseInt(card.querySelector('.rating-stars').dataset.userRating) || 0 : 0;\n Ajax.call([\n {\n methodname: 'block_category_courses_get_comments',\n args: {courseid: courseid},\n },\n ])[0]\n .then((comments) => {\n return Templates.render('block_category_courses/comments_modal', {\n courseid: courseid,\n comments: comments,\n userRating: userRating,\n });\n })\n .then((html) => {\n modal.setBody(html);\n initModalRatingStars(modal, userRating);\n return html;\n })\n .catch(Notification.exception);\n};\n\nconst submitComment = (modal, courseid) => {\n const form = modal.getRoot().find('form')[0];\n const formData = new FormData(form);\n const rating = formData.get('rating');\n const comment = formData.get('comment');\n\n Ajax.call([\n {\n methodname: 'block_category_courses_set_rating',\n args: {\n courseid: courseid,\n rating: parseInt(rating),\n comment: comment,\n },\n },\n ])[0]\n .then((result) => {\n updateRatingDisplay(courseid, result.average_rating, result.total_ratings, rating);\n updateCommentsCount(courseid);\n modal.destroy();\n return result;\n })\n .catch(Notification.exception);\n};\n\nconst updateCommentsCount = (courseid) => {\n Ajax.call([\n {\n methodname: 'block_category_courses_get_comments',\n args: {courseid: courseid, limit: 1},\n },\n ])[0]\n .then((comments) => {\n const card = document.querySelector(`[data-courseid=\"${courseid}\"]`);\n const countElement = card.querySelector('.comments-count');\n if (countElement) {\n countElement.textContent = comments.length;\n }\n return comments;\n })\n .catch(Notification.exception);\n};\n\nconst initModalRatingStars = (modal, userRating = 0) => {\n const modalRoot = modal.getRoot()[0];\n\n // Initialize rating displays\n modalRoot.querySelectorAll('.rating-display').forEach(display => {\n const rating = parseInt(display.dataset.rating) || 0;\n const stars = display.querySelectorAll('.star-filled');\n\n stars.forEach((star, index) => {\n if (index < rating) {\n star.style.color = '#ffc107';\n } else {\n star.style.color = '#ddd';\n star.innerHTML = '☆';\n }\n });\n });\n\n // Initialize form stars with user's current rating\n const formStars = modalRoot.querySelectorAll('.rating-star-form');\n const hiddenInput = modalRoot.querySelector('input[name=\"rating\"]');\n\n if (hiddenInput) {\n hiddenInput.value = userRating;\n }\n\n formStars.forEach((star, index) => {\n if (index < userRating) {\n star.classList.add('selected');\n star.style.color = '#ffc107';\n } else {\n star.classList.remove('selected');\n star.style.color = '#ddd';\n }\n });\n\n // Handle star selection\n modalRoot.addEventListener('click', (e) => {\n if (e.target.matches('.rating-star-form')) {\n const rating = parseInt(e.target.dataset.rating);\n const container = e.target.closest('.rating-stars-form');\n const stars = container.querySelectorAll('.rating-star-form');\n const hiddenInput = container.nextElementSibling;\n\n stars.forEach((star, index) => {\n if (index < rating) {\n star.classList.add('selected');\n star.style.color = '#ffc107';\n } else {\n star.classList.remove('selected');\n star.style.color = '#ddd';\n }\n });\n\n hiddenInput.value = rating;\n }\n });\n\n // Handle star hover\n modalRoot.addEventListener('mouseover', (e) => {\n if (e.target.matches('.rating-star-form')) {\n const rating = parseInt(e.target.dataset.rating);\n const container = e.target.closest('.rating-stars-form');\n const stars = container.querySelectorAll('.rating-star-form');\n\n stars.forEach((star, index) => {\n if (index < rating) {\n star.style.color = '#ffc107';\n } else {\n star.style.color = '#ddd';\n }\n });\n }\n });\n\n modalRoot.addEventListener('mouseout', (e) => {\n if (e.target.matches('.rating-stars-form')) {\n const container = e.target;\n const stars = container.querySelectorAll('.rating-star-form');\n\n stars.forEach((star) => {\n if (star.classList.contains('selected')) {\n star.style.color = '#ffc107';\n } else {\n star.style.color = '#ddd';\n }\n });\n }\n });\n};\n"],"names":["initStarRatings","initCommentButtons","initializeUserRatings","document","querySelectorAll","forEach","container","userRating","parseInt","dataset","star","index","classList","add","starRatingsInitialized","addEventListener","e","target","matches","closest","stopPropagation","handleStarClick","highlightStars","resetStarHighlight","rating","courseid","setRating","s","remove","call","methodname","args","then","result","updateRatingDisplay","average_rating","total_ratings","addNotification","message","M","util","get_string","type","catch","Notification","exception","average","total","card","querySelector","avgElement","totalElement","stars","starsContainer","textContent","parseFloat","toFixed","commentButtonsInitialized","btn","openCommentsModal","create","ModalFactory","types","SAVE_CANCEL","title","body","large","modal","loadCommentsContent","getRoot","on","ModalEvents","save","submitComment","show","comments","Templates","render","html","setBody","initModalRatingStars","form","find","formData","FormData","get","comment","updateCommentsCount","destroy","limit","countElement","length","modalRoot","display","style","color","innerHTML","formStars","hiddenInput","value","nextElementSibling","contains"],"mappings":";;;;;;;gVA6BoB,KAChBA,kBACAC,qBACAC,+BAGEA,sBAAwB,KAC1BC,SAASC,iBAAiB,iBAAiBC,SAASC,kBAC1CC,WAAaC,SAASF,UAAUG,QAAQF,aAAe,EAC/CD,UAAUF,iBAAiB,gBAEnCC,SAAQ,CAACK,KAAMC,SACbA,MAAQJ,YACRG,KAAKE,UAAUC,IAAI,uBAM/BC,wBAAyB,QAEvBd,gBAAkB,KAChBc,yBAGJA,wBAAyB,EACzBX,SAASY,iBAAiB,SAAUC,IAC5BA,EAAEC,OAAOC,QAAQ,iBAAmBF,EAAEC,OAAOE,QAAQ,4BACrDH,EAAEI,kBACFC,gBAAgBL,EAAEC,YAI1Bd,SAASY,iBAAiB,aAAcC,IAChCA,EAAEC,OAAOC,QAAQ,iBAAmBF,EAAEC,OAAOE,QAAQ,2BACrDG,eAAeN,EAAEC,WAIzBd,SAASY,iBAAiB,YAAaC,IAC/BA,EAAEC,OAAOC,QAAQ,kBAAoBF,EAAEC,OAAOE,QAAQ,2BACtDI,mBAAmBP,EAAEC,aAK3BI,gBAAmBX,aACfc,OAAShB,SAASE,KAAKD,QAAQe,QAC/BC,SAAWjB,SAASE,KAAKS,QAAQ,gBAAgBV,QAAQgB,UAE/DC,UAAUD,SAAUD,SAGlBF,eAAkBZ,aACdc,OAAShB,SAASE,KAAKD,QAAQe,QACnBd,KAAKS,QAAQ,iBACPf,iBAAiB,gBAEnCC,SAAQ,CAACsB,EAAGhB,SACVA,MAAQa,OACRG,EAAEf,UAAUC,IAAI,SAEhBc,EAAEf,UAAUgB,OAAO,aAKzBL,mBAAsBjB,YACVA,UAAUF,iBAAiB,gBACnCC,SAASsB,GAAMA,EAAEf,UAAUgB,OAAO,YAGtCF,UAAY,CAACD,SAAUD,wBACpBK,KAAK,CACN,CACIC,WAAY,oCACZC,KAAM,CACFN,SAAUA,SACVD,OAAQA,WAGjB,GACEQ,MAAMC,SACHC,oBAAoBT,SAAUQ,OAAOE,eAAgBF,OAAOG,cAAeZ,8BAC9Da,gBAAgB,CACzBC,QAASC,EAAEC,KAAKC,WAAW,cAAe,0BAC1CC,KAAM,YAEHT,UAEVU,MAAMC,sBAAaC,YAGtBX,oBAAsB,CAACT,SAAUqB,QAASC,MAAOxC,oBAC7CyC,KAAO7C,SAAS8C,wCAAiCxB,oBAClDuB,kBAICE,WAAaF,KAAKC,cAAc,mBAChCE,aAAeH,KAAKC,cAAc,kBAClCG,MAAQJ,KAAK5C,iBAAiB,gBAC9BiD,eAAiBL,KAAKC,cAAc,iBAGtCC,aACAA,WAAWI,YAAcC,WAAWT,SAASU,QAAQ,IAErDL,eACAA,aAAaG,uBAAkBP,YAI/BM,iBACAA,eAAe5C,QAAQF,WAAaA,YAGxC6C,MAAM/C,SAAQ,CAACK,KAAMC,SACjBD,KAAKE,UAAUgB,OAAO,WAAY,SAC9BjB,MAAQJ,YACRG,KAAKE,UAAUC,IAAI,oBAK3B4C,2BAA4B,QAE1BxD,mBAAqB,KACnBwD,4BAGJA,2BAA4B,EAC5BtD,SAASY,iBAAiB,SAAUC,QACVA,EAAEC,OAAOC,QAAQ,kBAAoBF,EAAEC,OAAOE,QAAQ,mBACvDH,EAAEC,OAAOE,QAAQ,0BAA2B,CAC7DH,EAAEI,wBACIsC,IAAM1C,EAAEC,OAAOE,QAAQ,iBACvBM,SAAWjB,SAASkD,IAAIvC,QAAQ,gBAAgBV,QAAQgB,UAC9DkC,kBAAkBlC,gBAKxBkC,kBAAqBlC,kCACVmC,OAAO,CAChBlB,KAAMmB,uBAAaC,MAAMC,YACzBC,MAAOzB,EAAEC,KAAKC,WAAW,WAAY,0BACrCwB,KAAM,uEACNC,OAAO,IAENlC,MAAMmC,QACHC,oBAAoBD,MAAO1C,UAE3B0C,MAAME,UAAUC,GAAGC,sBAAYC,MAAM,KACjCC,cAAcN,MAAO1C,aAGzB0C,MAAMO,OACCP,SAEVxB,MAAMC,sBAAaC,YAGtBuB,oBAAsB,CAACD,MAAO1C,kBAC1BuB,KAAO7C,SAAS8C,wCAAiCxB,gBACjDlB,WAAayC,MAAOxC,SAASwC,KAAKC,cAAc,iBAAiBxC,QAAQF,aAAmB,gBAC7FsB,KAAK,CACN,CACIC,WAAY,sCACZC,KAAM,CAACN,SAAUA,aAEtB,GACEO,MAAM2C,UACIC,mBAAUC,OAAO,wCAAyC,CAC7DpD,SAAUA,SACVkD,SAAUA,SACVpE,WAAYA,eAGnByB,MAAM8C,OACHX,MAAMY,QAAQD,MACdE,qBAAqBb,MAAO5D,YACrBuE,QAEVnC,MAAMC,sBAAaC,YAGtB4B,cAAgB,CAACN,MAAO1C,kBACpBwD,KAAOd,MAAME,UAAUa,KAAK,QAAQ,GACpCC,SAAW,IAAIC,SAASH,MACxBzD,OAAS2D,SAASE,IAAI,UACtBC,QAAUH,SAASE,IAAI,yBAExBxD,KAAK,CACN,CACIC,WAAY,oCACZC,KAAM,CACFN,SAAUA,SACVD,OAAQhB,SAASgB,QACjB8D,QAASA,YAGlB,GACEtD,MAAMC,SACHC,oBAAoBT,SAAUQ,OAAOE,eAAgBF,OAAOG,cAAeZ,QAC3E+D,oBAAoB9D,UACpB0C,MAAMqB,UACCvD,UAEVU,MAAMC,sBAAaC,YAGtB0C,oBAAuB9D,yBACpBI,KAAK,CACN,CACIC,WAAY,sCACZC,KAAM,CAACN,SAAUA,SAAUgE,MAAO,MAEvC,GACEzD,MAAM2C,iBAEGe,aADOvF,SAAS8C,wCAAiCxB,gBAC7BwB,cAAc,0BACpCyC,eACAA,aAAapC,YAAcqB,SAASgB,QAEjChB,YAEVhC,MAAMC,sBAAaC,YAGtBmC,qBAAuB,SAACb,WAAO5D,kEAAa,QACxCqF,UAAYzB,MAAME,UAAU,GAGlCuB,UAAUxF,iBAAiB,mBAAmBC,SAAQwF,gBAC5CrE,OAAShB,SAASqF,QAAQpF,QAAQe,SAAW,EACrCqE,QAAQzF,iBAAiB,gBAEjCC,SAAQ,CAACK,KAAMC,SACbA,MAAQa,OACRd,KAAKoF,MAAMC,MAAQ,WAEnBrF,KAAKoF,MAAMC,MAAQ,OACnBrF,KAAKsF,UAAY,iBAMvBC,UAAYL,UAAUxF,iBAAiB,qBACvC8F,YAAcN,UAAU3C,cAAc,wBAExCiD,cACAA,YAAYC,MAAQ5F,YAGxB0F,UAAU5F,SAAQ,CAACK,KAAMC,SACjBA,MAAQJ,YACRG,KAAKE,UAAUC,IAAI,YACnBH,KAAKoF,MAAMC,MAAQ,YAEnBrF,KAAKE,UAAUgB,OAAO,YACtBlB,KAAKoF,MAAMC,MAAQ,WAK3BH,UAAU7E,iBAAiB,SAAUC,OAC7BA,EAAEC,OAAOC,QAAQ,qBAAsB,OACjCM,OAAShB,SAASQ,EAAEC,OAAOR,QAAQe,QACnClB,UAAYU,EAAEC,OAAOE,QAAQ,sBAC7BiC,MAAQ9C,UAAUF,iBAAiB,qBACnC8F,YAAc5F,UAAU8F,mBAE9BhD,MAAM/C,SAAQ,CAACK,KAAMC,SACbA,MAAQa,QACRd,KAAKE,UAAUC,IAAI,YACnBH,KAAKoF,MAAMC,MAAQ,YAEnBrF,KAAKE,UAAUgB,OAAO,YACtBlB,KAAKoF,MAAMC,MAAQ,WAI3BG,YAAYC,MAAQ3E,WAK5BoE,UAAU7E,iBAAiB,aAAcC,OACjCA,EAAEC,OAAOC,QAAQ,qBAAsB,OACjCM,OAAShB,SAASQ,EAAEC,OAAOR,QAAQe,QACvBR,EAAEC,OAAOE,QAAQ,sBACXf,iBAAiB,qBAEnCC,SAAQ,CAACK,KAAMC,SAEbD,KAAKoF,MAAMC,MADXpF,MAAQa,OACW,UAEA,cAMnCoE,UAAU7E,iBAAiB,YAAaC,OAChCA,EAAEC,OAAOC,QAAQ,sBAAuB,CACtBF,EAAEC,OACIb,iBAAiB,qBAEnCC,SAASK,OACPA,KAAKE,UAAUyF,SAAS,YACxB3F,KAAKoF,MAAMC,MAAQ,UAEnBrF,KAAKoF,MAAMC,MAAQ"} \ No newline at end of file diff --git a/category_courses/amd/src/colorpicker.js b/category_courses/amd/src/colorpicker.js new file mode 100644 index 0000000..c4aeb4e --- /dev/null +++ b/category_courses/amd/src/colorpicker.js @@ -0,0 +1,73 @@ +// This file is part of Moodle: https://moodle.org/ +// +// @module block_category_courses/colorpicker +import $ from 'jquery'; +/** + * Initialize color picker for color fields + * Works with global plugin settings and category management forms + */ +export const init = () => { + // Wait for DOM to be ready + $(document).ready(function() { + // Add color picker to color fields - only global settings and category management + const selector = + 'input[name="categorycolor"], input[name*="categorycolor"], ' + + 'input[name="s_block_category_courses_buttoncolor"], input[id*="buttoncolor"], ' + + 'input[name="s_block_category_courses_progresscolor1"], input[name="s_block_category_courses_progresscolor2"]'; + const initColorPicker = function() { + $(selector).each(function() { + if ($(this).next('.color-picker-wrapper').length) { + return; // Already initialized + } + const $input = $(this); + // Create color picker wrapper + const $wrapper = $('
'); + const $colorInput = $(''); + // Set initial value - use different defaults based on field type + const isButtonColor = $input.attr('name').includes('buttoncolor'); + const isProgressColor1 = $input.attr('name').includes('progresscolor1'); + const isProgressColor2 = $input.attr('name').includes('progresscolor2'); + let defaultColor = '#667eea'; + if (isButtonColor) { + defaultColor = '#007bff'; + } + if (isProgressColor1) { + defaultColor = '#4285f4'; + } + if (isProgressColor2) { + defaultColor = '#34a853'; + } + $colorInput.val($input.val() || defaultColor); + // Update text input when color changes + $colorInput.on('change', function() { + $input.val($(this).val()); + }); + // Update color picker when text input changes + $input.on('change', function() { + const value = $(this).val(); + if (/^#[0-9A-F]{6}$/i.test(value)) { + $colorInput.val(value); + } + }); + // Insert color picker after text input + $wrapper.append($colorInput); + $input.after($wrapper); + // Add some styling + $input.css('width', '100px'); + $colorInput.css({ + width: '40px', + height: '30px', + border: '1px solid #ccc', + 'border-radius': '4px', + 'margin-left': '10px', + cursor: 'pointer', + }); + }); + }; + // Initialize immediately + initColorPicker(); + // Also try after a delay for dynamically loaded content + setTimeout(initColorPicker, 500); + setTimeout(initColorPicker, 1000); + }); +}; diff --git a/category_courses/amd/src/contrast_helper.js b/category_courses/amd/src/contrast_helper.js new file mode 100644 index 0000000..54f0cd9 --- /dev/null +++ b/category_courses/amd/src/contrast_helper.js @@ -0,0 +1,139 @@ +// This file is part of Moodle: https://moodle.org/ +// +// @module block_category_courses/contrast_helper + +import $ from 'jquery'; + +/** + * Calculate luminance of a color + * @param {string} color - Hex color string + * @returns {number} Luminance value + */ +const getLuminance = (color) => { + const hex = color.replace('#', ''); + const r = parseInt(hex.substr(0, 2), 16) / 255; + const g = parseInt(hex.substr(2, 2), 16) / 255; + const b = parseInt(hex.substr(4, 2), 16) / 255; + + const sRGB = [r, g, b].map(c => { + return c <= 0.03928 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4); + }); + + return 0.2126 * sRGB[0] + 0.7152 * sRGB[1] + 0.0722 * sRGB[2]; +}; + +/** + * Calculate contrast ratio between two colors + * @param {string} color1 - First color + * @param {string} color2 - Second color + * @returns {number} Contrast ratio + */ +const getContrastRatio = (color1, color2) => { + const lum1 = getLuminance(color1); + const lum2 = getLuminance(color2); + const brightest = Math.max(lum1, lum2); + const darkest = Math.min(lum1, lum2); + return (brightest + 0.05) / (darkest + 0.05); +}; + +/** + * Get optimal text color for background + * @param {string} backgroundColor - Background color + * @returns {string} Optimal text color (white or black) + */ +const getOptimalTextColor = (backgroundColor) => { + const whiteContrast = getContrastRatio(backgroundColor, '#ffffff'); + const blackContrast = getContrastRatio(backgroundColor, '#000000'); + return whiteContrast > blackContrast ? '#ffffff' : '#000000'; +}; + +/** + * Adjust progress container colors for better contrast + */ +const adjustProgressContainerColors = () => { + $('.category-card').each(function() { + const $card = $(this); + const bgColor = $card.css('background-color'); + + if (!bgColor || bgColor === 'transparent') { + return; + } + + // Convert RGB to hex if needed + let hexColor = bgColor; + if (bgColor.startsWith('rgb')) { + const rgb = bgColor.match(/\d+/g); + hexColor = '#' + rgb.map(x => { + const hex = parseInt(x).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); + } + + const optimalTextColor = getOptimalTextColor(hexColor); + const isDarkBg = optimalTextColor === '#ffffff'; + + // Set data attribute for CSS targeting + $card.attr('data-dark-bg', isDarkBg); + + // Apply optimal colors to progress elements + $card.find('.progress-label, .progress-info span').css('color', optimalTextColor); + $card.find('.course-info .course-count').css('color', optimalTextColor); + + // Adjust progress bar background for better visibility + const progressBg = isDarkBg ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'; + $card.find('.progress-bar').css('background-color', progressBg); + }); + + // Also handle course cards + $('.course-card .custom-progress-container').each(function() { + const $container = $(this); + const $card = $container.closest('.course-card'); + const bgColor = $card.css('background-color'); + + if (!bgColor || bgColor === 'transparent') { + return; + } + + let hexColor = bgColor; + if (bgColor.startsWith('rgb')) { + const rgb = bgColor.match(/\d+/g); + hexColor = '#' + rgb.map(x => { + const hex = parseInt(x).toString(16); + return hex.length === 1 ? '0' + hex : hex; + }).join(''); + } + + const optimalTextColor = getOptimalTextColor(hexColor); + + // Apply optimal colors to progress text and icon + $container.find('span, i').css('color', optimalTextColor); + + // Adjust progress bar background + const progressBg = optimalTextColor === '#ffffff' ? 'rgba(255,255,255,0.2)' : 'rgba(0,0,0,0.2)'; + $container.next('.progress').css('background-color', progressBg); + }); +}; + +/** + * Initialize contrast helper + */ +export const init = () => { + $(document).ready(() => { + // Initial adjustment + adjustProgressContainerColors(); + + // Re-adjust when content changes (for dynamic loading) + const observer = new MutationObserver(() => { + adjustProgressContainerColors(); + }); + + observer.observe(document.body, { + childList: true, + subtree: true + }); + + // Also adjust after a delay to catch any late-loading content + setTimeout(adjustProgressContainerColors, 500); + setTimeout(adjustProgressContainerColors, 1000); + }); +}; diff --git a/category_courses/amd/src/dashboard.js b/category_courses/amd/src/dashboard.js new file mode 100644 index 0000000..d56ad86 --- /dev/null +++ b/category_courses/amd/src/dashboard.js @@ -0,0 +1,79 @@ +// This file is part of Moodle: https://moodle.org/ +// @module block_category_courses/dashboard + +/** + * Maneja el clic (o enter/espacio) en la tarjeta de categoría. + * Envía el categoryid por POST hacia view_courses.php + * @param {Event} e - El evento de clic o teclado + */ +function handleCardClick(e) { + // No navegar si se hace clic en un elemento interactivo interno + if (e.target.closest('a, button, input, select, textarea')) { + return; + } + + const card = this; + const categoryId = card.dataset.categoryId; + const clickBehavior = card.dataset.clickBehavior || 'category'; + const linkElement = card.querySelector('.card-link'); + const url = linkElement ? linkElement.dataset.url : null; + + if (clickBehavior === 'courses') { + e.preventDefault(); + + // --- Crear formulario invisible para POST --- + const form = document.createElement('form'); + form.method = 'POST'; + form.action = M.cfg.wwwroot + '/blocks/category_courses/view_courses.php'; + + const cat = document.createElement('input'); + cat.type = 'hidden'; + cat.name = 'categoryid'; + cat.value = categoryId; + form.appendChild(cat); + + // Opcional: añadir sesskey si tu página valida tokens + // const sk = document.createElement('input'); + // sk.type = 'hidden'; + // sk.name = 'sesskey'; + // sk.value = M.cfg.sesskey; + // form.appendChild(sk); + + document.body.appendChild(form); + form.submit(); + return; + } + + // Comportamiento normal (navegación con URL) + if (url) { + e.preventDefault(); + card.classList.add('card-clicked'); + setTimeout(() => { + window.location.href = url; + }, 150); + } +} + +/** + * Inicializa la funcionalidad de tarjetas de categorías. + */ +export const init = () => { + const cards = document.querySelectorAll('.category-card'); + + cards.forEach((card) => { + // Accesibilidad + card.setAttribute('role', 'button'); + card.setAttribute('tabindex', '0'); + + // Click handler + card.addEventListener('click', handleCardClick); + + // Teclado (Enter o espacio) + card.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + handleCardClick.call(card, e); + } + }); + }); +}; diff --git a/category_courses/amd/src/hierarchy_navigation.js b/category_courses/amd/src/hierarchy_navigation.js new file mode 100644 index 0000000..e11ef26 --- /dev/null +++ b/category_courses/amd/src/hierarchy_navigation.js @@ -0,0 +1,351 @@ +// This file is part of Moodle: https://moodle.org/ +// @module block_category_courses/hierarchy_navigation +import Ajax from 'core/ajax'; +import Notification from 'core/notification'; +import Templates from 'core/templates'; + +/** + * Hierarchical category navigation + */ +class HierarchyNavigation { + constructor() { + this.currentCategoryId = 0; + this.navigationHistory = []; + this.container = null; + this.contentContainer = null; + this.isNavigating = false; + } + + /** + * Initializes the hierarchical navigation + */ + init() { + this.container = document.querySelector('.hierarchy-navigation'); + if (!this.container) { + return; + } + this.currentCategoryId = parseInt(this.container.dataset.currentCategory, 10) || 0; + this.contentContainer = this.container.querySelector('.navigation-content'); + this.setupEventListeners(); + this.setupHistoryNavigation(); + this.checkUrlCategory(); + } + + /** + * Sets up event listeners + */ + setupEventListeners() { + // Event delegation para tarjetas de categoría + this.container.addEventListener('click', (e) => { + const categoryCard = e.target.closest('[data-action="navigate-category"]'); + const breadcrumbLink = e.target.closest('[data-action="navigate-breadcrumb"]'); + const categoryBtn = e.target.closest('.category-navigate-btn'); + + if (categoryCard || categoryBtn) { + e.preventDefault(); + const categoryId = categoryCard ? categoryCard.dataset.categoryId : categoryBtn.dataset.categoryId; + if (!categoryId || isNaN(categoryId)) { + return; + } + // Agregar feedback visual + if (categoryCard) { + categoryCard.classList.add('navigating'); + } + this.navigateToCategory(parseInt(categoryId, 10)); + } + + if (breadcrumbLink) { + e.preventDefault(); + const categoryId = breadcrumbLink.dataset.categoryid; + if (!categoryId || isNaN(categoryId)) { + return; + } + this.navigateToCategory(parseInt(categoryId, 10)); + } + }); + + // Soporte para teclado + this.container.addEventListener('keydown', (e) => { + if (e.key === 'Enter' || e.key === ' ') { + const categoryCard = e.target.closest('[data-action="navigate-category"]'); + if (categoryCard) { + e.preventDefault(); + // Agregar feedback visual + categoryCard.classList.add('navigating'); + const categoryId = categoryCard.dataset.categoryId; + if (!categoryId || isNaN(categoryId)) { + return; + } + this.navigateToCategory(parseInt(categoryId, 10)); + } + } + }); + } + + /** + * Navigates to a specific category + * @param {number} categoryId - Category ID + * @param {boolean} updateHistory - Whether to update browser history + */ + async navigateToCategory(categoryId, updateHistory = true) { + // Bloquear completamente si ya está navegando + if (this.isNavigating) { + return; + } + + // Para navegación a raíz (categoryId = 0), siempre permitir navegación + if (categoryId !== 0 && categoryId === this.currentCategoryId) { + return; + } + + try { + this.isNavigating = true; + this.showLoading(); + // Scroll al inicio del bloque después de cargar el contenido + this.scrollToBlockStart(); + // Obtener datos de la categoría + const data = await this.getCategoryData(categoryId); + // Actualizar contenido + await this.updateContent(data); + + // Actualizar estado + this.currentCategoryId = categoryId; + + // Actualizar historial del navegador + if (updateHistory) { + const url = new URL(window.location); + url.searchParams.set('categoryid', categoryId); + history.pushState({categoryId}, '', url); + } + } catch (error) { + // En caso de error, liberar el bloqueo + this.hideLoading(); + this.isNavigating = false; + Notification.exception(error); + } + } + + /** + * Gets category data via AJAX + * @param {number} categoryId - Category ID + * @returns {Promise} Category data + */ + getCategoryData(categoryId) { + return Ajax.call([ + { + methodname: 'block_category_courses_get_category_data', + args: {categoryid: categoryId}, + }, + ])[0]; + } + + /** + * Updates the navigation content + * @param {Object} data - Category data + */ + async updateContent(data) { + // Actualizar breadcrumbs + if (data.breadcrumbs && data.breadcrumbs.length > 0) { + const breadcrumbsHtml = await Templates.render('block_category_courses/breadcrumbs', {breadcrumbs: data.breadcrumbs}); + const breadcrumbsContainer = this.container.querySelector('.breadcrumb-nav'); + if (breadcrumbsContainer) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = breadcrumbsHtml; + breadcrumbsContainer.replaceWith(tempDiv.firstElementChild); + } else { + // Insertar breadcrumbs al inicio si no existen + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = breadcrumbsHtml; + this.container.insertBefore(tempDiv.firstElementChild, this.container.firstChild); + } + } + + const contentContainer = this.container.querySelector('.navigation-content'); + if (!contentContainer) { + return; + } + + // Limpiar contenido anterior con transición suave + contentContainer.style.opacity = '0'; + + // Esperar a que termine la transición antes de limpiar + await new Promise((resolve) => setTimeout(resolve, 150)); + + contentContainer.innerHTML = ''; + + try { + // Renderizar categorías si las hay + if (data.hascategories && data.categories.length > 0) { + const categoriesContainer = document.createElement('div'); + categoriesContainer.className = 'level-content level-categories'; + categoriesContainer.setAttribute('data-level', data.currentcategoryid); + const cardsContainer = document.createElement('div'); + cardsContainer.className = 'category-cards-container'; + + // Renderizar categorías concurrentemente para mejor rendimiento + const categoryPromises = data.categories.map((category) => { + category.config = data.config; + return Templates.render('block_category_courses/category_card_hierarchical', category); + }); + const categoryHtmls = await Promise.all(categoryPromises); + + categoryHtmls.forEach((html) => { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + cardsContainer.appendChild(tempDiv.firstElementChild); + }); + categoriesContainer.appendChild(cardsContainer); + contentContainer.appendChild(categoriesContainer); + } + + // Renderizar cursos si los hay + if (data.hascourses && data.courses.length > 0) { + const coursesContainer = document.createElement('div'); + coursesContainer.className = 'level-content level-courses'; + coursesContainer.setAttribute('data-level', data.currentcategoryid); + const coursesGrid = document.createElement('div'); + coursesGrid.className = 'category-courses-grid'; + + // Renderizar cursos concurrentemente para mejor rendimiento + const coursePromises = data.courses.map((course) => + Templates.render('block_category_courses/course_card', course) + ); + const courseHtmls = await Promise.all(coursePromises); + + courseHtmls.forEach((html) => { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = html; + coursesGrid.appendChild(tempDiv.firstElementChild); + }); + coursesContainer.appendChild(coursesGrid); + contentContainer.appendChild(coursesContainer); + } + + // Mensaje si no hay contenido + if (!data.hascategories && !data.hascourses) { + const noContentDiv = document.createElement('div'); + noContentDiv.className = 'no-content'; + noContentDiv.textContent = data.nocontent || ''; + contentContainer.appendChild(noContentDiv); + } + + // Restaurar opacidad con transición suave + contentContainer.style.opacity = '1'; + + // Initialize rating system for new course cards + this.initializeRatingSystem(); + + // Re-initialize the main rating system for new elements + if (window.require) { + require(['block_category_courses/rating_system'], function(ratingSystem) { + ratingSystem.init(); + }); + } + } catch (error) { + // Si hay error durante la actualización, restaurar opacidad + contentContainer.style.opacity = '1'; + throw error; + } finally { + // Liberar bloqueo solo después de que TODO el renderizado esté completo + this.hideLoading(); + this.isNavigating = false; + } + } + + /** + * Shows loading indicator + */ + showLoading() { + if (this.contentContainer) { + this.contentContainer.classList.add('loading'); + } + // Agregar clase al contenedor principal para deshabilitar interacciones + this.container.classList.add('navigating'); + } + + /** + * Hides loading indicator + */ + hideLoading() { + if (this.contentContainer) { + this.contentContainer.classList.remove('loading'); + } + // Remover clase del contenedor principal + this.container.classList.remove('navigating'); + } + /** + * Sets up browser history navigation + */ + setupHistoryNavigation() { + // Escuchar eventos de navegación del navegador + window.addEventListener('popstate', (event) => { + if (event.state && event.state.categoryId !== undefined) { + this.navigateToCategory(event.state.categoryId, false); + } + }); + + // Establecer estado inicial + const url = new URL(window.location); + const initialCategoryId = url.searchParams.get('categoryid') || this.currentCategoryId; + history.replaceState({categoryId: parseInt(initialCategoryId, 10)}, '', url); + } + + /** + * Checks URL for category parameter and navigates if needed + */ + checkUrlCategory() { + const url = new URL(window.location); + const urlCategoryId = parseInt(url.searchParams.get('categoryid'), 10); + + if (urlCategoryId && urlCategoryId !== this.currentCategoryId) { + this.navigateToCategory(urlCategoryId, false); + } + } + + /** + * Scrolls to the beginning of the block after content loads + */ + scrollToBlockStart() { + const blockSection = document.body.querySelector('section.block_category_courses'); + if (blockSection) { + const parentElement = blockSection.parentElement; + if (parentElement) { + parentElement.scrollIntoView({ + behavior: 'smooth', + block: 'start' + }); + } + } + } + + /** + * Initialize rating system for newly rendered course cards + */ + initializeRatingSystem() { + // Initialize user ratings display + this.container.querySelectorAll('.rating-stars').forEach((container) => { + const userRating = parseInt(container.dataset.userRating) || 0; + const stars = container.querySelectorAll('.rating-star'); + + // Clear existing classes first + stars.forEach((star) => { + star.classList.remove('selected', 'hover'); + }); + + // Apply selected class based on user rating + stars.forEach((star, index) => { + if (index < userRating) { + star.classList.add('selected'); + } + }); + }); + } +} + +/** + * Initializes the hierarchical navigation + */ +export const init = () => { + const navigation = new HierarchyNavigation(); + navigation.init(); +}; diff --git a/category_courses/amd/src/manage_images.js b/category_courses/amd/src/manage_images.js new file mode 100644 index 0000000..1bdb2d3 --- /dev/null +++ b/category_courses/amd/src/manage_images.js @@ -0,0 +1,37 @@ +// This file is part of Moodle: https://moodle.org/ +// +// @module block_category_courses/manage_images + +import Ajax from 'core/ajax'; +import Notification from 'core/notification'; +import $ from 'jquery'; + +export const init = () => { + $('.save-category').on('click', function() { + const $item = $(this).closest('.category-item'); + const categoryid = $(this).data('categoryid'); + const imageurl = $item.find('.image-url').val(); + const bgcolor = $item.find('.bg-color').val(); + Ajax.call([ + { + methodname: 'block_category_courses_save_image', + args: { + categoryid: categoryid, + imageurl: imageurl, + bgcolor: bgcolor, + }, + }, + ])[0] + .done(function(response) { + if (response.success) { + Notification.addNotification({ + message: M.util.get_string('categoryupdated', 'block_category_courses'), + type: 'success', + }); + } + }) + .fail(function(error) { + Notification.exception(error); + }); + }); +}; diff --git a/category_courses/amd/src/rating_system.js b/category_courses/amd/src/rating_system.js new file mode 100644 index 0000000..b873218 --- /dev/null +++ b/category_courses/amd/src/rating_system.js @@ -0,0 +1,349 @@ +// This file is part of Moodle - http://moodle.org/ +// +// Moodle is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// Moodle is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with Moodle. If not, see . + +/** + * Rating system for category courses. + * + * @module block_category_courses/rating_system + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +import Ajax from 'core/ajax'; +import ModalEvents from 'core/modal_events'; +import ModalFactory from 'core/modal_factory'; +import Notification from 'core/notification'; +import Templates from 'core/templates'; + +export const init = () => { + initStarRatings(); + initCommentButtons(); + initializeUserRatings(); +}; + +const initializeUserRatings = () => { + document.querySelectorAll('.rating-stars').forEach((container) => { + const userRating = parseInt(container.dataset.userRating) || 0; + const stars = container.querySelectorAll('.rating-star'); + + stars.forEach((star, index) => { + if (index < userRating) { + star.classList.add('selected'); + } + }); + }); +}; + +let starRatingsInitialized = false; + +const initStarRatings = () => { + if (starRatingsInitialized) { + return; + } + starRatingsInitialized = true; + document.addEventListener('click', (e) => { + if (e.target.matches('.rating-star') && e.target.closest('.course-rating-section')) { + e.stopPropagation(); + handleStarClick(e.target); + } + }); + + document.addEventListener('mouseover', (e) => { + if (e.target.matches('.rating-star') && e.target.closest('.course-rating-section')) { + highlightStars(e.target); + } + }); + + document.addEventListener('mouseout', (e) => { + if (e.target.matches('.rating-stars') && e.target.closest('.course-rating-section')) { + resetStarHighlight(e.target); + } + }); +}; + +const handleStarClick = (star) => { + const rating = parseInt(star.dataset.rating); + const courseid = parseInt(star.closest('.course-card').dataset.courseid); + + setRating(courseid, rating); +}; + +const highlightStars = (star) => { + const rating = parseInt(star.dataset.rating); + const container = star.closest('.rating-stars'); + const stars = container.querySelectorAll('.rating-star'); + + stars.forEach((s, index) => { + if (index < rating) { + s.classList.add('hover'); + } else { + s.classList.remove('hover'); + } + }); +}; + +const resetStarHighlight = (container) => { + const stars = container.querySelectorAll('.rating-star'); + stars.forEach((s) => s.classList.remove('hover')); +}; + +const setRating = (courseid, rating) => { + Ajax.call([ + { + methodname: 'block_category_courses_set_rating', + args: { + courseid: courseid, + rating: rating, + }, + }, + ])[0] + .then((result) => { + updateRatingDisplay(courseid, result.average_rating, result.total_ratings, rating); + Notification.addNotification({ + message: M.util.get_string('ratingadded', 'block_category_courses'), + type: 'success', + }); + return result; + }) + .catch(Notification.exception); +}; + +const updateRatingDisplay = (courseid, average, total, userRating) => { + const card = document.querySelector(`[data-courseid="${courseid}"]`); + if (!card) { + return; + } + + const avgElement = card.querySelector('.average-rating'); + const totalElement = card.querySelector('.total-ratings'); + const stars = card.querySelectorAll('.rating-star'); + const starsContainer = card.querySelector('.rating-stars'); + + // Update average and total + if (avgElement) { + avgElement.textContent = parseFloat(average).toFixed(1); + } + if (totalElement) { + totalElement.textContent = `(${total})`; + } + + // Update user's stars and container data + if (starsContainer) { + starsContainer.dataset.userRating = userRating; + } + + stars.forEach((star, index) => { + star.classList.remove('selected', 'hover'); + if (index < userRating) { + star.classList.add('selected'); + } + }); +}; + +let commentButtonsInitialized = false; + +const initCommentButtons = () => { + if (commentButtonsInitialized) { + return; + } + commentButtonsInitialized = true; + document.addEventListener('click', (e) => { + const isCommentsBtn = e.target.matches('.comments-btn') || e.target.closest('.comments-btn'); + if (isCommentsBtn && e.target.closest('.course-rating-section')) { + e.stopPropagation(); + const btn = e.target.closest('.comments-btn'); + const courseid = parseInt(btn.closest('.course-card').dataset.courseid); + openCommentsModal(courseid); + } + }); +}; + +const openCommentsModal = (courseid) => { + ModalFactory.create({ + type: ModalFactory.types.SAVE_CANCEL, + title: M.util.get_string('comments', 'block_category_courses'), + body: '
', + large: true, + }) + .then((modal) => { + loadCommentsContent(modal, courseid); + + modal.getRoot().on(ModalEvents.save, () => { + submitComment(modal, courseid); + }); + + modal.show(); + return modal; + }) + .catch(Notification.exception); +}; + +const loadCommentsContent = (modal, courseid) => { + const card = document.querySelector(`[data-courseid="${courseid}"]`); + const userRating = card ? parseInt(card.querySelector('.rating-stars').dataset.userRating) || 0 : 0; + Ajax.call([ + { + methodname: 'block_category_courses_get_comments', + args: {courseid: courseid}, + }, + ])[0] + .then((comments) => { + return Templates.render('block_category_courses/comments_modal', { + courseid: courseid, + comments: comments, + userRating: userRating, + }); + }) + .then((html) => { + modal.setBody(html); + initModalRatingStars(modal, userRating); + return html; + }) + .catch(Notification.exception); +}; + +const submitComment = (modal, courseid) => { + const form = modal.getRoot().find('form')[0]; + const formData = new FormData(form); + const rating = formData.get('rating'); + const comment = formData.get('comment'); + + Ajax.call([ + { + methodname: 'block_category_courses_set_rating', + args: { + courseid: courseid, + rating: parseInt(rating), + comment: comment, + }, + }, + ])[0] + .then((result) => { + updateRatingDisplay(courseid, result.average_rating, result.total_ratings, rating); + updateCommentsCount(courseid); + modal.destroy(); + return result; + }) + .catch(Notification.exception); +}; + +const updateCommentsCount = (courseid) => { + Ajax.call([ + { + methodname: 'block_category_courses_get_comments', + args: {courseid: courseid, limit: 1}, + }, + ])[0] + .then((comments) => { + const card = document.querySelector(`[data-courseid="${courseid}"]`); + const countElement = card.querySelector('.comments-count'); + if (countElement) { + countElement.textContent = comments.length; + } + return comments; + }) + .catch(Notification.exception); +}; + +const initModalRatingStars = (modal, userRating = 0) => { + const modalRoot = modal.getRoot()[0]; + + // Initialize rating displays + modalRoot.querySelectorAll('.rating-display').forEach(display => { + const rating = parseInt(display.dataset.rating) || 0; + const stars = display.querySelectorAll('.star-filled'); + + stars.forEach((star, index) => { + if (index < rating) { + star.style.color = '#ffc107'; + } else { + star.style.color = '#ddd'; + star.innerHTML = '☆'; + } + }); + }); + + // Initialize form stars with user's current rating + const formStars = modalRoot.querySelectorAll('.rating-star-form'); + const hiddenInput = modalRoot.querySelector('input[name="rating"]'); + + if (hiddenInput) { + hiddenInput.value = userRating; + } + + formStars.forEach((star, index) => { + if (index < userRating) { + star.classList.add('selected'); + star.style.color = '#ffc107'; + } else { + star.classList.remove('selected'); + star.style.color = '#ddd'; + } + }); + + // Handle star selection + modalRoot.addEventListener('click', (e) => { + if (e.target.matches('.rating-star-form')) { + const rating = parseInt(e.target.dataset.rating); + const container = e.target.closest('.rating-stars-form'); + const stars = container.querySelectorAll('.rating-star-form'); + const hiddenInput = container.nextElementSibling; + + stars.forEach((star, index) => { + if (index < rating) { + star.classList.add('selected'); + star.style.color = '#ffc107'; + } else { + star.classList.remove('selected'); + star.style.color = '#ddd'; + } + }); + + hiddenInput.value = rating; + } + }); + + // Handle star hover + modalRoot.addEventListener('mouseover', (e) => { + if (e.target.matches('.rating-star-form')) { + const rating = parseInt(e.target.dataset.rating); + const container = e.target.closest('.rating-stars-form'); + const stars = container.querySelectorAll('.rating-star-form'); + + stars.forEach((star, index) => { + if (index < rating) { + star.style.color = '#ffc107'; + } else { + star.style.color = '#ddd'; + } + }); + } + }); + + modalRoot.addEventListener('mouseout', (e) => { + if (e.target.matches('.rating-stars-form')) { + const container = e.target; + const stars = container.querySelectorAll('.rating-star-form'); + + stars.forEach((star) => { + if (star.classList.contains('selected')) { + star.style.color = '#ffc107'; + } else { + star.style.color = '#ddd'; + } + }); + } + }); +}; diff --git a/category_courses/block_category_courses.php b/category_courses/block_category_courses.php new file mode 100644 index 0000000..53aac53 --- /dev/null +++ b/category_courses/block_category_courses.php @@ -0,0 +1,153 @@ +. + +/** + * Category Courses block. + * + * @package block_category_courses + * @copyright 2023 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +class block_category_courses extends block_base +{ + + /** + * Initialize the block. + */ + public function init() + { + $this->title = get_string('pluginname', 'block_category_courses'); + } + + /** + * Render the block. + */ + + public function get_content() + { + global $USER, $PAGE; + if ($this->content !== null) { + return $this->content; + } + $this->content = new stdClass(); + $this->content->text = ''; + $this->content->footer = ''; + + if (!isloggedin() || isguestuser()) { + $this->content = new stdClass(); + $this->content->text = get_string('notauth', 'block_category_courses'); + return $this->content; + } + + // Check view capability + $context = context_block::instance($this->instance->id); + if (!has_capability('block/category_courses:view', $context)) { + return $this->content; + } + + $renderer = $PAGE->get_renderer('core'); + $outputdata = (new \block_category_courses\output\main($USER->id, $this->config))->export_for_template($renderer); + $this->content->text = $renderer->render_from_template('block_category_courses/main', $outputdata); + + return $this->content; + } + /** + * Load required JS. + */ + public function get_required_javascript() + { + parent::get_required_javascript(); + $this->page->requires->js_call_amd('block_category_courses/hierarchy_navigation', 'init'); + $this->page->requires->js_call_amd('block_category_courses/contrast_helper', 'init'); + $this->page->requires->js_call_amd('block_category_courses/rating_system', 'init'); + + // Load language strings for rating system and other JS modules + $this->page->requires->strings_for_js([ + 'ratingadded', + 'ratingupdated', + 'ratingerror', + 'comments', + 'categoryupdated' + ], 'block_category_courses'); + } + + /** + * Where the block can be displayed. + * + * @return array + */ + public function applicable_formats() + { + return [ + 'site-index' => true, + 'course-view' => true, + 'my' => true, + 'my-index' => true + ]; + } + + /** + * The block should only be dockable when the title is not empty. + * + * @return bool + */ + public function instance_can_be_docked() + { + return (!empty($this->config->title) && parent::instance_can_be_docked()); + } + + /** + * The block has configuration. + * + * @return bool + */ + public function has_config() + { + return true; + } + + public function hide_header() + { + return true; + } + + + + /** + * Allow instance configuration. + * + * @return bool + */ + public function instance_allow_config() + { + return true; + } + + /** + * Specialization for this block. + */ + public function specialization() + { + if (isset($this->config->title)) { + $this->title = format_string($this->config->title, true, ['context' => $this->context]); + } else { + $this->title = get_string('pluginname', 'block_category_courses'); + } + } +} diff --git a/category_courses/category_image_form.php b/category_courses/category_image_form.php new file mode 100644 index 0000000..29a4e24 --- /dev/null +++ b/category_courses/category_image_form.php @@ -0,0 +1,73 @@ +libdir.'/formslib.php'); + +class category_image_form extends moodleform { + + protected function definition() { + global $DB; + + $mform = $this->_form; + $categoryid = $this->_customdata['categoryid']; + + $category = core_course_category::get($categoryid); + $customdata = $DB->get_record('block_catcourse_images', ['categoryid' => $categoryid]); + + $mform->addElement('header', 'categoryheader', format_string($category->name)); + + // File upload + $mform->addElement('filemanager', 'categoryimage', get_string('categoryimage', 'block_category_courses'), null, [ + 'subdirs' => 0, + 'maxbytes' => 2097152, // 2MB + 'maxfiles' => 1, + 'accepted_types' => ['web_image'] + ]); + $mform->addHelpButton('categoryimage', 'categoryimage', 'block_category_courses'); + + // Color picker group + $colorgroup = []; + $colorgroup[] = $mform->createElement('text', 'bgcolor', '', ['size' => 10, 'placeholder' => '#667eea', 'id' => 'id_bgcolor']); + $colorgroup[] = $mform->createElement('html', ''); + $mform->addGroup($colorgroup, 'colorgroup', get_string('categorycolor', 'block_category_courses'), ' ', false); + $mform->setType('bgcolor', PARAM_TEXT); + $mform->setDefault('bgcolor', $customdata->bgcolor ?? '#667eea'); + + // Hidden field + $mform->addElement('hidden', 'categoryid', $categoryid); + $mform->setType('categoryid', PARAM_INT); + + $this->add_action_buttons(true, get_string('savechanges')); + + // Add JavaScript for color picker + $mform->addElement('html', ''); + + // Set existing file data + if ($customdata && $customdata->imageurl) { + $context = context_system::instance(); + $draftitemid = file_get_submitted_draft_itemid('categoryimage'); + file_prepare_draft_area($draftitemid, $context->id, 'block_category_courses', 'categoryimage', $categoryid, [ + 'subdirs' => 0, + 'maxfiles' => 1 + ]); + $mform->setDefault('categoryimage', $draftitemid); + } + } +} \ No newline at end of file diff --git a/category_courses/classes/external/get_category_data.php b/category_courses/classes/external/get_category_data.php new file mode 100644 index 0000000..5823e5a --- /dev/null +++ b/category_courses/classes/external/get_category_data.php @@ -0,0 +1,176 @@ +. + +/** + * Servicio web para obtener datos de categorías. + * + * @package block_category_courses + * @copyright 2023 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_category_courses\external; + +use external_api; +use external_function_parameters; +use external_value; +use external_single_structure; +use external_multiple_structure; +use context_system; +use block_category_courses\output\main; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); + +/** + * Clase para el servicio web get_category_data + */ +class get_category_data extends external_api { + + /** + * Describe los parámetros de entrada + * @return external_function_parameters + */ + public static function execute_parameters() { + return new external_function_parameters([ + 'categoryid' => new external_value(PARAM_INT, 'Category ID', VALUE_DEFAULT, 0) + ]); + } + + /** + * Ejecuta el servicio web + * @param int $categoryid ID de la categoría + * @return array Datos de la categoría + */ + public static function execute($categoryid = 0) { + global $USER, $PAGE; + + // Validar parámetros + $params = self::validate_parameters(self::execute_parameters(), [ + 'categoryid' => $categoryid + ]); + + // Verificar contexto y permisos + $context = context_system::instance(); + self::validate_context($context); + + require_login(); + + if (isguestuser()) { + throw new \moodle_exception('guestsarenotallowed'); + } + + // Obtener datos usando la clase main + $renderer = $PAGE->get_renderer('core'); + $main = new main($USER->id, null, $params['categoryid']); + $data = $main->export_for_template($renderer); + + return $data; + } + + /** + * Describe la estructura de retorno + * @return external_single_structure + */ + public static function execute_returns() { + return new external_single_structure([ + 'breadcrumbs' => new external_multiple_structure( + new external_single_structure([ + 'name' => new external_value(PARAM_TEXT, 'Breadcrumb name'), + 'url' => new external_value(PARAM_RAW, 'Breadcrumb URL'), + 'categoryid' => new external_value(PARAM_INT, 'Category ID'), + 'active' => new external_value(PARAM_BOOL, 'Is active breadcrumb') + ]), VALUE_DEFAULT, [] + ), + 'categories' => new external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'Category ID'), + 'name' => new external_value(PARAM_TEXT, 'Category name'), + 'description' => new external_value(PARAM_RAW, 'Category description', VALUE_OPTIONAL), + 'image' => new external_single_structure([ + 'type' => new external_value(PARAM_TEXT, 'Image type', VALUE_OPTIONAL), + 'text' => new external_value(PARAM_TEXT, 'Image text/initials', VALUE_OPTIONAL), + 'color' => new external_value(PARAM_TEXT, 'Image color', VALUE_OPTIONAL), + 'url' => new external_value(PARAM_RAW, 'Image URL or data URI', VALUE_OPTIONAL) + ], 'Category image data', VALUE_OPTIONAL), + 'color' => new external_value(PARAM_TEXT, 'Category color'), + 'textcolor' => new external_value(PARAM_TEXT, 'Text color'), + 'visible' => new external_value(PARAM_BOOL, 'Category visibility'), + 'ishidden' => new external_value(PARAM_BOOL, 'Is category hidden (including parent inheritance)'), + 'url' => new external_value(PARAM_RAW, 'Category URL'), + 'courses' => new external_multiple_structure(new external_value(PARAM_RAW, 'Course data'), VALUE_DEFAULT, []), + 'total_courses' => new external_value(PARAM_INT, 'Total courses'), + 'completed_courses' => new external_value(PARAM_INT, 'Completed courses'), + 'progress_percentage' => new external_value(PARAM_INT, 'Progress percentage'), + 'hasprogress' => new external_value(PARAM_BOOL, 'Has progress'), + 'progress' => new external_value(PARAM_INT, 'Progress value'), + 'admin_total_courses' => new external_value(PARAM_INT, 'Total courses for admin', VALUE_DEFAULT, 0), + 'is_admin' => new external_value(PARAM_BOOL, 'Is site admin', VALUE_DEFAULT, false), + 'config' => new external_single_structure([ + 'showprogress' => new external_value(PARAM_BOOL, 'Show progress'), + 'showdescription' => new external_value(PARAM_BOOL, 'Show description'), + 'showcoursecount' => new external_value(PARAM_BOOL, 'Show course count'), + 'buttoncolor' => new external_value(PARAM_TEXT, 'Button color', VALUE_OPTIONAL), + 'progresscolor1' => new external_value(PARAM_TEXT, 'Progress start color', VALUE_OPTIONAL), + 'progresscolor2' => new external_value(PARAM_TEXT, 'Progress end color', VALUE_OPTIONAL) + ]) + ]) + ), + 'courses' => new external_multiple_structure( + new external_single_structure([ + 'id' => new external_value(PARAM_INT, 'Course ID'), + 'fullname' => new external_value(PARAM_TEXT, 'Course full name'), + 'shortname' => new external_value(PARAM_TEXT, 'Course short name'), + 'summary' => new external_value(PARAM_RAW, 'Course summary', VALUE_OPTIONAL), + 'viewurl' => new external_value(PARAM_RAW, 'Course view URL'), + 'courseimage' => new external_value(PARAM_RAW, 'Course image URL or data URI'), + 'visible' => new external_value(PARAM_BOOL, 'Course visibility'), + 'isenrolled' => new external_value(PARAM_BOOL, 'User is enrolled'), + 'hasprogress' => new external_value(PARAM_BOOL, 'Has progress data'), + 'progress' => new external_value(PARAM_INT, 'Progress percentage'), + 'average_rating' => new external_value(PARAM_TEXT, 'Average rating', VALUE_DEFAULT, '0.0'), + 'total_ratings' => new external_value(PARAM_INT, 'Total ratings', VALUE_DEFAULT, 0), + 'total_comments' => new external_value(PARAM_INT, 'Total comments', VALUE_DEFAULT, 0), + 'user_rating' => new external_value(PARAM_INT, 'User rating', VALUE_DEFAULT, 0), + 'config' => new external_single_structure([ + 'showcourseprogress' => new external_value(PARAM_BOOL, 'Show course progress'), + 'showcoursedescription' => new external_value(PARAM_BOOL, 'Show course description'), + 'showcourseshortname' => new external_value(PARAM_BOOL, 'Show course short name'), + 'buttoncolor' => new external_value(PARAM_TEXT, 'Button color', VALUE_OPTIONAL), + 'progresscolor1' => new external_value(PARAM_TEXT, 'Progress start color', VALUE_OPTIONAL), + 'progresscolor2' => new external_value(PARAM_TEXT, 'Progress end color', VALUE_OPTIONAL) + ]) + ]), VALUE_DEFAULT, [] + ), + 'config' => new external_single_structure([ + 'showprogress' => new external_value(PARAM_BOOL, 'Show progress'), + 'showdescription' => new external_value(PARAM_BOOL, 'Show description'), + 'showcoursecount' => new external_value(PARAM_BOOL, 'Show course count'), + 'showcourseprogress' => new external_value(PARAM_BOOL, 'Show course progress'), + 'showcoursedescription' => new external_value(PARAM_BOOL, 'Show course description'), + 'showcourseshortname' => new external_value(PARAM_BOOL, 'Show course short name'), + 'buttoncolor' => new external_value(PARAM_TEXT, 'Button color', VALUE_OPTIONAL), + 'progresscolor1' => new external_value(PARAM_TEXT, 'Progress start color', VALUE_OPTIONAL), + 'progresscolor2' => new external_value(PARAM_TEXT, 'Progress end color', VALUE_OPTIONAL) + ]), + 'currentcategoryid' => new external_value(PARAM_INT, 'Current category ID'), + 'isroot' => new external_value(PARAM_BOOL, 'Is root level'), + 'hascategories' => new external_value(PARAM_BOOL, 'Has categories', VALUE_DEFAULT, false), + 'hascourses' => new external_value(PARAM_BOOL, 'Has courses', VALUE_DEFAULT, false) + ]); + } +} \ No newline at end of file diff --git a/category_courses/classes/external/rating_service.php b/category_courses/classes/external/rating_service.php new file mode 100644 index 0000000..6566eb1 --- /dev/null +++ b/category_courses/classes/external/rating_service.php @@ -0,0 +1,167 @@ +. + +/** + * External rating service for AJAX calls. + * + * @package block_category_courses + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_category_courses\external; + +defined('MOODLE_INTERNAL') || die(); + +require_once($CFG->libdir . '/externallib.php'); + +use external_api; +use external_function_parameters; +use external_value; +use external_single_structure; +use external_multiple_structure; +use block_category_courses\rating_manager; + +/** + * External rating service class. + */ +class rating_service extends external_api { + + /** + * Set rating parameters. + */ + public static function set_rating_parameters() { + return new external_function_parameters([ + 'courseid' => new external_value(PARAM_INT, 'Course ID'), + 'rating' => new external_value(PARAM_INT, 'Rating 1-5'), + 'comment' => new external_value(PARAM_TEXT, 'Comment', VALUE_DEFAULT, '') + ]); + } + + /** + * Set course rating. + */ + public static function set_rating($courseid, $rating, $comment = '') { + global $USER; + + $params = self::validate_parameters(self::set_rating_parameters(), [ + 'courseid' => $courseid, + 'rating' => $rating, + 'comment' => $comment + ]); + + $context = \context_course::instance($params['courseid']); + self::validate_context($context); + + if (!isloggedin() || isguestuser()) { + throw new \moodle_exception('notloggedin'); + } + + $success = rating_manager::set_rating( + $params['courseid'], + $USER->id, + $params['rating'], + $params['comment'] + ); + + if (!$success) { + throw new \moodle_exception('errorsetrating', 'block_category_courses'); + } + + $stats = rating_manager::get_course_stats($params['courseid']); + + return [ + 'success' => true, + 'average_rating' => $stats->average_rating, + 'total_ratings' => $stats->total_ratings + ]; + } + + /** + * Set rating return values. + */ + public static function set_rating_returns() { + return new external_single_structure([ + 'success' => new external_value(PARAM_BOOL, 'Success'), + 'average_rating' => new external_value(PARAM_FLOAT, 'Average rating'), + 'total_ratings' => new external_value(PARAM_INT, 'Total ratings') + ]); + } + + /** + * Get comments parameters. + */ + public static function get_comments_parameters() { + return new external_function_parameters([ + 'courseid' => new external_value(PARAM_INT, 'Course ID'), + 'limit' => new external_value(PARAM_INT, 'Limit', VALUE_DEFAULT, 10) + ]); + } + + /** + * Get course comments. + */ + public static function get_comments($courseid, $limit = 10) { + global $OUTPUT; + + $params = self::validate_parameters(self::get_comments_parameters(), [ + 'courseid' => $courseid, + 'limit' => $limit + ]); + + $context = \context_course::instance($params['courseid']); + self::validate_context($context); + + $comments = rating_manager::get_course_comments($params['courseid'], $params['limit']); + $result = []; + + foreach ($comments as $comment) { + $user = (object)[ + 'id' => $comment->userid, + 'firstname' => $comment->firstname, + 'lastname' => $comment->lastname, + 'picture' => $comment->picture, + 'imagealt' => $comment->imagealt, + 'email' => $comment->email + ]; + + $result[] = [ + 'rating' => $comment->rating, + 'comment' => $comment->comment, + 'timemodified' => $comment->timemodified, + 'user_fullname' => fullname($user), + 'user_picture' => $OUTPUT->user_picture($user, ['size' => 35]) + ]; + } + + return $result; + } + + /** + * Get comments return values. + */ + public static function get_comments_returns() { + return new external_multiple_structure( + new external_single_structure([ + 'rating' => new external_value(PARAM_INT, 'Rating'), + 'comment' => new external_value(PARAM_TEXT, 'Comment'), + 'timemodified' => new external_value(PARAM_INT, 'Time modified'), + 'user_fullname' => new external_value(PARAM_TEXT, 'User full name'), + 'user_picture' => new external_value(PARAM_RAW, 'User picture HTML') + ]) + ); + } +} \ No newline at end of file diff --git a/category_courses/classes/external/save_image.php b/category_courses/classes/external/save_image.php new file mode 100644 index 0000000..b407562 --- /dev/null +++ b/category_courses/classes/external/save_image.php @@ -0,0 +1,59 @@ + new external_value(PARAM_INT, 'Category ID'), + 'imageurl' => new external_value(PARAM_URL, 'Image URL', VALUE_DEFAULT, ''), + 'bgcolor' => new external_value(PARAM_TEXT, 'Background color', VALUE_DEFAULT, '#667eea'), + ]); + } + + public static function execute($categoryid, $imageurl, $bgcolor) { + global $DB; + + $params = self::validate_parameters(self::execute_parameters(), [ + 'categoryid' => $categoryid, + 'imageurl' => $imageurl, + 'bgcolor' => $bgcolor, + ]); + + $context = \context_system::instance(); + self::validate_context($context); + require_capability('block/category_courses:manage', $context); + + $record = $DB->get_record('block_category_courses_images', ['categoryid' => $params['categoryid']]); + + if ($record) { + $record->imageurl = $params['imageurl']; + $record->bgcolor = $params['bgcolor']; + $record->timemodified = time(); + $DB->update_record('block_category_courses_images', $record); + } else { + $record = new \stdClass(); + $record->categoryid = $params['categoryid']; + $record->imageurl = $params['imageurl']; + $record->bgcolor = $params['bgcolor']; + $record->timecreated = time(); + $record->timemodified = time(); + $DB->insert_record('block_category_courses_images', $record); + } + + return ['success' => true]; + } + + public static function execute_returns() { + return new external_single_structure([ + 'success' => new external_value(PARAM_BOOL, 'Success status'), + ]); + } +} diff --git a/category_courses/classes/hook_callbacks/output_callbacks.php b/category_courses/classes/hook_callbacks/output_callbacks.php new file mode 100644 index 0000000..8e4a86d --- /dev/null +++ b/category_courses/classes/hook_callbacks/output_callbacks.php @@ -0,0 +1,46 @@ +. + +/** + * Hook callbacks for output events. + * + * @package block_category_courses + * @copyright 2025 Your Name + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_category_courses\hook_callbacks; + +/** + * Hook callbacks for output events. + */ +class output_callbacks { + + /** + * Callback for before footer HTML generation. + * + * @param \core\hook\output\before_footer_html_generation $hook + */ + public static function before_footer_html_generation(\core\hook\output\before_footer_html_generation $hook): void { + global $PAGE; + + // Load color picker on category edit pages + if (strpos($PAGE->url->get_path(), '/course/editcategory.php') !== false || + strpos($PAGE->url->get_path(), '/course/management.php') !== false) { + $PAGE->requires->js_call_amd('block_category_courses/colorpicker', 'init'); + } + } +} \ No newline at end of file diff --git a/category_courses/classes/observer.php b/category_courses/classes/observer.php new file mode 100644 index 0000000..f6ad79a --- /dev/null +++ b/category_courses/classes/observer.php @@ -0,0 +1,14 @@ +url->get_path(), '/course/editcategory.php') !== false) { + $PAGE->requires->js_call_amd('block_category_courses/colorpicker', 'init'); + } + } +} \ No newline at end of file diff --git a/category_courses/classes/output/main.php b/category_courses/classes/output/main.php new file mode 100644 index 0000000..1735585 --- /dev/null +++ b/category_courses/classes/output/main.php @@ -0,0 +1,883 @@ +userid = $userid; + $this->config = $config; + $this->currentcategoryid = $currentcategoryid; + } + + public function export_for_template($output = null) { + global $DB; + + // Obtener estructura jerárquica + $hierarchydata = $this->get_category_hierarchy(); + + $config = $this->get_display_config(); + + $data = [ + 'categories' => $hierarchydata['categories'], + 'courses' => $hierarchydata['courses'], + 'breadcrumbs' => $hierarchydata['breadcrumbs'], + 'config' => $config, + 'currentcategoryid' => $this->currentcategoryid, + 'isroot' => $this->currentcategoryid == 0, + 'hascategories' => !empty($hierarchydata['categories']), + 'hascourses' => !empty($hierarchydata['courses']) + ]; + + // Add config to each course for template access + foreach ($data['courses'] as &$course) { + $course['config'] = $config; + } + + return $data; + } + + /** + * Obtiene la jerarquía de categorías y cursos para el nivel actual + * @return array Estructura con categorías, cursos y breadcrumbs + */ + private function get_category_hierarchy() { + $categories = []; + $courses = []; + $breadcrumbs = $this->build_breadcrumbs(); + + if ($this->currentcategoryid == 0) { + // Nivel raíz: obtener categorías principales del usuario + $categories = $this->get_user_root_categories(); + } else { + // Nivel específico: obtener subcategorías y cursos + $categories = $this->get_subcategories($this->currentcategoryid); + $courses = $this->get_category_courses($this->currentcategoryid); + } + + $categories = $this->apply_sorting($categories); + $categories = $this->apply_limits($categories); + $courses = $this->apply_course_sorting($courses); + + return [ + 'categories' => $categories, + 'courses' => $courses, + 'breadcrumbs' => $breadcrumbs + ]; + } + + /** + * Construye breadcrumbs para navegación + * @return array Lista de breadcrumbs + */ + private function build_breadcrumbs() { + $breadcrumbs = []; + + // Siempre agregar "Inicio" como primer breadcrumb + $breadcrumbs[] = [ + 'name' => get_string('home', 'block_category_courses'), + 'url' => '#', + 'categoryid' => 0, + 'active' => $this->currentcategoryid == 0 + ]; + + if ($this->currentcategoryid > 0) { + try { + $category = core_course_category::get($this->currentcategoryid, IGNORE_MISSING); + if ($category) { + // Obtener path de padres (sin incluir la categoría actual) + $parents = $category->get_parents(); + + // Agregar breadcrumbs para cada padre + foreach ($parents as $parentid) { + if ($parentid == 0) continue; + try { + $parentcat = core_course_category::get($parentid, IGNORE_MISSING); + if ($parentcat) { + $breadcrumbs[] = [ + 'name' => format_string($parentcat->name), + 'url' => '#', + 'categoryid' => $parentid, + 'active' => false + ]; + } + } catch (Exception $e) { + continue; + } + } + + // Agregar la categoría actual como último breadcrumb + $breadcrumbs[] = [ + 'name' => format_string($category->name), + 'url' => '#', + 'categoryid' => $this->currentcategoryid, + 'active' => true + ]; + } + } catch (Exception $e) { + // Error obteniendo categoría, solo mostrar "Inicio" + } + } + + return $breadcrumbs; + } + + /** + * Obtiene categorías raíz del usuario (solo nivel superior) + * @return array Lista de categorías raíz + */ + private function get_user_root_categories() { + $categories = []; + $rootcategories = []; + + $showAllCategories = $this->get_config_value('showallcategories', false); + + if (is_siteadmin() || has_capability('moodle/category:manage', context_system::instance())) { + // Admins and managers: obtener todas las categorías raíz + $allcategories = core_course_category::get_all(); + foreach ($allcategories as $category) { + if ($category->parent == 0) { // Solo categorías raíz + $rootcategories[$category->id] = $category; + } + } + } else if ($showAllCategories) { + // Mostrar todas las categorías visibles + $allcategories = core_course_category::get_all(); + foreach ($allcategories as $category) { + if ($category->parent == 0 && $category->visible) { + $rootcategories[$category->id] = $category; + } + } + } else { + // Usuarios normales: solo categorías raíz visibles donde tienen cursos (recursivo) + $usercourses = enrol_get_my_courses(); + $usercategoryids = []; + + // Obtener todas las categorías donde el usuario tiene cursos + foreach ($usercourses as $course) { + $usercategoryids[] = $course->category; + } + + // Para cada categoría, encontrar su categoría raíz + foreach ($usercategoryids as $categoryid) { + try { + $category = core_course_category::get($categoryid); + $rootcategoryid = $this->get_root_category_id($category); + + if (!isset($rootcategories[$rootcategoryid])) { + $rootcategory = core_course_category::get($rootcategoryid, IGNORE_MISSING); + // Solo agregar si la categoría raíz es visible + if ($rootcategory && $rootcategory->visible) { + $rootcategories[$rootcategoryid] = $rootcategory; + } + } + } catch (Exception $e) { + // Categoría no encontrada, continuar + } + } + } + + // Construir datos para cada categoría raíz + foreach ($rootcategories as $category) { + // Para estudiantes, filtrar categorías ocultas + if (!is_siteadmin() && !has_capability('moodle/category:manage', context_system::instance())) { + if ($this->is_category_or_parent_hidden($category)) { + continue; // Saltar categorías ocultas para estudiantes + } + } + + $categorydata = $this->build_category_data($category); + + // Calcular estadísticas recursivas para toda la rama + $stats = $this->get_category_recursive_stats($category->id); + $categorydata['total_courses'] = $stats['total_courses']; + $categorydata['completed_courses'] = $stats['completed_courses']; + $categorydata['progress_percentage'] = $stats['progress_percentage']; + $categorydata['hasprogress'] = $stats['hasprogress']; + $categorydata['progress'] = $stats['progress_percentage']; + + $categories[] = $categorydata; + } + + return $categories; + } + + /** + * Obtiene subcategorías de una categoría específica + * @param int $parentid ID de la categoría padre + * @return array Lista de subcategorías + */ + private function get_subcategories($parentid) { + $categories = []; + + try { + $parentcategory = core_course_category::get($parentid); + + // Si la categoría padre está oculta, no mostrar subcategorías a usuarios normales + if (!$parentcategory->visible && !is_siteadmin() && !has_capability('moodle/category:manage', context_system::instance())) { + return []; + } + + $subcategories = $parentcategory->get_children(); + + $showAllCategories = $this->get_config_value('showallcategories', false); + + foreach ($subcategories as $subcategory) { + // Para estudiantes, filtrar subcategorías ocultas + if (!is_siteadmin() && !has_capability('moodle/category:manage', context_system::instance())) { + if (!$subcategory->visible) { + continue; // Saltar subcategorías ocultas para estudiantes + } + } + + // Verificar si el usuario tiene acceso a esta categoría + if ($showAllCategories || $this->user_has_category_access($subcategory)) { + $categorydata = $this->build_category_data($subcategory); + + // Para subcategorías, calcular estadísticas recursivamente + $stats = $this->get_category_recursive_stats($subcategory->id); + $categorydata['total_courses'] = $stats['total_courses']; + $categorydata['completed_courses'] = $stats['completed_courses']; + $categorydata['progress_percentage'] = $stats['progress_percentage']; + $categorydata['hasprogress'] = $stats['hasprogress']; + $categorydata['progress'] = $stats['progress_percentage']; + + $categories[] = $categorydata; + } + } + } catch (Exception $e) { + // Categoría no encontrada o sin permisos + } + + return $categories; + } + + /** + * Obtiene cursos directos de una categoría + * @param int $categoryid ID de la categoría + * @return array Lista de cursos + */ + private function get_category_courses($categoryid) { + global $DB; + $courses = []; + + try { + $category = core_course_category::get($categoryid); + + if (is_siteadmin() || has_capability('moodle/category:manage', context_system::instance())) { + // Admins and managers: obtener solo cursos directos de la categoría actual + $sql = "SELECT c.* FROM {course} c WHERE c.category = ? AND c.id != ?"; + $allcourses = $DB->get_records_sql($sql, [$categoryid, SITEID]); + $enrolledcourses = enrol_get_my_courses(); + + foreach ($allcourses as $course) { + $coursedata = $this->build_course_data($course); + $coursedata['isenrolled'] = isset($enrolledcourses[$course->id]); + + // Calcular progreso solo si está inscrito + if ($coursedata['isenrolled']) { + $progress = progress::get_course_progress_percentage($course, $this->userid); + $coursedata['hasprogress'] = !is_null($progress); + $coursedata['progress'] = $coursedata['hasprogress'] ? floor($progress) : 0; + } else { + $coursedata['hasprogress'] = false; + $coursedata['progress'] = 0; + } + + // Add rating data + $this->add_rating_data($coursedata, $course->id); + + $courses[] = $coursedata; + } + } else { + // Usuarios normales: solo cursos inscritos y visibles directos de la categoría + $enrolledcourses = enrol_get_my_courses(); + + foreach ($enrolledcourses as $course) { + if ($course->category == $categoryid && $course->visible) { + $coursedata = $this->build_course_data($course); + $coursedata['isenrolled'] = true; + + $progress = progress::get_course_progress_percentage($course, $this->userid); + $coursedata['hasprogress'] = !is_null($progress); + $coursedata['progress'] = $coursedata['hasprogress'] ? floor($progress) : 0; + + // Add rating data + $this->add_rating_data($coursedata, $course->id); + + $courses[] = $coursedata; + } + } + } + } catch (Exception $e) { + // Error getting courses for category + } + return $courses; + } + + /** + * Construye datos de un curso compatible con template estándar de Moodle + * @param object $course Objeto curso + * @return array Datos del curso + */ + private function build_course_data($course) { + global $OUTPUT; + + $courseimage = \core_course\external\course_summary_exporter::get_course_image($course); + if (!$courseimage) { + $courseimage = $OUTPUT->get_generated_image_for_id($course->id); + } + + return [ + 'id' => $course->id, + 'fullname' => format_string($course->fullname), + 'shortname' => format_string($course->shortname), + 'summary' => strip_tags($course->summary), + 'viewurl' => (new moodle_url('/course/view.php', ['id' => $course->id]))->out(false), + 'courseimage' => $courseimage, + 'visible' => $course->visible, + 'coursecategory' => '', + 'showcoursecategory' => false, + 'isenrolled' => false, // Se establece en el método llamador + 'hasprogress' => false, // Se establece en el método llamador + 'progress' => 0 // Se establece en el método llamador + ]; + } + + /** + * Verifica si el usuario tiene acceso a una categoría + * @param object $category Categoría + * @return bool True si tiene acceso + */ + private function user_has_category_access($category) { + if (is_siteadmin() || has_capability('moodle/category:manage', context_system::instance())) { + return true; + } + + // Verificar si tiene cursos inscritos y visibles en esta categoría o subcategorías + return $this->has_visible_enrolled_courses_in_category($category->id); + } + + /** + * Verifica si el usuario tiene cursos inscritos en una categoría (recursivo) + * @param int $categoryid ID de la categoría + * @return bool True si tiene cursos inscritos + */ + private function has_enrolled_courses_in_category($categoryid) { + $enrolledcourses = enrol_get_my_courses(); + + foreach ($enrolledcourses as $course) { + if ($this->course_belongs_to_category_tree($course->category, $categoryid)) { + return true; + } + } + + return false; + } + + /** + * Verifica si el usuario tiene cursos inscritos y visibles en una categoría (recursivo) + * @param int $categoryid ID de la categoría + * @return bool True si tiene cursos inscritos y visibles + */ + private function has_visible_enrolled_courses_in_category($categoryid) { + $enrolledcourses = enrol_get_my_courses(); + + foreach ($enrolledcourses as $course) { + if ($course->visible && $this->course_belongs_to_category_tree($course->category, $categoryid)) { + return true; + } + } + + return false; + } + + /** + * Verifica si un curso pertenece al árbol de una categoría + * @param int $coursecategoryid ID de categoría del curso + * @param int $targetcategoryid ID de categoría objetivo + * @return bool True si pertenece al árbol + */ + private function course_belongs_to_category_tree($coursecategoryid, $targetcategoryid) { + if ($coursecategoryid == $targetcategoryid) { + return true; + } + + try { + $coursecategory = core_course_category::get($coursecategoryid); + $parents = $coursecategory->get_parents(); + return in_array($targetcategoryid, $parents); + } catch (Exception $e) { + return false; + } + } + + /** + * Obtiene estadísticas recursivas de una categoría + * @param int $categoryid ID de la categoría + * @return array Estadísticas + */ + private function get_category_recursive_stats($categoryid) { + $totalcourses = 0; + $completedcourses = 0; + $enrolledcourses = enrol_get_my_courses(); + + if (is_siteadmin() || has_capability('moodle/category:manage', context_system::instance())) { + // Admins and managers: mostrar solo cursos inscritos (como usuarios normales) + // Esto evita confusión de mostrar "0 de 27" cuando no está inscrito + foreach ($enrolledcourses as $course) { + if ($course->visible && $this->course_belongs_to_category_tree($course->category, $categoryid)) { + $totalcourses++; + + $progress = progress::get_course_progress_percentage($course, $this->userid); + if ($progress === 100.0 || $progress === 100) { + $completedcourses++; + } + } + } + } else { + // Usuarios normales: solo cursos inscritos y visibles + foreach ($enrolledcourses as $course) { + if ($course->visible && $this->course_belongs_to_category_tree($course->category, $categoryid)) { + $totalcourses++; + + $progress = progress::get_course_progress_percentage($course, $this->userid); + if ($progress === 100.0 || $progress === 100) { + $completedcourses++; + } + } + } + } + + $percentage = $totalcourses > 0 ? round(($completedcourses / $totalcourses) * 100) : 0; + + return [ + 'total_courses' => $totalcourses, + 'completed_courses' => $completedcourses, + 'progress_percentage' => $percentage, + 'hasprogress' => $totalcourses > 0, + 'progress' => $percentage + ]; + } + + private function build_category_data($category) { + $image = $this->get_category_image($category); + $color = $this->get_category_color($category); + $textcolor = $this->get_text_color_for_background($color); + + // Para admins: mostrar conteo total de cursos en la categoría (recursivo) + // Para usuarios: se calculará con cursos inscritos + $total_courses = 0; + $admin_total_courses = 0; + + if (is_siteadmin() || has_capability('moodle/category:manage', context_system::instance())) { + $admin_total_courses = $this->get_category_total_courses($category->id); + } + + // Check if category or any parent is hidden (recursive) + $ishidden = $this->is_category_or_parent_hidden($category); + + return [ + 'id' => $category->id, + 'name' => format_string($category->name), + 'description' => $this->get_category_description($category), + 'image' => $image, + 'color' => $color, + 'textcolor' => $textcolor, + 'visible' => $category->visible, + 'ishidden' => $ishidden, + 'url' => (new moodle_url('/course/index.php', ['categoryid' => $category->id]))->out(false), + 'courses' => [], + 'total_courses' => $total_courses, + 'completed_courses' => 0, + 'progress_percentage' => 0, + 'hasprogress' => false, + 'progress' => 0, + 'admin_total_courses' => $admin_total_courses, + 'is_admin' => is_siteadmin() || has_capability('moodle/category:manage', context_system::instance()), + 'config' => $this->get_display_config() + ]; + } + + /** + * Obtiene el ID de la categoría raíz para una categoría dada + * @param object $category Categoría + * @return int ID de la categoría raíz + */ + private function get_root_category_id($category) { + if ($category->parent == 0) { + return $category->id; + } + + try { + $parentcategory = core_course_category::get($category->parent); + return $this->get_root_category_id($parentcategory); + } catch (Exception $e) { + return $category->id; // Fallback + } + } + + private function add_course_to_category(&$category, $course) { + $progress = progress::get_course_progress_percentage($course, $this->userid); + + + + $iscompleted = ($progress === 100.0 || $progress === 100); + + // For regular users, increment total_courses + // For admins, we need to track enrolled courses separately + if (!is_siteadmin() && !has_capability('moodle/category:manage', context_system::instance())) { + $category['total_courses']++; + } else { + // For admins and managers, track enrolled courses separately + if (!isset($category['enrolled_courses'])) { + $category['enrolled_courses'] = 0; + } + $category['enrolled_courses']++; + } + + if ($iscompleted) { + $category['completed_courses']++; + } + + // Calculate percentage based on enrolled courses for admins + if (is_siteadmin() || has_capability('moodle/category:manage', context_system::instance())) { + $percentage = $category['enrolled_courses'] > 0 ? + round(($category['completed_courses'] / $category['enrolled_courses']) * 100) : 0; + } else { + $percentage = $category['total_courses'] > 0 ? + round(($category['completed_courses'] / $category['total_courses']) * 100) : 0; + } + + $category['progress_percentage'] = $percentage; + $category['hasprogress'] = true; // Siempre mostrar si hay cursos inscritos + $category['progress'] = $percentage; + } + + private function get_category_description($category) { + if (!$this->get_config_value('showdescription', true)) { + return ''; + } + + $limit = $this->get_config_value('descriptionlimit', 150); + $description = strip_tags($category->description); + + // Limpiar entidades HTML y caracteres especiales + $description = html_entity_decode($description, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $description = preg_replace('/\s+/', ' ', $description); // Normalizar espacios + $description = trim($description); + + if (strlen($description) > $limit) { + $description = substr($description, 0, $limit) . '...'; + } + + return $description; + } + + private function get_category_image($category) { + global $DB; + + // 1. Try uploaded file first (if exists) + try { + if ($DB->get_manager()->table_exists('block_catcourse_images')) { + $customdata = $DB->get_record('block_catcourse_images', ['categoryid' => $category->id]); + if ($customdata && !empty($customdata->imageurl)) { + // Check if file actually exists + $context = context_system::instance(); + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'block_category_courses', 'categoryimage', $category->id, 'filename', false); + if (!empty($files)) { + $file = reset($files); + $imageurl = moodle_url::make_pluginfile_url( + $context->id, + 'block_category_courses', + 'categoryimage', + $category->id, + '/', + $file->get_filename() + )->out(); + return [ + 'type' => null, + 'text' => null, + 'color' => null, + 'url' => $imageurl + ]; + } + } + } + } catch (Exception $e) { + // Table doesn't exist yet + } + + // 2. Try to extract from description + if (!empty($category->description)) { + if (preg_match('/]+src=["\'](["\'">]+)["\'"][^>]*>/i', $category->description, $matches)) { + $imageurl = $matches[1]; + if ($this->is_valid_image_url($imageurl)) { + return [ + 'type' => null, + 'text' => null, + 'color' => null, + 'url' => $imageurl + ]; + } + } + if (preg_match('/pluginfile\.php\/[^\s"\'">]+\.(jpg|jpeg|png|gif|webp)/i', $category->description, $matches)) { + return [ + 'type' => null, + 'text' => null, + 'color' => null, + 'url' => $matches[0] + ]; + } + } + + // 3. Fallback: generate initials with custom color + $color = $this->get_category_color($category); + return $this->generate_fallback_image($category->name, $color); + } + + private function get_category_color($category) { + global $DB; + + // Try custom table first (if exists) + try { + if ($DB->get_manager()->table_exists('block_catcourse_images')) { + $customdata = $DB->get_record('block_catcourse_images', ['categoryid' => $category->id]); + if ($customdata && !empty($customdata->bgcolor)) { + return $customdata->bgcolor; + } + } + } catch (Exception $e) { + // Table doesn't exist yet + } + + // Fallback: generate color based on category name + $colors = ['#667eea', '#764ba2', '#f093fb', '#f5576c', '#4facfe', '#43e97b', '#fa709a', '#ffecd2']; + $index = abs(crc32($category->name)) % count($colors); + return $colors[$index]; + } + + private function is_valid_image_url($url) { + $imageExtensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg']; + $extension = strtolower(pathinfo(parse_url($url, PHP_URL_PATH), PATHINFO_EXTENSION)); + return in_array($extension, $imageExtensions); + } + + private function generate_fallback_image($name, $color = '#667eea') { + $initials = ''; + $words = explode(' ', $name); + foreach (array_slice($words, 0, 2) as $word) { + $initials .= strtoupper(substr($word, 0, 1)); + } + return [ + 'type' => 'initials', + 'text' => $initials, + 'color' => $color, + 'url' => null + ]; + } + + private function apply_sorting($categories) { + $sortorder = $this->get_config_value('sortorder', 'core'); + + switch ($sortorder) { + case 'alphabetical': + usort($categories, function($a, $b) { + return strcmp($a['name'], $b['name']); + }); + break; + case 'coursecount': + usort($categories, function($a, $b) { + return $b['total_courses'] - $a['total_courses']; + }); + break; + case 'progress': + usort($categories, function($a, $b) { + return $b['progress_percentage'] - $a['progress_percentage']; + }); + break; + // 'core' keeps original order + } + + return $categories; + } + + private function apply_limits($categories) { + // No limits applied in hierarchical navigation + return $categories; + } + + private function apply_course_sorting($courses) { + $sortorder = $this->get_config_value('sortorder', 'core'); + + switch ($sortorder) { + case 'alphabetical': + usort($courses, function($a, $b) { + return strcmp($a['fullname'], $b['fullname']); + }); + break; + case 'progress': + usort($courses, function($a, $b) { + return $b['progress'] - $a['progress']; + }); + break; + // 'core' and 'coursecount' keep original order for courses + } + + return $courses; + } + + private function get_display_config() { + return [ + 'showprogress' => $this->get_config_value('showprogress', true), + 'showdescription' => $this->get_config_value('showdescription', true), + 'showcoursecount' => $this->get_config_value('showcoursecount', true), + 'showcourseprogress' => $this->get_config_value('showcourseprogress', true), + 'showcoursedescription' => $this->get_config_value('showcoursedescription', true), + 'showcourseshortname' => $this->get_config_value('showcourseshortname', true), + 'buttoncolor' => $this->get_config_value('buttoncolor', ''), + 'progresscolor1' => $this->get_config_value('progresscolor1', '#4285f4'), + 'progresscolor2' => $this->get_config_value('progresscolor2', '#34a853') + ]; + } + + private function get_config_value($key, $default) { + // For color settings, only use global config + if (in_array($key, ['buttoncolor', 'progresscolor1', 'progresscolor2'])) { + return get_config('block_category_courses', $key) ?? $default; + } + + // Instance config takes precedence for other settings + if (isset($this->config->$key)) { + return $this->config->$key; + } + + // Fall back to global config + return get_config('block_category_courses', $key) ?? $default; + } + + private function get_text_color_for_background($hexcolor) { + // Remove # if present + $hex = ltrim($hexcolor, '#'); + + // Convert to RGB + $r = hexdec(substr($hex, 0, 2)); + $g = hexdec(substr($hex, 2, 2)); + $b = hexdec(substr($hex, 4, 2)); + + // Calculate luminance using WCAG formula + $luminance = (0.299 * $r + 0.587 * $g + 0.114 * $b) / 255; + + // Return white for dark backgrounds, dark for light backgrounds + return $luminance > 0.5 ? '#1f2937' : '#ffffff'; + } + + /** + * Obtiene el conteo total de cursos en una categoría (recursivo) + * @param int $categoryid ID de la categoría + * @return int Total de cursos + */ + private function get_category_total_courses($categoryid) { + try { + $category = core_course_category::get($categoryid, IGNORE_MISSING); + if (!$category) { + return 0; + } + + // Usar el método nativo de Moodle para contar cursos recursivamente + return $category->get_courses_count(['recursive' => true]); + } catch (Exception $e) { + return 0; + } + } + + /** + * Checks if a category is effectively hidden (itself or any parent hidden) + * @param object $category Category object + * @return bool True if category is effectively hidden for students + */ + private function is_category_or_parent_hidden($category) { + // For admins, show the actual visibility status with inheritance indication + if (is_siteadmin() || has_capability('moodle/category:manage', context_system::instance())) { + return $this->check_inherited_visibility($category); + } + + // For students, if any parent is hidden, this category is effectively hidden + return $this->check_inherited_visibility($category); + } + + /** + * Recursively checks if category or any parent is hidden + * @param object $category Category object + * @return bool True if category or any parent is hidden + */ + private function check_inherited_visibility($category) { + if (!$category->visible) { + return true; + } + + if ($category->parent == 0) { + return false; + } + + try { + $parent = core_course_category::get($category->parent, IGNORE_MISSING); + if ($parent) { + return $this->check_inherited_visibility($parent); + } + } catch (Exception $e) { + // Parent not found, assume visible + } + + return false; + } + + /** + * Adds rating data to course data array + * @param array &$coursedata Course data array (passed by reference) + * @param int $courseid Course ID + */ + private function add_rating_data(&$coursedata, $courseid) { + // Set default values + $coursedata['average_rating'] = '0.0'; + $coursedata['total_ratings'] = 0; + $coursedata['total_comments'] = 0; + $coursedata['user_rating'] = 0; + + try { + if (class_exists('block_category_courses\rating_manager')) { + $stats = rating_manager::get_course_stats($courseid); + $userRating = rating_manager::get_user_rating($courseid, $this->userid); + + if ($stats) { + $coursedata['average_rating'] = $stats->average_rating > 0 ? number_format($stats->average_rating, 1) : '0.0'; + $coursedata['total_ratings'] = $stats->total_ratings; + $coursedata['total_comments'] = $stats->total_comments; + } + + if ($userRating) { + $coursedata['user_rating'] = $userRating->rating; + } + } + } catch (Exception $e) { + // Keep default values + } + } +} \ No newline at end of file diff --git a/category_courses/classes/privacy/provider.php b/category_courses/classes/privacy/provider.php new file mode 100644 index 0000000..13dfb33 --- /dev/null +++ b/category_courses/classes/privacy/provider.php @@ -0,0 +1,24 @@ +. + +/** + * Rating manager for category courses block. + * + * @package block_category_courses + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +namespace block_category_courses; + +defined('MOODLE_INTERNAL') || die(); + +/** + * Manages course ratings and comments. + */ +class rating_manager { + + /** + * Add or update a course rating. + * + * @param int $courseid Course ID + * @param int $userid User ID + * @param int $rating Rating (1-5) + * @param string $comment Optional comment + * @return bool Success + */ + public static function set_rating($courseid, $userid, $rating, $comment = '') { + global $DB; + + if ($rating < 1 || $rating > 5) { + return false; + } + + $time = time(); + $record = $DB->get_record('block_catcourse_ratings', + ['courseid' => $courseid, 'userid' => $userid]); + + if ($record) { + $record->rating = $rating; + $record->comment = $comment; + $record->timemodified = $time; + $result = $DB->update_record('block_catcourse_ratings', $record); + } else { + $record = (object)[ + 'courseid' => $courseid, + 'userid' => $userid, + 'rating' => $rating, + 'comment' => $comment, + 'timecreated' => $time, + 'timemodified' => $time + ]; + $result = $DB->insert_record('block_catcourse_ratings', $record); + } + + if ($result) { + self::update_stats($courseid); + } + + return $result; + } + + /** + * Get user's rating for a course. + * + * @param int $courseid Course ID + * @param int $userid User ID + * @return object|false Rating record or false + */ + public static function get_user_rating($courseid, $userid) { + global $DB; + return $DB->get_record('block_catcourse_ratings', + ['courseid' => $courseid, 'userid' => $userid]); + } + + /** + * Get course statistics. + * + * @param int $courseid Course ID + * @return object Statistics + */ + public static function get_course_stats($courseid) { + global $DB; + $stats = $DB->get_record('block_catcourse_stats', ['courseid' => $courseid]); + + if (!$stats) { + return (object)[ + 'total_ratings' => 0, + 'average_rating' => 0, + 'total_comments' => 0 + ]; + } + + return $stats; + } + + /** + * Get course comments. + * + * @param int $courseid Course ID + * @param int $limit Limit results + * @return array Comments with user info + */ + public static function get_course_comments($courseid, $limit = 10) { + global $DB; + + $sql = "SELECT r.*, u.firstname, u.lastname, u.picture, u.imagealt, u.email + FROM {block_catcourse_ratings} r + JOIN {user} u ON r.userid = u.id + WHERE r.courseid = ? AND r.comment IS NOT NULL AND r.comment != '' + ORDER BY r.timemodified DESC"; + + return $DB->get_records_sql($sql, [$courseid], 0, $limit); + } + + /** + * Update course statistics. + * + * @param int $courseid Course ID + */ + private static function update_stats($courseid) { + global $DB; + + $sql = "SELECT COUNT(*) as total_ratings, + AVG(rating) as average_rating, + SUM(CASE WHEN comment IS NOT NULL AND comment != '' THEN 1 ELSE 0 END) as total_comments + FROM {block_catcourse_ratings} + WHERE courseid = ?"; + + $stats = $DB->get_record_sql($sql, [$courseid]); + $time = time(); + + $record = $DB->get_record('block_catcourse_stats', ['courseid' => $courseid]); + + if ($record) { + $record->total_ratings = $stats->total_ratings; + $record->average_rating = round($stats->average_rating, 2); + $record->total_comments = $stats->total_comments; + $record->timemodified = $time; + $DB->update_record('block_catcourse_stats', $record); + } else { + $record = (object)[ + 'courseid' => $courseid, + 'total_ratings' => $stats->total_ratings, + 'average_rating' => round($stats->average_rating, 2), + 'total_comments' => $stats->total_comments, + 'timemodified' => $time + ]; + $DB->insert_record('block_catcourse_stats', $record); + } + } +} \ No newline at end of file diff --git a/category_courses/db/access.php b/category_courses/db/access.php new file mode 100644 index 0000000..7d227a4 --- /dev/null +++ b/category_courses/db/access.php @@ -0,0 +1,39 @@ + [ + 'captype' => 'read', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => [ + 'user' => CAP_ALLOW, + 'student' => CAP_ALLOW, + 'teacher' => CAP_ALLOW, + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + ], + 'block/category_courses:manage' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'manager' => CAP_ALLOW + ], + ], + 'block/category_courses:myaddinstance' => [ + 'captype' => 'write', + 'contextlevel' => CONTEXT_SYSTEM, + 'archetypes' => [ + 'user' => CAP_ALLOW + ], + ], + 'block/category_courses:addinstance' => [ + 'riskbitmask' => RISK_SPAM | RISK_XSS, + 'captype' => 'write', + 'contextlevel' => CONTEXT_BLOCK, + 'archetypes' => [ + 'editingteacher' => CAP_ALLOW, + 'manager' => CAP_ALLOW + ], + ], +]; \ No newline at end of file diff --git a/category_courses/db/caches.php b/category_courses/db/caches.php new file mode 100644 index 0000000..e54dba3 --- /dev/null +++ b/category_courses/db/caches.php @@ -0,0 +1,13 @@ + [ + 'mode' => cache_store::MODE_APPLICATION, + 'simplekeys' => true, + 'simpledata' => false, + 'ttl' => 300, + 'staticacceleration' => true, + 'staticaccelerationsize' => 100, + ], +]; \ No newline at end of file diff --git a/category_courses/db/events.php b/category_courses/db/events.php new file mode 100644 index 0000000..2eb9837 --- /dev/null +++ b/category_courses/db/events.php @@ -0,0 +1,9 @@ + '\core\event\course_category_viewed', + 'callback' => 'block_category_courses_observer::load_colorpicker_js', + ], +]; \ No newline at end of file diff --git a/category_courses/db/hooks.php b/category_courses/db/hooks.php new file mode 100644 index 0000000..80d2537 --- /dev/null +++ b/category_courses/db/hooks.php @@ -0,0 +1,32 @@ +. + +/** + * Hook callbacks for block_category_courses. + * + * @package block_category_courses + * @copyright 2025 Your Name + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$callbacks = [ + [ + 'hook' => core\hook\output\before_footer_html_generation::class, + 'callback' => block_category_courses\hook_callbacks\output_callbacks::class . '::before_footer_html_generation', + ], +]; \ No newline at end of file diff --git a/category_courses/db/install.php b/category_courses/db/install.php new file mode 100644 index 0000000..9048827 --- /dev/null +++ b/category_courses/db/install.php @@ -0,0 +1,7 @@ + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + +
+
+
\ No newline at end of file diff --git a/category_courses/db/services.php b/category_courses/db/services.php new file mode 100644 index 0000000..4e0622a --- /dev/null +++ b/category_courses/db/services.php @@ -0,0 +1,60 @@ +. + +/** + * External services for block_category_courses. + * + * @package block_category_courses + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$functions = [ + 'block_category_courses_get_category_data' => [ + 'classname' => 'block_category_courses\external\get_category_data', + 'methodname' => 'execute', + 'description' => 'Get category data for hierarchical navigation', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => true, + ], + 'block_category_courses_save_image' => [ + 'classname' => 'block_category_courses\external\save_image', + 'methodname' => 'execute', + 'description' => 'Save category image and color', + 'type' => 'write', + 'ajax' => true, + 'loginrequired' => true, + ], + 'block_category_courses_set_rating' => [ + 'classname' => 'block_category_courses\external\rating_service', + 'methodname' => 'set_rating', + 'description' => 'Set course rating and comment', + 'type' => 'write', + 'ajax' => true, + 'loginrequired' => true, + ], + 'block_category_courses_get_comments' => [ + 'classname' => 'block_category_courses\external\rating_service', + 'methodname' => 'get_comments', + 'description' => 'Get course comments', + 'type' => 'read', + 'ajax' => true, + 'loginrequired' => false, + ], +]; \ No newline at end of file diff --git a/category_courses/db/upgrade.php b/category_courses/db/upgrade.php new file mode 100644 index 0000000..4143182 --- /dev/null +++ b/category_courses/db/upgrade.php @@ -0,0 +1,90 @@ +. + +/** + * Upgrade script for block_category_courses. + * + * @package block_category_courses + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +/** + * Upgrade function for block_category_courses. + * + * @param int $oldversion The old version of the plugin + * @return bool + */ +function xmldb_block_category_courses_upgrade($oldversion) { + global $DB; + + $dbman = $DB->get_manager(); + + // Add rating system tables if they don't exist + if ($oldversion < 2025073205) { + + // Define table block_catcourse_ratings to be created. + $table = new xmldb_table('block_catcourse_ratings'); + + // Adding fields to table block_catcourse_ratings. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('userid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('rating', XMLDB_TYPE_INTEGER, '1', null, XMLDB_NOTNULL, null, null); + $table->add_field('comment', XMLDB_TYPE_TEXT, null, null, null, null, null); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table block_catcourse_ratings. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Adding indexes to table block_catcourse_ratings. + $table->add_index('idx_courseid_userid', XMLDB_INDEX_UNIQUE, ['courseid', 'userid']); + + // Conditionally launch create table for block_catcourse_ratings. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Define table block_catcourse_stats to be created. + $table = new xmldb_table('block_catcourse_stats'); + + // Adding fields to table block_catcourse_stats. + $table->add_field('id', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, XMLDB_SEQUENCE, null); + $table->add_field('courseid', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + $table->add_field('average_rating', XMLDB_TYPE_NUMBER, '3,2', null, XMLDB_NOTNULL, null, '0.00'); + $table->add_field('total_ratings', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('total_comments', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, '0'); + $table->add_field('timemodified', XMLDB_TYPE_INTEGER, '10', null, XMLDB_NOTNULL, null, null); + + // Adding keys to table block_catcourse_stats. + $table->add_key('primary', XMLDB_KEY_PRIMARY, ['id']); + + // Adding indexes to table block_catcourse_stats. + $table->add_index('idx_courseid', XMLDB_INDEX_UNIQUE, ['courseid']); + + // Conditionally launch create table for block_catcourse_stats. + if (!$dbman->table_exists($table)) { + $dbman->create_table($table); + } + + // Block_category_courses savepoint reached. + upgrade_block_savepoint(true, 2025073205, 'category_courses'); + } + + return true; +} \ No newline at end of file diff --git a/category_courses/edit_form.php b/category_courses/edit_form.php new file mode 100644 index 0000000..ca60831 --- /dev/null +++ b/category_courses/edit_form.php @@ -0,0 +1,43 @@ +addElement('header', 'configheader', get_string('blocksettings', 'block')); + + // Custom title + $mform->addElement('text', 'config_title', get_string('customtitle', 'block_category_courses')); + $mform->setType('config_title', PARAM_TEXT); + + // Category display options + $mform->addElement('advcheckbox', 'config_showprogress', get_string('showprogress', 'block_category_courses')); + $mform->addElement('advcheckbox', 'config_showdescription', get_string('showdescription', 'block_category_courses')); + $mform->addElement('advcheckbox', 'config_showcoursecount', get_string('showcoursecount', 'block_category_courses')); + + // Course display options + $mform->addElement('advcheckbox', 'config_showcourseprogress', get_string('showcourseprogress', 'block_category_courses')); + $mform->addElement('advcheckbox', 'config_showcoursedescription', get_string('showcoursedescription', 'block_category_courses')); + $mform->addElement('advcheckbox', 'config_showcourseshortname', get_string('showcourseshortname', 'block_category_courses')); + + // Sort order override + $mform->addElement('select', 'config_sortorder', get_string('sortorder', 'block_category_courses'), [ + '' => get_string('default'), + 'core' => get_string('sortorder_core', 'block_category_courses'), + 'alphabetical' => get_string('sortorder_alphabetical', 'block_category_courses'), + 'coursecount' => get_string('sortorder_coursecount', 'block_category_courses'), + 'progress' => get_string('sortorder_progress', 'block_category_courses') + ]); + + + + // Set defaults + $mform->setDefault('config_showprogress', 1); + $mform->setDefault('config_showdescription', 1); + $mform->setDefault('config_showcoursecount', 1); + $mform->setDefault('config_showcourseprogress', 1); + $mform->setDefault('config_showcoursedescription', 1); + $mform->setDefault('config_showcourseshortname', 1); + } +} \ No newline at end of file diff --git a/category_courses/edit_image.php b/category_courses/edit_image.php new file mode 100644 index 0000000..07a3cad --- /dev/null +++ b/category_courses/edit_image.php @@ -0,0 +1,160 @@ +libdir.'/adminlib.php'); +require_once($CFG->libdir.'/formslib.php'); + +require_login(); +require_capability('block/category_courses:manage', context_system::instance()); + +$categoryid = required_param('categoryid', PARAM_INT); + +$PAGE->set_url('/blocks/category_courses/edit_image.php', ['categoryid' => $categoryid]); +$PAGE->set_context(context_system::instance()); + +$category = core_course_category::get($categoryid); +$PAGE->set_title(get_string('editcategoryimage', 'block_category_courses') . ': ' . $category->name); +$PAGE->set_heading(get_string('editcategoryimage', 'block_category_courses')); + +admin_externalpage_setup('block_category_courses_images'); + +// Load colorpicker JavaScript +$PAGE->requires->js_call_amd('block_category_courses/colorpicker', 'init'); + +class category_image_form extends moodleform { + protected function definition() { + $mform = $this->_form; + $categoryid = $this->_customdata['categoryid']; + + $mform->addElement('hidden', 'categoryid', $categoryid); + $mform->setType('categoryid', PARAM_INT); + + // Image upload + $mform->addElement('filemanager', 'categoryimage', get_string('categoryimage', 'block_category_courses'), + null, [ + 'subdirs' => 0, + 'maxbytes' => 2097152, // 2MB + 'maxfiles' => 1, + 'accepted_types' => ['web_image'] + ]); + $mform->addHelpButton('categoryimage', 'categoryimage', 'block_category_courses'); + + // Background color with colorpicker + $mform->addElement('text', 'categorycolor', get_string('categorycolor', 'block_category_courses')); + $mform->setType('categorycolor', PARAM_TEXT); + $mform->setDefault('categorycolor', '#667eea'); + $mform->addHelpButton('categorycolor', 'categorycolor', 'block_category_courses'); + + $this->add_action_buttons(); + } +} + +$customdata = $DB->get_record('block_catcourse_images', ['categoryid' => $categoryid]); + +$form = new category_image_form(null, ['categoryid' => $categoryid]); + +if ($form->is_cancelled()) { + redirect(new moodle_url('/blocks/category_courses/manage_images.php')); +} else if ($data = $form->get_data()) { + $context = context_system::instance(); + + // Handle file upload + $imageurl = ''; + if ($draftitemid = $data->categoryimage) { + file_save_draft_area_files($draftitemid, $context->id, 'block_category_courses', + 'categoryimage', $categoryid, ['subdirs' => false, 'maxfiles' => 1]); + + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'block_category_courses', 'categoryimage', $categoryid, 'filename', false); + if (!empty($files)) { + $file = reset($files); + $imageurl = moodle_url::make_pluginfile_url( + $context->id, 'block_category_courses', 'categoryimage', $categoryid, '/', $file->get_filename() + )->out(); + } + } + + // Save to database + if ($customdata) { + $customdata->bgcolor = $data->categorycolor; + if ($imageurl) { + $customdata->imageurl = $imageurl; + } + $customdata->timemodified = time(); + $DB->update_record('block_catcourse_images', $customdata); + } else { + $record = new stdClass(); + $record->categoryid = $categoryid; + $record->bgcolor = $data->categorycolor; + $record->imageurl = $imageurl; + $record->timecreated = time(); + $record->timemodified = time(); + $DB->insert_record('block_catcourse_images', $record); + } + + redirect(new moodle_url('/blocks/category_courses/manage_images.php'), + get_string('categoryupdated', 'block_category_courses')); +} + +// Set form data +$formdata = new stdClass(); +$formdata->categoryid = $categoryid; + +// Always prepare file area (required for filemanager) +$context = context_system::instance(); +$draftitemid = file_get_submitted_draft_itemid('categoryimage'); +file_prepare_draft_area($draftitemid, $context->id, 'block_category_courses', 'categoryimage', + $categoryid, ['subdirs' => false, 'maxfiles' => 1]); +$formdata->categoryimage = $draftitemid; + +if ($customdata) { + $formdata->categorycolor = $customdata->bgcolor; +} + +$form->set_data($formdata); + +echo $OUTPUT->header(); + +echo html_writer::tag('h2', get_string('editcategoryimage', 'block_category_courses') . ': ' . format_string($category->name)); + +// Show current preview +if ($customdata) { + echo html_writer::start_div('current-preview mb-3'); + echo html_writer::tag('h4', get_string('currentpreview', 'block_category_courses')); + + $bgcolor = !empty($customdata->bgcolor) ? $customdata->bgcolor : '#667eea'; + + echo html_writer::start_div('category-card', ['style' => "background: {$bgcolor}; color: white; width: 280px; height: 168px; border-radius: 12px; overflow: hidden; position: relative; box-shadow: 0 2px 8px rgba(0,0,0,0.1);"]); + + // Check for actual uploaded file + $context = context_system::instance(); + $fs = get_file_storage(); + $files = $fs->get_area_files($context->id, 'block_category_courses', 'categoryimage', $categoryid, 'filename', false); + + if (!empty($files)) { + $file = reset($files); + $imageurl = moodle_url::make_pluginfile_url( + $context->id, + 'block_category_courses', + 'categoryimage', + $categoryid, + '/', + $file->get_filename() + )->out(); + echo html_writer::img($imageurl, $category->name, ['style' => 'width: 100%; height: 100%; object-fit: cover;']); + } else { + // Show initials + $initials = ''; + $words = explode(' ', $category->name); + foreach (array_slice($words, 0, 2) as $word) { + $initials .= strtoupper(substr($word, 0, 1)); + } + echo html_writer::div($initials, 'image-initials', ['style' => 'width: 100%; height: 100%; display: flex; align-items: center; justify-content: center; font-size: 2rem; font-weight: 700; text-shadow: 0 1px 3px rgba(0,0,0,0.3);']); + } + + echo html_writer::end_div(); + echo html_writer::end_div(); +} + +$form->display(); + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/category_courses/lang/en/block_category_courses.php b/category_courses/lang/en/block_category_courses.php new file mode 100644 index 0000000..9ae4969 --- /dev/null +++ b/category_courses/lang/en/block_category_courses.php @@ -0,0 +1,182 @@ +. + +/** + * Language strings for block_category_courses. + * + * @package block_category_courses + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Category Courses'; +$string['notauth'] = 'You must be logged in to view this content.'; +$string['errorsetrating'] = 'Error setting rating'; +$string['rating'] = 'Rating'; +$string['comment'] = 'Comment'; +$string['addcomment'] = 'Add comment'; +$string['submitrating'] = 'Submit rating'; +$string['averagerating'] = 'Average: {$a}'; +$string['totalratings'] = '{$a} ratings'; +$string['nocomments'] = 'No comments yet'; +$string['ratingadded'] = 'Rating added successfully'; +$string['ratingupdated'] = 'Rating updated successfully'; +$string['updatingrating'] = 'Updating your existing rating'; +$string['ratingerror'] = 'Error saving rating'; +$string['invalidrating'] = 'Invalid rating value'; +$string['ratingrequired'] = 'Rating is required'; +$string['commentoptional'] = 'Comment (optional)'; +$string['ratingsubmitted'] = 'Your rating has been submitted'; +$string['ratingdeleted'] = 'Rating deleted successfully'; +$string['comments'] = 'Comments'; +$string['viewcomments'] = 'View comments'; +$string['rateandcomment'] = 'Rate and comment'; +$string['yourrating'] = 'Your rating'; +$string['noratings'] = 'No ratings yet'; + +// Settings strings +$string['categorysettings'] = 'Category Settings'; +$string['categorysettings_desc'] = 'Configure how categories are displayed'; +$string['showprogress'] = 'Show Progress'; +$string['showprogress_desc'] = 'Display progress bars for categories'; +$string['showdescription'] = 'Show Description'; +$string['showdescription_desc'] = 'Display category descriptions'; +$string['showcoursecount'] = 'Show Course Count'; +$string['showcoursecount_desc'] = 'Display number of courses in each category'; +$string['showallcategories'] = 'Show All Categories'; +$string['showallcategories_desc'] = 'Show all visible categories to students, not just those where they are enrolled'; +$string['coursesettings'] = 'Course Settings'; +$string['coursesettings_desc'] = 'Configure how courses are displayed'; +$string['showcourseprogress'] = 'Show Course Progress'; +$string['showcourseprogress_desc'] = 'Display progress bars for courses'; +$string['showcoursedescription'] = 'Show Course Description'; +$string['showcoursedescription_desc'] = 'Display course descriptions'; +$string['showcourseshortname'] = 'Show Course Short Name'; +$string['showcourseshortname_desc'] = 'Display course short names'; +$string['sortorder'] = 'Sort Order'; +$string['sortorder_desc'] = 'How to sort categories'; +$string['sortorder_core'] = 'Default (Moodle order)'; +$string['sortorder_alphabetical'] = 'Alphabetical'; +$string['sortorder_coursecount'] = 'By course count'; +$string['sortorder_progress'] = 'By progress'; +$string['colorsettings'] = 'Color Settings'; +$string['colorsettings_desc'] = 'Customize colors for the block'; +$string['buttoncolor'] = 'Button Color'; +$string['buttoncolor_help'] = 'Color for navigation buttons'; +$string['progresscolor1'] = 'Progress Color 1'; +$string['progresscolor1_help'] = 'Start color for progress bars'; +$string['progresscolor2'] = 'Progress Color 2'; +$string['progresscolor2_help'] = 'End color for progress bars'; +$string['manageimages'] = 'Manage Category Images'; + +// Template strings +$string['navigation'] = 'Navigation'; +$string['viewcategory'] = 'View category'; +$string['explore'] = 'Explore'; +$string['coursescounter'] = 'courses'; +$string['nocontent'] = 'No content available'; +$string['enter'] = 'Enter'; +$string['configured'] = 'Configured'; +$string['categories'] = 'Categories'; +$string['customtitle'] = 'Custom title'; + +// Image management strings +$string['editcategoryimage'] = 'Edit Category Image'; +$string['categoryimage'] = 'Category Image'; +$string['categoryimage_help'] = 'Upload an image for this category or provide an image URL'; +$string['categorycolor'] = 'Background Color'; +$string['categorycolor_help'] = 'Choose a background color for this category'; +$string['currentpreview'] = 'Current Preview'; +$string['categoryupdated'] = 'Category updated successfully'; +$string['imageurl'] = 'Image URL'; +$string['imageurl_help'] = 'Provide a direct URL to an image'; +$string['defaultstatus'] = 'Default Status'; + +// Privacy strings +$string['privacy:metadata:block_catcourse_ratings'] = 'Information about user ratings for courses'; +$string['privacy:metadata:block_catcourse_ratings:userid'] = 'The ID of the user who made the rating'; +$string['privacy:metadata:block_catcourse_ratings:courseid'] = 'The ID of the course being rated'; +$string['privacy:metadata:block_catcourse_ratings:rating'] = 'The rating value (1-5 stars)'; +$string['privacy:metadata:block_catcourse_ratings:comment'] = 'Optional comment with the rating'; +$string['privacy:metadata:block_catcourse_ratings:timemodified'] = 'When the rating was last modified'; + +// Additional template strings +$string['completed'] = 'Completed'; +$string['inprogress'] = 'In Progress'; +$string['notstarted'] = 'Not Started'; +$string['enrolled'] = 'Enrolled'; +$string['notenrolled'] = 'Not Enrolled'; +$string['totalcourses'] = 'total courses'; +$string['completedcourses'] = 'Completed Courses'; +$string['progresspercentage'] = 'Progress'; +$string['viewcourse'] = 'View Course'; +$string['entercategory'] = 'Enter Category'; +$string['backtocategories'] = 'Back to Categories'; +$string['loading'] = 'Loading...'; +$string['nocoursesavailable'] = 'No courses available in this category'; +$string['nocategoriesavailable'] = 'No categories available'; +$string['errorloadingcontent'] = 'Error loading content'; +$string['tryagain'] = 'Try Again'; +$string['home'] = 'Home'; +$string['breadcrumbnavigation'] = 'Breadcrumb Navigation'; + +// Course card strings +$string['coursesummary'] = 'Course Summary'; +$string['courseshortname'] = 'Course Short Name'; +$string['coursefullname'] = 'Course Full Name'; +$string['courseimage'] = 'Course Image'; +$string['courserating'] = 'Course Rating'; +$string['ratethiscourse'] = 'Rate this course'; +$string['viewallcomments'] = 'View all comments'; +$string['addyourrating'] = 'Add your rating'; +$string['updateyourrating'] = 'Update your rating'; + +// Error messages +$string['errornocategory'] = 'Category not found'; +$string['errornocourse'] = 'Course not found'; +$string['errornopermission'] = 'You do not have permission to view this content'; +$string['errornodata'] = 'No data available'; +$string['errorajax'] = 'Error loading data via AJAX'; +$string['errordatabase'] = 'Database error occurred'; + +// Success messages +$string['successsaved'] = 'Settings saved successfully'; +$string['successupdated'] = 'Updated successfully'; +$string['successdeleted'] = 'Deleted successfully'; + +// Configuration strings +$string['configtitle'] = 'Block Title'; +$string['configtitle_help'] = 'Custom title for this block instance'; +$string['configshowprogress'] = 'Show Progress Bars'; +$string['configshowdescription'] = 'Show Descriptions'; +$string['configshowcoursecount'] = 'Show Course Count'; +$string['configsortorder'] = 'Sort Order'; + +// Capability strings +$string['category_courses:addinstance'] = 'Add a new Category Courses block'; +$string['category_courses:myaddinstance'] = 'Add a new Category Courses block to Dashboard'; +$string['category_courses:manage'] = 'Manage Category Courses settings'; +$string['category_courses:rate'] = 'Rate courses'; +$string['category_courses:viewratings'] = 'View course ratings'; + +// Block configuration +$string['blocktitle'] = 'Category Courses'; +$string['blockstring'] = 'Category Courses Block'; +$string['descconfig'] = 'Description of the config section'; +$string['descfoo'] = 'Config description'; +$string['headerconfig'] = 'Config section header'; +$string['labelfoo'] = 'Config label'; +$string['categoryhidden'] = 'Hidden category'; \ No newline at end of file diff --git a/category_courses/lang/es_mx/block_category_courses.php b/category_courses/lang/es_mx/block_category_courses.php new file mode 100644 index 0000000..2b465b3 --- /dev/null +++ b/category_courses/lang/es_mx/block_category_courses.php @@ -0,0 +1,182 @@ +. + +/** + * Cadenas de idioma para block_category_courses (Español México). + * + * @package block_category_courses + * @copyright 2025 Tu Nombre + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Cursos por Categoría'; +$string['notauth'] = 'Debes iniciar sesión para ver este contenido.'; +$string['errorsetrating'] = 'Error al establecer calificación'; +$string['rating'] = 'Calificación'; +$string['comment'] = 'Comentario'; +$string['addcomment'] = 'Agregar comentario'; +$string['submitrating'] = 'Enviar calificación'; +$string['averagerating'] = 'Promedio: {$a}'; +$string['totalratings'] = '{$a} calificaciones'; +$string['nocomments'] = 'Aún no hay comentarios'; +$string['ratingadded'] = 'Calificación agregada exitosamente'; +$string['ratingupdated'] = 'Calificación actualizada exitosamente'; +$string['updatingrating'] = 'Actualizando tu calificación existente'; +$string['ratingerror'] = 'Error al guardar calificación'; +$string['invalidrating'] = 'Valor de calificación inválido'; +$string['ratingrequired'] = 'La calificación es requerida'; +$string['commentoptional'] = 'Comentario (opcional)'; +$string['ratingsubmitted'] = 'Tu calificación ha sido enviada'; +$string['ratingdeleted'] = 'Calificación eliminada exitosamente'; +$string['comments'] = 'Comentarios'; +$string['viewcomments'] = 'Ver comentarios'; +$string['rateandcomment'] = 'Calificar y comentar'; +$string['yourrating'] = 'Tu calificación'; +$string['noratings'] = 'Aún no hay calificaciones'; + +// Cadenas de configuración +$string['categorysettings'] = 'Configuración de Categorías'; +$string['categorysettings_desc'] = 'Configura cómo se muestran las categorías'; +$string['showprogress'] = 'Mostrar Progreso'; +$string['showprogress_desc'] = 'Mostrar barras de progreso para las categorías'; +$string['showdescription'] = 'Mostrar Descripción'; +$string['showdescription_desc'] = 'Mostrar descripciones de las categorías'; +$string['showcoursecount'] = 'Mostrar Contador de Cursos'; +$string['showcoursecount_desc'] = 'Mostrar número de cursos en cada categoría'; +$string['showallcategories'] = 'Mostrar Todas las Categorías'; +$string['showallcategories_desc'] = 'Mostrar todas las categorías visibles a los estudiantes, no solo aquellas donde están inscritos'; +$string['coursesettings'] = 'Configuración de Cursos'; +$string['coursesettings_desc'] = 'Configura cómo se muestran los cursos'; +$string['showcourseprogress'] = 'Mostrar Progreso del Curso'; +$string['showcourseprogress_desc'] = 'Mostrar barras de progreso para los cursos'; +$string['showcoursedescription'] = 'Mostrar Descripción del Curso'; +$string['showcoursedescription_desc'] = 'Mostrar descripciones de los cursos'; +$string['showcourseshortname'] = 'Mostrar Nombre Corto del Curso'; +$string['showcourseshortname_desc'] = 'Mostrar nombres cortos de los cursos'; +$string['sortorder'] = 'Orden de Clasificación'; +$string['sortorder_desc'] = 'Cómo ordenar las categorías'; +$string['sortorder_core'] = 'Predeterminado (orden de Moodle)'; +$string['sortorder_alphabetical'] = 'Alfabético'; +$string['sortorder_coursecount'] = 'Por cantidad de cursos'; +$string['sortorder_progress'] = 'Por progreso'; +$string['colorsettings'] = 'Configuración de Colores'; +$string['colorsettings_desc'] = 'Personalizar colores para el bloque'; +$string['buttoncolor'] = 'Color de Botones'; +$string['buttoncolor_help'] = 'Color para los botones de navegación'; +$string['progresscolor1'] = 'Color de Progreso 1'; +$string['progresscolor1_help'] = 'Color inicial para las barras de progreso'; +$string['progresscolor2'] = 'Color de Progreso 2'; +$string['progresscolor2_help'] = 'Color final para las barras de progreso'; +$string['manageimages'] = 'Administrar Imágenes de Categorías'; + +// Cadenas de plantillas +$string['navigation'] = 'Navegación'; +$string['viewcategory'] = 'Ver categoría'; +$string['explore'] = 'Explorar'; +$string['coursescounter'] = 'cursos'; +$string['nocontent'] = 'No hay contenido disponible'; +$string['enter'] = 'Ingresar'; +$string['configured'] = 'Configurado'; +$string['categories'] = 'Categorías'; +$string['customtitle'] = 'Título personalizado'; + +// Cadenas de gestión de imágenes +$string['editcategoryimage'] = 'Editar Imagen de Categoría'; +$string['categoryimage'] = 'Imagen de Categoría'; +$string['categoryimage_help'] = 'Sube una imagen para esta categoría o proporciona una URL de imagen'; +$string['categorycolor'] = 'Color de Fondo'; +$string['categorycolor_help'] = 'Elige un color de fondo para esta categoría'; +$string['currentpreview'] = 'Vista Previa Actual'; +$string['categoryupdated'] = 'Categoría actualizada exitosamente'; +$string['imageurl'] = 'URL de Imagen'; +$string['imageurl_help'] = 'Proporciona una URL directa a una imagen'; +$string['defaultstatus'] = 'Estado Predeterminado'; + +// Cadenas de privacidad +$string['privacy:metadata:block_catcourse_ratings'] = 'Información sobre las calificaciones de usuarios para cursos'; +$string['privacy:metadata:block_catcourse_ratings:userid'] = 'El ID del usuario que hizo la calificación'; +$string['privacy:metadata:block_catcourse_ratings:courseid'] = 'El ID del curso siendo calificado'; +$string['privacy:metadata:block_catcourse_ratings:rating'] = 'El valor de la calificación (1-5 estrellas)'; +$string['privacy:metadata:block_catcourse_ratings:comment'] = 'Comentario opcional con la calificación'; +$string['privacy:metadata:block_catcourse_ratings:timemodified'] = 'Cuándo fue modificada la calificación por última vez'; + +// Cadenas adicionales de plantillas +$string['completed'] = 'Completado'; +$string['inprogress'] = 'En Progreso'; +$string['notstarted'] = 'No Iniciado'; +$string['enrolled'] = 'Inscrito'; +$string['notenrolled'] = 'No Inscrito'; +$string['totalcourses'] = 'cursos totales'; +$string['completedcourses'] = 'Cursos Completados'; +$string['progresspercentage'] = 'Progreso'; +$string['viewcourse'] = 'Ver Curso'; +$string['entercategory'] = 'Ingresar a Categoría'; +$string['backtocategories'] = 'Volver a Categorías'; +$string['loading'] = 'Cargando...'; +$string['nocoursesavailable'] = 'No hay cursos disponibles en esta categoría'; +$string['nocategoriesavailable'] = 'No hay categorías disponibles'; +$string['errorloadingcontent'] = 'Error al cargar contenido'; +$string['tryagain'] = 'Intentar de Nuevo'; +$string['home'] = 'Inicio'; +$string['breadcrumbnavigation'] = 'Navegación de Migas de Pan'; + +// Cadenas de tarjetas de curso +$string['coursesummary'] = 'Resumen del Curso'; +$string['courseshortname'] = 'Nombre Corto del Curso'; +$string['coursefullname'] = 'Nombre Completo del Curso'; +$string['courseimage'] = 'Imagen del Curso'; +$string['courserating'] = 'Calificación del Curso'; +$string['ratethiscourse'] = 'Calificar este curso'; +$string['viewallcomments'] = 'Ver todos los comentarios'; +$string['addyourrating'] = 'Agregar tu calificación'; +$string['updateyourrating'] = 'Actualizar tu calificación'; + +// Mensajes de error +$string['errornocategory'] = 'Categoría no encontrada'; +$string['errornocourse'] = 'Curso no encontrado'; +$string['errornopermission'] = 'No tienes permiso para ver este contenido'; +$string['errornodata'] = 'No hay datos disponibles'; +$string['errorajax'] = 'Error al cargar datos vía AJAX'; +$string['errordatabase'] = 'Ocurrió un error de base de datos'; + +// Mensajes de éxito +$string['successsaved'] = 'Configuración guardada exitosamente'; +$string['successupdated'] = 'Actualizado exitosamente'; +$string['successdeleted'] = 'Eliminado exitosamente'; + +// Cadenas de configuración +$string['configtitle'] = 'Título del Bloque'; +$string['configtitle_help'] = 'Título personalizado para esta instancia del bloque'; +$string['configshowprogress'] = 'Mostrar Barras de Progreso'; +$string['configshowdescription'] = 'Mostrar Descripciones'; +$string['configshowcoursecount'] = 'Mostrar Contador de Cursos'; +$string['configsortorder'] = 'Orden de Clasificación'; + +// Cadenas de capacidades +$string['category_courses:addinstance'] = 'Agregar un nuevo bloque de Cursos por Categoría'; +$string['category_courses:myaddinstance'] = 'Agregar un nuevo bloque de Cursos por Categoría al Tablero'; +$string['category_courses:manage'] = 'Administrar configuración de Cursos por Categoría'; +$string['category_courses:rate'] = 'Calificar cursos'; +$string['category_courses:viewratings'] = 'Ver calificaciones de cursos'; + +// Configuración del bloque +$string['blocktitle'] = 'Cursos por Categoría'; +$string['blockstring'] = 'Bloque de Cursos por Categoría'; +$string['descconfig'] = 'Descripción de la sección de configuración'; +$string['descfoo'] = 'Descripción de configuración'; +$string['headerconfig'] = 'Encabezado de sección de configuración'; +$string['labelfoo'] = 'Etiqueta de configuración'; +$string['categoryhidden'] = 'Categoría oculta'; \ No newline at end of file diff --git a/category_courses/lang/fil/block_category_courses.php b/category_courses/lang/fil/block_category_courses.php new file mode 100644 index 0000000..20b3b50 --- /dev/null +++ b/category_courses/lang/fil/block_category_courses.php @@ -0,0 +1,182 @@ +. + +/** + * Mga string ng wika para sa block_category_courses (Filipino). + * + * @package block_category_courses + * @copyright 2025 Your Name + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Mga Kurso ayon sa Kategorya'; +$string['notauth'] = 'Kailangan mong mag-login upang makita ang nilalaman na ito.'; +$string['errorsetrating'] = 'Error sa pagtatakda ng rating'; +$string['rating'] = 'Rating'; +$string['comment'] = 'Komento'; +$string['addcomment'] = 'Magdagdag ng komento'; +$string['submitrating'] = 'Isumite ang rating'; +$string['averagerating'] = 'Average: {$a}'; +$string['totalratings'] = '{$a} mga rating'; +$string['nocomments'] = 'Walang mga komento pa'; +$string['ratingadded'] = 'Matagumpay na naidagdag ang rating'; +$string['ratingupdated'] = 'Matagumpay na na-update ang rating'; +$string['updatingrating'] = 'Ina-update ang inyong kasalukuyang rating'; +$string['ratingerror'] = 'Error sa pag-save ng rating'; +$string['invalidrating'] = 'Hindi valid na halaga ng rating'; +$string['ratingrequired'] = 'Kailangan ang rating'; +$string['commentoptional'] = 'Komento (opsyonal)'; +$string['ratingsubmitted'] = 'Naisumite na ang inyong rating'; +$string['ratingdeleted'] = 'Matagumpay na natanggal ang rating'; +$string['comments'] = 'Mga Komento'; +$string['viewcomments'] = 'Tingnan ang mga komento'; +$string['rateandcomment'] = 'Mag-rate at mag-komento'; +$string['yourrating'] = 'Inyong rating'; +$string['noratings'] = 'Walang mga rating pa'; + +// Mga string ng settings +$string['categorysettings'] = 'Mga Setting ng Kategorya'; +$string['categorysettings_desc'] = 'I-configure kung paano ipapakita ang mga kategorya'; +$string['showprogress'] = 'Ipakita ang Progress'; +$string['showprogress_desc'] = 'Ipakita ang mga progress bar para sa mga kategorya'; +$string['showdescription'] = 'Ipakita ang Paglalarawan'; +$string['showdescription_desc'] = 'Ipakita ang mga paglalarawan ng kategorya'; +$string['showcoursecount'] = 'Ipakita ang Bilang ng Kurso'; +$string['showcoursecount_desc'] = 'Ipakita ang bilang ng mga kurso sa bawat kategorya'; +$string['coursesettings'] = 'Mga Setting ng Kurso'; +$string['coursesettings_desc'] = 'I-configure kung paano ipapakita ang mga kurso'; +$string['showcourseprogress'] = 'Ipakita ang Progress ng Kurso'; +$string['showcourseprogress_desc'] = 'Ipakita ang mga progress bar para sa mga kurso'; +$string['showcoursedescription'] = 'Ipakita ang Paglalarawan ng Kurso'; +$string['showcoursedescription_desc'] = 'Ipakita ang mga paglalarawan ng kurso'; +$string['showcourseshortname'] = 'Ipakita ang Maikling Pangalan ng Kurso'; +$string['showcourseshortname_desc'] = 'Ipakita ang mga maikling pangalan ng kurso'; +$string['sortorder'] = 'Pagkakaayos'; +$string['sortorder_desc'] = 'Paano ayusin ang mga kategorya'; +$string['sortorder_core'] = 'Default (pagkakaayos ng Moodle)'; +$string['sortorder_alphabetical'] = 'Alphabetical'; +$string['sortorder_coursecount'] = 'Ayon sa bilang ng kurso'; +$string['sortorder_progress'] = 'Ayon sa progress'; +$string['colorsettings'] = 'Mga Setting ng Kulay'; +$string['colorsettings_desc'] = 'I-customize ang mga kulay para sa block'; +$string['buttoncolor'] = 'Kulay ng Button'; +$string['buttoncolor_help'] = 'Kulay para sa mga navigation button'; +$string['progresscolor1'] = 'Kulay ng Progress 1'; +$string['progresscolor1_help'] = 'Simula ng kulay para sa mga progress bar'; +$string['progresscolor2'] = 'Kulay ng Progress 2'; +$string['progresscolor2_help'] = 'Dulo ng kulay para sa mga progress bar'; +$string['manageimages'] = 'Pamahalaan ang mga Larawan ng Kategorya'; + +// Mga string ng template +$string['navigation'] = 'Navigation'; +$string['viewcategory'] = 'Tingnan ang kategorya'; +$string['explore'] = 'Tuklasin'; +$string['coursescounter'] = 'mga kurso'; +$string['nocontent'] = 'Walang available na nilalaman'; +$string['enter'] = 'Pumasok'; +$string['configured'] = 'Na-configure'; +$string['categories'] = 'Mga Kategorya'; +$string['customtitle'] = 'Custom na pamagat'; + +// Mga string ng pamamahala ng larawan +$string['editcategoryimage'] = 'I-edit ang Larawan ng Kategorya'; +$string['categoryimage'] = 'Larawan ng Kategorya'; +$string['categoryimage_help'] = 'Mag-upload ng larawan para sa kategoryang ito o magbigay ng URL ng larawan'; +$string['categorycolor'] = 'Kulay ng Background'; +$string['categorycolor_help'] = 'Pumili ng kulay ng background para sa kategoryang ito'; +$string['currentpreview'] = 'Kasalukuyang Preview'; +$string['categoryupdated'] = 'Matagumpay na na-update ang kategorya'; +$string['imageurl'] = 'URL ng Larawan'; +$string['imageurl_help'] = 'Magbigay ng direktang URL sa larawan'; +$string['defaultstatus'] = 'Default na Status'; + +// Mga string ng privacy +$string['privacy:metadata:block_catcourse_ratings'] = 'Impormasyon tungkol sa mga rating ng user para sa mga kurso'; +$string['privacy:metadata:block_catcourse_ratings:userid'] = 'Ang ID ng user na gumawa ng rating'; +$string['privacy:metadata:block_catcourse_ratings:courseid'] = 'Ang ID ng kursong na-rate'; +$string['privacy:metadata:block_catcourse_ratings:rating'] = 'Ang halaga ng rating (1-5 bituin)'; +$string['privacy:metadata:block_catcourse_ratings:comment'] = 'Opsyonal na komento kasama ng rating'; +$string['privacy:metadata:block_catcourse_ratings:timemodified'] = 'Kailan huling na-modify ang rating'; + +// Mga karagdagang string ng template +$string['completed'] = 'Tapos na'; +$string['inprogress'] = 'Ginagawa pa'; +$string['notstarted'] = 'Hindi pa nagsimula'; +$string['enrolled'] = 'Naka-enroll'; +$string['notenrolled'] = 'Hindi naka-enroll'; +$string['totalcourses'] = 'kabuuang mga kurso'; +$string['completedcourses'] = 'Mga Tapos na Kurso'; +$string['progresspercentage'] = 'Progress'; +$string['viewcourse'] = 'Tingnan ang Kurso'; +$string['entercategory'] = 'Pumasok sa Kategorya'; +$string['backtocategories'] = 'Bumalik sa mga Kategorya'; +$string['loading'] = 'Naglo-load...'; +$string['nocoursesavailable'] = 'Walang available na kurso sa kategoryang ito'; +$string['nocategoriesavailable'] = 'Walang available na mga kategorya'; +$string['errorloadingcontent'] = 'Error sa pag-load ng nilalaman'; +$string['tryagain'] = 'Subukan Ulit'; +$string['home'] = 'Home'; +$string['breadcrumbnavigation'] = 'Breadcrumb Navigation'; + +// Mga string ng course card +$string['coursesummary'] = 'Buod ng Kurso'; +$string['courseshortname'] = 'Maikling Pangalan ng Kurso'; +$string['coursefullname'] = 'Buong Pangalan ng Kurso'; +$string['courseimage'] = 'Larawan ng Kurso'; +$string['courserating'] = 'Rating ng Kurso'; +$string['ratethiscourse'] = 'I-rate ang kursong ito'; +$string['viewallcomments'] = 'Tingnan ang lahat ng komento'; +$string['addyourrating'] = 'Idagdag ang inyong rating'; +$string['updateyourrating'] = 'I-update ang inyong rating'; + +// Mga mensahe ng error +$string['errornocategory'] = 'Hindi nahanap ang kategorya'; +$string['errornocourse'] = 'Hindi nahanap ang kurso'; +$string['errornopermission'] = 'Wala kayong pahintulot na tingnan ang nilalaman na ito'; +$string['errornodata'] = 'Walang available na data'; +$string['errorajax'] = 'Error sa pag-load ng data sa pamamagitan ng AJAX'; +$string['errordatabase'] = 'Naganap ang database error'; + +// Mga mensahe ng tagumpay +$string['successsaved'] = 'Matagumpay na na-save ang mga setting'; +$string['successupdated'] = 'Matagumpay na na-update'; +$string['successdeleted'] = 'Matagumpay na natanggal'; + +// Mga string ng configuration +$string['configtitle'] = 'Pamagat ng Block'; +$string['configtitle_help'] = 'Custom na pamagat para sa instance ng block na ito'; +$string['configshowprogress'] = 'Ipakita ang mga Progress Bar'; +$string['configshowdescription'] = 'Ipakita ang mga Paglalarawan'; +$string['configshowcoursecount'] = 'Ipakita ang Bilang ng Kurso'; +$string['configsortorder'] = 'Pagkakaayos'; + +// Mga string ng capability +$string['category_courses:addinstance'] = 'Magdagdag ng bagong Category Courses block'; +$string['category_courses:myaddinstance'] = 'Magdagdag ng Category Courses block sa Dashboard'; +$string['category_courses:manage'] = 'Pamahalaan ang mga setting ng Category Courses'; +$string['category_courses:rate'] = 'Mag-rate ng mga kurso'; +$string['category_courses:viewratings'] = 'Tingnan ang mga rating ng kurso'; + +// Configuration ng block +$string['blocktitle'] = 'Mga Kurso ayon sa Kategorya'; +$string['blockstring'] = 'Category Courses Block'; +$string['descconfig'] = 'Paglalarawan ng configuration section'; +$string['descfoo'] = 'Paglalarawan ng configuration'; +$string['headerconfig'] = 'Header ng configuration section'; +$string['labelfoo'] = 'Label ng configuration'; +$string['categoryhidden'] = 'Nakatagong kategorya'; +$string['showallcategories'] = 'Ipakita ang Lahat ng Kategorya'; +$string['showallcategories_desc'] = 'Payagan ang mga estudyante na makita ang lahat ng nakikitang kategorya, anuman ang status ng enrollment'; \ No newline at end of file diff --git a/category_courses/lang/id/block_category_courses.php b/category_courses/lang/id/block_category_courses.php new file mode 100644 index 0000000..6e2bc22 --- /dev/null +++ b/category_courses/lang/id/block_category_courses.php @@ -0,0 +1,182 @@ +. + +/** + * String bahasa untuk block_category_courses (Bahasa Indonesia). + * + * @package block_category_courses + * @copyright 2025 Your Name + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Kursus berdasarkan Kategori'; +$string['notauth'] = 'Anda harus masuk untuk melihat konten ini.'; +$string['errorsetrating'] = 'Kesalahan saat mengatur penilaian'; +$string['rating'] = 'Penilaian'; +$string['comment'] = 'Komentar'; +$string['addcomment'] = 'Tambah komentar'; +$string['submitrating'] = 'Kirim penilaian'; +$string['averagerating'] = 'Rata-rata: {$a}'; +$string['totalratings'] = '{$a} penilaian'; +$string['nocomments'] = 'Belum ada komentar'; +$string['ratingadded'] = 'Penilaian berhasil ditambahkan'; +$string['ratingupdated'] = 'Penilaian berhasil diperbarui'; +$string['updatingrating'] = 'Memperbarui penilaian Anda yang ada'; +$string['ratingerror'] = 'Kesalahan saat menyimpan penilaian'; +$string['invalidrating'] = 'Nilai penilaian tidak valid'; +$string['ratingrequired'] = 'Penilaian diperlukan'; +$string['commentoptional'] = 'Komentar (opsional)'; +$string['ratingsubmitted'] = 'Penilaian Anda telah dikirim'; +$string['ratingdeleted'] = 'Penilaian berhasil dihapus'; +$string['comments'] = 'Komentar'; +$string['viewcomments'] = 'Lihat komentar'; +$string['rateandcomment'] = 'Beri nilai dan komentar'; +$string['yourrating'] = 'Penilaian Anda'; +$string['noratings'] = 'Belum ada penilaian'; + +// String pengaturan +$string['categorysettings'] = 'Pengaturan Kategori'; +$string['categorysettings_desc'] = 'Konfigurasi cara menampilkan kategori'; +$string['showprogress'] = 'Tampilkan Kemajuan'; +$string['showprogress_desc'] = 'Tampilkan bilah kemajuan untuk kategori'; +$string['showdescription'] = 'Tampilkan Deskripsi'; +$string['showdescription_desc'] = 'Tampilkan deskripsi kategori'; +$string['showcoursecount'] = 'Tampilkan Jumlah Kursus'; +$string['showcoursecount_desc'] = 'Tampilkan jumlah kursus di setiap kategori'; +$string['coursesettings'] = 'Pengaturan Kursus'; +$string['coursesettings_desc'] = 'Konfigurasi cara menampilkan kursus'; +$string['showcourseprogress'] = 'Tampilkan Kemajuan Kursus'; +$string['showcourseprogress_desc'] = 'Tampilkan bilah kemajuan untuk kursus'; +$string['showcoursedescription'] = 'Tampilkan Deskripsi Kursus'; +$string['showcoursedescription_desc'] = 'Tampilkan deskripsi kursus'; +$string['showcourseshortname'] = 'Tampilkan Nama Pendek Kursus'; +$string['showcourseshortname_desc'] = 'Tampilkan nama pendek kursus'; +$string['sortorder'] = 'Urutan Sortir'; +$string['sortorder_desc'] = 'Cara mengurutkan kategori'; +$string['sortorder_core'] = 'Default (urutan Moodle)'; +$string['sortorder_alphabetical'] = 'Alfabetis'; +$string['sortorder_coursecount'] = 'Berdasarkan jumlah kursus'; +$string['sortorder_progress'] = 'Berdasarkan kemajuan'; +$string['colorsettings'] = 'Pengaturan Warna'; +$string['colorsettings_desc'] = 'Sesuaikan warna untuk blok'; +$string['buttoncolor'] = 'Warna Tombol'; +$string['buttoncolor_help'] = 'Warna untuk tombol navigasi'; +$string['progresscolor1'] = 'Warna Kemajuan 1'; +$string['progresscolor1_help'] = 'Warna awal untuk bilah kemajuan'; +$string['progresscolor2'] = 'Warna Kemajuan 2'; +$string['progresscolor2_help'] = 'Warna akhir untuk bilah kemajuan'; +$string['manageimages'] = 'Kelola Gambar Kategori'; + +// String template +$string['navigation'] = 'Navigasi'; +$string['viewcategory'] = 'Lihat kategori'; +$string['explore'] = 'Jelajahi'; +$string['coursescounter'] = 'kursus'; +$string['nocontent'] = 'Tidak ada konten tersedia'; +$string['enter'] = 'Masuk'; +$string['configured'] = 'Dikonfigurasi'; +$string['categories'] = 'Kategori'; +$string['customtitle'] = 'Judul kustom'; + +// String manajemen gambar +$string['editcategoryimage'] = 'Edit Gambar Kategori'; +$string['categoryimage'] = 'Gambar Kategori'; +$string['categoryimage_help'] = 'Unggah gambar untuk kategori ini atau berikan URL gambar'; +$string['categorycolor'] = 'Warna Latar Belakang'; +$string['categorycolor_help'] = 'Pilih warna latar belakang untuk kategori ini'; +$string['currentpreview'] = 'Pratinjau Saat Ini'; +$string['categoryupdated'] = 'Kategori berhasil diperbarui'; +$string['imageurl'] = 'URL Gambar'; +$string['imageurl_help'] = 'Berikan URL langsung ke gambar'; +$string['defaultstatus'] = 'Status Default'; + +// String privasi +$string['privacy:metadata:block_catcourse_ratings'] = 'Informasi tentang penilaian pengguna untuk kursus'; +$string['privacy:metadata:block_catcourse_ratings:userid'] = 'ID pengguna yang memberikan penilaian'; +$string['privacy:metadata:block_catcourse_ratings:courseid'] = 'ID kursus yang dinilai'; +$string['privacy:metadata:block_catcourse_ratings:rating'] = 'Nilai penilaian (1-5 bintang)'; +$string['privacy:metadata:block_catcourse_ratings:comment'] = 'Komentar opsional dengan penilaian'; +$string['privacy:metadata:block_catcourse_ratings:timemodified'] = 'Kapan penilaian terakhir dimodifikasi'; + +// String template tambahan +$string['completed'] = 'Selesai'; +$string['inprogress'] = 'Sedang Berlangsung'; +$string['notstarted'] = 'Belum Dimulai'; +$string['enrolled'] = 'Terdaftar'; +$string['notenrolled'] = 'Tidak Terdaftar'; +$string['totalcourses'] = 'total kursus'; +$string['completedcourses'] = 'Kursus Selesai'; +$string['progresspercentage'] = 'Kemajuan'; +$string['viewcourse'] = 'Lihat Kursus'; +$string['entercategory'] = 'Masuk Kategori'; +$string['backtocategories'] = 'Kembali ke Kategori'; +$string['loading'] = 'Memuat...'; +$string['nocoursesavailable'] = 'Tidak ada kursus tersedia di kategori ini'; +$string['nocategoriesavailable'] = 'Tidak ada kategori tersedia'; +$string['errorloadingcontent'] = 'Kesalahan memuat konten'; +$string['tryagain'] = 'Coba Lagi'; +$string['home'] = 'Beranda'; +$string['breadcrumbnavigation'] = 'Navigasi Breadcrumb'; + +// String kartu kursus +$string['coursesummary'] = 'Ringkasan Kursus'; +$string['courseshortname'] = 'Nama Pendek Kursus'; +$string['coursefullname'] = 'Nama Lengkap Kursus'; +$string['courseimage'] = 'Gambar Kursus'; +$string['courserating'] = 'Penilaian Kursus'; +$string['ratethiscourse'] = 'Nilai kursus ini'; +$string['viewallcomments'] = 'Lihat semua komentar'; +$string['addyourrating'] = 'Tambahkan penilaian Anda'; +$string['updateyourrating'] = 'Perbarui penilaian Anda'; + +// Pesan kesalahan +$string['errornocategory'] = 'Kategori tidak ditemukan'; +$string['errornocourse'] = 'Kursus tidak ditemukan'; +$string['errornopermission'] = 'Anda tidak memiliki izin untuk melihat konten ini'; +$string['errornodata'] = 'Tidak ada data tersedia'; +$string['errorajax'] = 'Kesalahan memuat data melalui AJAX'; +$string['errordatabase'] = 'Terjadi kesalahan database'; + +// Pesan sukses +$string['successsaved'] = 'Pengaturan berhasil disimpan'; +$string['successupdated'] = 'Berhasil diperbarui'; +$string['successdeleted'] = 'Berhasil dihapus'; + +// String konfigurasi +$string['configtitle'] = 'Judul Blok'; +$string['configtitle_help'] = 'Judul kustom untuk instance blok ini'; +$string['configshowprogress'] = 'Tampilkan Bilah Kemajuan'; +$string['configshowdescription'] = 'Tampilkan Deskripsi'; +$string['configshowcoursecount'] = 'Tampilkan Jumlah Kursus'; +$string['configsortorder'] = 'Urutan Sortir'; + +// String kemampuan +$string['category_courses:addinstance'] = 'Tambahkan blok Kursus Kategori baru'; +$string['category_courses:myaddinstance'] = 'Tambahkan blok Kursus Kategori ke Dashboard'; +$string['category_courses:manage'] = 'Kelola pengaturan Kursus Kategori'; +$string['category_courses:rate'] = 'Nilai kursus'; +$string['category_courses:viewratings'] = 'Lihat penilaian kursus'; + +// Konfigurasi blok +$string['blocktitle'] = 'Kursus berdasarkan Kategori'; +$string['blockstring'] = 'Blok Kursus Kategori'; +$string['descconfig'] = 'Deskripsi bagian konfigurasi'; +$string['descfoo'] = 'Deskripsi konfigurasi'; +$string['headerconfig'] = 'Header bagian konfigurasi'; +$string['labelfoo'] = 'Label konfigurasi'; +$string['categoryhidden'] = 'Kategori tersembunyi'; +$string['showallcategories'] = 'Tampilkan Semua Kategori'; +$string['showallcategories_desc'] = 'Izinkan siswa melihat semua kategori yang terlihat, terlepas dari status pendaftaran'; \ No newline at end of file diff --git a/category_courses/lang/vi/block_category_courses.php b/category_courses/lang/vi/block_category_courses.php new file mode 100644 index 0000000..f0eefb3 --- /dev/null +++ b/category_courses/lang/vi/block_category_courses.php @@ -0,0 +1,182 @@ +. + +/** + * Chuỗi ngôn ngữ cho block_category_courses (Tiếng Việt). + * + * @package block_category_courses + * @copyright 2025 Your Name + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +$string['pluginname'] = 'Khóa học theo Danh mục'; +$string['notauth'] = 'Bạn phải đăng nhập để xem nội dung này.'; +$string['errorsetrating'] = 'Lỗi khi đặt đánh giá'; +$string['rating'] = 'Đánh giá'; +$string['comment'] = 'Bình luận'; +$string['addcomment'] = 'Thêm bình luận'; +$string['submitrating'] = 'Gửi đánh giá'; +$string['averagerating'] = 'Trung bình: {$a}'; +$string['totalratings'] = '{$a} đánh giá'; +$string['nocomments'] = 'Chưa có bình luận'; +$string['ratingadded'] = 'Đã thêm đánh giá thành công'; +$string['ratingupdated'] = 'Đã cập nhật đánh giá thành công'; +$string['updatingrating'] = 'Đang cập nhật đánh giá hiện tại của bạn'; +$string['ratingerror'] = 'Lỗi khi lưu đánh giá'; +$string['invalidrating'] = 'Giá trị đánh giá không hợp lệ'; +$string['ratingrequired'] = 'Đánh giá là bắt buộc'; +$string['commentoptional'] = 'Bình luận (tùy chọn)'; +$string['ratingsubmitted'] = 'Đánh giá của bạn đã được gửi'; +$string['ratingdeleted'] = 'Đã xóa đánh giá thành công'; +$string['comments'] = 'Bình luận'; +$string['viewcomments'] = 'Xem bình luận'; +$string['rateandcomment'] = 'Đánh giá và bình luận'; +$string['yourrating'] = 'Đánh giá của bạn'; +$string['noratings'] = 'Chưa có đánh giá'; + +// Chuỗi cài đặt +$string['categorysettings'] = 'Cài đặt Danh mục'; +$string['categorysettings_desc'] = 'Cấu hình cách hiển thị danh mục'; +$string['showprogress'] = 'Hiển thị Tiến độ'; +$string['showprogress_desc'] = 'Hiển thị thanh tiến độ cho danh mục'; +$string['showdescription'] = 'Hiển thị Mô tả'; +$string['showdescription_desc'] = 'Hiển thị mô tả danh mục'; +$string['showcoursecount'] = 'Hiển thị Số lượng Khóa học'; +$string['showcoursecount_desc'] = 'Hiển thị số lượng khóa học trong mỗi danh mục'; +$string['coursesettings'] = 'Cài đặt Khóa học'; +$string['coursesettings_desc'] = 'Cấu hình cách hiển thị khóa học'; +$string['showcourseprogress'] = 'Hiển thị Tiến độ Khóa học'; +$string['showcourseprogress_desc'] = 'Hiển thị thanh tiến độ cho khóa học'; +$string['showcoursedescription'] = 'Hiển thị Mô tả Khóa học'; +$string['showcoursedescription_desc'] = 'Hiển thị mô tả khóa học'; +$string['showcourseshortname'] = 'Hiển thị Tên ngắn Khóa học'; +$string['showcourseshortname_desc'] = 'Hiển thị tên ngắn khóa học'; +$string['sortorder'] = 'Thứ tự Sắp xếp'; +$string['sortorder_desc'] = 'Cách sắp xếp danh mục'; +$string['sortorder_core'] = 'Mặc định (thứ tự Moodle)'; +$string['sortorder_alphabetical'] = 'Theo bảng chữ cái'; +$string['sortorder_coursecount'] = 'Theo số lượng khóa học'; +$string['sortorder_progress'] = 'Theo tiến độ'; +$string['colorsettings'] = 'Cài đặt Màu sắc'; +$string['colorsettings_desc'] = 'Tùy chỉnh màu sắc cho khối'; +$string['buttoncolor'] = 'Màu Nút'; +$string['buttoncolor_help'] = 'Màu cho các nút điều hướng'; +$string['progresscolor1'] = 'Màu Tiến độ 1'; +$string['progresscolor1_help'] = 'Màu bắt đầu cho thanh tiến độ'; +$string['progresscolor2'] = 'Màu Tiến độ 2'; +$string['progresscolor2_help'] = 'Màu kết thúc cho thanh tiến độ'; +$string['manageimages'] = 'Quản lý Hình ảnh Danh mục'; + +// Chuỗi template +$string['navigation'] = 'Điều hướng'; +$string['viewcategory'] = 'Xem danh mục'; +$string['explore'] = 'Khám phá'; +$string['coursescounter'] = 'khóa học'; +$string['nocontent'] = 'Không có nội dung'; +$string['enter'] = 'Vào'; +$string['configured'] = 'Đã cấu hình'; +$string['categories'] = 'Danh mục'; +$string['customtitle'] = 'Tiêu đề tùy chỉnh'; + +// Chuỗi quản lý hình ảnh +$string['editcategoryimage'] = 'Chỉnh sửa Hình ảnh Danh mục'; +$string['categoryimage'] = 'Hình ảnh Danh mục'; +$string['categoryimage_help'] = 'Tải lên hình ảnh cho danh mục này hoặc cung cấp URL hình ảnh'; +$string['categorycolor'] = 'Màu Nền'; +$string['categorycolor_help'] = 'Chọn màu nền cho danh mục này'; +$string['currentpreview'] = 'Xem trước Hiện tại'; +$string['categoryupdated'] = 'Danh mục đã được cập nhật thành công'; +$string['imageurl'] = 'URL Hình ảnh'; +$string['imageurl_help'] = 'Cung cấp URL trực tiếp đến hình ảnh'; +$string['defaultstatus'] = 'Trạng thái Mặc định'; + +// Chuỗi bảo mật +$string['privacy:metadata:block_catcourse_ratings'] = 'Thông tin về đánh giá của người dùng cho khóa học'; +$string['privacy:metadata:block_catcourse_ratings:userid'] = 'ID của người dùng đã đánh giá'; +$string['privacy:metadata:block_catcourse_ratings:courseid'] = 'ID của khóa học được đánh giá'; +$string['privacy:metadata:block_catcourse_ratings:rating'] = 'Giá trị đánh giá (1-5 sao)'; +$string['privacy:metadata:block_catcourse_ratings:comment'] = 'Bình luận tùy chọn với đánh giá'; +$string['privacy:metadata:block_catcourse_ratings:timemodified'] = 'Thời gian đánh giá được sửa đổi lần cuối'; + +// Chuỗi template bổ sung +$string['completed'] = 'Hoàn thành'; +$string['inprogress'] = 'Đang tiến hành'; +$string['notstarted'] = 'Chưa bắt đầu'; +$string['enrolled'] = 'Đã đăng ký'; +$string['notenrolled'] = 'Chưa đăng ký'; +$string['totalcourses'] = 'tổng khóa học'; +$string['completedcourses'] = 'Khóa học Hoàn thành'; +$string['progresspercentage'] = 'Tiến độ'; +$string['viewcourse'] = 'Xem Khóa học'; +$string['entercategory'] = 'Vào Danh mục'; +$string['backtocategories'] = 'Quay lại Danh mục'; +$string['loading'] = 'Đang tải...'; +$string['nocoursesavailable'] = 'Không có khóa học trong danh mục này'; +$string['nocategoriesavailable'] = 'Không có danh mục'; +$string['errorloadingcontent'] = 'Lỗi khi tải nội dung'; +$string['tryagain'] = 'Thử lại'; +$string['home'] = 'Trang chủ'; +$string['breadcrumbnavigation'] = 'Điều hướng Breadcrumb'; + +// Chuỗi thẻ khóa học +$string['coursesummary'] = 'Tóm tắt Khóa học'; +$string['courseshortname'] = 'Tên ngắn Khóa học'; +$string['coursefullname'] = 'Tên đầy đủ Khóa học'; +$string['courseimage'] = 'Hình ảnh Khóa học'; +$string['courserating'] = 'Đánh giá Khóa học'; +$string['ratethiscourse'] = 'Đánh giá khóa học này'; +$string['viewallcomments'] = 'Xem tất cả bình luận'; +$string['addyourrating'] = 'Thêm đánh giá của bạn'; +$string['updateyourrating'] = 'Cập nhật đánh giá của bạn'; + +// Thông báo lỗi +$string['errornocategory'] = 'Không tìm thấy danh mục'; +$string['errornocourse'] = 'Không tìm thấy khóa học'; +$string['errornopermission'] = 'Bạn không có quyền xem nội dung này'; +$string['errornodata'] = 'Không có dữ liệu'; +$string['errorajax'] = 'Lỗi khi tải dữ liệu qua AJAX'; +$string['errordatabase'] = 'Đã xảy ra lỗi cơ sở dữ liệu'; + +// Thông báo thành công +$string['successsaved'] = 'Đã lưu cài đặt thành công'; +$string['successupdated'] = 'Đã cập nhật thành công'; +$string['successdeleted'] = 'Đã xóa thành công'; + +// Chuỗi cấu hình +$string['configtitle'] = 'Tiêu đề Khối'; +$string['configtitle_help'] = 'Tiêu đề tùy chỉnh cho phiên bản khối này'; +$string['configshowprogress'] = 'Hiển thị Thanh Tiến độ'; +$string['configshowdescription'] = 'Hiển thị Mô tả'; +$string['configshowcoursecount'] = 'Hiển thị Số lượng Khóa học'; +$string['configsortorder'] = 'Thứ tự Sắp xếp'; + +// Chuỗi khả năng +$string['category_courses:addinstance'] = 'Thêm khối Khóa học theo Danh mục mới'; +$string['category_courses:myaddinstance'] = 'Thêm khối Khóa học theo Danh mục vào Bảng điều khiển'; +$string['category_courses:manage'] = 'Quản lý cài đặt Khóa học theo Danh mục'; +$string['category_courses:rate'] = 'Đánh giá khóa học'; +$string['category_courses:viewratings'] = 'Xem đánh giá khóa học'; + +// Cấu hình khối +$string['blocktitle'] = 'Khóa học theo Danh mục'; +$string['blockstring'] = 'Khối Khóa học theo Danh mục'; +$string['descconfig'] = 'Mô tả phần cấu hình'; +$string['descfoo'] = 'Mô tả cấu hình'; +$string['headerconfig'] = 'Tiêu đề phần cấu hình'; +$string['labelfoo'] = 'Nhãn cấu hình'; +$string['categoryhidden'] = 'Danh mục ẩn'; +$string['showallcategories'] = 'Hiển thị Tất cả Danh mục'; +$string['showallcategories_desc'] = 'Cho phép học sinh xem tất cả danh mục có thể nhìn thấy, bất kể trạng thái đăng ký'; \ No newline at end of file diff --git a/category_courses/lib.php b/category_courses/lib.php new file mode 100644 index 0000000..75a52f7 --- /dev/null +++ b/category_courses/lib.php @@ -0,0 +1,40 @@ +contextlevel != CONTEXT_SYSTEM) { + return false; + } + + // Check if user is logged in + require_login(); + + // Check filearea + if ($filearea !== 'categoryimage') { + return false; + } + + // Get file details + $itemid = (int)array_shift($args); + $filename = array_pop($args); + $filepath = $args ? '/'.implode('/', $args).'/' : '/'; + + // Get file from storage + $fs = get_file_storage(); + $file = $fs->get_file($context->id, 'block_category_courses', $filearea, $itemid, $filepath, $filename); + + if (!$file || $file->is_directory()) { + return false; + } + + // Send the file + send_stored_file($file, 86400, 0, $forcedownload, $options); + return true; +} + diff --git a/category_courses/manage_images.php b/category_courses/manage_images.php new file mode 100644 index 0000000..2e78173 --- /dev/null +++ b/category_courses/manage_images.php @@ -0,0 +1,156 @@ +libdir.'/adminlib.php'); + +require_login(); +require_capability('block/category_courses:manage', context_system::instance()); + +$categoryid = optional_param('categoryid', 0, PARAM_INT); +$action = optional_param('action', 'list', PARAM_ALPHA); + +$PAGE->set_url('/blocks/category_courses/manage_images.php'); +$PAGE->set_context(context_system::instance()); +$PAGE->set_title(get_string('manageimages', 'block_category_courses')); +$PAGE->set_heading(get_string('manageimages', 'block_category_courses')); + +admin_externalpage_setup('block_category_courses_images'); + +// Load colorpicker JavaScript for any forms on this page +$PAGE->requires->js_call_amd('block_category_courses/colorpicker', 'init'); + +if ($action === 'edit' && $categoryid > 0) { + // Redirect to edit form + redirect(new moodle_url('/blocks/category_courses/edit_image.php', ['categoryid' => $categoryid])); +} + +echo $OUTPUT->header(); + +// Helper function to check if category is effectively hidden (inherited visibility) +function is_category_or_parent_hidden($category) { + // Check if this category itself is hidden + if (!$category->visible) { + return true; + } + + // If it's a root category and visible, it's not hidden + if ($category->parent == 0) { + return false; + } + + // Check parent categories recursively + try { + $parent = core_course_category::get($category->parent, IGNORE_MISSING); + if ($parent) { + // If any parent is hidden, this category is effectively hidden + return is_category_or_parent_hidden($parent); + } + } catch (Exception $e) { + // Parent not found, assume visible + } + + return false; +} + +// Build accordion structure +function render_category_accordion($parentid = 0) { + global $DB; + $categories = core_course_category::get_all(); + $output = ''; + + foreach ($categories as $category) { + if ($category->parent == $parentid) { + $customdata = $DB->get_record('block_catcourse_images', ['categoryid' => $category->id]); + $status = (!empty($customdata->imageurl) || !empty($customdata->bgcolor)) ? + get_string('configured', 'block_category_courses') : + get_string('defaultstatus', 'block_category_courses'); + $statusclass = $status === get_string('configured', 'block_category_courses') ? 'badge-success' : 'badge-secondary'; + + // Check if category or any parent is hidden (recursive) + $ishidden = is_category_or_parent_hidden($category); + $visibilitystatus = $ishidden ? ' (Oculta)' : ''; + $visibilityclass = $ishidden ? 'text-muted' : ''; + + $editurl = new moodle_url('/blocks/category_courses/edit_image.php', ['categoryid' => $category->id]); + + // Check if has children + $haschildren = false; + foreach ($categories as $child) { + if ($child->parent == $category->id) { + $haschildren = true; + break; + } + } + + $output .= html_writer::start_div('card mb-2'); + $output .= html_writer::start_div('card-header d-flex justify-content-between align-items-center'); + + if ($haschildren) { + $output .= html_writer::start_tag('button', [ + 'class' => 'btn btn-link text-left p-0 collapsed ' . $visibilityclass, + 'type' => 'button', + 'data-toggle' => 'collapse', + 'data-target' => '#collapse' . $category->id, + 'aria-expanded' => 'false' + ]); + $output .= html_writer::tag('i', '', ['class' => 'fa fa-chevron-right mr-2']); + $output .= format_string($category->name) . $visibilitystatus; + $output .= html_writer::end_tag('button'); + } else { + $output .= html_writer::tag('span', format_string($category->name) . $visibilitystatus, ['class' => $visibilityclass]); + } + + $output .= html_writer::start_div('d-flex align-items-center'); + $output .= html_writer::tag('span', $status, ['class' => "badge $statusclass mr-2"]); + $output .= html_writer::link($editurl, get_string('edit'), ['class' => 'btn btn-sm btn-primary']); + $output .= html_writer::end_div(); + + $output .= html_writer::end_div(); // card-header + + if ($haschildren) { + $output .= html_writer::start_div('collapse', ['id' => 'collapse' . $category->id]); + $output .= html_writer::start_div('card-body'); + $output .= render_category_accordion($category->id); + $output .= html_writer::end_div(); + $output .= html_writer::end_div(); + } + + $output .= html_writer::end_div(); // card + } + } + + return $output; +} + +echo html_writer::start_tag('div', ['class' => 'category-images-manager']); + +echo html_writer::tag('h2', get_string('manageimages', 'block_category_courses')); + +echo render_category_accordion(); + +// Add JavaScript for accordion functionality +$PAGE->requires->js_init_code(' +require(["jquery"], function($) { + $(document).ready(function() { + $(".card-header button[data-toggle=collapse]").click(function() { + var $button = $(this); + var target = $button.attr("data-target"); + var $target = $(target); + var icon = $button.find("i"); + + $target.toggle(); + + if ($target.is(":visible")) { + $button.removeClass("collapsed"); + icon.removeClass("fa-chevron-right").addClass("fa-chevron-down"); + } else { + $button.addClass("collapsed"); + icon.removeClass("fa-chevron-down").addClass("fa-chevron-right"); + } + }); + }); +}); +'); + +echo html_writer::end_tag('div'); + +echo $OUTPUT->footer(); \ No newline at end of file diff --git a/category_courses/settings.php b/category_courses/settings.php new file mode 100644 index 0000000..480f623 --- /dev/null +++ b/category_courses/settings.php @@ -0,0 +1,97 @@ +fulltree) { + // Category display settings + $settings->add(new admin_setting_heading('block_category_courses/categoryheading', + get_string('categorysettings', 'block_category_courses'), + get_string('categorysettings_desc', 'block_category_courses'))); + + $settings->add(new admin_setting_configcheckbox('block_category_courses/showprogress', + get_string('showprogress', 'block_category_courses'), + get_string('showprogress_desc', 'block_category_courses'), + 1)); + + $settings->add(new admin_setting_configcheckbox('block_category_courses/showdescription', + get_string('showdescription', 'block_category_courses'), + get_string('showdescription_desc', 'block_category_courses'), + 1)); + + $settings->add(new admin_setting_configcheckbox('block_category_courses/showcoursecount', + get_string('showcoursecount', 'block_category_courses'), + get_string('showcoursecount_desc', 'block_category_courses'), + 1)); + + $settings->add(new admin_setting_configcheckbox('block_category_courses/showallcategories', + get_string('showallcategories', 'block_category_courses'), + get_string('showallcategories_desc', 'block_category_courses'), + 0)); + + // Course display settings + $settings->add(new admin_setting_heading('block_category_courses/courseheading', + get_string('coursesettings', 'block_category_courses'), + get_string('coursesettings_desc', 'block_category_courses'))); + + $settings->add(new admin_setting_configcheckbox('block_category_courses/showcourseprogress', + get_string('showcourseprogress', 'block_category_courses'), + get_string('showcourseprogress_desc', 'block_category_courses'), + 1)); + + $settings->add(new admin_setting_configcheckbox('block_category_courses/showcoursedescription', + get_string('showcoursedescription', 'block_category_courses'), + get_string('showcoursedescription_desc', 'block_category_courses'), + 1)); + + $settings->add(new admin_setting_configcheckbox('block_category_courses/showcourseshortname', + get_string('showcourseshortname', 'block_category_courses'), + get_string('showcourseshortname_desc', 'block_category_courses'), + 1)); + + // Sort order setting + $settings->add(new admin_setting_configselect('block_category_courses/sortorder', + get_string('sortorder', 'block_category_courses'), + get_string('sortorder_desc', 'block_category_courses'), + 'core', + [ + 'core' => get_string('sortorder_core', 'block_category_courses'), + 'alphabetical' => get_string('sortorder_alphabetical', 'block_category_courses'), + 'coursecount' => get_string('sortorder_coursecount', 'block_category_courses'), + 'progress' => get_string('sortorder_progress', 'block_category_courses') + ])); + + // Color settings + $settings->add(new admin_setting_heading('block_category_courses/colorheading', + get_string('colorsettings', 'block_category_courses'), + get_string('colorsettings_desc', 'block_category_courses'))); + + $settings->add(new admin_setting_configtext('block_category_courses/buttoncolor', + get_string('buttoncolor', 'block_category_courses'), + get_string('buttoncolor_help', 'block_category_courses'), + '', + PARAM_TEXT)); + + $settings->add(new admin_setting_configtext('block_category_courses/progresscolor1', + get_string('progresscolor1', 'block_category_courses'), + get_string('progresscolor1_help', 'block_category_courses'), + '#4285f4', + PARAM_TEXT)); + + $settings->add(new admin_setting_configtext('block_category_courses/progresscolor2', + get_string('progresscolor2', 'block_category_courses'), + get_string('progresscolor2_help', 'block_category_courses'), + '#34a853', + PARAM_TEXT)); + + // Include color picker JavaScript + global $PAGE; + if ($PAGE) { + $PAGE->requires->js_call_amd('block_category_courses/colorpicker', 'init'); + } +} + +// Category image management - separate external page +if ($hassiteconfig) { + $ADMIN->add('blocksettings', new admin_externalpage('block_category_courses_images', + get_string('manageimages', 'block_category_courses'), + new moodle_url('/blocks/category_courses/manage_images.php'))); +} \ No newline at end of file diff --git a/category_courses/styles.css b/category_courses/styles.css new file mode 100644 index 0000000..223401c --- /dev/null +++ b/category_courses/styles.css @@ -0,0 +1,983 @@ +@charset "UTF-8"; + +/* Mejoras de contraste para progress elements */ +.category-card .progress-info { + margin-top: 1rem; +} + +.category-card .progress-label { + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.category-card .progress-bar { + height: 8px; + border-radius: 4px; + overflow: hidden; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.category-card .course-info .course-count { + font-size: 0.875rem; + font-weight: 500; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +/* Contraste automático para fondos oscuros */ +.category-card[data-dark-bg="true"] .progress-label, +.category-card[data-dark-bg="true"] .course-count { + color: #ffffff !important; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.category-card[data-dark-bg="true"] .progress-bar { + background-color: rgba(255, 255, 255, 0.2) !important; +} + +/* Contraste automático para fondos claros */ +.category-card[data-dark-bg="false"] .progress-label, +.category-card[data-dark-bg="false"] .course-count { + color: #333333 !important; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.3); +} + +.category-card[data-dark-bg="false"] .progress-bar { + background-color: rgba(0, 0, 0, 0.15) !important; +} + +/* Mejoras de contraste para custom progress container */ +.custom-progress-container { + padding: 0.5rem; + border-radius: 6px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(5px); +} + +.custom-progress-container span, +.custom-progress-container i { + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.card.block_category_courses { + border: none !important; + border-color: transparent; + margin: 0; + padding: 4em 0 2em 0; +} + +.card.block_category_courses .card-body { + padding: 0; +} + +.category-cards-container { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; + padding: 1rem 0; + width: 100%; + margin-top: 1.5em; + justify-items: stretch; + justify-content: space-evenly; +} + +.category-card { + position: relative; + background: var(--white); + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: all 0.3s ease; + cursor: pointer; + border: 1px solid var(--gray-300); + display: flex; + flex-direction: column; + height: 100%; +} + +.category-card:hover, +.category-card:focus { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); + outline: none; +} + +.category-card:focus-visible { + outline: 2px solid var(--primary); + outline-offset: 2px; +} + +.category-card .category-title, +.category-card .category-description, +.category-card .progress-percentage { + color: inherit; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.category-card .course-count-chip { + background: rgba(0, 0, 0, 0); + color: inherit; + backdrop-filter: blur(10px); + border: none; + font-weight: 600; + letter-spacing: 0.5px; +} + +.card-image { + height: 168px; + display: flex; + align-items: center; + justify-content: center; + position: relative; + overflow: hidden; + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(5px); +} + +.category-image { + width: 100%; + height: 100%; + object-fit: cover; + aspect-ratio: 16/9; +} + +.image-initials { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 2rem; + font-weight: 700; + color: var(--white); + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); +} + +.card-content { + padding: 1.25rem; + flex: 1; +} + +.card-footer-button { + padding: 0.75rem 1.25rem 1.25rem; + display: flex; + justify-content: flex-end; + margin-top: auto; +} + +.card-footer-button .btn { + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; +} + +.card-footer-button .btn i { + margin-right: 0.5rem; +} + +.category-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--dark); + margin: 0 0 0.75rem 0; + line-height: 1.4; +} + +.category-description { + font-size: 0.875rem; + color: var(--gray-600); + margin: 0 0 1rem 0; + line-height: 1.5; +} + +.course-count-chip { + display: inline-flex; + align-items: center; + background: var(--gray-200); + color: var(--gray-800); + padding: 0.25rem 0.75rem; + border-radius: 9999px; + font-size: 0.75rem; + font-weight: 500; + margin-bottom: 1rem; +} + +.progress-container { + margin-top: auto; +} + +.progress, +.rui-progress { + background-color: transparent; +} + +.card-link { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + z-index: 1; +} + +.category-card.card-clicked { + transform: scale(0.98); + transition: transform 0.1s ease; +} + +.category-card.loading .card-content>* { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; + border-radius: 4px; + color: transparent; +} + +.category-card.loading .card-image { + background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); + background-size: 200% 100%; + animation: loading 1.5s infinite; +} + +@keyframes loading { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +@media (max-width: 768px) { + .category-cards-container { + grid-template-columns: repeat(auto-fill, minmax(300px, 350px)); + gap: 1rem; + } +} + +@media (max-width: 320px) { + .category-cards-container { + grid-template-columns: 1fr; + gap: 1rem; + padding: 0.5rem 0; + } + + .card-content { + padding: 1rem; + } + + .card-image { + height: 168px; + } +} + +#page-blocks-category_courses-view_courses .category-courses-grid, +.category-courses-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); + gap: 1.5rem; + padding: 1rem 0; +} + +.hierarchy-navigation { + width: 100%; +} + +.navigation-content { + transition: opacity 0.15s ease; + min-height: 200px; +} + +.navigation-content.loading { + pointer-events: none; + position: relative; +} + +.navigation-content.loading::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 32px; + height: 32px; + margin: -16px 0 0 -16px; + border: 3px solid var(--gray-300); + border-top: 3px solid var(--primary); + border-radius: 50%; + animation: spin 1s linear infinite; + z-index: 10; +} + +@keyframes spin { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.hierarchy-navigation.navigating .category-card { + pointer-events: none; + opacity: 0.7; + transition: opacity 0.15s ease; +} + +.hierarchy-navigation .breadcrumb-nav { + box-shadow: 0 8px 14px -2px rgba(0, 0, 0, 0.1), 0 4px 6px -2px rgba(0, 0, 0, 0.05); + padding: 0.75rem 1.25rem; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-content: center; + align-items: center; + border-radius: 10px; + background-color: var(--gray-100); +} + +.hierarchy-navigation .breadcrumb { + margin: 0; + padding: 0; + list-style: none; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; + font-size: 1.2em; +} + +.hierarchy-navigation .breadcrumb-item { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.hierarchy-navigation .breadcrumb-link { + background: none; + border: none; + color: var(--primary); + text-decoration: none; + padding: 0; + font-size: 1em; + cursor: pointer; + transition: color 0.2s ease; +} + +.hierarchy-navigation .breadcrumb-link:hover, +.hierarchy-navigation .breadcrumb-link:focus { + color: var(--primary-color-700); + text-decoration: underline; +} + +.hierarchy-navigation .breadcrumb-separator { + color: var(--gray-600); + font-size: 1em; +} + +.current-page { + color: var(--gray-700); + font-weight: 500; +} + +.level-content { + margin-bottom: 2rem; +} + +.level-categories+.level-courses { + border-top: 2px solid var(--gray-300); + padding-top: 2rem; +} + +.no-content { + text-align: center; + padding: 3rem 1rem; + color: var(--gray-600); +} + +.no-content p { + margin: 0; + font-size: 1.1rem; +} + +.course-card { + background: var(--white); + border-radius: 12px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); + overflow: hidden; + transition: all 0.3s ease; + border: 1px solid var(--gray-300); + display: flex; + flex-direction: column; + height: 100%; +} + +.course-card:hover { + transform: translateY(-2px); + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15); +} + +body.theme-dark .course-card { + background: #2d3748 !important; + border-color: #4a5568 !important; + color: #e2e8f0 !important; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3) !important; +} + +body.theme-dark .course-card:hover { + box-shadow: 0 8px 25px rgba(0, 0, 0, 0.4) !important; +} + +body.theme-dark .course-title { + color: #f7fafc !important; +} + +body.theme-dark .course-shortname, +body.theme-dark .course-summary { + color: #a0aec0 !important; +} + +body.theme-dark .progress-text { + color: #cbd5e0 !important; +} + +.course-card:hover .course-rating-section { + background: var(--gray-100); +} + +body.theme-dark .course-card:hover .course-rating-section { + background: #4a5568 !important; +} + +.course-link { + text-decoration: none; + color: inherit; + display: flex; + flex-direction: column; + height: 100%; +} + +.card-inner { + display: flex; + flex-direction: column; + height: 100%; +} + +.course-image { + height: 168px; + position: relative; + overflow: hidden; +} + +.course-img { + width: 100%; + height: 100%; + object-fit: cover; +} + +/* Mejoras de contraste para progress elements */ +.category-card .progress-info { + margin-top: 1rem; +} + +.category-card .progress-label { + font-size: 0.875rem; + font-weight: 500; + margin-bottom: 0.5rem; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.category-card .progress-bar { + height: 8px; + border-radius: 4px; + overflow: hidden; + box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); +} + +.category-card .course-info .course-count { + font-size: 0.875rem; + font-weight: 500; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); +} + +/* Contraste automático para fondos oscuros */ +.category-card[data-dark-bg="true"] .progress-label, +.category-card[data-dark-bg="true"] .course-count { + color: #ffffff !important; + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.3); +} + +.category-card[data-dark-bg="true"] .progress-bar { + background-color: rgba(255, 255, 255, 0.2) !important; +} + +/* Contraste automático para fondos claros */ +.category-card[data-dark-bg="false"] .progress-label, +.category-card[data-dark-bg="false"] .course-count { + color: #333333 !important; + text-shadow: 0 1px 2px rgba(255, 255, 255, 0.3); +} + +.category-card[data-dark-bg="false"] .progress-bar { + background-color: rgba(0, 0, 0, 0.15) !important; +} + +/* Mejoras de contraste para custom progress container */ +.custom-progress-container { + padding: 0.5rem; + border-radius: 6px; + background: rgba(255, 255, 255, 0.1); + backdrop-filter: blur(5px); +} + +.custom-progress-container span, +.custom-progress-container i { + text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); +} + +.enrollment-badge { + position: absolute; + top: 0.5rem; + right: 0.5rem; + background: var(--success); + color: var(--white); + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; +} + +.course-content { + padding: 1.25rem; + flex: 1; + display: flex; + flex-direction: column; +} + +.course-title { + font-size: 1.125rem; + font-weight: 600; + color: var(--dark); + margin: 0 0 0.5rem 0; + line-height: 1.4; +} + +.course-shortname { + font-size: 0.875rem; + color: var(--gray-600); + margin: 0 0 1rem 0; +} + +.course-summary { + font-size: 0.875rem; + color: var(--gray-700); + margin-bottom: 1rem; + flex: 1; +} + +.progress-section { + margin-top: auto; +} + +.progress-bar { + width: 100%; + height: 8px; + background: var(--gray-300); + border-radius: 4px; + overflow: hidden; + margin-bottom: 0.5rem; +} + +.progress-fill { + height: 100%; + background: var(--primary); + transition: width 0.3s ease; +} + +.progress-text { + font-size: 0.75rem; + color: var(--gray-600); + font-weight: 500; +} + +.hidden-course-badge { + display: inline-flex; + align-items: center; + gap: 0.25rem; + background: var(--danger); + color: var(--white); + padding: 0.25rem 0.5rem; + border-radius: 4px; + font-size: 0.75rem; + font-weight: 500; + margin-bottom: 0.5rem; +} + +.hidden-course-badge i { + font-size: 0.7rem; +} + +.visibility-overlay { + position: absolute; + top: 0.5rem; + left: 0.5rem; + background: color-mix(in srgb, var(--danger) 90%, transparent); + color: var(--white); + border-radius: 50%; + width: 28px; + height: 28px; + display: flex; + align-items: center; + justify-content: center; + font-size: 0.75rem; +} + +.category-card.navigating { + transform: scale(0.98); + opacity: 0.8; + transition: all 0.15s ease; +} + +@media (max-width: 768px) { + .hierarchy-navigation .breadcrumb-nav { + padding: 0.5rem 0.75rem; + margin-bottom: 1rem; + } + + .hierarchy-navigation .breadcrumb { + gap: 0.25rem; + } + + .hierarchy-navigation .breadcrumb-separator { + font-size: 0.7rem; + } +} + +.color-picker-wrapper { + display: inline-block; + vertical-align: middle; + margin-left: 10px; +} + +input[type='color'].color-picker-input { + width: 40px; + height: 30px; + border: 1px solid #ccc; + border-radius: 4px; + cursor: pointer; + padding: 0; + background: none; +} + +input[type='color'].color-picker-input::-webkit-color-swatch-wrapper { + padding: 0; + border: none; + border-radius: 4px; +} + +input[type='color'].color-picker-input::-webkit-color-swatch { + border: none; + border-radius: 3px; +} + +input[type='color'].color-picker-input::-moz-color-swatch { + border: none; + border-radius: 3px; +} + +.fgroup .color-picker-wrapper { + margin-left: 0; + margin-top: 5px; +} + +.fgroup .felement { + display: flex; + align-items: center; + gap: 10px; +} + +.fgroup .felement input[name='categorycolor'], +.fgroup .felement input[name*='buttoncolor'] { + width: 100px; + flex-shrink: 0; +} + +/* Block configuration form specific styles */ +.block-config-form .fitem .felement { + display: flex; + align-items: center; + gap: 10px; +} + +.block-config-form input[name*='buttoncolor']+.color-picker-wrapper { + display: inline-block; +} + +/* Ensure color picker appears in block edit form */ +#page-blocks-edit .fitem .felement { + display: flex; + align-items: center; + gap: 10px; +} + +#page-blocks-edit input[name*='buttoncolor'] { + width: 120px; +} + +.category-navigate-btn { + border-style: none; +} + +/* Premium badge styles */ +.course-badges { + display: flex; + flex-direction: row; + gap: .5rem; + margin: .75rem 0; + flex-wrap: nowrap; + align-items: center; + justify-content: flex-start; +} + +.badge-premium { + background: rgba(255, 255, 255, 0.2); + backdrop-filter: blur(10px); + border: 1px solid rgba(255, 255, 255, 0.1); + color: inherit; + font-weight: 500; + padding: 8px 14px; + border-radius: 20px; + font-size: 0.85rem; + display: inline-flex; + align-items: center; + gap: 6px; + transition: all 0.2s ease; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); +} + +.badge-premium.badge-total { + background: rgba(255, 255, 255, 0.25); +} + +.badge-premium.badge-progress { + background: rgba(255, 255, 255, 0.15); +} + +.badge-premium:hover { + transform: translateY(-1px); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); +} + +.badge-premium i { + font-size: 0.9rem; + opacity: 0.9; +} + +/* Rating System Styles */ +.course-rating-section { + padding: 0.75rem 1.25rem; + border-top: 1px solid var(--gray-200); + display: flex; + justify-content: space-between; + align-items: center; + background: var(--gray-50); +} + +.rating-container { + display: flex; + align-items: center; + gap: 0.5rem; +} + +.rating-stars { + display: flex; + gap: 2px; + font-size: 1.2rem; + cursor: pointer; +} + +.rating-star { + color: #ddd; + transition: color 0.2s ease; + cursor: pointer; + user-select: none; +} + +.rating-star:hover, +.rating-star.hover { + color: #ffc107; +} + +.rating-star.selected { + color: #ffc107; +} + +.rating-info { + display: flex; + align-items: center; + gap: 0.25rem; + font-size: 0.875rem; + color: var(--gray-600); +} + +.average-rating { + font-weight: 600; + color: var(--dark); +} + +.total-ratings { + color: var(--gray-500); +} + +.comments-btn { + background: none; + border: none; + color: var(--gray-600); + cursor: pointer; + display: flex; + align-items: center; + gap: 0.25rem; + padding: 0.25rem 0.5rem; + border-radius: 4px; + transition: all 0.2s ease; + font-size: 0.875rem; +} + +.comments-btn:hover { + background: var(--gray-200); + color: var(--primary); +} + +.comments-count { + font-weight: 500; +} + +/* Prevent rating section from being clickable as course link */ +.course-rating-section { + position: relative; + z-index: 2; +} + +.course-link { + z-index: 1; +} + +/* Modal rating styles */ +.rating-stars-form { + font-size: 1.5rem; + color: #ddd; + cursor: pointer; + display: flex; + gap: 4px; +} + +.rating-star-form { + transition: color 0.2s; + cursor: pointer; + user-select: none; +} + +.rating-star-form:hover, +.rating-star-form.selected { + color: #ffc107; +} + +.rating-display { + color: #ffc107; + font-size: 0.9rem; +} + +.comment-item { + background-color: #f8f9fa; +} + +.comments-modal-content { + max-height: 70vh; + overflow-y: auto; +} + +/* Force dark mode styles for Moodle dark theme */ +.theme-boost-union-dark .comment-item, +[data-theme="dark"] .comment-item, +body.theme-dark .comment-item { + background-color: #2d3748 !important; + border-color: #4a5568 !important; + color: #e2e8f0 !important; +} + +.theme-boost-union-dark .comment-item strong, +[data-theme="dark"] .comment-item strong, +body.theme-dark .comment-item strong { + color: #f7fafc !important; +} + +.theme-boost-union-dark .comment-item .text-muted, +[data-theme="dark"] .comment-item .text-muted, +body.theme-dark .comment-item .text-muted { + color: #a0aec0 !important; +} + +.theme-boost-union-dark .comments-modal-content, +[data-theme="dark"] .comments-modal-content, +body.theme-dark .comments-modal-content { + background-color: transparent !important; + color: #e2e8f0 !important; +} + +.theme-boost-union-dark .comments-modal-content h6, +[data-theme="dark"] .comments-modal-content h6, +body.theme-dark .comments-modal-content h6 { + color: #f7fafc !important; +} + +.theme-boost-union-dark .comments-modal-content .form-label, +[data-theme="dark"] .comments-modal-content .form-label, +body.theme-dark .comments-modal-content .form-label { + color: #e2e8f0 !important; +} + +.theme-boost-union-dark .comments-modal-content .form-control, +[data-theme="dark"] .comments-modal-content .form-control, +body.theme-dark .comments-modal-content .form-control { + background-color: #2d3748 !important; + border-color: #4a5568 !important; + color: #e2e8f0 !important; + padding: 1em; +} + +.theme-boost-union-dark .comments-modal-content .form-control::placeholder, +[data-theme="dark"] .comments-modal-content .form-control::placeholder, +body.theme-dark .comments-modal-content .form-control::placeholder { + color: #a0aec0 !important; +} + +.theme-boost-union-dark .comments-modal-content .form-text, +[data-theme="dark"] .comments-modal-content .form-text, +body.theme-dark .comments-modal-content .form-text { + color: #a0aec0 !important; +} + +/* Hidden category styles */ +.category-card.category-hidden { + filter: grayscale(100%); + opacity: 0.6; + position: relative; +} + +.category-card.category-hidden::after { + content: ''; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; + background: rgba(0, 0, 0, 0.1); + pointer-events: none; +} + + diff --git a/category_courses/templates/breadcrumbs.mustache b/category_courses/templates/breadcrumbs.mustache new file mode 100644 index 0000000..9bda5b7 --- /dev/null +++ b/category_courses/templates/breadcrumbs.mustache @@ -0,0 +1,44 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_category_courses/breadcrumbs + + Breadcrumbs para navegación jerárquica. + + Context variables required for this template: + * name - Nombre del breadcrumb + * url - URL del breadcrumb + * categoryid - ID de la categoría + * active - Si es el breadcrumb activo +}} + + diff --git a/category_courses/templates/category_card.mustache b/category_courses/templates/category_card.mustache new file mode 100644 index 0000000..c68ade3 --- /dev/null +++ b/category_courses/templates/category_card.mustache @@ -0,0 +1,76 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_category_courses/category_card + + Category card template with original beautiful design. + + Context variables required for this template: + * id - Category ID + * name - Category name + * description - Category description + * image - Category image data + * color - Background color + * textcolor - Text color + * total_courses - Total courses + * completed_courses - Completed courses + * progress_percentage - Progress percentage + * hasprogress - Has progress data +}} + +
+
+
+ {{#image.type}} +
+ {{image.text}} +
+ {{/image.type}} + {{^image.type}} + {{name}} + {{/image.type}} +
+
+ +
+

{{name}}

+ + {{#description}} +

{{description}}

+ {{/description}} + +
+ {{completed_courses}} / {{total_courses}} cursos +
+ + {{#hasprogress}} +
+ {{progress_percentage}}% completado +
+
+
+
+ {{/hasprogress}} +
+ + +
\ No newline at end of file diff --git a/category_courses/templates/category_card_hierarchical.mustache b/category_courses/templates/category_card_hierarchical.mustache new file mode 100644 index 0000000..07514e6 --- /dev/null +++ b/category_courses/templates/category_card_hierarchical.mustache @@ -0,0 +1,106 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_category_courses/category_card_hierarchical + + Tarjeta de categoría para navegación jerárquica - mantiene diseño actual. + + Context variables required for this template: + * id - Category ID + * name - Category name + * description - Category description + * image - Category image data + * color - Background color + * textcolor - Text color + * total_courses - Total courses + * completed_courses - Completed courses + * progress_percentage - Progress percentage + * hasprogress - Has progress data + * config - Display configuration +}} + +
+ +
+ {{#image.type}} + {{#image.text}} + + {{/image.text}} + {{/image.type}} + {{#image.url}} + {{name}} + {{/image.url}} +
+ +
+

+ {{name}} + {{#ishidden}} + + + {{#str}}categoryhidden, block_category_courses{{/str}} + + {{/ishidden}} +

+ + {{#config.showdescription}} + {{#description}} +

{{description}}

+ {{/description}} + {{/config.showdescription}} + + {{#config.showcoursecount}} +
+ {{#is_admin}} + + + {{admin_total_courses}} {{#str}}totalcourses, block_category_courses{{/str}} + + + {{/is_admin}} + {{#hasprogress}} + + + {{#str}}coursescounter, block_category_courses, {"completed": {{completed_courses}}, "total": {{total_courses}}}{{/str}} + + + {{/hasprogress}} +
+ {{/config.showcoursecount}} + + {{#config.showprogress}} + {{#hasprogress}} +
+ {{> block_category_courses/progress_bar}} +
+ {{/hasprogress}} + {{/config.showprogress}} +
+ + + +
diff --git a/category_courses/templates/comments_modal.mustache b/category_courses/templates/comments_modal.mustache new file mode 100644 index 0000000..d3c5089 --- /dev/null +++ b/category_courses/templates/comments_modal.mustache @@ -0,0 +1,85 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_category_courses/comments_modal + + Comments modal template. + + Context variables required for this template: + * courseid - Course ID + * comments - Array of comments +}} + +
+
+ + +
+ +
+ + + + + +
+ +
+ +
+ + + {{#userRating}} + {{#str}}updatingrating, block_category_courses{{/str}} + {{/userRating}} +
+
+ +
+ +
+
{{#str}}comments, block_category_courses{{/str}}
+ + {{#comments}} +
+
+
+ {{{user_picture}}} +
+
+
+ {{user_fullname}} +
+ + + + + +
+
+

{{comment}}

+ {{#userdate}}{{timemodified}}, {{#str}}strftimedatetimeshort, langconfig{{/str}}{{/userdate}} +
+
+
+ {{/comments}} + + {{^comments}} +

{{#str}}nocomments, block_category_courses{{/str}}

+ {{/comments}} +
+
diff --git a/category_courses/templates/course_card.mustache b/category_courses/templates/course_card.mustache new file mode 100644 index 0000000..1a1151c --- /dev/null +++ b/category_courses/templates/course_card.mustache @@ -0,0 +1,101 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_category_courses/course_card + + Tarjeta de curso con progreso y enlace directo. + + Context variables required for this template: + * id - ID del curso + * fullname - Nombre completo del curso + * shortname - Nombre corto del curso + * summary - Resumen del curso + * viewurl - URL para ver el curso + * courseimage - Imagen del curso + * visible - Si el curso es visible + * isenrolled - Si el usuario está inscrito + * hasprogress - Si tiene progreso + * progress - Porcentaje de progreso +}} + + diff --git a/category_courses/templates/main.mustache b/category_courses/templates/main.mustache new file mode 100644 index 0000000..5767d7d --- /dev/null +++ b/category_courses/templates/main.mustache @@ -0,0 +1,44 @@ +{{! Navegación jerárquica de categorías }} +
+ + {{! Breadcrumbs para navegación }} + {{#breadcrumbs}} + {{> block_category_courses/breadcrumbs}} + {{/breadcrumbs}} + + {{! Contenedor principal con niveles }} + +
diff --git a/category_courses/templates/progress_bar.mustache b/category_courses/templates/progress_bar.mustache new file mode 100644 index 0000000..b9a68d2 --- /dev/null +++ b/category_courses/templates/progress_bar.mustache @@ -0,0 +1,25 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + @template block_category_courses/progress_bar + + Custom progress bar with configurable gradient colors. + + Context variables required for this template: + * progress - Progress percentage (0-100) + * config - Configuration object with progresscolor1 and progresscolor2 +}} + +
+ + {{progress}}% {{#str}}completed, block_category_courses{{/str}} +
+
+
+
+
diff --git a/category_courses/templates/view_courses.mustache b/category_courses/templates/view_courses.mustache new file mode 100644 index 0000000..d9a0cb4 --- /dev/null +++ b/category_courses/templates/view_courses.mustache @@ -0,0 +1,78 @@ +{{! + This file is part of Moodle - http://moodle.org/ + + Moodle is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + Moodle is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with Moodle. If not, see . +}} +{{! + @template block_myoverview/view-cards + + This template renders the cards view for the myoverview block. + + Example context (json): + { + "courses": [ + { + "name": "Assignment due 1", + "viewurl": "https://moodlesite/course/view.php?id=2", + "courseimage": "https://moodlesite/pluginfile/123/course/overviewfiles/123.jpg", + "fullname": "Course 3 for \"Statistical and Computational Tools\" ", + "hasprogress": true, + "progress": 10, + "coursecategory": "Category 1", + "visible": true + } + ] + } +}} + +{{#hascourses}} +{{< core_course/coursecards }} + {{$classes}}category-courses-grid{{/classes}} + + {{$progress}} + {{#hasprogress}} + + {{/hasprogress}} + {{/progress}} + {{$coursename}} + + {{{fullname}}} + + + {{/coursename}} + {{$coursecategory}} + {{#showcoursecategory}} + + {{#str}}aria:coursecategory, core_course{{/str}} + + + {{{coursecategory}}} + + {{/showcoursecategory}} + {{/coursecategory}} + {{$divider}} + {{#showcoursecategory}} +
|
+ {{/showcoursecategory}} + {{/divider}} +{{/ core_course/coursecards }} +{{/hascourses}} + +{{^hascourses}} +
{{message}}
+{{/hascourses}} diff --git a/category_courses/version.php b/category_courses/version.php new file mode 100644 index 0000000..6aebfe3 --- /dev/null +++ b/category_courses/version.php @@ -0,0 +1,31 @@ +. + +/** + * Version information for block_category_courses. + * + * @package block_category_courses + * @copyright 2025 Your Name + * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later + */ + +defined('MOODLE_INTERNAL') || die(); + +$plugin->component = 'block_category_courses'; +$plugin->version = 2025073217; +$plugin->requires = 2024100700; // Moodle 4.5+ +$plugin->maturity = MATURITY_STABLE; +$plugin->release = '4.5.3'; diff --git a/category_courses/view_courses.php b/category_courses/view_courses.php new file mode 100644 index 0000000..bcb28df --- /dev/null +++ b/category_courses/view_courses.php @@ -0,0 +1,124 @@ +set_context($context); +$PAGE->set_url(new moodle_url('/blocks/category_courses/view_courses.php', ['categoryid' => $categoryid])); +$PAGE->set_pagelayout('standard'); + +$catname = format_string($category->get_formatted_name()); +$PAGE->set_title($catname . ' - ' . get_string('mycourses', 'moodle')); +$PAGE->set_heading($catname); + +echo $OUTPUT->header(); + +// Preparar datos para el template +$templatedata = []; + +// Obtener cursos inscritos del usuario para verificar progreso +$fields = 'id, fullname, shortname, summary, category, startdate, enddate, visible'; +$mycourses = enrol_get_my_courses($fields, 'sortorder'); +$enrolledcourses = []; +foreach ($mycourses as $course) { + $enrolledcourses[$course->id] = $course; +} + +// Si es administrador, obtener TODOS los cursos de la categoría +if (is_siteadmin()) { + $courses = $category->get_courses(['recursive' => false, 'coursecontacts' => false]); + $filtered = []; + foreach ($courses as $course) { + // Verificar si está inscrito para mostrar progreso + $isenrolled = isset($enrolledcourses[$course->id]); + $hasprogress = false; + $progress = 0; + + if ($isenrolled && completion_info::is_enabled_for_site()) { + $completion = new completion_info($course); + if ($completion->is_enabled()) { + $percentage = \core_completion\progress::get_course_progress_percentage($course, $USER->id); + if (!is_null($percentage)) { + $hasprogress = true; + $progress = floor($percentage); + } + } + } + + $courseimage = \core_course\external\course_summary_exporter::get_course_image($course); + if (!$courseimage) { + $courseimage = $OUTPUT->get_generated_image_for_id($course->id); + } + + $filtered[] = [ + 'id' => $course->id, + 'fullname' => format_string($course->fullname), + 'shortname' => format_string($course->shortname), + 'summary' => format_text($course->summary, FORMAT_HTML), + 'viewurl' => (new moodle_url('/course/view.php', ['id' => $course->id]))->out(false), + 'courseimage' => $courseimage, + 'visible' => $course->visible, + 'coursecategory' => format_string($category->name), + 'showcoursecategory' => true, + 'hasprogress' => $hasprogress, + 'progress' => $progress + ]; + } + $templatedata['message'] = get_string('nocoursesincat', 'block_category_courses'); +} else { + // Usuario normal: solo cursos donde está inscrito con progreso + $filtered = []; + foreach ($mycourses as $course) { + if ((int)$course->category === (int)$categoryid) { + // Calcular progreso + $hasprogress = false; + $progress = 0; + + if (completion_info::is_enabled_for_site()) { + $completion = new completion_info($course); + if ($completion->is_enabled()) { + $percentage = \core_completion\progress::get_course_progress_percentage($course, $USER->id); + if (!is_null($percentage)) { + $hasprogress = true; + $progress = floor($percentage); + } + } + } + + $courseimage = \core_course\external\course_summary_exporter::get_course_image($course); + if (!$courseimage) { + $courseimage = $OUTPUT->get_generated_image_for_id($course->id); + } + + $filtered[] = [ + 'id' => $course->id, + 'fullname' => format_string($course->fullname), + 'shortname' => format_string($course->shortname), + 'summary' => format_text($course->summary, FORMAT_HTML), + 'viewurl' => (new moodle_url('/course/view.php', ['id' => $course->id]))->out(false), + 'courseimage' => $courseimage, + 'visible' => $course->visible, + 'coursecategory' => format_string($category->name), + 'showcoursecategory' => true, + 'hasprogress' => $hasprogress, + 'progress' => $progress + ]; + } + } + $templatedata['message'] = get_string('notenrolledincat', 'block_category_courses'); +} + +$templatedata['courses'] = $filtered; +$templatedata['hascourses'] = !empty($filtered); + +// Renderizar template +echo $OUTPUT->render_from_template('block_category_courses/view_courses', $templatedata); + +echo $OUTPUT->footer();