Compare commits
No commits in common. "main" and "oggui/commands" have entirely different histories.
main
...
oggui/comm
|
@ -1,9 +0,0 @@
|
|||
ogWebconsole/.env
|
||||
ogWebconsole/test-results/ogGui-junit-report.xml
|
||||
node_modules/
|
||||
### Debian packaging
|
||||
debian/oggui
|
||||
debian/*.substvars
|
||||
debian/*.log
|
||||
debian/.debhelper/
|
||||
debian/files
|
119
CHANGELOG.md
119
CHANGELOG.md
|
@ -1,119 +0,0 @@
|
|||
# Changelog
|
||||
## [0.11.2] - 2025-4-16
|
||||
### Fixed
|
||||
- Se ha corregido un error en la actualizacion del estado de los pcs en la vista tarjetas.
|
||||
|
||||
---
|
||||
## [0.11.1] - 2025-4-16
|
||||
### Improved
|
||||
- Nuevos campos en la tabla de clientes. Tipo de firmware y mac.
|
||||
|
||||
## Fixed
|
||||
- Se ha corregido error al crear OUs, que no refrescaba la web.
|
||||
- Se ha corregido error en el formulario de creacion de imagenes. Si se seleccionaba una imagen para un versionado, no dejaba deseleccionar.
|
||||
- Se ha corregido un bug en el particionador que impedia ejecutar, cuando eliminabamos una particion.
|
||||
|
||||
---
|
||||
## [0.11.0] - 2025-4-11
|
||||
### Added
|
||||
- Se ha diseñado el nuevo formulario para poder ejecutar script. Sistema mejorado con variables etiquetadas.
|
||||
- Se puede añadir descripcion a una imagen.
|
||||
- Se han añadido al formulario de crear/editar repositorio, la posibilidad de añadir usuario y puerto ssh.
|
||||
- Nuevo estado en pc => desconectado.
|
||||
- Se ha añadido nueva accion para renombrar imagen monolitica.
|
||||
|
||||
### Improved
|
||||
- Se ha mejorado la interfaz de usuario tanto para el despliegue de imagenes, como el particionado.
|
||||
- Se ha mejorado la responsividad de la vista de grupos.
|
||||
- Cambios en el comportamiento general de muchos componentes modales. Se han añadido spinners de carga mas intuitivos.
|
||||
|
||||
---
|
||||
|
||||
## [0.10.1] - 2025-3-27
|
||||
### Improved
|
||||
- Mejoras en el comportamiento del arbol de grupos.
|
||||
- Nueva regexp para controlar las "macs" en la creacion de clientes.
|
||||
|
||||
---
|
||||
## [0.10.0] - 2025-3-25
|
||||
### Added
|
||||
- Nuevo componenten de estado global.
|
||||
- Servicio para que el ogGui obtenga de forma dinamica las variables de entorno.
|
||||
- Nueva funcionalidad para convertir imagen en imagen virtual.
|
||||
- Nueva funcionalidad para importar imágenes externas al sistema.
|
||||
- Despliegue de imangenes sin cache. Cambios en el formulario de "despliegue".
|
||||
|
||||
### Improved
|
||||
- Mejoras en la internacionalización.
|
||||
- Nueva UX ogRepository. Ahora se gestionan las imagenes de forma mas sencilla.
|
||||
- Cambios en ogLive. Mejora en la sincronizacion y obtención de datos en la API
|
||||
|
||||
### Fixed
|
||||
- Cambios en la expresion regular para la validacion de documentos DHCP en la carga masiva de pc.
|
||||
|
||||
---
|
||||
## [0.9.2] - 2025-03-19
|
||||
### Changed
|
||||
- Jenkinsfile to pubilsh packages in repo in case og release
|
||||
|
||||
---
|
||||
## [0.9.1] - 2025-03-12
|
||||
### Changed
|
||||
- Se ha modificado el acceso a Mercure añadiendo nueva variable de entorno.
|
||||
|
||||
---
|
||||
## [0.9.0] - 2025-3-4
|
||||
### Added
|
||||
- Integracion con Mercure. Subscriber tanto en "Trazas" con en "Clientes".
|
||||
- Nueva funcionalidad para checkear la integridad de una imagen. Boton en apartado "imagenes" dentro del repositorio.
|
||||
- Centralizacion de estilos.
|
||||
- Nueva funcionalidad para realizar backup de imágenes.
|
||||
- Botón para cancelar despliegues de imagenes. Aparece en "trazas" tan solo para los comendos "deploy" y para el estado "en progreso".
|
||||
|
||||
### Changed
|
||||
- Nueva interfaz en "Grupos". Se ha aprovechado mejor el espacio y acortado el tamaño de las filas, para poder tener mas elementos por pantalla.
|
||||
- Cambios en filtros de "Grupos". Ahora se pueden filtrar por "Centro" y "Unidad Organizativa" y estado. Ahora se busca en base de datos, y no en una lista de clientes dados.
|
||||
- Refactorizados compontentes de crear/editar clientes en uno solo.
|
||||
- Cambios en DHCP. Nueva UX en "ver clientes". Ahora tenemos un buscador detallado.
|
||||
- Para gestionar/añadir clientes a subredes ahora tenemos un botón para "añadir todos" y tan solo nos aparecn los equipos que no estén previamente asignados en una subred.
|
||||
|
||||
---
|
||||
## [0.7.0] - 2024-12-10
|
||||
### Refactored
|
||||
- Refactored the group screen, removing the separate tabs for clients, advanced search, and organizational units.
|
||||
- Added support for partitioning functionality in the client detail view.
|
||||
|
||||
---
|
||||
## [0.6.1] - 2024-11-19
|
||||
### Improved
|
||||
- Introduced a new automatic sync mode for the ogdhcp and ogBoot components.
|
||||
- Improve test coverage.
|
||||
- New view for clients inside the classroom on the main page.
|
||||
|
||||
---
|
||||
## [0.6.0] - 2024-11-19
|
||||
### Added
|
||||
- Added functionality to execute actions from the menu in the general groups screen.
|
||||
- Displayed the selected center on the general screen for better context.
|
||||
- Implemented the option to collapse the sidebar for improved usability.
|
||||
- Introduced a dropdown menu to select boot files (BootfileNames).
|
||||
- Simplified the process of adding multiple clients in the subnets section.
|
||||
- Enabled ogBoot to manage storage units more intuitively.
|
||||
- Added preloading of template models in ogBoot for faster setup.
|
||||
- Created a contextual help tour to guide users through various components and features.
|
||||
|
||||
### Improved
|
||||
- Renamed the field "Reserved Room" to "Available RemotePC" for better clarity.
|
||||
- Added view sizes for visual cards.
|
||||
- Refactored the task stepper to improve efficiency and readability.
|
||||
- Replaced red "X" icons with more consistent and user-friendly visuals.
|
||||
- Allowed logical names for IP addresses in the subnets section.
|
||||
- Enabled reordering of commands when creating a command group.
|
||||
- Made predefined commands read-only to prevent accidental modifications.
|
||||
- Simplified the task creation modal to enhance user experience.
|
||||
- Adjusted the translation system to cover new elements and improve consistency (work in progress).
|
||||
- New element view from clients on groups main view.
|
||||
|
||||
### Fixed
|
||||
- Resolved an issue that prevented editing software profiles correctly.
|
||||
- Fixed a bug where newly created commands failed to execute in the commands section.
|
|
@ -1,107 +0,0 @@
|
|||
@Library('jenkins-shared-library') _
|
||||
pipeline {
|
||||
agent {
|
||||
label 'jenkins-slave'
|
||||
}
|
||||
environment {
|
||||
DEBIAN_FRONTEND = 'noninteractive'
|
||||
DEFAULT_DEV_NAME = 'Opengnsys Team'
|
||||
DEFAULT_DEV_EMAIL = 'opengnsys@qindel.com'
|
||||
}
|
||||
options {
|
||||
skipDefaultCheckout()
|
||||
}
|
||||
parameters {
|
||||
string(name: 'DEV_NAME', defaultValue: '', description: 'Nombre del desarrollador')
|
||||
string(name: 'DEV_EMAIL', defaultValue: '', description: 'Email del desarrollador')
|
||||
}
|
||||
stages {
|
||||
stage('Prepare Workspace') {
|
||||
steps {
|
||||
script {
|
||||
env.BUILD_DIR = "${WORKSPACE}/oggui"
|
||||
sh "mkdir -p ${env.BUILD_DIR}"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Checkout') {
|
||||
steps {
|
||||
dir("${env.BUILD_DIR}") {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
stage('Generate Changelog') {
|
||||
when {
|
||||
expression {
|
||||
return env.TAG_NAME != null
|
||||
}
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME
|
||||
def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL
|
||||
|
||||
generateDebianChangelog(env.BUILD_DIR, devName, devEmail)
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Generate Changelog (Nightly)'){
|
||||
when {
|
||||
branch 'main'
|
||||
}
|
||||
steps {
|
||||
script {
|
||||
def devName = params.DEV_NAME ? params.DEV_NAME : env.DEFAULT_DEV_NAME
|
||||
def devEmail = params.DEV_EMAIL ? params.DEV_EMAIL : env.DEFAULT_DEV_EMAIL
|
||||
generateDebianChangelog(env.BUILD_DIR, devName, devEmail,"nightly")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
stage('Build') {
|
||||
steps {
|
||||
script {
|
||||
construirPaquete(env.BUILD_DIR, "../artifacts", "172.17.8.68", "/var/tmp/opengnsys/debian-repo/oggui")
|
||||
}
|
||||
}
|
||||
}
|
||||
stage ('Publish to Debian Repository') {
|
||||
when {
|
||||
expression {
|
||||
return env.TAG_NAME != null
|
||||
}
|
||||
}
|
||||
agent { label 'debian-repo' }
|
||||
steps {
|
||||
script {
|
||||
// Construir el patrón de versión esperado en el nombre del paquete
|
||||
def versionPattern = "${env.TAG_NAME}-${env.BUILD_NUMBER}"
|
||||
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'opengnsys-devel', versionPattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
stage ('Publish to Debian Repository (Nightly)') {
|
||||
when {
|
||||
branch 'main'
|
||||
}
|
||||
agent { label 'debian-repo' }
|
||||
steps {
|
||||
script {
|
||||
// Construir el patrón de versión esperado en el nombre del paquete
|
||||
def versionPattern = "-${env.BUILD_NUMBER}~nightly"
|
||||
publicarEnAptly('/var/tmp/opengnsys/debian-repo/oggui', 'nightly', versionPattern)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
notifyBuildStatus('narenas@qindel.com')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
CONFIG_FILE="/opt/opengnsys/oggui/src/.env"
|
||||
HASH_FILE="/opt/opengnsys/oggui/var/lib/oggui/oggui.config.hash"
|
||||
APP_DIR="/opt/opengnsys/oggui/browser"
|
||||
SRC_DIR="/opt/opengnsys/oggui/src"
|
||||
COMPILED_DIR=$SRC_DIR/dist
|
||||
NGINX_SERVICE="nginx"
|
||||
|
||||
# Verificar si el archivo de configuración cambió
|
||||
if [ -f "$CONFIG_FILE" ] && [ -f "$HASH_FILE" ]; then
|
||||
OLD_HASH=$(cat "$HASH_FILE")
|
||||
NEW_HASH=$(md5sum "$CONFIG_FILE")
|
||||
|
||||
if [ "$OLD_HASH" != "$NEW_HASH" ]; then
|
||||
echo "🔄 Cambios detectados en $CONFIG_FILE, recompilando Angular..."
|
||||
cd "$SRC_DIR"
|
||||
npm install -g @angular/cli
|
||||
npm install
|
||||
/usr/local/bin/ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production --localize=false
|
||||
md5sum "$CONFIG_FILE" > "$HASH_FILE"
|
||||
exit 0
|
||||
else
|
||||
echo "No hay cambios en $CONFIG_FILE, no es necesario recompilar."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "Archivo de configuración no encontrado o sin hash previo. No se recompilará."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Iniciar Nginx
|
||||
systemctl restart "$NGINX_SERVICE"
|
|
@ -1,14 +0,0 @@
|
|||
oggui (0.0.1-1) unstable; urgency=medium
|
||||
|
||||
* Add debian files
|
||||
* Update .gitignore
|
||||
* refs #1637 refactor: remove unused client edit and create components; add manage client component
|
||||
* refs #1619. Style: enhance cards view layout and paginator integration in groups component
|
||||
* refactor: update paginator settings and improve page change handling in groups component
|
||||
* Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
|
||||
* style: clean up and optimize CSS for groups component; enhance HTML structure and improve responsiveness
|
||||
* Updated groups paginator
|
||||
* Merge branch 'develop' of ssh://ognproject.evlt.uma.es:21987/opengnsys/oggui into develop
|
||||
* refs #1567. New subnet field: 'dns'
|
||||
|
||||
-- Tu Nombre <tuemail@example.com> Mon, 10 Mar 2025 14:48:36 +0000
|
|
@ -1 +0,0 @@
|
|||
12
|
|
@ -1,13 +0,0 @@
|
|||
Source: oggui
|
||||
Section: web
|
||||
Priority: optional
|
||||
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
|
||||
Build-Depends: debhelper (>= 12), nodejs, npm
|
||||
Standards-Version: 4.5.0
|
||||
|
||||
Package: oggui
|
||||
Architecture: any
|
||||
Maintainer: Nicolas Arenas <nicolas.arenas@qindel.com>
|
||||
Depends: ${shlibs:Depends}, ${misc:Depends}, nginx
|
||||
Description: OpenGnsys GUI created for the Opengnsys Team
|
||||
Opengnsys Graphical Intercface
|
|
@ -1 +0,0 @@
|
|||
oggui
|
|
@ -1,2 +0,0 @@
|
|||
oggui_1.0.1+deb-pkg20250310-1_amd64.buildinfo web optional
|
||||
oggui_1.0.1+deb-pkg20250310-1_amd64.deb web optional
|
|
@ -1,10 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
db_input high opengnsys/oggui_ogcoreUrl || true
|
||||
db_input high opengnsys/oggui_ogmercureUrl || true
|
||||
|
||||
db_go
|
|
@ -1,4 +0,0 @@
|
|||
ogWebconsole/dist/oggui/browser /opt/opengnsys/oggui/
|
||||
etc /opt/opengnsys/oggui/
|
||||
ogWebconsole/ssl/* /opt/opengnsys/oggui/etc/nginx/certs/
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
. /usr/share/debconf/confmodule
|
||||
|
||||
db_get opengnsys/oggui_ogcoreUrl
|
||||
OGCORE_URL="$RET"
|
||||
db_get opengnsys/oggui_ogmercureUrl
|
||||
OGMERCURE_URL="$RET"
|
||||
|
||||
# Asegurarse de que el usuario exista
|
||||
USER="opengnsys"
|
||||
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json"
|
||||
|
||||
restore_config_if_modified() {
|
||||
local new="$1"
|
||||
local backup="$1.bak"
|
||||
|
||||
if [ -f "$backup" ]; then
|
||||
if ! cmp -s "$new" "$backup"; then
|
||||
echo ">>> Archivo modificado por el usuario detectado en $new"
|
||||
echo " - Guardando archivo nuevo como ${new}.new"
|
||||
mv -f "$new" "${new}.new"
|
||||
echo " - Restaurando archivo anterior desde backup"
|
||||
mv -f "$backup" "$new"
|
||||
else
|
||||
echo ">>> El archivo $new no ha cambiado desde la última versión, eliminando backup"
|
||||
rm -f "$backup"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
|
||||
# Detectar si es una instalación nueva o una actualización
|
||||
if [ "$1" = "configure" ] && [ -z "$2" ]; then
|
||||
if [ ! -f "$CONFIG_FILE" ]; then
|
||||
jq --arg apiUrl "$OGCORE_URL" --arg mercureUrl "$OGMERCURE_URL" \
|
||||
'.apiUrl = $apiUrl | .mercureUrl = $mercureUrl' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE"
|
||||
fi
|
||||
ln -s /opt/opengnsys/oggui/etc/nginx/oggui.conf /etc/nginx/sites-enabled/oggui.conf
|
||||
ln -s $CONFIG_FILE /opt/opengnsys/oggui/etc/config.json
|
||||
mkdir -p /etc/nginx/certs/
|
||||
cp -p /opt/opengnsys/oggui/etc/nginx/certs/* /etc/nginx/certs/
|
||||
chown -R www-data:www-data /etc/nginx/certs
|
||||
systemctl daemon-reload
|
||||
systemctl restart nginx
|
||||
elif [ "$1" = "configure" ] && [ -n "$2" ]; then
|
||||
cd /opt/opengnsys/oggui
|
||||
echo "Actualización desde la versión $2"
|
||||
# Si upgrade recupero los archivos de configuracion
|
||||
echo ">>> Backup de archivos de configuración reales en /opt/opengnsys"
|
||||
restore_config_if_modified "/opt/opengnsys/oggui/etc/nginx/oggui.conf"
|
||||
restore_config_if_modified "$CONFIG_FILE"
|
||||
|
||||
fi
|
||||
|
||||
# Cambiar la propiedad de los archivos al usuario especificado
|
||||
chown opengnsys:www-data /opt/opengnsys/
|
||||
chown -R opengnsys:www-data /opt/opengnsys/oggui
|
||||
exit 0
|
|
@ -1,32 +0,0 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
NGINX_FILE="/etc/nginx/sites-enabled/oggui.conf"
|
||||
UNIT_FILE="/etc/systemd/system/oggui.service"
|
||||
|
||||
|
||||
case "$1" in
|
||||
remove)
|
||||
echo "El paquete se está desinstalando..."
|
||||
# Aquí puedes hacer limpieza de archivos o servicios
|
||||
if [ -L "$NGINX_FILE" ]; then
|
||||
rm -f "$NGINX_FILE"
|
||||
systemctl restart nginx
|
||||
fi
|
||||
if [ -L "$UNIT_FILE" ]; then
|
||||
rm -f "$UNIT_FILE"
|
||||
systemctl daemon-reload
|
||||
fi
|
||||
;;
|
||||
purge)
|
||||
echo "Eliminando configuración residual..."
|
||||
;;
|
||||
|
||||
upgrade)
|
||||
echo "Actualizando paquete..."
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
|
@ -1,6 +0,0 @@
|
|||
# Automatically added by dh_installdebconf/13.14.1ubuntu5
|
||||
if [ "$1" = purge ] && [ -e /usr/share/debconf/confmodule ]; then
|
||||
. /usr/share/debconf/confmodule
|
||||
db_purge
|
||||
fi
|
||||
# End automatically added section
|
|
@ -1,32 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
backup_file_if_exists() {
|
||||
local original="$1"
|
||||
local backup="$1.bak"
|
||||
|
||||
if [ -e "$original" ]; then
|
||||
echo " - Guardando backup de $original en $backup"
|
||||
cp -a "$original" "$backup"
|
||||
fi
|
||||
}
|
||||
CONFIG_FILE="/opt/opengnsys/oggui/browser/assets/config.json"
|
||||
# Asegurarse de que el usuario exista
|
||||
USER="opengnsys"
|
||||
HOME_DIR="/opt/opengnsys"
|
||||
if id "$USER" &>/dev/null; then
|
||||
echo "El usuario $USER ya existe."
|
||||
else
|
||||
echo "Creando el usuario $USER con home en $HOME_DIR."
|
||||
useradd -m -d "$HOME_DIR" -s /bin/bash "$USER"
|
||||
fi
|
||||
|
||||
# Si upgrade hago backup del archivo de configuración
|
||||
if [ "$1" = "upgrade" ]; then
|
||||
echo ">>> Backup de archivos de configuración reales en /opt/opengnsys"
|
||||
backup_file_if_exists "/opt/opengnsys/oggui/etc/nginx/sites-available/oggui.conf"
|
||||
backup_file_if_exists "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
exit 0
|
|
@ -1,13 +0,0 @@
|
|||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
# Solo eliminar archivos de configuración si se está eliminando el paquete
|
||||
if [ "$1" = "remove" ] || [ "$1" = "purge" ]; then
|
||||
rm -f /etc/nginx/sites-enabled/oggui.conf
|
||||
systemctl daemon-reload
|
||||
systemctl restart nginx
|
||||
fi
|
||||
|
||||
exit 0
|
|
@ -1,9 +0,0 @@
|
|||
Template: opengnsys/oggui_ogcoreUrl
|
||||
Type: string
|
||||
Default: https://127.0.0.1:8443
|
||||
Description: Introduzca la URL delAPI de OgCore
|
||||
|
||||
Template: opengnsys/oggui_ogmercureUrl
|
||||
Type: string
|
||||
Default: https://127.0.0.1:3000/.well-known/mercure
|
||||
Description: Introduzca el endpoint de mercure
|
|
@ -1,11 +0,0 @@
|
|||
#!/usr/bin/make -f
|
||||
|
||||
%:
|
||||
dh $@
|
||||
|
||||
override_dh_auto_build:
|
||||
cd ogWebconsole && npm install
|
||||
cd ogWebconsole && npx ng build --base-href=/ --output-path=dist/oggui --optimization=true --configuration=production
|
||||
|
||||
override_dh_auto_install:
|
||||
dh_auto_install
|
|
@ -1,20 +0,0 @@
|
|||
server {
|
||||
listen 4200 ssl;
|
||||
server_name localhost;
|
||||
|
||||
root /opt/opengnsys/oggui/browser;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# Manejo de archivos estáticos
|
||||
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
|
||||
try_files $uri =404;
|
||||
}
|
||||
ssl_certificate /opt/opengnsys/oggui/etc/nginx/certs/oggui.uds-test.net.crt.pem;
|
||||
ssl_certificate_key /opt/opengnsys/oggui/etc/nginx/certs/oggui.uds-test.net.key.pem;
|
||||
# Configuración para evitar problemas con rutas de Angular
|
||||
error_page 404 /index.html;
|
||||
}
|
|
@ -1,2 +1,2 @@
|
|||
# NG_APP_BASE_API_URL=https://127.0.0.1:8443
|
||||
# NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure
|
||||
#NG_APP_BASE_API_URL=http://127.0.0.1:8090
|
||||
NG_APP_BASE_API_URL=http://127.0.0.1:8001
|
|
@ -1,2 +0,0 @@
|
|||
NG_APP_BASE_API_URL=https://localhost:8443
|
||||
NG_APP_OGCORE_MERCURE_BASE_URL=http://localhost:3000/.well-known/mercure
|
|
@ -41,5 +41,3 @@ testem.log
|
|||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
test-results/
|
||||
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
FROM node:22.10-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN npm install -g npm@latest
|
||||
|
||||
RUN npm install -g @angular/cli@^12.0.0
|
||||
|
||||
COPY . /app
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 4200
|
||||
|
||||
CMD ["ng", "serve", "--host", "0.0.0.0", "--disable-host-check"]
|
|
@ -1,15 +0,0 @@
|
|||
FROM node:22.10
|
||||
|
||||
WORKDIR /app
|
||||
RUN apt -y update && apt -y install chromium
|
||||
|
||||
RUN npm install -g npm@latest
|
||||
|
||||
RUN npm install -g @angular/cli@^12.0.0
|
||||
|
||||
COPY . /app
|
||||
RUN npm install
|
||||
|
||||
EXPOSE 4200
|
||||
|
||||
CMD ["ng", "serve", "--host", "0.0.0.0", "--disable-host-check"]
|
|
@ -1,111 +0,0 @@
|
|||
pipeline {
|
||||
agent { label 'jenkins-slave' }
|
||||
environment {
|
||||
DOCKER_REPO = "opengnsys"
|
||||
DOCKER_CREDENTIALS = credentials('docker-hub-credentials')
|
||||
DOCKER_IMAGE_NAME = "oggui"
|
||||
}
|
||||
stages {
|
||||
stage ('Checkout') {
|
||||
steps {
|
||||
checkout scm
|
||||
}
|
||||
}
|
||||
stage('Build Testing Image') {
|
||||
steps {
|
||||
sh "printenv"
|
||||
echo 'Building....'
|
||||
script {
|
||||
def DOCKER_TAG = "${env.BUILD_NUMBER}"
|
||||
|
||||
dir('ogWebconsole') {
|
||||
IMAGE_ID = "${DOCKER_REPO}/${DOCKER_IMAGE_NAME}:${BRANCH_NAME}-${DOCKER_TAG}"
|
||||
IMAGE_ID_TESTING = "${DOCKER_REPO}/${DOCKER_IMAGE_NAME}:${BRANCH_NAME}-${DOCKER_TAG}-testing"
|
||||
if (BRANCH_NAME == 'main') {
|
||||
LATEST_ID = "${DOCKER_REPO}/${DOCKER_IMAGE_NAME}:latest"
|
||||
} else {
|
||||
LATEST_ID = "${DOCKER_REPO}/${DOCKER_IMAGE_NAME}:${BRANCH_NAME}-latest"
|
||||
}
|
||||
|
||||
env.IMAGE_ID_TESTING = IMAGE_ID_TESTING
|
||||
env.IMAGE_ID = IMAGE_ID
|
||||
env.LATEST_ID = LATEST_ID
|
||||
docker.build("${IMAGE_ID_TESTING}", "-f Dockerfile-testing .")
|
||||
if (env.TAG_NAME) {
|
||||
TAG_ID = "${DOCKER_REPO}/${DOCKER_IMAGE_NAME}:${env.TAG_NAME}"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Testing') {
|
||||
steps {
|
||||
echo 'Running Tests....'
|
||||
sh '''
|
||||
cd ogWebconsole
|
||||
docker run -p 4200:4200 --name oggui-testing -e CHROME_BIN=/usr/bin/chromium -v $(pwd)/karma.conf.js:/app/karma.conf.js -v $(pwd)/.env:/app/.env -d $IMAGE_ID_TESTING
|
||||
docker exec oggui-testing ng test --watch=false --source-map=false --karma-config=karma.conf.js
|
||||
'''
|
||||
}
|
||||
}
|
||||
stage('Build') {
|
||||
steps {
|
||||
echo 'Building....'
|
||||
script {
|
||||
dir('ogWebconsole') {
|
||||
docker.build("${IMAGE_ID}", "-f Dockerfile .")
|
||||
docker.build("${LATEST_ID}", "-f Dockerfile .")
|
||||
if (env.TAG_NAME) {
|
||||
docker.build("${TAG_ID}", "-f Dockerfile .")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
stage('Push') {
|
||||
steps {
|
||||
echo 'Pushing....'
|
||||
script {
|
||||
docker.withRegistry('https://index.docker.io/v1/', 'docker-hub-credentials') {
|
||||
docker.image("${IMAGE_ID}").push()
|
||||
docker.image("${LATEST_ID}").push()
|
||||
if (env.TAG_NAME) {
|
||||
docker.image("${TAG_ID}").push()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post {
|
||||
always {
|
||||
echo 'Get test results....'
|
||||
sh "mkdir -p test-results"
|
||||
sh "docker cp oggui-testing:/app/test-results/ogGui-junit-report.xml ./test-results/ogGui-junit-report.xml"
|
||||
junit '**/test-results/*.xml'
|
||||
echo 'Cleaning up....'
|
||||
sh "docker stop oggui-testing"
|
||||
sh "docker rm oggui-testing"
|
||||
sh "docker rmi ${IMAGE_ID} || true"
|
||||
sh "docker rmi ${LATEST_ID} || true"
|
||||
sh "docker rmi ${IMAGE_ID_TESTING} || true"
|
||||
script {
|
||||
def committerEmail = sh (
|
||||
script: "git show -s --pretty=%ae",
|
||||
returnStdout: true
|
||||
).trim()
|
||||
def buildResult = currentBuild.currentResult
|
||||
mail to: committerEmail,
|
||||
subject: "Opengnsys CI Build ${env.JOB_NAME} - ${env.BRANCH_NAME} - ${buildResult}",
|
||||
body: """
|
||||
<h1>Opengnsys CI Build ${JOB_NAME} - ${BRANCH_NAME} - ${buildResult}</h1>
|
||||
<p>Build Number: ${BUILD_NUMBER}</p>
|
||||
<p>Build URL: ${BUILD_URL}</p>º
|
||||
|
||||
Saludos cordiales,
|
||||
Opengnsys CI
|
||||
"""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -4,6 +4,12 @@
|
|||
"newProjectRoot": "projects",
|
||||
"projects": {
|
||||
"ogWebconsole": {
|
||||
"i18n": {
|
||||
"sourceLocale": "es",
|
||||
"locales": {
|
||||
"en-US": "src/locale/messages.en.json"
|
||||
}
|
||||
},
|
||||
"projectType": "application",
|
||||
"schematics": {
|
||||
"@schematics/angular:component": {
|
||||
|
@ -23,8 +29,7 @@
|
|||
"build": {
|
||||
"builder": "@ngx-env/builder:application",
|
||||
"options": {
|
||||
"baseHref": "/oggui/",
|
||||
"localize": false,
|
||||
"localize": true,
|
||||
"aot": true,
|
||||
"outputPath": "dist/og-webconsole",
|
||||
"index": "src/index.html",
|
||||
|
@ -36,35 +41,27 @@
|
|||
"tsConfig": "tsconfig.app.json",
|
||||
"assets": [
|
||||
"src/favicon.ico",
|
||||
"src/assets",
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/locale",
|
||||
"output": "/locale"
|
||||
}
|
||||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/custom-theme.scss",
|
||||
"src/styles.css",
|
||||
"node_modules/ngx-toastr/toastr.css"
|
||||
],
|
||||
"scripts": [],
|
||||
"allowedCommonJsDependencies": [
|
||||
"rfdc"
|
||||
]
|
||||
"scripts": []
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"budgets": [
|
||||
{
|
||||
"type": "initial",
|
||||
"maximumWarning": "4mb",
|
||||
"maximumError": "8mb"
|
||||
"maximumWarning": "500kb",
|
||||
"maximumError": "1mb"
|
||||
},
|
||||
{
|
||||
"type": "anyComponentStyle",
|
||||
"maximumWarning": "7kb",
|
||||
"maximumError": "10kb"
|
||||
"maximumWarning": "2kb",
|
||||
"maximumError": "4kb"
|
||||
}
|
||||
],
|
||||
"outputHashing": "all"
|
||||
|
@ -72,35 +69,52 @@
|
|||
"development": {
|
||||
"optimization": false,
|
||||
"extractLicenses": false,
|
||||
"sourceMap": false
|
||||
"sourceMap": true
|
||||
},
|
||||
"es": {
|
||||
"localize": [
|
||||
"es-ES"
|
||||
]
|
||||
},
|
||||
"en": {
|
||||
"localize": [
|
||||
"en-US"
|
||||
]
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "production"
|
||||
},
|
||||
"serve": {
|
||||
"builder": "@ngx-env/builder:dev-server",
|
||||
"options": {
|
||||
"port": 4200,
|
||||
"ssl": true,
|
||||
"sslKey": "./ssl/oggui.uds-test.net.key.pem",
|
||||
"sslCert": "./ssl/oggui.uds-test.net.crt.pem"
|
||||
},
|
||||
"configurations": {
|
||||
"production": {
|
||||
"buildTarget": "ogWebconsole:build:production"
|
||||
},
|
||||
"development": {
|
||||
"buildTarget": "ogWebconsole:build:development"
|
||||
},
|
||||
"es": {
|
||||
"buildTarget": "ogWebconsole:build:es"
|
||||
},
|
||||
"en": {
|
||||
"buildTarget": "ogWebconsole:build:en"
|
||||
}
|
||||
},
|
||||
"defaultConfiguration": "development"
|
||||
},
|
||||
"extract-i18n": {
|
||||
"builder": "@ngx-env/builder:extract-i18n",
|
||||
"options": {
|
||||
"buildTarget": "ogWebconsole:build"
|
||||
}
|
||||
},
|
||||
"test": {
|
||||
"builder": "@ngx-env/builder:karma",
|
||||
"options": {
|
||||
"polyfills": [
|
||||
"zone.js",
|
||||
"zone.js/testing"
|
||||
"zone.js/testing",
|
||||
"@angular/localize/init"
|
||||
],
|
||||
"tsConfig": "tsconfig.spec.json",
|
||||
"assets": [
|
||||
|
@ -108,8 +122,7 @@
|
|||
"src/assets"
|
||||
],
|
||||
"styles": [
|
||||
"src/styles.css",
|
||||
"src/custom-theme.scss"
|
||||
"src/styles.css"
|
||||
],
|
||||
"scripts": []
|
||||
}
|
||||
|
|
|
@ -1,40 +0,0 @@
|
|||
module.exports = function(config) {
|
||||
config.set({
|
||||
frameworks: ['jasmine', '@angular-devkit/build-angular'],
|
||||
|
||||
plugins: [
|
||||
require('karma-jasmine'),
|
||||
require('karma-chrome-launcher'),
|
||||
require('karma-jasmine-html-reporter'),
|
||||
require('karma-coverage'),
|
||||
require('karma-junit-reporter'),
|
||||
require('@angular-devkit/build-angular/plugins/karma')
|
||||
],
|
||||
|
||||
client: {
|
||||
clearContext: false
|
||||
},
|
||||
|
||||
reporters: ['progress', 'kjhtml', 'junit'],
|
||||
|
||||
junitReporter: {
|
||||
outputDir: 'test-results',
|
||||
outputFile: 'ogGui-junit-report.xml',
|
||||
useBrowserName: false,
|
||||
},
|
||||
|
||||
port: 9876,
|
||||
colors: true,
|
||||
logLevel: config.LOG_INFO,
|
||||
autoWatch: true,
|
||||
browsers: ['ChromeHeadlessNoSandbox'],
|
||||
customLaunchers: {
|
||||
ChromeHeadlessNoSandbox: {
|
||||
base: 'ChromeHeadless',
|
||||
flags: ['--no-sandbox','--disable-setuid-sandbox']
|
||||
}
|
||||
},
|
||||
singleRun: false,
|
||||
restartOnFileChange: true
|
||||
});
|
||||
};
|
|
@ -1,12 +1,12 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "0.5.0",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "og-webconsole",
|
||||
"version": "0.5.0",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@angular/animations": "^18.0.0",
|
||||
"@angular/cdk": "~18.0.0",
|
||||
|
@ -18,13 +18,9 @@
|
|||
"@angular/platform-browser": "^18.0.0",
|
||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||
"@angular/router": "^18.0.0",
|
||||
"@ngx-translate/core": "^16.0.3",
|
||||
"@ngx-translate/http-loader": "^16.0.0",
|
||||
"@swimlane/ngx-charts": "^20.5.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"ngx-joyride": "^2.5.0",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "^0.14.6"
|
||||
|
@ -36,14 +32,12 @@
|
|||
"@angular/localize": "^18.1.0",
|
||||
"@ngx-env/builder": "^18.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"karma-junit-reporter": "^2.0.1",
|
||||
"typescript": "~5.4.5"
|
||||
}
|
||||
},
|
||||
|
@ -4980,30 +4974,6 @@
|
|||
"url": "https://github.com/sponsors/isaacs"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/core": {
|
||||
"version": "16.0.3",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/core/-/core-16.0.3.tgz",
|
||||
"integrity": "sha512-UPse66z9tRUmIpeorYodXBQY6O4foUmj9jy9cCuuja7lqdOwRBWPzCWqc+qYIXv5L2QoqZdxgHtqoUz+Q9weSA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16",
|
||||
"@angular/core": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@ngx-translate/http-loader": {
|
||||
"version": "16.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ngx-translate/http-loader/-/http-loader-16.0.0.tgz",
|
||||
"integrity": "sha512-l3okOHGVxZ1Bm55OpakSfXvI2yYmVmhYqgwGU4aIQIRUqpkBCrSDZnmrHTcZfsGJzXKB5E2D2rko9i28gBijmA==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.3.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=16",
|
||||
"@angular/core": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@nodelib/fs.scandir": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||
|
@ -5904,15 +5874,6 @@
|
|||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/papaparse": {
|
||||
"version": "5.3.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/papaparse/-/papaparse-5.3.15.tgz",
|
||||
"integrity": "sha512-JHe6vF6x/8Z85nCX4yFdDslN11d+1pr12E526X8WAfhadOeaOTx5AuIkvDKIBopfvlzpzkdMx4YyvSKCM9oqtw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/qs": {
|
||||
"version": "6.9.15",
|
||||
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.15.tgz",
|
||||
|
@ -6571,9 +6532,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/body-parser": {
|
||||
"version": "1.20.3",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz",
|
||||
"integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==",
|
||||
"version": "1.20.2",
|
||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz",
|
||||
"integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"bytes": "3.1.2",
|
||||
|
@ -6584,7 +6545,7 @@
|
|||
"http-errors": "2.0.0",
|
||||
"iconv-lite": "0.4.24",
|
||||
"on-finished": "2.4.1",
|
||||
"qs": "6.13.0",
|
||||
"qs": "6.11.0",
|
||||
"raw-body": "2.5.2",
|
||||
"type-is": "~1.6.18",
|
||||
"unpipe": "1.0.0"
|
||||
|
@ -7259,9 +7220,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/cookie": {
|
||||
"version": "0.7.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
|
||||
"integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
|
||||
"version": "0.4.2",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz",
|
||||
"integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
|
@ -8119,9 +8080,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/engine.io": {
|
||||
"version": "6.6.2",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz",
|
||||
"integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==",
|
||||
"version": "6.5.5",
|
||||
"resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz",
|
||||
"integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/cookie": "^0.4.1",
|
||||
|
@ -8129,7 +8090,7 @@
|
|||
"@types/node": ">=10.0.0",
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "2.0.0",
|
||||
"cookie": "~0.7.2",
|
||||
"cookie": "~0.4.1",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.1",
|
||||
"engine.io-parser": "~5.2.1",
|
||||
|
@ -8435,37 +8396,37 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/express": {
|
||||
"version": "4.21.1",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz",
|
||||
"integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==",
|
||||
"version": "4.19.2",
|
||||
"resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz",
|
||||
"integrity": "sha512-5T6nhjsT+EOMzuck8JjBHARTHfMht0POzlA60WV2pMD3gyXw2LZnZ+ueGdNxG+0calOJcWKbpFcuzLZ91YWq9Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.8",
|
||||
"array-flatten": "1.1.1",
|
||||
"body-parser": "1.20.3",
|
||||
"body-parser": "1.20.2",
|
||||
"content-disposition": "0.5.4",
|
||||
"content-type": "~1.0.4",
|
||||
"cookie": "0.7.1",
|
||||
"cookie": "0.6.0",
|
||||
"cookie-signature": "1.0.6",
|
||||
"debug": "2.6.9",
|
||||
"depd": "2.0.0",
|
||||
"encodeurl": "~2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"etag": "~1.8.1",
|
||||
"finalhandler": "1.3.1",
|
||||
"finalhandler": "1.2.0",
|
||||
"fresh": "0.5.2",
|
||||
"http-errors": "2.0.0",
|
||||
"merge-descriptors": "1.0.3",
|
||||
"merge-descriptors": "1.0.1",
|
||||
"methods": "~1.1.2",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
"path-to-regexp": "0.1.10",
|
||||
"path-to-regexp": "0.1.7",
|
||||
"proxy-addr": "~2.0.7",
|
||||
"qs": "6.13.0",
|
||||
"qs": "6.11.0",
|
||||
"range-parser": "~1.2.1",
|
||||
"safe-buffer": "5.2.1",
|
||||
"send": "0.19.0",
|
||||
"serve-static": "1.16.2",
|
||||
"send": "0.18.0",
|
||||
"serve-static": "1.15.0",
|
||||
"setprototypeof": "1.2.0",
|
||||
"statuses": "2.0.1",
|
||||
"type-is": "~1.6.18",
|
||||
|
@ -8477,9 +8438,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/express/node_modules/cookie": {
|
||||
"version": "0.7.1",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz",
|
||||
"integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==",
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/cookie/-/cookie-0.6.0.tgz",
|
||||
"integrity": "sha512-U71cyTamuh1CRNCfpGY6to28lxvNwPG4Guz/EVjgf3Jmzv0vlDp1atT9eS5dDjMYHucpHbWns6Lwf3BKz6svdw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
|
@ -8494,23 +8455,14 @@
|
|||
"ms": "2.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/express/node_modules/finalhandler": {
|
||||
"version": "1.3.1",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz",
|
||||
"integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==",
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz",
|
||||
"integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
"encodeurl": "~2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"on-finished": "2.4.1",
|
||||
"parseurl": "~1.3.3",
|
||||
|
@ -10075,22 +10027,6 @@
|
|||
"integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/karma-junit-reporter": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/karma-junit-reporter/-/karma-junit-reporter-2.0.1.tgz",
|
||||
"integrity": "sha512-VtcGfE0JE4OE1wn0LK8xxDKaTP7slN8DO3I+4xg6gAi1IoAHAXOJ1V9G/y45Xg6sxdxPOR3THCFtDlAfBo9Afw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"path-is-absolute": "^1.0.0",
|
||||
"xmlbuilder": "12.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"karma": ">=0.9"
|
||||
}
|
||||
},
|
||||
"node_modules/karma-source-map-support": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz",
|
||||
|
@ -10830,13 +10766,10 @@
|
|||
}
|
||||
},
|
||||
"node_modules/merge-descriptors": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz",
|
||||
"integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==",
|
||||
"dev": true,
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz",
|
||||
"integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/merge-stream": {
|
||||
"version": "2.0.0",
|
||||
|
@ -10863,9 +10796,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/micromatch": {
|
||||
"version": "4.0.8",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
|
||||
"integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
|
||||
"version": "4.0.7",
|
||||
"resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.7.tgz",
|
||||
"integrity": "sha512-LPP/3KorzCwBxfeUuZmaR6bG2kdeHSbe0P2tY3FLRU4vYrjYz5hI4QZwV0njUx3jeuKe67YukQ1LSPZBKDqO/Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"braces": "^3.0.3",
|
||||
|
@ -11278,18 +11211,6 @@
|
|||
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/ngx-joyride": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-joyride/-/ngx-joyride-2.5.0.tgz",
|
||||
"integrity": "sha512-C/J8C4uWZjKl9aMmRBt9egVjuIpwWFplJgBZDl1EfqNVTJkdEC51nt9DpAOuDwOgkbArhJ9sZIk3bZT4vkud/w==",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@angular/common": ">=8.2.14",
|
||||
"@angular/core": ">=8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/ngx-toastr": {
|
||||
"version": "19.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ngx-toastr/-/ngx-toastr-19.0.0.tgz",
|
||||
|
@ -11639,9 +11560,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/object-inspect": {
|
||||
"version": "1.13.3",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz",
|
||||
"integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==",
|
||||
"version": "1.13.2",
|
||||
"resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz",
|
||||
"integrity": "sha512-IRZSRuzJiynemAXPYtPe5BoI/RESNYR7TYm50MC5Mqbd3Jmw5y790sErYw3V6SryFJD64b74qQQs9wn5Bg/k3g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
|
@ -11966,11 +11887,6 @@
|
|||
"node": "^16.14.0 || >=18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/papaparse": {
|
||||
"version": "5.4.1",
|
||||
"resolved": "https://registry.npmjs.org/papaparse/-/papaparse-5.4.1.tgz",
|
||||
"integrity": "sha512-HipMsgJkZu8br23pW15uvo6sib6wne/4woLZPlFf3rpDyMe9ywEXUsuD7+6K9PRkJlVT51j/sCOYDKGGS3ZJrw=="
|
||||
},
|
||||
"node_modules/parent-module": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz",
|
||||
|
@ -12119,9 +12035,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/path-to-regexp": {
|
||||
"version": "0.1.10",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz",
|
||||
"integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==",
|
||||
"version": "0.1.7",
|
||||
"resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz",
|
||||
"integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/path-type": {
|
||||
|
@ -12410,12 +12326,12 @@
|
|||
}
|
||||
},
|
||||
"node_modules/qs": {
|
||||
"version": "6.13.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz",
|
||||
"integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==",
|
||||
"version": "6.11.0",
|
||||
"resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz",
|
||||
"integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"side-channel": "^1.0.6"
|
||||
"side-channel": "^1.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.6"
|
||||
|
@ -12990,9 +12906,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/send": {
|
||||
"version": "0.19.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
|
||||
"integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
|
||||
"version": "0.18.0",
|
||||
"resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
|
||||
"integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"debug": "2.6.9",
|
||||
|
@ -13134,29 +13050,20 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/serve-static": {
|
||||
"version": "1.16.2",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
|
||||
"integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
|
||||
"version": "1.15.0",
|
||||
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
|
||||
"integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"encodeurl": "~2.0.0",
|
||||
"encodeurl": "~1.0.2",
|
||||
"escape-html": "~1.0.3",
|
||||
"parseurl": "~1.3.3",
|
||||
"send": "0.19.0"
|
||||
"send": "0.18.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/serve-static/node_modules/encodeurl": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
|
||||
"integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/set-function-length": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
|
||||
|
@ -13320,16 +13227,16 @@
|
|||
}
|
||||
},
|
||||
"node_modules/socket.io": {
|
||||
"version": "4.8.1",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz",
|
||||
"integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==",
|
||||
"version": "4.7.5",
|
||||
"resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz",
|
||||
"integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"accepts": "~1.3.4",
|
||||
"base64id": "~2.0.0",
|
||||
"cors": "~2.8.5",
|
||||
"debug": "~4.3.2",
|
||||
"engine.io": "~6.6.0",
|
||||
"engine.io": "~6.5.2",
|
||||
"socket.io-adapter": "~2.5.2",
|
||||
"socket.io-parser": "~4.2.4"
|
||||
},
|
||||
|
@ -14545,9 +14452,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/webpack-dev-server/node_modules/http-proxy-middleware": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz",
|
||||
"integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==",
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.6.tgz",
|
||||
"integrity": "sha512-ya/UeJ6HVBYxrgYotAZo1KvPWlgB48kUJLDePFeneHsVujFaW5WNj2NgWCAE//B1Dl02BIfYlpNgBy8Kf8Rjmw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/http-proxy": "^1.17.8",
|
||||
|
@ -14924,15 +14831,6 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/xmlbuilder": {
|
||||
"version": "12.0.0",
|
||||
"resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-12.0.0.tgz",
|
||||
"integrity": "sha512-lMo8DJ8u6JRWp0/Y4XLa/atVDr75H9litKlb2E5j3V3MesoL50EBgZDWoLT3F/LztVnG67GjPXLZpqcky/UMnQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/y18n": {
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "og-webconsole",
|
||||
"version": "0.5.0",
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"ng": "ng",
|
||||
"start": "ng serve",
|
||||
|
@ -20,13 +20,9 @@
|
|||
"@angular/platform-browser": "^18.0.0",
|
||||
"@angular/platform-browser-dynamic": "^18.0.0",
|
||||
"@angular/router": "^18.0.0",
|
||||
"@ngx-translate/core": "^16.0.3",
|
||||
"@ngx-translate/http-loader": "^16.0.0",
|
||||
"@swimlane/ngx-charts": "^20.5.0",
|
||||
"jwt-decode": "^4.0.0",
|
||||
"ngx-joyride": "^2.5.0",
|
||||
"ngx-toastr": "^19.0.0",
|
||||
"papaparse": "^5.4.1",
|
||||
"rxjs": "~7.8.0",
|
||||
"tslib": "^2.3.0",
|
||||
"zone.js": "^0.14.6"
|
||||
|
@ -38,14 +34,12 @@
|
|||
"@angular/localize": "^18.1.0",
|
||||
"@ngx-env/builder": "^18.0.1",
|
||||
"@types/jasmine": "~5.1.0",
|
||||
"@types/papaparse": "^5.3.15",
|
||||
"jasmine-core": "~5.1.0",
|
||||
"karma": "~6.4.0",
|
||||
"karma-chrome-launcher": "~3.2.0",
|
||||
"karma-coverage": "~2.2.0",
|
||||
"karma-jasmine": "~5.1.0",
|
||||
"karma-jasmine-html-reporter": "~2.1.0",
|
||||
"karma-junit-reporter": "^2.0.1",
|
||||
"typescript": "~5.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,82 +9,45 @@ import { AdminComponent } from './components/admin/admin.component';
|
|||
import { UsersComponent } from './components/admin/users/users/users.component';
|
||||
import { RolesComponent } from './components/admin/roles/roles/roles.component';
|
||||
import { GroupsComponent } from './components/groups/groups.component';
|
||||
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
|
||||
import { ImagesComponent } from './components/ogboot/images/images.component';
|
||||
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
||||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||
import {OgbootStatusComponent} from "./components/ogboot/ogboot-status/ogboot-status.component";
|
||||
import { CalendarComponent } from "./components/calendar/calendar.component";
|
||||
import { OgdhcpComponent } from './components/ogdhcp/ogdhcp.component';
|
||||
import { OgDhcpSubnetsComponent } from './components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component';
|
||||
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
||||
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
||||
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import {SoftwareComponent} from "./components/software/software.component";
|
||||
import {SoftwareProfileComponent} from "./components/software-profile/software-profile.component";
|
||||
import {OperativeSystemComponent} from "./components/operative-system/operative-system.component";
|
||||
import {
|
||||
PartitionAssistantComponent
|
||||
} from "./components/groups/components/client-main-view/partition-assistant/partition-assistant.component";
|
||||
import {RepositoriesComponent} from "./components/repositories/repositories.component";
|
||||
import {
|
||||
CreateClientImageComponent
|
||||
} from "./components/groups/components/client-main-view/create-image/create-image.component";
|
||||
import {
|
||||
DeployImageComponent
|
||||
} from "./components/groups/components/client-main-view/deploy-image/deploy-image.component";
|
||||
import {
|
||||
MainRepositoryViewComponent
|
||||
} from "./components/repositories/main-repository-view/main-repository-view.component";
|
||||
import {EnvVarsComponent} from "./components/admin/env-vars/env-vars.component";
|
||||
import {MenusComponent} from "./components/menus/menus.component";
|
||||
import {OgDhcpSubnetsComponent} from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import {StatusComponent} from "./components/ogdhcp/status/status.component";
|
||||
import {
|
||||
RunScriptAssistantComponent
|
||||
} from "./components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component";
|
||||
const routes: Routes = [
|
||||
{ path: '', redirectTo: 'auth/login', pathMatch: 'full' },
|
||||
{ path: '', component: MainLayoutComponent,
|
||||
children: [
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'admin', component: AdminComponent },
|
||||
{ path: 'users', component: UsersComponent },
|
||||
{ path: 'env-vars', component: EnvVarsComponent },
|
||||
{ path: 'user-groups', component: RolesComponent },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'pxe-images', component: PXEimagesComponent },
|
||||
{ path: 'pxe', component: PxeComponent },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||
{ path: 'subnets', component: OgDhcpSubnetsComponent },
|
||||
{ path: 'ogdhcp-status', component: StatusComponent },
|
||||
{ path: 'commands', component: CommandsComponent },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
{ path: 'commands-logs', component: TaskLogsComponent },
|
||||
{ path: 'calendars', component: CalendarComponent },
|
||||
{ path: 'clients/deploy-image', component: DeployImageComponent },
|
||||
{ path: 'clients/partition-assistant', component: PartitionAssistantComponent },
|
||||
{ path: 'clients/run-script', component: RunScriptAssistantComponent },
|
||||
{ path: 'clients/:id', component: ClientMainViewComponent },
|
||||
{ path: 'clients/:id/create-image', component: CreateClientImageComponent },
|
||||
{ path: 'repositories', component: RepositoriesComponent },
|
||||
{ path: 'repository/:id', component: MainRepositoryViewComponent },
|
||||
{ path: 'software', component: SoftwareComponent },
|
||||
{ path: 'software-profiles', component: SoftwareProfileComponent },
|
||||
{ path: 'operative-systems', component: OperativeSystemComponent },
|
||||
{ path: 'menus', component: MenusComponent },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
],
|
||||
},
|
||||
{ path: '**', component: PageNotFoundComponent },
|
||||
{
|
||||
path: '',
|
||||
component: MainLayoutComponent,
|
||||
children: [
|
||||
{ path: 'dashboard', component: DashboardComponent },
|
||||
{ path: 'admin', component: AdminComponent },
|
||||
{ path: 'users', component: UsersComponent },
|
||||
{ path: 'user-groups', component: RolesComponent },
|
||||
{ path: 'groups', component: GroupsComponent },
|
||||
{ path: 'images', component: ImagesComponent },
|
||||
{ path: 'pxe', component: PxeComponent },
|
||||
{ path: 'pxe-boot-file', component: PxeBootFilesComponent },
|
||||
{ path: 'ogboot-status', component: OgbootStatusComponent },
|
||||
{ path: 'dhcp', component: OgdhcpComponent },
|
||||
{ path: 'dhcp-subnets', component: OgDhcpSubnetsComponent },
|
||||
{ path: 'commands', component: CommandsComponent },
|
||||
{ path: 'commands-groups', component: CommandsGroupsComponent },
|
||||
{ path: 'commands-task', component: CommandsTaskComponent },
|
||||
],
|
||||
},
|
||||
{
|
||||
path: 'auth',
|
||||
component: AuthLayoutComponent,
|
||||
children: [
|
||||
{ path: 'login', component: LoginComponent },
|
||||
],
|
||||
},
|
||||
{ path: '**', component: PageNotFoundComponent },
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
|
|
@ -1,23 +1,17 @@
|
|||
import { TestBed } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { RouterTestingModule } from '@angular/router/testing'; // Asegúrate de que está importado
|
||||
import { AppComponent } from './app.component';
|
||||
|
||||
describe('AppComponent', () => {
|
||||
let translateService: TranslateService;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
TranslateModule.forRoot()
|
||||
RouterTestingModule
|
||||
],
|
||||
declarations: [
|
||||
AppComponent
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
translateService = TestBed.inject(TranslateService);
|
||||
});
|
||||
|
||||
it('should create the app', () => {
|
||||
|
@ -26,41 +20,4 @@ describe('AppComponent', () => {
|
|||
expect(app).toBeTruthy();
|
||||
});
|
||||
|
||||
it(`should have as title 'ogWebconsole'`, () => {
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
expect(app.title).toEqual('ogWebconsole');
|
||||
});
|
||||
|
||||
it('should set the language from localStorage on creation', () => {
|
||||
spyOn(localStorage, 'getItem').and.returnValue('en'); // Simula que el idioma guardado es "en"
|
||||
spyOn(translateService, 'use');
|
||||
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith('language');
|
||||
expect(translateService.use).toHaveBeenCalledWith('en');
|
||||
});
|
||||
|
||||
it('should default to Spanish if no language is saved in localStorage', () => {
|
||||
spyOn(localStorage, 'getItem').and.returnValue(null); // Simula que no hay idioma guardado
|
||||
spyOn(translateService, 'use');
|
||||
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith('language');
|
||||
expect(translateService.use).toHaveBeenCalledWith('es');
|
||||
});
|
||||
|
||||
it('should set language to Spanish in sessionStorage on ngOnInit', () => {
|
||||
spyOn(sessionStorage, 'setItem');
|
||||
|
||||
const fixture = TestBed.createComponent(AppComponent);
|
||||
const app = fixture.componentInstance;
|
||||
|
||||
app.ngOnInit();
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith('language', 'es');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
|
@ -8,11 +7,4 @@ import { TranslateService } from '@ngx-translate/core';
|
|||
})
|
||||
export class AppComponent {
|
||||
title = 'ogWebconsole';
|
||||
constructor(private translateService: TranslateService) {
|
||||
const savedLanguage = localStorage.getItem('language') || 'es';
|
||||
this.translateService.use(savedLanguage);
|
||||
}
|
||||
ngOnInit() {
|
||||
sessionStorage.setItem('language', 'es');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA, LOCALE_ID, APP_INITIALIZER } from '@angular/core';
|
||||
import { ConfigService } from './services/config.service';
|
||||
// @ts-ignore
|
||||
|
||||
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { AppRoutingModule } from './app-routing.module';
|
||||
import { AppComponent } from './app.component';
|
||||
|
@ -9,7 +10,7 @@ import { HeaderComponent } from './layout/header/header.component';
|
|||
import { SidebarComponent } from './layout/sidebar/sidebar.component';
|
||||
import { LoginComponent } from './components/login/login.component';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
import { HTTP_INTERCEPTORS, HttpClient, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||
import { CustomInterceptor } from './core/services/custom.interceptor';
|
||||
import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
|
||||
import { MatToolbarModule } from '@angular/material/toolbar';
|
||||
|
@ -26,31 +27,35 @@ import { MatListModule } from '@angular/material/list';
|
|||
import { UsersComponent } from './components/admin/users/users/users.component';
|
||||
import { RolesComponent } from './components/admin/roles/roles/roles.component';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatButtonToggleModule } from '@angular/material/button-toggle';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { AddUserModalComponent } from './components/admin/users/users/add-user-modal/add-user-modal.component';
|
||||
import { MatSelectModule } from '@angular/material/select';
|
||||
import { EditUserModalComponent } from './components/admin/users/users/edit-user-modal/edit-user-modal.component';
|
||||
import { AddRoleModalComponent } from './components/admin/roles/roles/add-role-modal/add-role-modal.component';
|
||||
import { ChangePasswordModalComponent } from './components/admin/users/users/change-password-modal/change-password-modal.component';
|
||||
import { GroupsComponent } from './components/groups/groups.component';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { CreateOrganizationalUnitComponent } from './components/groups/organizational-units/create-organizational-unit/create-organizational-unit.component';
|
||||
import { MatStepperModule } from '@angular/material/stepper';
|
||||
import { MatSlideToggleModule } from '@angular/material/slide-toggle';
|
||||
import { CreateClientComponent } from './components/groups/clients/create-client/create-client.component';
|
||||
import { DeleteModalComponent } from './shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { ClassroomViewComponent } from './components/groups/shared/classroom-view/classroom-view.component';
|
||||
import { EditOrganizationalUnitComponent } from './components/groups/organizational-units/edit-organizational-unit/edit-organizational-unit.component';
|
||||
import { EditClientComponent } from './components/groups/clients/edit-client/edit-client.component';
|
||||
import { ClassroomViewComponent } from './components/groups/classroom-view/classroom-view.component';
|
||||
import { MatProgressSpinner } from "@angular/material/progress-spinner";
|
||||
import { MatProgressBarModule } from '@angular/material/progress-bar';
|
||||
import { MatMenu, MatMenuItem, MatMenuTrigger } from "@angular/material/menu";
|
||||
import { MatAutocomplete, MatAutocompleteTrigger } from "@angular/material/autocomplete";
|
||||
import { MatAutocomplete } from "@angular/material/autocomplete";
|
||||
import { MatChip, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule } from "@angular/material/chips";
|
||||
import { ClientViewComponent } from './components/groups/shared/client-view/client-view.component';
|
||||
import { ClientViewComponent } from './components/groups/client-view/client-view.component';
|
||||
import { MatTab, MatTabGroup } from "@angular/material/tabs";
|
||||
import { MatTooltip } from "@angular/material/tooltip";
|
||||
import { MatExpansionModule } from '@angular/material/expansion';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { ShowOrganizationalUnitComponent } from './components/groups/shared/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||
import { ShowOrganizationalUnitComponent } from './components/groups/organizational-units/show-organizational-unit/show-organizational-unit.component';
|
||||
import { MatGridList, MatGridTile } from "@angular/material/grid-list";
|
||||
import { TreeViewComponent } from './components/groups/tree-view/tree-view.component';
|
||||
import {
|
||||
MatNestedTreeNode,
|
||||
MatTree,
|
||||
|
@ -59,101 +64,37 @@ import {
|
|||
MatTreeNodePadding,
|
||||
MatTreeNodeToggle
|
||||
} from "@angular/material/tree";
|
||||
import { LegendComponent } from './components/groups/shared/legend/legend.component';
|
||||
import { ClassroomViewDialogComponent } from './components/groups/shared/classroom-view/classroom-view-modal';
|
||||
import { LegendComponent } from './components/groups/legend/legend.component';
|
||||
import { ClassroomViewDialogComponent } from './components/groups/classroom-view/classroom-view-modal';
|
||||
import { MatPaginator } from "@angular/material/paginator";
|
||||
import { SaveFiltersDialogComponent } from './components/groups/shared/save-filters-dialog/save-filters-dialog.component';
|
||||
import { PXEimagesComponent } from './components/ogboot/pxe-images/pxe-images.component';
|
||||
import { CreatePXEImageComponent } from './components/ogboot/pxe-images/create-image/create-image/create-image.component';
|
||||
import { InfoImageComponent } from './components/ogboot/pxe-images/info-image/info-image/info-image.component';
|
||||
import { SaveFiltersDialogComponent } from './components/groups/save-filters-dialog/save-filters-dialog.component';
|
||||
import { AcctionsModalComponent } from './components/groups/acctions-modal/acctions-modal.component';
|
||||
import { ImagesComponent } from './components/ogboot/images/images.component';
|
||||
import { CreateImageComponent } from './components/ogboot/images/create-image/create-image/create-image.component';
|
||||
import { InfoImageComponent } from './components/ogboot/images/info-image/info-image/info-image.component';
|
||||
import { PxeComponent } from './components/ogboot/pxe/pxe.component';
|
||||
import { CreatePxeTemplateComponent } from './components/ogboot/pxe/create-pxeTemplate/create-pxe-template.component';
|
||||
import { PxeBootFilesComponent } from './components/ogboot/pxe-boot-files/pxe-boot-files.component';
|
||||
import { MatExpansionPanel, MatExpansionPanelDescription, MatExpansionPanelTitle } from "@angular/material/expansion";
|
||||
import { OgbootStatusComponent } from './components/ogboot/ogboot-status/ogboot-status.component';
|
||||
import { CreatePxeBootFileComponent } from './components/ogboot/pxe-boot-files/create-pxeBootFile/create-pxe-boot-file/create-pxe-boot-file.component';
|
||||
import { NgxChartsModule } from '@swimlane/ngx-charts';
|
||||
import { OgdhcpComponent } from './components/ogdhcp/ogdhcp.component';
|
||||
import { OgDhcpSubnetsComponent } from './components/ogdhcp/og-dhcp-subnets/og-dhcp-subnets.component';
|
||||
import { CreateSubnetComponent } from './components/ogdhcp/og-dhcp-subnets/create-subnet/create-subnet.component';
|
||||
import { AddClientsToSubnetComponent } from './components/ogdhcp/og-dhcp-subnets/add-clients-to-subnet/add-clients-to-subnet.component';
|
||||
import { CommandsComponent } from './components/commands/main-commands/commands.component';
|
||||
import { CommandDetailComponent } from './components/commands/main-commands/detail-command/command-detail.component';
|
||||
import { CreateCommandComponent } from './components/commands/main-commands/create-command/create-command.component';
|
||||
import { MatDatepickerModule } from '@angular/material/datepicker';
|
||||
import { MatNativeDateModule } from '@angular/material/core';
|
||||
import { CalendarComponent } from './components/calendar/calendar.component';
|
||||
import { CreateCalendarComponent } from './components/calendar/create-calendar/create-calendar.component';
|
||||
import { MatRadioButton, MatRadioGroup } from "@angular/material/radio";
|
||||
import { CreateCalendarRuleComponent } from './components/calendar/create-calendar-rule/create-calendar-rule.component';
|
||||
import { CommandsGroupsComponent } from './components/commands/commands-groups/commands-groups.component';
|
||||
import { CommandsTaskComponent } from './components/commands/commands-task/commands-task.component';
|
||||
import { CreateCommandGroupComponent } from './components/commands/commands-groups/create-command-group/create-command-group.component';
|
||||
import { DetailCommandGroupComponent } from './components/commands/commands-groups/detail-command-group/detail-command-group.component';
|
||||
import { CreateTaskComponent } from './components/commands/commands-task/create-task/create-task.component';
|
||||
import { DetailTaskComponent } from './components/commands/commands-task/detail-task/detail-task.component';
|
||||
import { TaskLogsComponent } from './components/commands/commands-task/task-logs/task-logs.component';
|
||||
import { MatSliderModule } from '@angular/material/slider';
|
||||
import { ClientMainViewComponent } from './components/groups/components/client-main-view/client-main-view.component';
|
||||
import { ImagesComponent } from './components/images/images.component';
|
||||
import { CreateImageComponent } from './components/images/create-image/create-image.component';
|
||||
import { CreateClientImageComponent } from './components/groups/components/client-main-view/create-image/create-image.component';
|
||||
import { PartitionAssistantComponent } from './components/groups/components/client-main-view/partition-assistant/partition-assistant.component';
|
||||
import { SoftwareComponent } from './components/software/software.component';
|
||||
import { CreateSoftwareComponent } from './components/software/create-software/create-software.component';
|
||||
import { SoftwareProfileComponent } from './components/software-profile/software-profile.component';
|
||||
import { CreateSoftwareProfileComponent } from './components/software-profile/create-software-profile/create-software-profile.component';
|
||||
import { OperativeSystemComponent } from './components/operative-system/operative-system.component';
|
||||
import { CreateOperativeSystemComponent } from './components/operative-system/create-operative-system/create-operative-system.component';
|
||||
import { ShowTemplateContentComponent } from './components/ogboot/pxe/show-template-content/show-template-content.component';
|
||||
import { RepositoriesComponent } from './components/repositories/repositories.component';
|
||||
import { ManageRepositoryComponent } from './components/repositories/manage-repository/manage-repository.component';
|
||||
import { ExecuteCommandComponent } from './components/commands/main-commands/execute-command/execute-command.component';
|
||||
import { DeployImageComponent } from './components/groups/components/client-main-view/deploy-image/deploy-image.component';
|
||||
import { MainRepositoryViewComponent } from './components/repositories/main-repository-view/main-repository-view.component';
|
||||
import { ExecuteCommandOuComponent } from './components/groups/shared/execute-command-ou/execute-command-ou.component';
|
||||
import { JoyrideModule } from 'ngx-joyride';
|
||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
|
||||
import { EnvVarsComponent } from './components/admin/env-vars/env-vars.component';
|
||||
import { MatSortModule } from '@angular/material/sort';
|
||||
import { MenusComponent } from './components/menus/menus.component';
|
||||
import { CreateMenuComponent } from './components/menus/create-menu/create-menu.component';
|
||||
import { CreateMultipleClientComponent } from './components/groups/shared/clients/create-multiple-client/create-multiple-client.component';
|
||||
import { ExportImageComponent } from './components/images/export-image/export-image.component';
|
||||
import { ImportImageComponent } from "./components/repositories/import-image/import-image.component";
|
||||
import { LoadingComponent } from './shared/loading/loading.component';
|
||||
import { InputDialogComponent } from './components/commands/commands-task/task-logs/input-dialog/input-dialog.component';
|
||||
import { ManageOrganizationalUnitComponent } from './components/groups/shared/organizational-units/manage-organizational-unit/manage-organizational-unit.component';
|
||||
import { BackupImageComponent } from './components/repositories/backup-image/backup-image.component';
|
||||
import { ServerInfoDialogComponent } from "./components/ogdhcp/server-info-dialog/server-info-dialog.component";
|
||||
import { StatusComponent } from "./components/ogdhcp/status/status.component";
|
||||
import { OgDhcpSubnetsComponent } from "./components/ogdhcp/og-dhcp-subnets.component";
|
||||
import { CreateSubnetComponent } from "./components/ogdhcp/create-subnet/create-subnet.component";
|
||||
import { AddClientsToSubnetComponent } from "./components/ogdhcp/add-clients-to-subnet/add-clients-to-subnet.component";
|
||||
import { ShowClientsComponent } from './components/ogdhcp/show-clients/show-clients.component';
|
||||
import { OperationResultDialogComponent } from './components/ogdhcp/operation-result-dialog/operation-result-dialog.component';
|
||||
import { ManageClientComponent } from './components/groups/shared/clients/manage-client/manage-client.component';
|
||||
import { ConvertImageComponent } from './components/repositories/convert-image/convert-image.component';
|
||||
import { registerLocaleData } from '@angular/common';
|
||||
import localeEs from '@angular/common/locales/es';
|
||||
import { GlobalStatusComponent } from './components/global-status/global-status.component';
|
||||
import { ShowMonoliticImagesComponent } from './components/repositories/show-monolitic-images/show-monolitic-images.component';
|
||||
import { StatusTabComponent } from './components/global-status/status-tab/status-tab.component';
|
||||
import { ConvertImageToVirtualComponent } from './components/repositories/convert-image-to-virtual/convert-image-to-virtual.component';
|
||||
import { RunScriptAssistantComponent } from './components/groups/components/client-main-view/run-script-assistant/run-script-assistant.component';
|
||||
import {
|
||||
SaveScriptComponent
|
||||
} from "./components/groups/components/client-main-view/run-script-assistant/save-script/save-script.component";
|
||||
import { EditImageComponent } from './components/repositories/edit-image/edit-image.component';
|
||||
import { ShowGitImagesComponent } from './components/repositories/show-git-images/show-git-images.component';
|
||||
import { RenameImageComponent } from './components/repositories/rename-image/rename-image.component';
|
||||
|
||||
export function HttpLoaderFactory(http: HttpClient) {
|
||||
return new TranslateHttpLoader(http, './locale/', '.json');
|
||||
}
|
||||
|
||||
export function initializeApp(configService: ConfigService) {
|
||||
return () => configService.loadConfig();
|
||||
}
|
||||
|
||||
registerLocaleData(localeEs, 'es-ES');
|
||||
|
||||
import { ClientTabViewComponent } from './components/groups/client-tab-view/client-tab-view.component';
|
||||
@NgModule({
|
||||
declarations: [
|
||||
AppComponent,
|
||||
|
@ -167,83 +108,45 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
UsersComponent,
|
||||
RolesComponent,
|
||||
AddUserModalComponent,
|
||||
EditUserModalComponent,
|
||||
AddRoleModalComponent,
|
||||
ChangePasswordModalComponent,
|
||||
GroupsComponent,
|
||||
ManageClientComponent,
|
||||
CreateOrganizationalUnitComponent,
|
||||
CreateClientComponent,
|
||||
DeleteModalComponent,
|
||||
EditOrganizationalUnitComponent,
|
||||
EditClientComponent,
|
||||
ClassroomViewComponent,
|
||||
ClientViewComponent,
|
||||
ShowOrganizationalUnitComponent,
|
||||
TreeViewComponent,
|
||||
LegendComponent,
|
||||
ClassroomViewDialogComponent,
|
||||
SaveFiltersDialogComponent,
|
||||
PXEimagesComponent,
|
||||
CreatePXEImageComponent,
|
||||
AcctionsModalComponent,
|
||||
ImagesComponent,
|
||||
CreateImageComponent,
|
||||
InfoImageComponent,
|
||||
PxeComponent,
|
||||
CreatePxeTemplateComponent,
|
||||
PxeBootFilesComponent,
|
||||
OgbootStatusComponent,
|
||||
CreatePxeBootFileComponent,
|
||||
OgdhcpComponent,
|
||||
OgDhcpSubnetsComponent,
|
||||
CreateSubnetComponent,
|
||||
AddClientsToSubnetComponent,
|
||||
CommandsComponent,
|
||||
CommandDetailComponent,
|
||||
CreateCommandComponent,
|
||||
CalendarComponent,
|
||||
CreateCalendarComponent,
|
||||
CreateClientImageComponent,
|
||||
CreateCalendarRuleComponent,
|
||||
CommandsGroupsComponent,
|
||||
CommandsTaskComponent,
|
||||
CreateCommandGroupComponent,
|
||||
DetailCommandGroupComponent,
|
||||
CreateTaskComponent,
|
||||
DetailTaskComponent,
|
||||
TaskLogsComponent,
|
||||
ServerInfoDialogComponent,
|
||||
StatusComponent,
|
||||
ClientMainViewComponent,
|
||||
ImagesComponent,
|
||||
CreateImageComponent,
|
||||
PartitionAssistantComponent,
|
||||
SoftwareComponent,
|
||||
CreateSoftwareComponent,
|
||||
SoftwareProfileComponent,
|
||||
CreateSoftwareProfileComponent,
|
||||
OperativeSystemComponent,
|
||||
CreateOperativeSystemComponent,
|
||||
ShowTemplateContentComponent,
|
||||
RepositoriesComponent,
|
||||
ManageRepositoryComponent,
|
||||
ExecuteCommandComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
DeployImageComponent,
|
||||
MainRepositoryViewComponent,
|
||||
ExecuteCommandOuComponent,
|
||||
EnvVarsComponent,
|
||||
MenusComponent,
|
||||
CreateMenuComponent,
|
||||
CreateMultipleClientComponent,
|
||||
ExportImageComponent,
|
||||
ImportImageComponent,
|
||||
LoadingComponent,
|
||||
InputDialogComponent,
|
||||
ManageOrganizationalUnitComponent,
|
||||
BackupImageComponent,
|
||||
ShowClientsComponent,
|
||||
OperationResultDialogComponent,
|
||||
ConvertImageComponent,
|
||||
GlobalStatusComponent,
|
||||
ShowMonoliticImagesComponent,
|
||||
StatusTabComponent,
|
||||
ConvertImageToVirtualComponent,
|
||||
RunScriptAssistantComponent,
|
||||
SaveScriptComponent,
|
||||
EditImageComponent,
|
||||
ShowGitImagesComponent,
|
||||
RenameImageComponent
|
||||
ClientTabViewComponent
|
||||
],
|
||||
bootstrap: [AppComponent],
|
||||
imports: [BrowserModule,
|
||||
|
@ -252,7 +155,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
ReactiveFormsModule,
|
||||
MatToolbarModule,
|
||||
MatIconModule,
|
||||
MatButtonToggleModule,
|
||||
MatButtonModule,
|
||||
MatSidenavModule,
|
||||
NoopAnimationsModule,
|
||||
|
@ -265,7 +167,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
MatDialogModule,
|
||||
MatSelectModule,
|
||||
MatDividerModule,
|
||||
MatProgressBarModule,
|
||||
MatStepperModule,
|
||||
DragDropModule,
|
||||
MatSlideToggleModule, MatMenu, MatMenuTrigger, MatMenuItem, MatAutocomplete, MatChipListbox, MatChipOption, MatChipSet, MatChipsModule, MatChip, MatProgressSpinner, MatTabGroup, MatTab, MatTooltip,
|
||||
|
@ -273,16 +174,6 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
NgxChartsModule,
|
||||
MatDatepickerModule,
|
||||
MatNativeDateModule,
|
||||
MatSliderModule,
|
||||
MatSortModule,
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useFactory: HttpLoaderFactory,
|
||||
deps: [HttpClient]
|
||||
}
|
||||
}),
|
||||
JoyrideModule.forRoot(),
|
||||
ToastrModule.forRoot(
|
||||
{
|
||||
timeOut: 5000,
|
||||
|
@ -292,7 +183,7 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
progressAnimation: 'increasing',
|
||||
closeButton: true
|
||||
}
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription, MatRadioGroup, MatRadioButton, MatAutocompleteTrigger
|
||||
), MatGridList, MatTree, MatTreeNode, MatNestedTreeNode, MatTreeNodeToggle, MatTreeNodeDef, MatTreeNodePadding, MatTreeNodeOutlet, MatPaginator, MatGridTile, MatExpansionPanel, MatExpansionPanelTitle, MatExpansionPanelDescription
|
||||
],
|
||||
schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA,
|
||||
|
@ -303,16 +194,8 @@ registerLocaleData(localeEs, 'es-ES');
|
|||
useClass: CustomInterceptor,
|
||||
multi: true
|
||||
},
|
||||
{ provide: LOCALE_ID, useValue: 'es-ES' },
|
||||
provideAnimationsAsync(),
|
||||
provideHttpClient(withInterceptorsFromDi()),
|
||||
ConfigService,
|
||||
{
|
||||
provide: APP_INITIALIZER,
|
||||
useFactory: initializeApp,
|
||||
deps: [ConfigService],
|
||||
multi: true
|
||||
}
|
||||
provideHttpClient(withInterceptorsFromDi())
|
||||
],
|
||||
})
|
||||
export class AppModule { }
|
||||
|
|
|
@ -14,6 +14,16 @@
|
|||
margin: 0 10px;
|
||||
}
|
||||
|
||||
/* Estilos de los botones */
|
||||
button {
|
||||
height: 150px;
|
||||
width: 150px;
|
||||
margin: 5px;
|
||||
padding: 5px;
|
||||
cursor: pointer;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
/* Estilos del texto debajo de los botones */
|
||||
span{
|
||||
margin: 0;
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<div class="container">
|
||||
<button class="action-button" routerLink="/users">
|
||||
<button mat-fab color="primary" class="fab-button" routerLink="/users">
|
||||
<mat-icon>group</mat-icon>
|
||||
<span>{{ 'labelUsers' | translate }}</span>
|
||||
<span i18n="@@labelUsers">Usuarios</span>
|
||||
</button>
|
||||
<button class="action-button" routerLink="/user-groups">
|
||||
<button mat-fab color="primary" class="fab-button" routerLink="/user-groups">
|
||||
<mat-icon>admin_panel_settings</mat-icon>
|
||||
<span>{{ 'labelRoles' | translate }}</span>
|
||||
<span i18n="@@labelRoles">Roles</span>
|
||||
</button>
|
||||
</div>
|
||||
|
|
|
@ -3,46 +3,55 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||
import { AdminComponent } from './admin.component';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('AdminComponent', () => {
|
||||
let component: AdminComponent;
|
||||
let fixture: ComponentFixture<AdminComponent>;
|
||||
let router: Router;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AdminComponent],
|
||||
imports: [
|
||||
RouterTestingModule,
|
||||
MatButtonModule,
|
||||
MatIconModule,
|
||||
TranslateModule.forRoot()
|
||||
RouterTestingModule, // Importa RouterTestingModule para manejar routerLink
|
||||
MatButtonModule, // Importa MatButtonModule para botones
|
||||
MatIconModule // Importa MatIconModule para iconos
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(AdminComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
fixture.detectChanges(); // Detecta cambios para renderizar el componente
|
||||
});
|
||||
|
||||
it('debería crear el componente', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('debería renderizar dos botones', () => {
|
||||
const buttons = fixture.nativeElement.querySelectorAll('button');
|
||||
expect(buttons.length).toBe(2);
|
||||
it('debería contener dos botones', () => {
|
||||
const buttons = fixture.debugElement.queryAll(By.css('button'));
|
||||
expect(buttons.length).toBe(2); // Verifica que hay dos botones
|
||||
});
|
||||
|
||||
it('debería tener un botón con routerLink a "/users"', () => {
|
||||
const button = fixture.nativeElement.querySelector('button[routerLink="/users"]');
|
||||
expect(button).toBeTruthy();
|
||||
expect(button.querySelector('mat-icon').textContent.trim()).toBe('group');
|
||||
it('el primer botón debería tener el texto "Usuarios"', () => {
|
||||
const firstButton = fixture.debugElement.query(By.css('.fab-button:first-child'));
|
||||
expect(firstButton.nativeElement.textContent).toContain('Usuarios'); // Verifica que el texto sea "Usuarios"
|
||||
});
|
||||
|
||||
it('el segundo botón debería tener el texto "Roles"', () => {
|
||||
const secondButton = fixture.debugElement.query(By.css('.fab-button:last-child'));
|
||||
expect(secondButton.nativeElement.textContent).toContain('Roles'); // Verifica que el texto sea "Roles"
|
||||
});
|
||||
|
||||
it('el primer botón debería tener el routerLink correcto', () => {
|
||||
const firstButton = fixture.debugElement.query(By.css('.fab-button:first-child'));
|
||||
expect(firstButton.nativeElement.getAttribute('ng-reflect-router-link')).toBe('/users'); // Verifica el routerLink
|
||||
});
|
||||
|
||||
it('el segundo botón debería tener el routerLink correcto', () => {
|
||||
const secondButton = fixture.debugElement.query(By.css('.fab-button:last-child'));
|
||||
expect(secondButton.nativeElement.getAttribute('ng-reflect-router-link')).toBe('/user-groups'); // Verifica el routerLink
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,30 +0,0 @@
|
|||
.env-settings {
|
||||
padding: 0rem 1rem 0rem 1rem;
|
||||
|
||||
.mat-table {
|
||||
margin-bottom: 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.value-input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.actions {
|
||||
display: flex;
|
||||
gap: 16px;
|
||||
justify-content: flex-end;
|
||||
margin-top: 16px;
|
||||
}
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
<div class="env-settings">
|
||||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2>Editar Variables de Entorno</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<mat-table [dataSource]="envVars" class="mat-elevation-z8">
|
||||
<!-- Nombre de la variable -->
|
||||
<ng-container matColumnDef="name">
|
||||
<mat-header-cell *matHeaderCellDef> Variable </mat-header-cell>
|
||||
<mat-cell *matCellDef="let variable">{{ variable.name }}</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<!-- Valor de la variable -->
|
||||
<ng-container matColumnDef="value">
|
||||
<mat-header-cell *matHeaderCellDef> Valor </mat-header-cell>
|
||||
<mat-cell *matCellDef="let variable">
|
||||
<mat-form-field class="value-input">
|
||||
<input matInput [(ngModel)]="variable.value" />
|
||||
</mat-form-field>
|
||||
</mat-cell>
|
||||
</ng-container>
|
||||
|
||||
<mat-header-row *matHeaderRowDef="displayedColumns"></mat-header-row>
|
||||
<mat-row *matRowDef="let row; columns: displayedColumns;"></mat-row>
|
||||
</mat-table>
|
||||
|
||||
<div class="actions">
|
||||
<button class="action-button" (click)="loadEnvVars()">Recargar</button>
|
||||
<button class="submit-button" (click)="saveEnvVars()">Guardar Cambios</button>
|
||||
</div>
|
||||
</div>
|
|
@ -1,71 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { EnvVarsComponent } from './env-vars.component';
|
||||
import { provideHttpClient } from '@angular/common/http';
|
||||
import { provideHttpClientTesting } from '@angular/common/http/testing';
|
||||
import { ReactiveFormsModule, FormsModule, FormBuilder } from '@angular/forms';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatCheckboxModule } from '@angular/material/checkbox';
|
||||
import { MatDialogModule, MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ToastrModule, ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from '../users/users/data.service';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('EnvVarsComponent', () => {
|
||||
let component: EnvVarsComponent;
|
||||
let fixture: ComponentFixture<EnvVarsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [EnvVarsComponent],
|
||||
imports: [
|
||||
ReactiveFormsModule,
|
||||
FormsModule,
|
||||
MatDialogModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatCheckboxModule,
|
||||
MatButtonModule,
|
||||
BrowserAnimationsModule,
|
||||
MatTableModule,
|
||||
ToastrModule.forRoot(),
|
||||
TranslateModule.forRoot()
|
||||
],
|
||||
providers: [
|
||||
FormBuilder,
|
||||
ToastrService,
|
||||
DataService,
|
||||
provideHttpClient(),
|
||||
provideHttpClientTesting(),
|
||||
{
|
||||
provide: MatDialogRef,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: MAT_DIALOG_DATA,
|
||||
useValue: {}
|
||||
},
|
||||
{
|
||||
provide: ConfigService,
|
||||
useValue: mockConfigService
|
||||
}
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EnvVarsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,56 +0,0 @@
|
|||
import { Component } from '@angular/core';
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-env-vars',
|
||||
templateUrl: './env-vars.component.html',
|
||||
styleUrl: './env-vars.component.css'
|
||||
})
|
||||
export class EnvVarsComponent {
|
||||
envVars: { name: string; value: string }[] = [];
|
||||
displayedColumns: string[] = ['name', 'value'];
|
||||
private apiUrl: string;
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
this.apiUrl = `${this.configService.apiUrl}/env-vars`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadEnvVars();
|
||||
}
|
||||
|
||||
loadEnvVars(): void {
|
||||
this.http.get<{ vars: Record<string, string> }>(this.apiUrl).subscribe({
|
||||
next: (response) => {
|
||||
this.envVars = Object.entries(response.vars).map(([name, value]) => ({ name, value }));
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error al cargar las variables de entorno:', err);
|
||||
this.toastService.error('No se pudieron cargar las variables de entorno.');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
saveEnvVars(): void {
|
||||
const vars = this.envVars.reduce((acc, variable) => {
|
||||
acc[variable.name] = variable.value;
|
||||
return acc;
|
||||
}, {} as Record<string, string>);
|
||||
|
||||
this.http.post(this.apiUrl, { vars }).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Variables de entorno guardadas correctamente.');
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error al guardar las variables de entorno:', err);
|
||||
this.toastService.error('No se pudieron cargar las variables de entorno.');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,40 +1,10 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-top: 2em;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 15px 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
/* Espacio entre los campos */
|
||||
}
|
||||
|
||||
.time-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
.role-form .form-field {
|
||||
display: block;
|
||||
margin-bottom: 10px; /* Puedes ajustar el valor para cambiar la separación */
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px; /* Ajusta este valor según necesites */
|
||||
}
|
||||
|
|
@ -1,22 +1,24 @@
|
|||
<h1 mat-dialog-title>{{ 'dialogTitleAddRole' | translate }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<form [formGroup]="roleForm" class="role-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelRoleName' | translate }}</mat-label>
|
||||
<input matInput formControlName="name" required>
|
||||
<h1 mat-dialog-title i18n="@@dialogTitleAddRole">Añadir Rol (TBD)</h1>
|
||||
<div mat-dialog-content>
|
||||
<form class="role-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelRoleName">Nombre</mat-label>
|
||||
<input matInput formControlName="rolename" required>
|
||||
</mat-form-field>
|
||||
|
||||
<section class="example-section">
|
||||
<h4>{{ 'sectionTitlePermissions' | translate }}</h4>
|
||||
<p><mat-checkbox formControlName="superAdmin">{{ 'checkboxSuperAdmin' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgAdmin">{{ 'checkboxOrgAdmin' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgOperator">{{ 'checkboxOrgOperator' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="orgMinimal">{{ 'checkboxOrgMinimal' | translate }}</mat-checkbox></p>
|
||||
<p><mat-checkbox formControlName="userRole">{{ 'checkboxUserRole' | translate }}</mat-checkbox></p>
|
||||
<h4 i18n="@@sectionTitlePermissions">Permisos:</h4>
|
||||
<p><mat-checkbox i18n="@@checkboxManageUsers">Gestionar los usuarios</mat-checkbox></p>
|
||||
<p><mat-checkbox i18n="@@checkboxPXEConfig">Configuración PXE</mat-checkbox></p>
|
||||
<p><mat-checkbox i18n="@@checkboxConsoleImages">Imágenes de la consola web</mat-checkbox></p>
|
||||
<p><mat-checkbox i18n="@@checkboxManageComponents">Gestionar los distintos componentes</mat-checkbox></p>
|
||||
<p><mat-checkbox i18n="@@checkboxCreateImages">Crear imágenes</mat-checkbox></p>
|
||||
<p><mat-checkbox i18n="@@checkboxServerConfigScript">script de configuración del servidor</mat-checkbox></p>
|
||||
<p><mat-checkbox i18n="@@checkboxOther">...</mat-checkbox></p>
|
||||
</section>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()">{{ 'buttonAdd' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onNoClick()" i18n="@@buttonCancel">Cancelar</button>
|
||||
<button mat-button i18n="@@buttonAdd">Añadir</button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddRoleModalComponent } from './add-role-modal.component';
|
||||
|
||||
describe('AddRoleModalComponent', () => {
|
||||
let component: AddRoleModalComponent;
|
||||
let fixture: ComponentFixture<AddRoleModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AddRoleModalComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddRoleModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,111 +1,19 @@
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import {FormBuilder, FormGroup, Validators} from '@angular/forms';
|
||||
import {DataService} from "../data.service";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-add-role-modal',
|
||||
templateUrl: './add-role-modal.component.html',
|
||||
styleUrls: ['./add-role-modal.component.css']
|
||||
styleUrl: './add-role-modal.component.css'
|
||||
})
|
||||
export class AddRoleModalComponent {
|
||||
roleForm: FormGroup<any>;
|
||||
roleId: string | null = null;
|
||||
baseUrl: string;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AddRoleModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private fb: FormBuilder,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.roleForm = this.fb.group({
|
||||
name: ['', Validators.required],
|
||||
superAdmin: [false],
|
||||
orgAdmin: [false],
|
||||
orgOperator: [false],
|
||||
orgMinimal: [false],
|
||||
userRole: [false],
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.load()
|
||||
}
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.dataService.getUserGroup(this.data).subscribe({
|
||||
next: (response) => {
|
||||
this.roleForm.patchValue({
|
||||
name: response.name,
|
||||
superAdmin: response.permissions.includes('ROLE_SUPER_ADMIN'),
|
||||
orgAdmin: response.permissions.includes('ROLE_ORGANIZATIONAL_UNIT_ADMIN'),
|
||||
orgOperator: response.permissions.includes('ROLE_ORGANIZATIONAL_UNIT_OPERATOR'),
|
||||
orgMinimal: response.permissions.includes('ROLE_ORGANIZATIONAL_UNIT_MINIMAL'),
|
||||
userRole: response.permissions.includes('ROLE_USER'),
|
||||
});
|
||||
this.roleId = response['@id'];
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error fetching remote calendar:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private http: HttpClient
|
||||
) {}
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
if (this.roleForm.valid) {
|
||||
const formValues = this.roleForm.value;
|
||||
|
||||
const selectedPermissions: string[] = [];
|
||||
if (formValues.superAdmin) selectedPermissions.push('ROLE_SUPER_ADMIN');
|
||||
if (formValues.orgAdmin) selectedPermissions.push('ROLE_ORGANIZATIONAL_UNIT_ADMIN');
|
||||
if (formValues.orgOperator) selectedPermissions.push('ROLE_ORGANIZATIONAL_UNIT_OPERATOR');
|
||||
if (formValues.orgMinimal) selectedPermissions.push('ROLE_ORGANIZATIONAL_UNIT_MINIMAL');
|
||||
if (formValues.userRole) selectedPermissions.push('ROLE_USER');
|
||||
|
||||
const payload = {
|
||||
name: formValues.name,
|
||||
permissions: selectedPermissions,
|
||||
enabled: true
|
||||
};
|
||||
|
||||
if (this.roleId) {
|
||||
this.http.put(`${this.baseUrl}${this.roleId}`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Rol actualizado correctamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error('Error al editar el rol:', error);
|
||||
console.error('Error al editar el rol', error);
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/user-groups`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Rol añadido correctamente');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error('Error al añadir rol:', error);
|
||||
console.error('Error al añadir rol', error);
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,55 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
|
||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/user-groups?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
|
||||
getUserGroups(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
||||
return this.http.get<any>(this.apiUrl, { params }).pipe(
|
||||
map(response => {
|
||||
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
|
||||
return {
|
||||
data: response['hydra:member'],
|
||||
totalItems: response['hydra:totalItems']
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching user groups', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getUserGroup(id: string): Observable<any> {
|
||||
return this.http.get<any>(`${this.baseUrl}${id}`).pipe(
|
||||
map(response => {
|
||||
if (response.name && response.permissions) {
|
||||
return response;
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching user group', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,46 +1,21 @@
|
|||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
.header-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
.header-container h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
|
@ -1,52 +1,20 @@
|
|||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2>{{ 'adminRolesTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="addUser()">
|
||||
{{ 'addRole' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<h1 i18n="@@headerRoleManagement">Gestión de roles</h1>
|
||||
<button mat-flat-button color="primary" (click)="addUser()" i18n="@@buttonAddRole">+ Añadir</button>
|
||||
</div>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'searchRoleLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let role"> {{ column.cell(role) }} </td>
|
||||
</ng-container>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let role"> {{ column.cell(role) }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let role" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editRole(role)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteRole(role)"
|
||||
[disabled]="role.permissions.includes('ROLE_SUPER_ADMIN')">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@headerActions">Acciones</th>
|
||||
<td mat-cell *matCellDef="let role">
|
||||
<button mat-button color="warn" [disabled]="role.name === 'Super Admin'" (click)="deleteRole(role)" i18n="@@buttonDelete">Eliminar</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
|
|
@ -1,73 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { RolesComponent } from './roles.component';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from './data.service';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { MatDivider } from '@angular/material/divider';
|
||||
import { MatFormField } from '@angular/material/form-field';
|
||||
import { MatLabel } from '@angular/material/form-field';
|
||||
import { MatIcon } from '@angular/material/icon';
|
||||
import { MatHint } from '@angular/material/form-field';
|
||||
import { MatPaginator } from '@angular/material/paginator';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LoadingComponent } from '../../../../shared/loading/loading.component';
|
||||
|
||||
describe('RolesComponent', () => {
|
||||
let component: RolesComponent;
|
||||
let fixture: ComponentFixture<RolesComponent>;
|
||||
let mockMatDialog: jasmine.SpyObj<MatDialog>;
|
||||
let mockHttpClient: jasmine.SpyObj<HttpClient>;
|
||||
let mockToastrService: jasmine.SpyObj<ToastrService>;
|
||||
let mockDataService: jasmine.SpyObj<DataService>;
|
||||
let mockConfigService: jasmine.SpyObj<ConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const matDialogSpy = jasmine.createSpyObj('MatDialog', ['open']);
|
||||
const httpClientSpy = jasmine.createSpyObj('HttpClient', ['get', 'post', 'put', 'delete']);
|
||||
const toastrServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
|
||||
const dataServiceSpy = jasmine.createSpyObj('DataService', ['getRoles']);
|
||||
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [RolesComponent, LoadingComponent],
|
||||
imports: [MatDivider, MatFormField, MatLabel, MatIcon, MatHint, MatPaginator,
|
||||
TranslateModule.forRoot()],
|
||||
providers: [
|
||||
{ provide: MatDialog, useValue: matDialogSpy },
|
||||
{ provide: HttpClient, useValue: httpClientSpy },
|
||||
{ provide: ToastrService, useValue: toastrServiceSpy },
|
||||
{ provide: DataService, useValue: dataServiceSpy },
|
||||
{ provide: ConfigService, useValue: configServiceSpy }
|
||||
]
|
||||
}).compileComponents();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
declarations: [RolesComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(RolesComponent);
|
||||
component = fixture.componentInstance;
|
||||
mockMatDialog = TestBed.inject(MatDialog) as jasmine.SpyObj<MatDialog>;
|
||||
mockHttpClient = TestBed.inject(HttpClient) as jasmine.SpyObj<HttpClient>;
|
||||
mockToastrService = TestBed.inject(ToastrService) as jasmine.SpyObj<ToastrService>;
|
||||
mockDataService = TestBed.inject(DataService) as jasmine.SpyObj<DataService>;
|
||||
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have a default itemsPerPage value', () => {
|
||||
expect(component.itemsPerPage).toBeDefined();
|
||||
});
|
||||
|
||||
it('should initialize the dataSource', () => {
|
||||
expect(component.dataSource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should have a defined columns array', () => {
|
||||
expect(component.columns).toBeDefined();
|
||||
expect(component.columns.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,13 +1,11 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { RoleService } from './roles.service';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AddRoleModalComponent } from './add-role-modal/add-role-modal.component';
|
||||
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { DataService } from "./data.service";
|
||||
import { PageEvent } from "@angular/material/paginator";
|
||||
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { AddRoleModalComponent } from './add-role-modal/add-role-modal.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-roles',
|
||||
|
@ -15,15 +13,8 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./roles.component.css']
|
||||
})
|
||||
export class RolesComponent implements OnInit {
|
||||
baseUrl: string = this.configService.apiUrl;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
filters: { [key: string]: string } = {};
|
||||
loading: boolean = false;
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 0;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
|
@ -43,85 +34,53 @@ export class RolesComponent implements OnInit {
|
|||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
||||
private apiUrl = `${this.baseUrl}/user-groups`;
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
) {}
|
||||
constructor(private roleService: RoleService, public dialog: MatDialog, private http: HttpClient,
|
||||
private toastService: ToastrService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.search();
|
||||
this.loadRoles();
|
||||
}
|
||||
|
||||
search() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching roles', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
loadRoles() {
|
||||
this.roleService.getRoles().subscribe(response => {
|
||||
console.log(response);
|
||||
this.dataSource.data = response['hydra:member'];
|
||||
});
|
||||
}
|
||||
|
||||
addUser() {
|
||||
const dialogRef = this.dialog.open(AddRoleModalComponent, {
|
||||
width: '600px'
|
||||
});
|
||||
const dialogRef = this.dialog.open(AddRoleModalComponent);
|
||||
|
||||
dialogRef.afterClosed().subscribe((result) => {
|
||||
this.search();
|
||||
});
|
||||
}
|
||||
/* dialogRef.componentInstance.roleAdded.subscribe(() => {
|
||||
this.loadRoles();
|
||||
}); */
|
||||
|
||||
editRole(role: any): void {
|
||||
const dialogRef = this.dialog.open(AddRoleModalComponent, {
|
||||
width: '600px',
|
||||
data: role['@id']
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteRole(role: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '500px',
|
||||
data: { name: role.name }
|
||||
width: '300px',
|
||||
data: { name: role.name }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
const apiUrl = `${this.baseUrl}/user-groups/${role.uuid}`;
|
||||
|
||||
|
||||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
this.search();
|
||||
this.toastService.success('Role deleted successfully');
|
||||
console.log('Role deleted successfully');
|
||||
this.loadRoles();
|
||||
this.toastService.success('Role deleted successfully');
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error('Error deleting role:', error);
|
||||
console.error('Error deleting role:', error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('Role deletion cancelled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
interface Role {
|
||||
'@id': string;
|
||||
name: string;
|
||||
permissions: string[];
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class RoleService {
|
||||
private apiUrl = import.meta.env.NG_APP_BASE_API_URL;
|
||||
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
getRoles(): Observable<{ 'hydra:member': Role[] }> {
|
||||
return this.http.get<{ 'hydra:member': Role[] }>(`${this.apiUrl}/user-groups?page=1&itemsPerPage=30`);
|
||||
}
|
||||
|
||||
}
|
|
@ -1,12 +1,9 @@
|
|||
.user-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: 2rem;
|
||||
.user-form .form-field {
|
||||
display: block;
|
||||
margin-bottom: 10px; /* Puedes ajustar el valor para cambiar la separación */
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
.checkbox-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px; /* Ajusta este valor según necesites */
|
||||
}
|
||||
|
|
|
@ -1,46 +1,35 @@
|
|||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<h1 mat-dialog-title>{{ isEditMode ? ('dialogTitleEditUser' | translate) : ('dialogTitleAddUser' | translate) }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<h1 mat-dialog-title i18n="@@dialogTitleAddUser">Añadir Usuario</h1>
|
||||
<div mat-dialog-content>
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'addUserlabelUsername' | translate }}</mat-label>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@addUserlabelUsername">Nombre de usuario</mat-label>
|
||||
<input matInput formControlName="username" required>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'addUserlabelPassword' | translate }}</mat-label>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@addUserlabelPassword">Contraseña</mat-label>
|
||||
<input matInput formControlName="password" type="password" required>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelRole' | translate }}</mat-label>
|
||||
<mat-select formControlName="role" required>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelRole">Rol</mat-label>
|
||||
<mat-select formControlName="role">
|
||||
<mat-option *ngFor="let group of userGroups" [value]="group['@id']">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelOrganizationalUnit' | translate }}</mat-label>
|
||||
<mat-select multiple formControlName="organizationalUnits">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelOrganizationalUnit">Unidad organiativa</mat-label>
|
||||
<mat-select multiple formControlName="organizationalUnit">
|
||||
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Vista tarjetas</mat-label>
|
||||
<mat-select formControlName="groupsView" required>
|
||||
<mat-option *ngFor="let option of views" [value]="option.value">
|
||||
{{ option.name }}
|
||||
{{unit.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" [disabled]="userForm.invalid">{{ isEditMode ? ('buttonEdit' | translate) : ('buttonAdd' | translate) }}</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onNoClick()" i18n="@@buttonCancel">Cancelar</button>
|
||||
<button mat-button [disabled]="!userForm.valid" (click)="onSubmit()" i18n="@@buttonAdd">Añadir</button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { AddUserModalComponent } from './add-user-modal.component';
|
||||
|
||||
describe('AddUserModalComponent', () => {
|
||||
let component: AddUserModalComponent;
|
||||
let fixture: ComponentFixture<AddUserModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [AddUserModalComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(AddUserModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,10 +1,8 @@
|
|||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { DataService } from "../data.service";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { UserService } from '../users.service';
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
interface UserGroup {
|
||||
'@id': string;
|
||||
|
@ -18,83 +16,34 @@ interface UserGroup {
|
|||
styleUrls: ['./add-user-modal.component.css']
|
||||
})
|
||||
export class AddUserModalComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
@Output() userAdded = new EventEmitter<void>();
|
||||
@Output() userEdited = new EventEmitter<void>();
|
||||
userForm: FormGroup<any>;
|
||||
userForm: FormGroup;
|
||||
userGroups: UserGroup[] = [];
|
||||
organizationalUnits: any[] = [];
|
||||
userId: string | null = null;
|
||||
loading: boolean = false;
|
||||
isEditMode: boolean = false;
|
||||
|
||||
protected views = [
|
||||
{ value: 'card', name: 'Tarjetas' },
|
||||
{ value: 'list', name: 'Listado' },
|
||||
];
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AddUserModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private http: HttpClient,
|
||||
private fb: FormBuilder,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService
|
||||
private userService: UserService,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.userForm = this.fb.group({
|
||||
username: ['', Validators.required],
|
||||
password: ['', Validators.required],
|
||||
role: ['', Validators.required],
|
||||
groupsView: ['card', Validators.required],
|
||||
organizationalUnits: [[]]
|
||||
organizationalUnit: [[], Validators.required]
|
||||
});
|
||||
|
||||
if (data) {
|
||||
this.isEditMode = true;
|
||||
}
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.dataService.getUserGroups().subscribe((data) => {
|
||||
this.userService.getUserGroups().subscribe((data) => {
|
||||
this.userGroups = data['hydra:member'];
|
||||
});
|
||||
|
||||
this.dataService.getOrganizationalUnits().subscribe((data) => {
|
||||
this.userService.getOrganizationalUnits().subscribe((data) => {
|
||||
this.organizationalUnits = data['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
|
||||
});
|
||||
|
||||
if (this.data) {
|
||||
this.load();
|
||||
} else {
|
||||
this.userForm.get('password')?.setValidators([Validators.required]);
|
||||
}
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.loading = true;
|
||||
this.dataService.getUser(this.data).subscribe({
|
||||
next: (response) => {
|
||||
const organizationalUnitIds = response.allowedOrganizationalUnits.map((unit: any) => unit['@id']);
|
||||
|
||||
this.userForm.patchValue({
|
||||
username: response.username,
|
||||
role: response.userGroups.length > 0 ? response.userGroups[0]['@id'] : null,
|
||||
organizationalUnits: organizationalUnitIds,
|
||||
groupsView: response.groupsView
|
||||
});
|
||||
|
||||
this.userId = response['@id'];
|
||||
this.userForm.get('password')?.clearValidators();
|
||||
this.userForm.get('password')?.updateValueAndValidity();
|
||||
this.loading = false;
|
||||
},
|
||||
error: (err) => {
|
||||
this.loading = false;
|
||||
console.error('Error fetching user:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
|
@ -103,49 +52,35 @@ export class AddUserModalComponent implements OnInit {
|
|||
|
||||
onSubmit(): void {
|
||||
if (this.userForm.valid) {
|
||||
const payload: any = {
|
||||
const userPayload = {
|
||||
username: this.userForm.value.username,
|
||||
allowedOrganizationalUnits: this.userForm.value.organizationalUnits,
|
||||
allowedOrganizationalUnits: this.userForm.value.organizationalUnit,
|
||||
password: this.userForm.value.password,
|
||||
enabled: true,
|
||||
userGroups: [this.userForm.value.role],
|
||||
groupsView: this.userForm.value.groupsView
|
||||
userGroups: [this.userForm.value.role ]
|
||||
};
|
||||
|
||||
if (!this.userId && this.userForm.value.password) {
|
||||
payload.password = this.userForm.value.password;
|
||||
} else if (this.userId && this.userForm.value.password.trim() !== '') {
|
||||
payload.password = this.userForm.value.password;
|
||||
}
|
||||
|
||||
this.loading = true;
|
||||
|
||||
if (this.userId) {
|
||||
this.http.put(`${this.baseUrl}${this.userId}`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Usuario editado correctamente');
|
||||
this.userEdited.emit();
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/users`, payload).subscribe(
|
||||
(response) => {
|
||||
this.toastService.success('Usuario añadido correctamente');
|
||||
this.userAdded.emit();
|
||||
this.dialogRef.close();
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
this.toastService.error(error['error']['hydra:description']);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
this.userService.addUser(userPayload).subscribe(
|
||||
response => {
|
||||
console.log('User playload:', userPayload);
|
||||
console.log('User added successfully:', response);
|
||||
this.userAdded.emit();
|
||||
this.dialogRef.close(this.userForm.value);
|
||||
this.openSnackBar(false, 'Usuario creado correctamente')
|
||||
},
|
||||
error => {
|
||||
console.error('Error adding user:', error);
|
||||
this.openSnackBar(true, error.error['hydra:description']);
|
||||
// Agregar alguna lógica para manejar el error en la interfaz de usuario
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
openSnackBar(isError: boolean, message: string) {
|
||||
if (isError) {
|
||||
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
|
||||
} else
|
||||
this.toastService.success(message, 'Éxito');
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
.user-form .form-field {
|
||||
display: block;
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
|
@ -16,14 +17,3 @@ mat-spinner {
|
|||
margin: 0 auto;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
margin-top: 2em;
|
||||
}
|
|
@ -1,29 +1,31 @@
|
|||
<h1 mat-dialog-title>{{ 'dialogTitleChangePassword' | translate }}</h1>
|
||||
<mat-dialog-content class="form-container">
|
||||
<h1 mat-dialog-title i18n="@@dialogTitleEditUser">Editar Usuario</h1>
|
||||
<div mat-dialog-content>
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'labelCurrentPassword' | translate }}</mat-label>
|
||||
<mat-label i18n="@@labelCurrentPassword">Contraseña actual</mat-label>
|
||||
<input matInput formControlName="currentPassword" type="password">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'labelNewPassword' | translate }}</mat-label>
|
||||
<mat-label i18n="@@labelNewPassword">Nueva contraseña</mat-label>
|
||||
<input matInput formControlName="newPassword" type="password">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label>{{ 'labelRepeatPassword' | translate }}</mat-label>
|
||||
<mat-label i18n="@@labelRepeatPassword">Repite la contraseña</mat-label>
|
||||
<input matInput formControlName="repeatNewPassword" type="password">
|
||||
</mat-form-field>
|
||||
|
||||
<div class="error-message" *ngIf="passwordMismatch">
|
||||
{{ 'errorPasswordMismatch' | translate }}
|
||||
<mat-spinner *ngIf="loading"></mat-spinner>
|
||||
|
||||
<div class="error-message" *ngIf="passwordMismatch" i18n="@@errorPasswordMismatch">
|
||||
Las contraseñas no coinciden
|
||||
</div>
|
||||
|
||||
<div class="error-message" *ngIf="updateError">
|
||||
<div class="error-message" *ngIf="updateError" i18n="@@errorUpdate">
|
||||
{{ resetPasswordError }}
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" [disabled]="loading">{{ 'buttonEdit' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onNoClick()" i18n="@@buttonCancel">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" [disabled]="loading" i18n="@@buttonEdit">Editar</button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { ChangePasswordModalComponent } from './change-password-modal.component';
|
||||
|
||||
describe('ChangePasswordModalComponent', () => {
|
||||
let component: ChangePasswordModalComponent;
|
||||
let fixture: ComponentFixture<ChangePasswordModalComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [ChangePasswordModalComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ChangePasswordModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,9 +1,9 @@
|
|||
import { Component, EventEmitter, Inject, Output } from '@angular/core';
|
||||
import { FormGroup, FormBuilder, Validators } from '@angular/forms';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { DataService } from '../data.service';
|
||||
import {AddUserModalComponent} from "../add-user-modal/add-user-modal.component";
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
import { EditUserModalComponent } from '../edit-user-modal/edit-user-modal.component';
|
||||
import { UserService } from '../users.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-change-password-modal',
|
||||
templateUrl: './change-password-modal.component.html',
|
||||
|
@ -18,11 +18,10 @@ export class ChangePasswordModalComponent {
|
|||
resetPasswordError: string = '';
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<AddUserModalComponent>,
|
||||
public dialogRef: MatDialogRef<EditUserModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private fb: FormBuilder,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService
|
||||
private userService: UserService
|
||||
) {
|
||||
this.userForm = this.fb.group({
|
||||
currentPassword: ['', Validators.required],
|
||||
|
@ -50,21 +49,20 @@ export class ChangePasswordModalComponent {
|
|||
newPassword: this.userForm.value.newPassword,
|
||||
repeatNewPassword: this.userForm.value.repeatNewPassword
|
||||
};
|
||||
|
||||
this.dataService.changePassword(this.data.uuid, userPayload).subscribe({
|
||||
next: () => {
|
||||
console.log("THIS IS THE USER PAYLOAD: ", userPayload);
|
||||
this.userService.changePassword(this.data.uuid, userPayload).subscribe(
|
||||
response => {
|
||||
console.log('User updated successfully:', response);
|
||||
this.userEdited.emit();
|
||||
this.dialogRef.close();
|
||||
this.toastService.success('Contraseña cambiada con éxito');
|
||||
this.dialogRef.close(this.userForm.value);
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error changing password:', err);
|
||||
this.toastService.error(err.error['hydra:description']);
|
||||
this.resetPasswordError = err.error.message;
|
||||
error => {
|
||||
console.error('Error updating user:', error.error['hydra:description']);
|
||||
this.resetPasswordError = error.error['hydra:description']
|
||||
this.updateError = true;
|
||||
this.loading = false;
|
||||
this.loading = false
|
||||
}
|
||||
});
|
||||
);
|
||||
} else {
|
||||
console.error('Form is invalid');
|
||||
this.passwordMismatch = this.userForm.hasError('mismatch');
|
||||
|
|
|
@ -1,92 +0,0 @@
|
|||
|
||||
import { Injectable } from '@angular/core';
|
||||
import {HttpClient, HttpParams} from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
|
||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/users?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
|
||||
getUsers(filters: { [key: string]: string }): Observable<{ totalItems: any; data: any }> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
||||
return this.http.get<any>(this.apiUrl, { params }).pipe(
|
||||
map(response => {
|
||||
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
|
||||
return {
|
||||
data: response['hydra:member'],
|
||||
totalItems: response['hydra:totalItems']
|
||||
}
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching users', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getUser(id: string): Observable<any> {
|
||||
return this.http.get<any>(`${this.baseUrl}${id}`).pipe(
|
||||
map(response => {
|
||||
if (response.username) {
|
||||
return response;
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching user group', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getUserGroups(): Observable<any> {
|
||||
return this.http.get<any>(`${this.baseUrl}/user-groups`).pipe(
|
||||
map(response => {
|
||||
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
|
||||
return response;
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching user groups', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getOrganizationalUnits(): Observable<any> {
|
||||
return this.http.get<any>(`${this.baseUrl}/organizational-units`).pipe(
|
||||
map(response => {
|
||||
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
|
||||
return response;
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching organizational units', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
changePassword(userId: number, userPayload: any): Observable<any> {
|
||||
return this.http.put(`${this.baseUrl}/users/${userId}/reset-password`, userPayload);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
.user-form .form-field {
|
||||
display: block;
|
||||
margin-bottom: 10px; /* Puedes ajustar el valor para cambiar la separación */
|
||||
}
|
||||
|
||||
.checkbox-group label {
|
||||
display: block;
|
||||
margin-bottom: 8px; /* Ajusta este valor según necesites */
|
||||
}
|
||||
|
||||
.loading-container{
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
<h1 mat-dialog-title i18n="@@dialogTitleEditUser">Editar Usuario</h1>
|
||||
<mat-spinner *ngIf="loading" class="loading-container"></mat-spinner>
|
||||
<div *ngIf="!loading" mat-dialog-content>
|
||||
<form [formGroup]="userForm" class="user-form">
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@editUserlabelUsername">Nombre de usuario</mat-label>
|
||||
<input matInput formControlName="username">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@editUserlabelPassword">Contraseña</mat-label>
|
||||
<input matInput formControlName="password" type="password">
|
||||
</mat-form-field>
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelRole">Rol</mat-label>
|
||||
<mat-select multiple formControlName="userGroups">
|
||||
<mat-option *ngFor="let group of userGroups" [value]="group['@id']">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field class="form-field">
|
||||
<mat-label i18n="@@labelOrgUnit">Unidad organizativa</mat-label>
|
||||
<mat-select multiple formControlName="allowedOrganizationalUnits">
|
||||
<mat-option *ngFor="let unit of organizationalUnits" [value]="unit['@id']">
|
||||
{{unit.name}}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
</div>
|
||||
<div mat-dialog-actions>
|
||||
<button mat-button (click)="onNoClick()" i18n="@@buttonCancel">Cancelar</button>
|
||||
<button mat-button (click)="onSubmit()" i18n="@@buttonEdit">Editar</button>
|
||||
</div>
|
|
@ -0,0 +1,40 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { EditUserModalComponent } from './edit-user-modal.component';
|
||||
import { UserService } from '../users.service';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
describe('EditUserModalComponent', () => {
|
||||
let component: EditUserModalComponent;
|
||||
let fixture: ComponentFixture<EditUserModalComponent>;
|
||||
let dialogRefSpy: jasmine.SpyObj<MatDialogRef<EditUserModalComponent>>;
|
||||
let userServiceSpy: jasmine.SpyObj<UserService>;
|
||||
let toastServiceSpy: jasmine.SpyObj<ToastrService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
dialogRefSpy = jasmine.createSpyObj('MatDialogRef', ['close']);
|
||||
userServiceSpy = jasmine.createSpyObj('UserService', ['getUserGroups', 'getOrganizationalUnits']);
|
||||
toastServiceSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [EditUserModalComponent],
|
||||
imports: [ReactiveFormsModule],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: dialogRefSpy },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: { user: { username: 'testUser', userGroups: [], allowedOrganizationalUnits: [] } } },
|
||||
{ provide: UserService, useValue: userServiceSpy },
|
||||
{ provide: ToastrService, useValue: toastServiceSpy }
|
||||
]
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(EditUserModalComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,82 @@
|
|||
import { Component, EventEmitter, Inject, OnInit, Output } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { UserService } from '../users.service';
|
||||
import {ToastrService} from "ngx-toastr";
|
||||
|
||||
@Component({
|
||||
selector: 'app-edit-user-modal',
|
||||
templateUrl: './edit-user-modal.component.html',
|
||||
styleUrls: ['./edit-user-modal.component.css']
|
||||
})
|
||||
export class EditUserModalComponent implements OnInit {@Output() userEdited = new EventEmitter<void>();
|
||||
userForm: FormGroup;
|
||||
userGroups: any[] = [];
|
||||
organizationalUnits: any[] = [];
|
||||
loading:boolean = false;
|
||||
|
||||
constructor(
|
||||
public dialogRef: MatDialogRef<EditUserModalComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private fb: FormBuilder,
|
||||
private userService: UserService, // Inyecta el servicio
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.userForm = this.fb.group({
|
||||
username: [this.data.user.username],
|
||||
password: [null],
|
||||
userGroups: [this.data.user.userGroups.map((group: { '@id': any; }) => group['@id'])],
|
||||
allowedOrganizationalUnits: [this.data.user.allowedOrganizationalUnits.map((unit: { '@id': any; }) => unit['@id'])]
|
||||
});
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loading = true
|
||||
this.userService.getUserGroups().subscribe((data) => {
|
||||
this.userGroups = data['hydra:member'];
|
||||
});
|
||||
this.userService.getOrganizationalUnits().subscribe((data) => {
|
||||
this.organizationalUnits = data['hydra:member'].filter((item: any) => item.type === 'organizational-unit');
|
||||
this.loading = false
|
||||
}, (error) => {
|
||||
console.error('Error fetching organizational units', error);
|
||||
this.loading = false
|
||||
});
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
console.log(this.userForm.value);
|
||||
const userPayload = {
|
||||
username: this.userForm.value.username,
|
||||
allowedOrganizationalUnits: this.userForm.value.allowedOrganizationalUnits,
|
||||
password: this.userForm.value.password,
|
||||
enabled: true,
|
||||
userGroups: this.userForm.value.userGroups
|
||||
};
|
||||
|
||||
this.userService.updateUser(this.data.user.uuid, userPayload).subscribe(
|
||||
response => {
|
||||
console.log('User added successfully:', response);
|
||||
this.userEdited.emit();
|
||||
this.dialogRef.close(this.userForm.value);
|
||||
this.openSnackBar(false, 'Usuario actualizado correctamente')
|
||||
},
|
||||
error => {
|
||||
console.error('Error adding user:', error);
|
||||
this.openSnackBar(true, error.message);
|
||||
// Agregar alguna lógica para manejar el error en la interfaz de usuario
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
openSnackBar(isError: boolean, message: string) {
|
||||
if (isError) {
|
||||
this.toastService.error(' Error al eliminar la entidad: ' + message, 'Error');
|
||||
} else
|
||||
this.toastService.success(message, 'Éxito');
|
||||
}
|
||||
}
|
|
@ -1,47 +1,20 @@
|
|||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
.header-container {
|
||||
margin-top: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
.header-container h1 {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,60 +1,22 @@
|
|||
<div class="header-container">
|
||||
<div class="header-container-title">
|
||||
<h2>{{ 'adminUsersTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="images-button-row">
|
||||
<button class="action-button" (click)="addUser()">
|
||||
{{ 'addUser' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
<h1 i18n="@@headerUserManagement">Gestión de usuarios</h1>
|
||||
<button mat-flat-button color="primary" (click)="addUser()" i18n="@@buttonAddUser">+ Añadir</button>
|
||||
</div>
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'searchLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let user"> {{ column.cell(user) }} </td>
|
||||
</ng-container>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef i18n="@@columnActions">Acciones</th>
|
||||
<td mat-cell *matCellDef="let user">
|
||||
<button mat-button color="primary" (click)="editUser(user)" i18n="@@buttonEditUser">Editar</button>
|
||||
<button mat-button color="warn" (click)="deleteUser(user)" i18n="@@buttonDeleteUser">Eliminar</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let user">
|
||||
<ng-container *ngIf="column.columnDef === 'groupsView'">
|
||||
<mat-chip>
|
||||
{{ user[column.columnDef] === 'card' ? 'Vista tarjetas' : 'Listado' }}
|
||||
</mat-chip>
|
||||
</ng-container>
|
||||
<ng-container *ngIf="column.columnDef !== 'groupsView'">
|
||||
{{ column.cell(user) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let user" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editUser(user)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteUser(user)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
|
|
@ -5,13 +5,19 @@ import { MatDialogModule } from '@angular/material/dialog';
|
|||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { of } from 'rxjs';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
import { UserService } from './users.service';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core'; // Add this import for schema
|
||||
|
||||
class MockToastrService {
|
||||
success() {}
|
||||
error() {}
|
||||
// Create a mock for UserService
|
||||
class MockUserService {
|
||||
getUsers() {
|
||||
return of({
|
||||
'hydra:member': [
|
||||
{ id: 1, username: 'user1', allowedOrganizationalUnits: [], roles: ['admin'] },
|
||||
{ id: 2, username: 'user2', allowedOrganizationalUnits: [], roles: ['user'] }
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
describe('UsersComponent', () => {
|
||||
|
@ -19,26 +25,21 @@ describe('UsersComponent', () => {
|
|||
let fixture: ComponentFixture<UsersComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [UsersComponent],
|
||||
imports: [
|
||||
MatTableModule,
|
||||
MatDialogModule,
|
||||
HttpClientTestingModule,
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: ToastrService, useClass: MockToastrService },
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
{ provide: UserService, useClass: MockUserService },
|
||||
{ provide: ToastrService, useValue: { success: () => {} } }, // Mock ToastrService
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA], // Ignorar elementos desconocidos
|
||||
}).compileComponents();
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA], // Use this to ignore unrecognized components in template
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(UsersComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
@ -48,27 +49,8 @@ describe('UsersComponent', () => {
|
|||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should have default values for pagination', () => {
|
||||
expect(component.itemsPerPage).toBe(10);
|
||||
expect(component.page).toBe(0);
|
||||
expect(component.pageSizeOptions).toEqual([5, 10, 20, 40, 100]);
|
||||
});
|
||||
|
||||
it('should call search on init', () => {
|
||||
spyOn(component, 'search');
|
||||
it('should load users on init', () => {
|
||||
component.ngOnInit();
|
||||
expect(component.search).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should initialize the dataSource', () => {
|
||||
expect(component.dataSource).toBeDefined();
|
||||
});
|
||||
|
||||
it('should define displayedColumns', () => {
|
||||
expect(component.displayedColumns).toBeDefined();
|
||||
expect(component.displayedColumns).toContain('id');
|
||||
expect(component.displayedColumns).toContain('username');
|
||||
expect(component.displayedColumns).toContain('roles');
|
||||
expect(component.displayedColumns).toContain('actions');
|
||||
expect(component.dataSource.data.length).toBe(2); // Expect 2 mock users
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
import { Component, OnInit } from '@angular/core';
|
||||
import { UserService } from './users.service';
|
||||
import { MatTableDataSource } from '@angular/material/table';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { AddUserModalComponent } from './add-user-modal/add-user-modal.component';
|
||||
import { EditUserModalComponent } from './edit-user-modal/edit-user-modal.component';
|
||||
import { DeleteModalComponent } from '../../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-users',
|
||||
templateUrl: './users.component.html',
|
||||
styleUrls: ['./users.component.css']
|
||||
})
|
||||
|
||||
export class UsersComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
filters: { [key: string]: string } = {};
|
||||
loading: boolean = false;
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 0;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
|
@ -33,11 +28,6 @@ export class UsersComponent implements OnInit {
|
|||
header: 'Nombre de Usuario',
|
||||
cell: (user: any) => `${user.username}`
|
||||
},
|
||||
{
|
||||
columnDef: 'groupsView',
|
||||
header: 'Vista de Grupos',
|
||||
cell: (user: any) => `${user.groupsView}`
|
||||
},
|
||||
{
|
||||
columnDef: 'allowedOrganizationalUnits',
|
||||
header: 'Unidades Organizacionales Permitidas',
|
||||
|
@ -51,88 +41,64 @@ export class UsersComponent implements OnInit {
|
|||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private configService: ConfigService,
|
||||
constructor(private userService: UserService, public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/users`;
|
||||
}
|
||||
private toastService: ToastrService) {}
|
||||
|
||||
ngOnInit() {
|
||||
this.search();
|
||||
this.loadUsers();
|
||||
}
|
||||
|
||||
search() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?&page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching users', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
loadUsers() {
|
||||
this.userService.getUsers().subscribe(response => {
|
||||
this.dataSource.data = response['hydra:member'];
|
||||
});
|
||||
}
|
||||
|
||||
addUser() {
|
||||
const dialogRef = this.dialog.open(AddUserModalComponent, { width: '500px' });
|
||||
|
||||
dialogRef.componentInstance.userAdded.subscribe(() => {
|
||||
this.search();
|
||||
this.loadUsers();
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
editUser(user: any): void {
|
||||
const dialogRef = this.dialog.open(AddUserModalComponent, {
|
||||
width: '600px',
|
||||
data: user['@id']
|
||||
editUser(user: any) {
|
||||
// Implementar la lógica de edición
|
||||
const dialogRef = this.dialog.open(EditUserModalComponent, {
|
||||
data: { user: user },
|
||||
width: '500px'
|
||||
});
|
||||
|
||||
dialogRef.componentInstance.userEdited.subscribe(() => {
|
||||
this.search();
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
this.loadUsers();
|
||||
});
|
||||
}
|
||||
|
||||
deleteUser(user: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '500px',
|
||||
data: { name: user.username }
|
||||
width: '300px',
|
||||
data: { name: user.name }
|
||||
});
|
||||
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
const apiUrl = `${this.baseUrl}/users/${user.uuid}`;
|
||||
|
||||
|
||||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
console.log('User deleted successfully');
|
||||
this.toastService.success('User deleted successfully');
|
||||
this.search();
|
||||
this.loadUsers();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error deleting user:', error);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('User deletion cancelled');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,66 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Observable } from 'rxjs';
|
||||
|
||||
interface UserPayload {
|
||||
username: string;
|
||||
password: string;
|
||||
enabled: boolean;
|
||||
userGroups: string[];
|
||||
allowedOrganizationalUnits: any[];
|
||||
}
|
||||
|
||||
interface ChangePasswordPayload {
|
||||
currentPassword: string;
|
||||
newPassword: string;
|
||||
repeatNewPassword: string;
|
||||
}
|
||||
|
||||
interface UserGroup {
|
||||
'@id': string;
|
||||
name: string;
|
||||
role: string[];
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class UserService {
|
||||
private apiUrl = import.meta.env.NG_APP_BASE_API_URL;
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
|
||||
addUser(userPayload: UserPayload): Observable<any> {
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/ld+json',
|
||||
});
|
||||
return this.http.post(`${this.apiUrl}/users`, userPayload, { headers });
|
||||
}
|
||||
|
||||
updateUser(userId: number, userPayload: UserPayload): Observable<any> {
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/ld+json',
|
||||
});
|
||||
return this.http.put(`${this.apiUrl}/users/${userId}`, userPayload, { headers });
|
||||
}
|
||||
|
||||
changePassword(userId: number, userPayload: ChangePasswordPayload): Observable<any> {
|
||||
const headers = new HttpHeaders({
|
||||
'Content-Type': 'application/ld+json',
|
||||
});
|
||||
return this.http.put(`${this.apiUrl}/users/${userId}/reset-password`, userPayload, { headers });
|
||||
}
|
||||
|
||||
getUserGroups(): Observable<{ 'hydra:member': UserGroup[] }> {
|
||||
return this.http.get<{ 'hydra:member': UserGroup[] }>(`${this.apiUrl}/user-groups`);
|
||||
}
|
||||
|
||||
getUsers(): Observable<any> {
|
||||
return this.http.get<any>(`${this.apiUrl}/users?page=1&itemsPerPage=30`);
|
||||
}
|
||||
|
||||
getOrganizationalUnits(): Observable<any> {
|
||||
return this.http.get<any>(`${this.apiUrl}/organizational-units?page=1&itemsPerPage=10000`);
|
||||
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.calendar-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.card.unidad-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
|
@ -1,74 +0,0 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCalendarsTitle' | translate }}</h2>
|
||||
</div>
|
||||
<div class="calendar-button-row">
|
||||
<button joyrideStep="addButtonStep" text="{{ 'addButtonStepText' | translate }}" class="action-button"
|
||||
(click)="addCalendar()">
|
||||
{{ 'addCalendar' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container">
|
||||
<mat-form-field joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}" appearance="fill"
|
||||
class="search-string">
|
||||
<mat-label>{{ 'searchCalendarLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']"
|
||||
(keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep"
|
||||
text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let image">
|
||||
<ng-container *ngIf="column.columnDef === 'isDefault' || column.columnDef === 'installed'">
|
||||
<mat-icon [color]="image[column.columnDef] ? 'primary' : 'warn'">
|
||||
{{ image[column.columnDef] ? 'check_circle' : 'cancel' }}
|
||||
</mat-icon>
|
||||
</ng-container>
|
||||
<ng-container
|
||||
*ngIf="column.columnDef !== 'isDefault' && column.columnDef !== 'installed' && column.columnDef !== 'downloadUrl'">
|
||||
{{ column.cell(image) }}
|
||||
</ng-container>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let calendar" style="text-align: center;">
|
||||
<button mat-icon-button color="primary" (click)="editCalendar(calendar)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="!syncUds" mat-icon-button color="primary" (click)="sync(calendar)">
|
||||
<mat-icon>sync</mat-icon>
|
||||
</button>
|
||||
<button *ngIf="syncUds" mat-icon-button color="primary">
|
||||
<app-loading [isLoading]="syncUds"></app-loading>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteCalendar(calendar)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container">
|
||||
<mat-paginator [length]="length" [pageSize]="itemsPerPage" [pageIndex]="page" [pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
|
@ -1,64 +0,0 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { MatIconModule } from '@angular/material/icon';
|
||||
import { MatButtonModule } from '@angular/material/button';
|
||||
import { MatTableModule } from '@angular/material/table';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { MatTooltipModule } from '@angular/material/tooltip';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { CalendarComponent } from './calendar.component';
|
||||
import { MatProgressSpinner } from '@angular/material/progress-spinner';
|
||||
import { JoyrideModule, JoyrideService } from 'ngx-joyride';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('CalendarComponent', () => {
|
||||
let component: CalendarComponent;
|
||||
let fixture: ComponentFixture<CalendarComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const mockConfigService = {
|
||||
apiUrl: 'http://mock-api-url',
|
||||
mercureUrl: 'http://mock-mercure-url'
|
||||
};
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CalendarComponent, LoadingComponent],
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
ToastrModule.forRoot(),
|
||||
BrowserAnimationsModule,
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatInputModule,
|
||||
MatIconModule,
|
||||
MatButtonModule,
|
||||
MatTableModule,
|
||||
MatPaginatorModule,
|
||||
MatTooltipModule,
|
||||
FormsModule,
|
||||
MatProgressSpinner,
|
||||
JoyrideModule.forRoot(),
|
||||
TranslateModule.forRoot(),
|
||||
],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: mockConfigService }
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CalendarComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,176 +0,0 @@
|
|||
import { Component, OnInit, signal } from '@angular/core';
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { MatDialog } from "@angular/material/dialog";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { DataService } from "./data.service";
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { PageEvent } from "@angular/material/paginator";
|
||||
import { CreateCalendarComponent } from "./create-calendar/create-calendar.component";
|
||||
import { DeleteModalComponent } from "../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-calendar',
|
||||
templateUrl: './calendar.component.html',
|
||||
styleUrl: './calendar.component.css'
|
||||
})
|
||||
export class CalendarComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
images: { downloadUrl: string; name: string; uuid: string }[] = [];
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 1;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
loading: boolean = false;
|
||||
filters: { [key: string]: string } = {};
|
||||
alertMessage: string | null = null;
|
||||
readonly panelOpenState = signal(false);
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
syncUds: boolean = false;
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'ID',
|
||||
cell: (remoteCalendar: any) => `${remoteCalendar.id}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre',
|
||||
cell: (remoteCalendar: any) => `${remoteCalendar.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'rulesLength',
|
||||
header: 'Nº de reglas',
|
||||
cell: (remoteCalendar: any) => `${remoteCalendar.remoteCalendarRules.length}`
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'Fecha de creación',
|
||||
cell: (remoteCalendar: any) => `${this.datePipe.transform(remoteCalendar.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
|
||||
constructor(
|
||||
public dialog: MatDialog,
|
||||
private http: HttpClient,
|
||||
private dataService: DataService,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private joyrideService: JoyrideService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/remote-calendars`;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
}
|
||||
|
||||
addCalendar(): void {
|
||||
const dialogRef = this.dialog.open(CreateCalendarComponent, {
|
||||
width: '400px'
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
this.search();
|
||||
});
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.dataService.getRemoteCalendars(this.filters).subscribe(
|
||||
data => {
|
||||
this.dataSource.data = data;
|
||||
this.loading = false;
|
||||
},
|
||||
error => {
|
||||
console.error('Error fetching calendars', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
sync(calendar: any): void {
|
||||
this.syncUds = true;
|
||||
this.http.post(`${this.apiUrl}/${calendar.uuid}/sync-uds`, {}).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Calendarios sincronizados correctamente');
|
||||
this.search();
|
||||
this.syncUds = false;
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
this.syncUds = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
editCalendar(calendar: any): void {
|
||||
const dialogRef = this.dialog.open(CreateCalendarComponent, {
|
||||
width: '700px',
|
||||
data: calendar['@id']
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.search();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteCalendar(calendar: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '400px',
|
||||
data: { name: calendar.name }
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
const apiUrl = `${this.baseUrl}${calendar['@id']}`;
|
||||
|
||||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
this.search();
|
||||
this.toastService.success('Calendar deleted successfully');
|
||||
},
|
||||
error: () => {
|
||||
this.toastService.error('Error deleting calendar');
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
applyFilter() {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`).subscribe({
|
||||
next: (response) => {
|
||||
this.dataSource.data = response['hydra:member'];
|
||||
this.length = response['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
error: () => {
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
onPageChange(event: PageEvent) {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.applyFilter();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: ['titleStep', 'addButtonStep', 'searchStep', 'tableStep', 'actionsStep'],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.form-container {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
margin: 15px 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
}
|
||||
|
||||
.time-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<mat-slide-toggle [(ngModel)]="isRemoteAvailable" class="example-margin">
|
||||
{{ 'remoteAvailability' | translate }}
|
||||
</mat-slide-toggle>
|
||||
|
||||
<div *ngIf="!isRemoteAvailable" class="form-group">
|
||||
<mat-label>{{ 'selectWeekDays' | translate }}</mat-label>
|
||||
<div class="row">
|
||||
<div class="col-md-6 checkbox-group">
|
||||
<mat-checkbox *ngFor="let day of weekDays.slice(0, (weekDays.length / 2) + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||
{{ day }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="col-md-6 checkbox-group">
|
||||
<mat-checkbox *ngFor="let day of weekDays.slice(weekDays.length / 2 + 1)" [(ngModel)]="busyWeekDays[day]">
|
||||
{{ day }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'startTime' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="busyFromHour" type="time" placeholder="{{ 'startTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="time-field">
|
||||
<mat-label>{{ 'endTime' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="busyToHour" type="time" placeholder="{{ 'endTimePlaceholder' | translate }}" [required]="!isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div *ngIf="isRemoteAvailable" class="form-group">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'reasonLabel' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="availableReason" placeholder="{{ 'reasonPlaceholder' | translate }}" [required]="isRemoteAvailable">
|
||||
</mat-form-field>
|
||||
<div class="time-fields">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'startDate' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="picker1" [(ngModel)]="availableFromDate" [required]="isRemoteAvailable">
|
||||
<mat-hint>MM/DD/YYYY</mat-hint>
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker1"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker1></mat-datepicker>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'endDate' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="picker2" [(ngModel)]="availableToDate" [required]="isRemoteAvailable">
|
||||
<mat-hint>MM/DD/YYYY</mat-hint>
|
||||
<mat-datepicker-toggle matIconSuffix [for]="picker2"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker2></mat-datepicker>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button
|
||||
class="submit-button"
|
||||
(click)="submitRule()"
|
||||
cdkFocusInitial
|
||||
[disabled]="(!isRemoteAvailable && (!busyFromHour || !busyToHour)) || (isRemoteAvailable && (!availableReason || !availableFromDate || !availableToDate))">
|
||||
{{ isEditMode ? ('buttonSave' | translate) : ('buttonAdd' | translate) }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,119 +0,0 @@
|
|||
import { Component, Inject } from '@angular/core';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from "@angular/material/dialog";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-calendar-rule',
|
||||
templateUrl: './create-calendar-rule.component.html',
|
||||
styleUrl: './create-calendar-rule.component.css'
|
||||
})
|
||||
export class CreateCalendarRuleComponent {
|
||||
baseUrl: string;
|
||||
name: string = '';
|
||||
remoteCalendarRules: any[] = [];
|
||||
weekDays: string[] = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
|
||||
busyWeekDays: { [key: string]: boolean } = {};
|
||||
busyFromHour: any = null;
|
||||
busyToHour: any = null;
|
||||
availableFromDate: any = null;
|
||||
availableToDate: any = null;
|
||||
isRemoteAvailable: boolean = false;
|
||||
showAdditionalForm: boolean = false;
|
||||
availableReason: any = null;
|
||||
isEditMode: boolean = false;
|
||||
ruleId: string | null = null;
|
||||
calendarId: string | null = null;
|
||||
selectedDaysIndices: number[] = [];
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
public dialogRef: MatDialogRef<CreateCalendarRuleComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.calendarId = this.data.calendar
|
||||
if (this.data) {
|
||||
this.isEditMode = true;
|
||||
this.availableFromDate = this.data.rule ? this.data.rule.availableFromDate : null;
|
||||
this.availableToDate = this.data.rule ? this.data.rule.availableToDate : null;
|
||||
this.isRemoteAvailable = this.data.rule ? this.data.rule.isRemoteAvailable : false;
|
||||
this.availableReason = this.data.rule ? this.data.rule.availableReason : null;
|
||||
this.busyFromHour = this.data.rule ? this.data.rule.busyFromHour : null;
|
||||
this.busyToHour = this.data.rule ? this.data.rule.busyToHour : null;
|
||||
if (this.data.rule && this.data.rule.busyWeekDays) {
|
||||
this.busyWeekDays = this.data.rule.busyWeekDays.reduce((acc: {
|
||||
[x: string]: boolean;
|
||||
}, day: string | number) => {
|
||||
// @ts-ignore
|
||||
acc[this.weekDays[day]] = true;
|
||||
return acc;
|
||||
}, {});
|
||||
}
|
||||
this.ruleId = this.data.rule ? this.data.rule['@id'] : null
|
||||
}
|
||||
}
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
toggleAdditionalForm(): void {
|
||||
this.showAdditionalForm = !this.showAdditionalForm;
|
||||
}
|
||||
|
||||
getSelectedDaysIndices() {
|
||||
this.selectedDaysIndices = this.weekDays
|
||||
.map((day, index) => this.busyWeekDays[day] ? index : -1)
|
||||
.filter(index => index !== -1);
|
||||
}
|
||||
|
||||
submitRule(): void {
|
||||
this.getSelectedDaysIndices()
|
||||
const selectedDaysArray = Object.keys(this.busyWeekDays).map((day, index) => this.busyWeekDays[index]);
|
||||
|
||||
const formData = {
|
||||
remoteCalendar: this.calendarId,
|
||||
busyWeekDays: this.selectedDaysIndices,
|
||||
busyFromHour: this.busyFromHour,
|
||||
busyToHour: this.busyToHour,
|
||||
availableFromDate: this.availableFromDate,
|
||||
availableToDate: this.availableToDate,
|
||||
isRemoteAvailable: this.isRemoteAvailable,
|
||||
availableReason: this.availableReason
|
||||
};
|
||||
|
||||
if (this.isEditMode && this.ruleId) {
|
||||
this.http.put(`${this.baseUrl}${this.ruleId}`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar updated successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/remote-calendar-rules`, formData)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar created successfully');
|
||||
this.dialogRef.close(true);
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
console.log(formData);
|
||||
}
|
||||
}
|
|
@ -1,63 +0,0 @@
|
|||
.form-container {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-top: 20px;
|
||||
margin-bottom: 26px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
.additional-form {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin: 15px 0;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.time-fields {
|
||||
display: flex;
|
||||
gap: 15px; /* Espacio entre los campos */
|
||||
}
|
||||
|
||||
.time-field {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.list-item-content {
|
||||
display: flex;
|
||||
align-items: flex-start; /* Alinea el contenido al inicio */
|
||||
justify-content: space-between; /* Espacio entre los textos y los íconos */
|
||||
width: 100%; /* Asegúrate de que el contenido ocupe todo el ancho */
|
||||
}
|
||||
|
||||
.text-content {
|
||||
flex-grow: 1; /* Permite que este contenedor ocupe el espacio disponible */
|
||||
margin-right: 16px; /* Espaciado a la derecha para separar de los íconos */
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.icon-container {
|
||||
display: flex;
|
||||
align-items: center; /* Alinea los íconos verticalmente */
|
||||
}
|
||||
|
||||
.right-icon {
|
||||
margin-left: 8px; /* Espaciado entre los íconos */
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
|
@ -1,49 +0,0 @@
|
|||
<h2 mat-dialog-title>{{ isEditMode ? ('editCalendar' | translate) : ('addCalendar' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<!-- Campo para el nombre -->
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'labelName' | translate }}</mat-label>
|
||||
<input matInput [(ngModel)]="name" required>
|
||||
<mat-icon *ngIf="isEditMode" matSuffix (click)="submitForm()">mode_edit</mat-icon>
|
||||
</mat-form-field>
|
||||
|
||||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||||
<div *ngIf="isEditMode" mat-subheader>{{ 'rulesHeader' | translate }}</div>
|
||||
<button class="action-button" *ngIf="isEditMode" (click)="createRule()" style="padding: 10px;">
|
||||
{{ 'addRule' | translate }}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<mat-divider *ngIf="isEditMode"></mat-divider>
|
||||
|
||||
<mat-list *ngIf="isEditMode">
|
||||
<ng-container *ngFor="let rule of remoteCalendarRules;">
|
||||
<mat-list-item>
|
||||
<div class="list-item-content">
|
||||
<mat-icon matListItemIcon>event_available</mat-icon>
|
||||
<div class="text-content">
|
||||
<div matListItemTitle>{{ rule.isRemoteAvailable ? ('statusAvailable' | translate) : ('statusUnavailable' | translate) }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyFromHour }} - {{ rule.busyToHour }}</div>
|
||||
<div matListItemLine *ngIf="!rule.isRemoteAvailable">{{ rule.busyWeekDaysMap }}</div>
|
||||
<div matListItemLine *ngIf="rule.isRemoteAvailable">{{ rule.availableReason }} | {{ rule.availableFromDate | date }} - {{ rule.availableToDate | date }}</div>
|
||||
</div>
|
||||
<div class="icon-container">
|
||||
<button mat-icon-button color="primary" class="right-icon" (click)="createRule(rule)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" class="right-icon" (click)="deleteCalendarRule(rule)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</mat-list-item>
|
||||
</ng-container>
|
||||
</mat-list>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="onNoClick()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="submitForm()" [disabled]="!name || name === ''" cdkFocusInitial>
|
||||
{{ 'buttonSave' | translate }}
|
||||
</button>
|
||||
</mat-dialog-actions>
|
|
@ -1,143 +0,0 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { ToastrService } from "ngx-toastr";
|
||||
import { HttpClient } from "@angular/common/http";
|
||||
import { MAT_DIALOG_DATA, MatDialog, MatDialogRef } from "@angular/material/dialog";
|
||||
import { CreateCalendarRuleComponent } from "../create-calendar-rule/create-calendar-rule.component";
|
||||
import { DataService } from "../data.service";
|
||||
import { DeleteModalComponent } from "../../../shared/delete_modal/delete-modal/delete-modal.component";
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-calendar',
|
||||
templateUrl: './create-calendar.component.html',
|
||||
styleUrl: './create-calendar.component.css'
|
||||
})
|
||||
export class CreateCalendarComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
name: string = '';
|
||||
remoteCalendarRules: any[] = [];
|
||||
isEditMode: boolean = false;
|
||||
calendarId: string | null = null;
|
||||
busyWeekDaysMap: string[] = [];
|
||||
|
||||
constructor(
|
||||
private toastService: ToastrService,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
public dialogRef: MatDialogRef<CreateCalendarComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
public dialog: MatDialog,
|
||||
private dataService: DataService,
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
if (this.data) {
|
||||
this.load()
|
||||
}
|
||||
}
|
||||
|
||||
load(): void {
|
||||
this.dataService.getRemoteCalendar(this.data).subscribe({
|
||||
next: (response) => {
|
||||
this.isEditMode = true;
|
||||
this.name = response.name;
|
||||
this.remoteCalendarRules = response.remoteCalendarRules;
|
||||
this.calendarId = this.data;
|
||||
this.remoteCalendarRules.forEach(rule => {
|
||||
rule.busyWeekDaysMap = this.getBusyWeekDaysMap(rule.busyWeekDays);
|
||||
});
|
||||
},
|
||||
error: (err) => {
|
||||
console.error('Error fetching remote calendar:', err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
getBusyWeekDaysMap(indices: number[]): string[] {
|
||||
console.log(indices)
|
||||
const weekDays = ['Lunes', 'Martes', 'Miércoles', 'Jueves', 'Viernes', 'Sábado', 'Domingo'];
|
||||
return indices.map(index => weekDays[index]);
|
||||
}
|
||||
|
||||
|
||||
onNoClick(): void {
|
||||
this.dialogRef.close();
|
||||
}
|
||||
|
||||
submitForm(): void {
|
||||
const payload = {
|
||||
name: this.name
|
||||
};
|
||||
|
||||
if (this.isEditMode && this.calendarId) {
|
||||
this.http.patch(`${this.baseUrl}${this.calendarId}`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar updated successfully');
|
||||
this.dialogRef.close(true);
|
||||
this.load()
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/remote-calendars`, payload)
|
||||
.subscribe({
|
||||
next: (response) => {
|
||||
this.toastService.success('Calendar created successfully');
|
||||
this.dialogRef.close(true);
|
||||
this.load()
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error:', error);
|
||||
this.toastService.error(error.error['hydra:description']);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
createRule(rule: any = null): void {
|
||||
const dialogRef = this.dialog.open(CreateCalendarRuleComponent, {
|
||||
width: '700px',
|
||||
data: {
|
||||
calendar: this.calendarId,
|
||||
rule: rule
|
||||
},
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
this.load()
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
deleteCalendarRule(rule: any): void {
|
||||
const dialogRef = this.dialog.open(DeleteModalComponent, {
|
||||
width: '400px',
|
||||
});
|
||||
|
||||
dialogRef.afterClosed().subscribe(result => {
|
||||
if (result) {
|
||||
const apiUrl = `${this.baseUrl}${rule['@id']}`;
|
||||
|
||||
this.http.delete(apiUrl).subscribe({
|
||||
next: () => {
|
||||
console.log('Calendar deleted successfully');
|
||||
this.load();
|
||||
this.toastService.success('Calendar deleted successfully');
|
||||
},
|
||||
error: (error) => {
|
||||
this.toastService.error('Error deleting calendar');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
console.log('calendar deletion cancelled');
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,52 +0,0 @@
|
|||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpParams } from '@angular/common/http';
|
||||
import { Observable, throwError } from 'rxjs';
|
||||
import { catchError, map } from 'rxjs/operators';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class DataService {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
|
||||
constructor(private http: HttpClient, private configService: ConfigService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/remote-calendars?page=1&itemsPerPage=1000`;
|
||||
}
|
||||
|
||||
getRemoteCalendars(filters: { [key: string]: string }): Observable<any[]> {
|
||||
const params = new HttpParams({ fromObject: filters });
|
||||
|
||||
return this.http.get<any>(this.apiUrl, { params }).pipe(
|
||||
map(response => {
|
||||
if (response['hydra:member'] && Array.isArray(response['hydra:member'])) {
|
||||
return response['hydra:member'];
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching remote calendars', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
getRemoteCalendar(id: string): Observable<any> {
|
||||
return this.http.get<any>(`${this.baseUrl}${id}`).pipe(
|
||||
map(response => {
|
||||
if (response.name && response.remoteCalendarRules) {
|
||||
return response;
|
||||
} else {
|
||||
throw new Error('Unexpected response format');
|
||||
}
|
||||
}),
|
||||
catchError(error => {
|
||||
console.error('Error fetching calendar', error);
|
||||
return throwError(error);
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,78 +1,70 @@
|
|||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
.commands-list {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
cursor: pointer;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.command-item:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
.command-details {
|
||||
padding: 20px;
|
||||
border: 1px solid #ddd;
|
||||
background-color: #f4f4f4;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.script-display {
|
||||
margin-top: 20px;
|
||||
background-color: #000;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
pre {
|
||||
margin: 0;
|
||||
font-family: 'Courier New', Courier, monospace;
|
||||
}
|
||||
|
||||
.mat-elevation-z8{
|
||||
margin-top: 20px;
|
||||
}
|
||||
tr:hover {
|
||||
background-color: rgba(219, 219, 219, 0.219);
|
||||
}
|
||||
|
||||
.detailBtn{
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
padding: 0 5px;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.command-groups-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.imagesLists-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card.unidad-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0, 0, 0, 0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.mat-chip-readonly-true {
|
||||
background-color: #4CAF50 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.mat-chip-readonly-false {
|
||||
background-color: #F44336 !important;
|
||||
color: white !important;
|
||||
.command-groups-button-row{
|
||||
margin-bottom: 10px;
|
||||
}
|
|
@ -1,75 +1,72 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'adminCommandGroupsTitle' | translate }}</h2>
|
||||
<h2 class="title" i18n="@@adminCommandGroupsTitle">Administrar Grupos de Comandos</h2>
|
||||
<div class="command-groups-button-row">
|
||||
<button mat-flat-button color="primary" (click)="openCreateCommandGroupModal()">Añadir Grupo de Comandos</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="command-groups-button-row">
|
||||
<button class="action-button" (click)="openCreateCommandGroupModal()" joyrideStep="addCommandGroupStep" text="{{ 'addCommandGroupStepText' | translate }}">
|
||||
{{ 'addCommandGroup' | translate }}
|
||||
</button>
|
||||
<mat-divider class="divider"></mat-divider>
|
||||
<div class="search-container">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label i18n="@@searchLabel">Buscar nombre de grupo</mat-label>
|
||||
<input matInput placeholder="Búsqueda" [(ngModel)]="filters['name']" (keyup.enter)="search()" i18n-placeholder="@@searchPlaceholder">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint i18n="@@searchHint">Pulsar 'enter' para buscar</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'searchGroupNameLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()">
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
|
||||
<div *ngIf="loading" class="loading-container" joyrideStep="loadingStep" text="{{ 'loadingStepText' | translate }}">
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
</div>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="dataSource" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container *ngFor="let column of columns" [matColumnDef]="column.columnDef">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ column.header }} </th>
|
||||
<td mat-cell *matCellDef="let commandGroup">
|
||||
<ng-container *ngIf="column.columnDef !== 'commands'">
|
||||
{{ column.cell(commandGroup) }}
|
||||
</ng-container>
|
||||
|
||||
<ng-container *ngIf="column.columnDef === 'commands'" joyrideStep="viewCommandsStep" text="{{ 'viewCommandsStepText' | translate }}">
|
||||
<button class="action-button" [matMenuTriggerFor]="menu">{{ 'viewCommands' | translate }}</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item *ngFor="let command of commandGroup.commands">
|
||||
{{ command.name }}
|
||||
</button>
|
||||
</mat-menu>
|
||||
</ng-container>
|
||||
</td>
|
||||
|
||||
<table mat-table [dataSource]="commandGroups" class="mat-elevation-z8">
|
||||
<!-- Nombre del grupo columna -->
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Nombre del grupo </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<!-- Creado por columna -->
|
||||
<ng-container matColumnDef="createdBy">
|
||||
<th mat-header-cell *matHeaderCellDef> Creado por </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.createdBy }} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Fecha de creación columna -->
|
||||
<ng-container matColumnDef="createdAt">
|
||||
<th mat-header-cell *matHeaderCellDef> Fecha de creación </th>
|
||||
<td mat-cell *matCellDef="let group"> {{ group.createdAt | date:'short' }} </td>
|
||||
</ng-container>
|
||||
|
||||
<!-- Columna para el menú de acciones -->
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let client" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="info" (click)="viewGroupDetails(client)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editCommandGroup(client)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteCommandGroup(client)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<th mat-header-cell *matHeaderCellDef> Acciones </th>
|
||||
<td mat-cell *matCellDef="let group">
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="viewGroupDetails(group)">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>Detalles</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="editCommandGroup(group)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>Editar</span>
|
||||
</button>
|
||||
<button mat-menu-item (click)="deleteCommandGroup(group)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>Eliminar</span>
|
||||
</button>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container" joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
|
||||
<div class="paginator-container">
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageIndex]="page"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
</mat-paginator>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CommandsGroupsComponent } from './commands-groups.component';
|
||||
|
||||
describe('CommandsGroupsComponent', () => {
|
||||
let component: CommandsGroupsComponent;
|
||||
let fixture: ComponentFixture<CommandsGroupsComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CommandsGroupsComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CommandsGroupsComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -2,13 +2,9 @@ import { Component, OnInit } from '@angular/core';
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { MatDialog } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { CreateCommandGroupComponent } from './create-command-group/create-command-group.component';
|
||||
import { CreateCommandGroupComponent } from './create-command-group/create-command-group.component'
|
||||
import { DetailCommandGroupComponent } from './detail-command-group/detail-command-group.component';
|
||||
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { MatTableDataSource } from "@angular/material/table";
|
||||
import { DatePipe } from "@angular/common";
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands-groups',
|
||||
|
@ -16,61 +12,30 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./commands-groups.component.css']
|
||||
})
|
||||
export class CommandsGroupsComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
private apiUrl: string;
|
||||
dataSource = new MatTableDataSource<any>();
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
commandGroups: any[] = [];
|
||||
filters: { [key: string]: string | boolean } = {};
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 20;
|
||||
page: number = 0;
|
||||
pageSizeOptions: number[] = [10, 20, 40, 100];
|
||||
datePipe: DatePipe = new DatePipe('es-ES');
|
||||
loading: boolean = false;
|
||||
columns = [
|
||||
{
|
||||
columnDef: 'id',
|
||||
header: 'ID',
|
||||
cell: (group: any) => `${group.id}`,
|
||||
},
|
||||
{
|
||||
columnDef: 'name',
|
||||
header: 'Nombre',
|
||||
cell: (group: any) => `${group.name}`
|
||||
},
|
||||
{
|
||||
columnDef: 'commands',
|
||||
header: 'Lista de comandos',
|
||||
cell: (group: any) => `${group.commands}`
|
||||
},
|
||||
{
|
||||
columnDef: 'createdAt',
|
||||
header: 'Fecha de creación',
|
||||
cell: (group: any) => `${this.datePipe.transform(group.createdAt, 'dd/MM/yyyy hh:mm:ss')}`,
|
||||
}
|
||||
];
|
||||
displayedColumns = [...this.columns.map(column => column.columnDef), 'actions'];
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 1;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
displayedColumns: string[] = ['name', 'createdBy', 'createdAt', 'actions'];
|
||||
private apiUrl = `${this.baseUrl}/command-groups`;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private configService: ConfigService, private joyrideService: JoyrideService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-groups`;
|
||||
}
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.search();
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page + 1}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.dataSource.data = data['hydra:member'];
|
||||
this.commandGroups = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching command groups', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -84,13 +49,13 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
|
||||
openCreateCommandGroupModal(): void {
|
||||
this.dialog.open(CreateCommandGroupComponent, {
|
||||
width: '700px',
|
||||
width: '600px',
|
||||
}).afterClosed().subscribe(() => this.search());
|
||||
}
|
||||
|
||||
editCommandGroup(group: any): void {
|
||||
this.dialog.open(CreateCommandGroupComponent, {
|
||||
width: '700px',
|
||||
width: '600px',
|
||||
data: { group },
|
||||
}).afterClosed().subscribe(() => this.search());
|
||||
}
|
||||
|
@ -117,24 +82,6 @@ export class CommandsGroupsComponent implements OnInit {
|
|||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.length = event.length;
|
||||
this.search();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'addCommandGroupStep',
|
||||
'searchStep',
|
||||
'tableStep',
|
||||
'viewCommandsStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,107 +1,58 @@
|
|||
.create-command-group-container {
|
||||
padding: 20px;
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.form-container {
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.command-group-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.command-selection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.available-commands, .selected-commands {
|
||||
width: 48%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 200px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.selected-commands-list {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.commands-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
cursor: pointer;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
margin: 0 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.command-group-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.available-commands, .selected-commands {
|
||||
width: 48%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: 500px;
|
||||
}
|
||||
|
||||
.table-wrapper {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.command-selection {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.command-group-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.available-commands, .selected-commands {
|
||||
width: 100%;
|
||||
margin-bottom: 20px;
|
||||
|
||||
.command-selection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.action-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
}
|
||||
|
||||
.available-commands, .selected-commands {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
.selected-commands-list {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
.commands-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 8px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.remove-icon {
|
||||
cursor: pointer;
|
||||
color: #f44336; /* Rojo para eliminar */
|
||||
}
|
||||
|
||||
.chevron-icon {
|
||||
margin: 0 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.command-group-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
|
@ -1,59 +1,58 @@
|
|||
<h2 mat-dialog-title>{{ editing ? ('editCommandGroup' | translate) : ('createCommandGroup' | translate) }}</h2>
|
||||
<mat-dialog-content class="form-container">
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<form *ngIf="!loading" class="command-group-form" (ngSubmit)="onSubmit()">
|
||||
<div class="create-command-group-container">
|
||||
<h2>Crear Grupo de Comandos</h2>
|
||||
|
||||
<form class="command-group-form" (ngSubmit)="onSubmit()">
|
||||
<mat-form-field>
|
||||
<mat-label>{{ 'groupNameLabel' | translate }}</mat-label>
|
||||
<mat-label>Nombre del Grupo</mat-label>
|
||||
<input matInput [(ngModel)]="groupName" name="groupName" required />
|
||||
</mat-form-field>
|
||||
|
||||
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">{{ 'enabledToggle' | translate }}</mat-slide-toggle>
|
||||
<mat-slide-toggle [(ngModel)]="enabled" name="enabled">Habilitado</mat-slide-toggle>
|
||||
|
||||
<div class="command-selection">
|
||||
<div class="available-commands">
|
||||
<h3>{{ 'availableCommandsTitle' | translate }}</h3>
|
||||
<div class="table-wrapper">
|
||||
<table mat-table [dataSource]="availableCommands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
<h3>Comandos Disponibles</h3>
|
||||
<table mat-table [dataSource]="availableCommands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Nombre </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'actionsColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command">
|
||||
<button mat-icon-button type="button" (click)="addCommand(command)">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef> Acciones </th>
|
||||
<td mat-cell *matCellDef="let command">
|
||||
<button mat-icon-button type="button" (click)="addCommand(command)">
|
||||
<mat-icon>add</mat-icon>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'actions']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['name', 'actions'];"></tr>
|
||||
</table>
|
||||
</div>
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'actions']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['name', 'actions'];"></tr>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="selected-commands">
|
||||
<h3>{{ 'selectedCommandsTitle' | translate }}</h3>
|
||||
<div class="selected-commands-list" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<h3>Comandos Seleccionados</h3>
|
||||
<div class="selected-commands-list">
|
||||
<div class="commands-container">
|
||||
<div *ngFor="let command of selectedCommands" cdkDrag>
|
||||
<ng-container *ngFor="let command of selectedCommands; let last = last">
|
||||
<div class="command-item">
|
||||
<mat-icon class="drag-handle" cdkDragHandle>drag_indicator</mat-icon>
|
||||
{{ command.name }}
|
||||
<mat-icon class="remove-icon" (click)="removeCommand(command)">close</mat-icon>
|
||||
</div>
|
||||
</div>
|
||||
<ng-container *ngIf="!last">
|
||||
<mat-icon class="chevron-icon">chevron_right</mat-icon>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</mat-dialog-content>
|
||||
|
||||
<mat-dialog-actions class="action-container">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button class="submit-button" (click)="onSubmit()" cdkFocusInitial>{{ 'buttonSave' | translate }}</button>
|
||||
</mat-dialog-actions>
|
||||
<div class="command-group-actions">
|
||||
<button mat-flat-button color="primary" type="submit">Crear Grupo</button>
|
||||
<button mat-flat-button color="warn" (click)="close()">Cancelar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { CreateCommandGroupComponent } from './create-command-group.component';
|
||||
|
||||
describe('CreateCommandGroupComponent', () => {
|
||||
let component: CreateCommandGroupComponent;
|
||||
let fixture: ComponentFixture<CreateCommandGroupComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [CreateCommandGroupComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateCommandGroupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -2,8 +2,6 @@ import { Component, OnInit, Inject } from '@angular/core';
|
|||
import { HttpClient } from '@angular/common/http';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-command-group',
|
||||
|
@ -11,25 +9,20 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./create-command-group.component.css']
|
||||
})
|
||||
export class CreateCommandGroupComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
availableCommands: any[] = [];
|
||||
selectedCommands: any[] = [];
|
||||
groupName: string = '';
|
||||
enabled: boolean = true;
|
||||
editing: boolean = false;
|
||||
loading: boolean = false;
|
||||
private apiUrl: string;
|
||||
private apiUrl = 'http://127.0.0.1:8001/commands';
|
||||
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private dialogRef: MatDialogRef<CreateCommandGroupComponent>,
|
||||
private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/commands`;
|
||||
}
|
||||
) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadAvailableCommands();
|
||||
|
@ -41,15 +34,12 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadAvailableCommands(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any>(this.apiUrl).subscribe(
|
||||
(data) => {
|
||||
this.availableCommands = data['hydra:member'];
|
||||
this.loading = false;
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching available commands', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -61,9 +51,7 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
}
|
||||
|
||||
addCommand(command: any): void {
|
||||
if (!this.selectedCommands.includes(command)) {
|
||||
this.selectedCommands.push(command);
|
||||
}
|
||||
this.selectedCommands.push(command);
|
||||
}
|
||||
|
||||
removeCommand(command: any): void {
|
||||
|
@ -73,10 +61,6 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
drop(event: CdkDragDrop<any[]>): void {
|
||||
moveItemInArray(this.selectedCommands, event.previousIndex, event.currentIndex);
|
||||
}
|
||||
|
||||
onSubmit(): void {
|
||||
const payload = {
|
||||
name: this.groupName,
|
||||
|
@ -86,25 +70,23 @@ export class CreateCommandGroupComponent implements OnInit {
|
|||
|
||||
if (this.editing) {
|
||||
const groupId = this.data.group.uuid;
|
||||
this.http.patch(`${this.baseUrl}/command-groups/${groupId}`, payload).subscribe({
|
||||
this.http.patch(`http://127.0.0.1:8001/command-groups/${groupId}`, payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Grupo de comandos actualizado con éxito');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error actualizando el grupo de comandos', error);
|
||||
this.toastService.error('Error al actualizar el grupo de comandos');
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.http.post(`${this.baseUrl}/command-groups`, payload).subscribe({
|
||||
this.http.post('http://127.0.0.1:8001/command-groups', payload).subscribe({
|
||||
next: () => {
|
||||
this.toastService.success('Grupo de comandos creado con éxito');
|
||||
this.dialogRef.close();
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error creando el grupo de comandos', error);
|
||||
this.toastService.error('Error al crear el grupo de comandos');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,109 +1,131 @@
|
|||
.detail-command-group-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.mat-card {
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
|
||||
.mat-card-header {
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
||||
.mat-card-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
|
||||
.mat-card-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
|
||||
.mat-card-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
th.mat-header-cell {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
||||
|
||||
td.mat-cell {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(219, 219, 219, 0.5);
|
||||
}
|
||||
|
||||
|
||||
.mat-card-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
background-color: #3f51b5; /* Color primario */
|
||||
color: white;
|
||||
padding: 10px 15px;
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
background-color: #2c387e; /* Color primario oscuro */
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.mat-card {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
|
||||
h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-button {
|
||||
background-color: #dc3545;
|
||||
color: white;
|
||||
border-radius: 5px;
|
||||
}
|
||||
.cancel-button:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.create-command-group-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
|
||||
.command-group-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.command-selection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
|
||||
.available-commands, .selected-commands {
|
||||
width: 48%;
|
||||
}
|
||||
|
||||
|
||||
.selected-commands-list {
|
||||
border: 1px solid #ccc;
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
|
||||
|
||||
.commands-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
|
||||
.command-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
@ -112,21 +134,22 @@
|
|||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 8px;
|
||||
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
|
||||
.remove-icon {
|
||||
cursor: pointer;
|
||||
color: #f44336;
|
||||
color: #f44336;
|
||||
}
|
||||
|
||||
|
||||
.chevron-icon {
|
||||
margin: 0 10px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-container {
|
||||
|
||||
.command-group-actions {
|
||||
margin-top: 20px;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 1em;
|
||||
padding: 1.5em;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
|
@ -1,58 +1,37 @@
|
|||
<div class="detail-command-group-container">
|
||||
<h2>{{ 'commandGroupDetailsTitle' | translate }}</h2>
|
||||
|
||||
<app-loading [isLoading]="loading"></app-loading>
|
||||
|
||||
<mat-card *ngIf="!loading">
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ data.name }}</mat-card-title>
|
||||
<mat-card-subtitle>{{ 'createdBy' | translate }}: {{ data.createdBy }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<p><strong>{{ 'groupId' | translate }}:</strong> {{ data.uuid }}</p>
|
||||
<p><strong>{{ 'creationDate' | translate }}:</strong> {{ data.createdAt | date:'short' }}</p>
|
||||
|
||||
<h3>{{ 'includedCommands' | translate }}</h3>
|
||||
<table mat-table [dataSource]="data.commands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'nameColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="uuid">
|
||||
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'uuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['name', 'uuid'];"></tr>
|
||||
</table>
|
||||
</mat-card-content>
|
||||
</mat-card>
|
||||
|
||||
<!-- Sección para seleccionar clientes -->
|
||||
<div class="additional-section" *ngIf="showClientSelect && !loading">
|
||||
<form [formGroup]="form">
|
||||
<h4>{{ 'selectClients' | translate }}</h4>
|
||||
<mat-form-field appearance="fill">
|
||||
<mat-label>{{ 'clientsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple (selectionChange)="onClientSelectionChange($event)">
|
||||
<mat-option *ngFor="let client of clients" [value]="client.uuid">
|
||||
{{ client.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="form.get('selectedClients')?.invalid && form.get('selectedClients')?.touched">
|
||||
{{ 'selectAtLeastOneClient' | translate }}
|
||||
</mat-error>
|
||||
</mat-form-field>
|
||||
</form>
|
||||
<h2>Detalles del Grupo de Comandos</h2>
|
||||
|
||||
<mat-card>
|
||||
<mat-card-header>
|
||||
<mat-card-title>{{ data.name }}</mat-card-title>
|
||||
<mat-card-subtitle>Creado por: {{ data.createdBy }}</mat-card-subtitle>
|
||||
</mat-card-header>
|
||||
|
||||
<mat-card-content>
|
||||
<p><strong>ID del Grupo:</strong> {{ data.uuid }}</p>
|
||||
<p><strong>Posición:</strong> {{ data.position }}</p>
|
||||
<p><strong>Fecha de Creación:</strong> {{ data.createdAt | date:'short' }}</p>
|
||||
|
||||
<h3>Comandos Incluidos:</h3>
|
||||
<table mat-table [dataSource]="data.commands" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Nombre </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.name }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="uuid">
|
||||
<th mat-header-cell *matHeaderCellDef> UUID </th>
|
||||
<td mat-cell *matCellDef="let command"> {{ command.uuid }} </td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="['name', 'uuid']"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: ['name', 'uuid'];"></tr>
|
||||
</table>
|
||||
</mat-card-content>
|
||||
|
||||
</mat-card>
|
||||
<div class="command-group-actions">
|
||||
<button mat-flat-button color="warn" (click)="close()">Cancelar</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="action-container" *ngIf="!loading">
|
||||
<button class="ordinary-button" (click)="close()">{{ 'buttonCancel' | translate }}</button>
|
||||
<button [ngClass]="showClientSelect ? 'submit-button' : 'action-button'" (click)="toggleClientSelect()">
|
||||
{{ showClientSelect ? ('execute' | translate) : ('scheduleExecution' | translate) }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
|
||||
import { DetailCommandGroupComponent } from './detail-command-group.component';
|
||||
|
||||
describe('DetailCommandGroupComponent', () => {
|
||||
let component: DetailCommandGroupComponent;
|
||||
let fixture: ComponentFixture<DetailCommandGroupComponent>;
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
declarations: [DetailCommandGroupComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(DetailCommandGroupComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -1,93 +1,18 @@
|
|||
import { Component, Inject, OnInit } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { Component, Inject } from '@angular/core';
|
||||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-detail-command-group',
|
||||
templateUrl: './detail-command-group.component.html',
|
||||
styleUrls: ['./detail-command-group.component.css']
|
||||
})
|
||||
export class DetailCommandGroupComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
form!: FormGroup;
|
||||
clients: any[] = [];
|
||||
showClientSelect = false;
|
||||
canExecute = false;
|
||||
loading: boolean = false;
|
||||
|
||||
export class DetailCommandGroupComponent {
|
||||
constructor(
|
||||
@Inject(MAT_DIALOG_DATA) public data: any,
|
||||
private dialogRef: MatDialogRef<DetailCommandGroupComponent>,
|
||||
private fb: FormBuilder,
|
||||
private configService: ConfigService,
|
||||
private http: HttpClient,
|
||||
private toastService: ToastrService
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.form = this.fb.group({
|
||||
selectedClients: [[], Validators.required],
|
||||
});
|
||||
|
||||
this.loadClients();
|
||||
}
|
||||
|
||||
loadClients(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.baseUrl}/clients?page=1&itemsPerPage=30`).subscribe({
|
||||
next: (response) => {
|
||||
this.clients = response['hydra:member'];
|
||||
this.loading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error fetching clients:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
toggleClientSelect(): void {
|
||||
if (!this.showClientSelect) {
|
||||
this.showClientSelect = true;
|
||||
} else {
|
||||
this.execute();
|
||||
}
|
||||
}
|
||||
|
||||
execute(): void {
|
||||
if (this.form.get('selectedClients')?.value.length > 0) {
|
||||
const payload = {
|
||||
clients: this.form.value.selectedClients.map((uuid: any) => `/clients/${uuid}`)
|
||||
};
|
||||
|
||||
const apiUrl = `${this.baseUrl}/command-groups/${this.data.uuid}/execute`;
|
||||
this.loading = true;
|
||||
this.http.post(apiUrl, payload).subscribe({
|
||||
next: () => {
|
||||
this.dialogRef.close();
|
||||
this.toastService.success('Grupo de comandos ejecutado exitosamente');
|
||||
this.loading = false;
|
||||
},
|
||||
error: (error) => {
|
||||
console.error('Error ejecutando grupo de comandos:', error);
|
||||
this.loading = false;
|
||||
}
|
||||
});
|
||||
} else {
|
||||
this.form.get('selectedClients')?.markAsTouched();
|
||||
}
|
||||
}
|
||||
|
||||
onClientSelectionChange(event: any): void {
|
||||
this.canExecute = this.form.get('selectedClients')?.value.length > 0;
|
||||
}
|
||||
private dialogRef: MatDialogRef<DetailCommandGroupComponent>
|
||||
) {}
|
||||
|
||||
close(): void {
|
||||
this.dialogRef.close();
|
||||
this.dialogRef.close();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,79 +1,3 @@
|
|||
.header-container-title {
|
||||
flex-grow: 1;
|
||||
text-align: left;
|
||||
margin-left: 1em;
|
||||
}
|
||||
|
||||
.task-button-row {
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.divider {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
.lists-container {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.imagesLists-container {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.card.unidad-card {
|
||||
height: 100%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
margin: 1.5rem 0rem 1.5rem 0rem;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-string {
|
||||
flex: 2;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.search-boolean {
|
||||
flex: 1;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 10px 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mat-elevation-z8 {
|
||||
box-shadow: 0px 0px 0px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.paginator-container {
|
||||
display: flex;
|
||||
justify-content: end;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.mat-chip-readonly-true {
|
||||
background-color: #4CAF50 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.mat-chip-readonly-false {
|
||||
background-color: #F44336 !important;
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.task-button-row{
|
||||
margin-bottom: 10px;
|
||||
}
|
|
@ -1,75 +1,59 @@
|
|||
<div class="header-container">
|
||||
<button mat-icon-button color="primary" (click)="iniciarTour()">
|
||||
<mat-icon>help</mat-icon>
|
||||
</button>
|
||||
<div class="header-container-title">
|
||||
<h2 class="title" joyrideStep="titleStep" text="{{ 'titleStepText' | translate }}">{{ 'manageTasksTitle' | translate }}</h2>
|
||||
</div>
|
||||
<h2 class="title">Administrar Tareas</h2>
|
||||
<div class="task-button-row">
|
||||
<button class="action-button" (click)="openCreateTaskModal()" joyrideStep="addTaskStep" text="{{ 'addTaskStepText' | translate }}">
|
||||
{{ 'addTask' | translate }}
|
||||
</button>
|
||||
<button mat-flat-button color="primary" (click)="openCreateTaskModal()">Añadir Tarea</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="search-container" joyrideStep="searchStep" text="{{ 'searchStepText' | translate }}">
|
||||
<mat-form-field appearance="fill" class="search-string">
|
||||
<mat-label>{{ 'searchTaskLabel' | translate }}</mat-label>
|
||||
<input matInput placeholder="{{ 'searchPlaceholder' | translate }}" [(ngModel)]="filters['name']" (keyup.enter)="search()" />
|
||||
<mat-icon matSuffix>search</mat-icon>
|
||||
<mat-hint>{{ 'searchHint' | translate }}</mat-hint>
|
||||
</mat-form-field>
|
||||
</div>
|
||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8">
|
||||
<ng-container matColumnDef="notes">
|
||||
<th mat-header-cell *matHeaderCellDef> Info</th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.notes }} </td>
|
||||
</ng-container>
|
||||
|
||||
<div *ngIf="!loading">
|
||||
<table mat-table [dataSource]="tasks" class="mat-elevation-z8" joyrideStep="tableStep" text="{{ 'tableStepText' | translate }}">
|
||||
<ng-container matColumnDef="taskid">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'idColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.id }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> Creado por </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.createdBy }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="notes">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'infoColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.notes }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="scheduledDate">
|
||||
<th mat-header-cell *matHeaderCellDef> Fecha de Ejecución </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.dateTime | date:'short' }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="name">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'createdByColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.createdBy }} </td>
|
||||
</ng-container>
|
||||
<ng-container matColumnDef="enabled">
|
||||
<th mat-header-cell *matHeaderCellDef> Estado </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.enabled ? 'Habilitado' : 'Deshabilitado' }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="scheduledDate">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'executionDateColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.dateTime | date:'short' }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="enabled">
|
||||
<th mat-header-cell *matHeaderCellDef> {{ 'statusColumn' | translate }} </th>
|
||||
<td mat-cell *matCellDef="let task"> {{ task.enabled ? ('enabled' | translate) : ('disabled' | translate) }} </td>
|
||||
</ng-container>
|
||||
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef style="text-align: center;">{{ 'columnActions' | translate }}</th>
|
||||
<td mat-cell *matCellDef="let task" style="text-align: center;" joyrideStep="actionsStep" text="{{ 'actionsStepText' | translate }}">
|
||||
<button mat-icon-button color="info" (click)="viewTaskDetails(task)">
|
||||
<mat-icon>visibility</mat-icon>
|
||||
<ng-container matColumnDef="actions">
|
||||
<th mat-header-cell *matHeaderCellDef> Acciones </th>
|
||||
<td mat-cell *matCellDef="let task">
|
||||
<button mat-icon-button [matMenuTriggerFor]="menu">
|
||||
<mat-icon>more_vert</mat-icon>
|
||||
</button>
|
||||
<mat-menu #menu="matMenu">
|
||||
<button mat-menu-item (click)="viewTaskDetails(task)">
|
||||
<mat-icon>info</mat-icon>
|
||||
<span>Detalles</span>
|
||||
</button>
|
||||
<button mat-icon-button color="primary" (click)="editTask(task)">
|
||||
<button mat-menu-item (click)="editTask(task)">
|
||||
<mat-icon>edit</mat-icon>
|
||||
<span>Editar</span>
|
||||
</button>
|
||||
<button mat-icon-button color="warn" (click)="deleteTask(task)">
|
||||
<button mat-menu-item (click)="deleteTask(task)">
|
||||
<mat-icon>delete</mat-icon>
|
||||
<span>Eliminar</span>
|
||||
</button>
|
||||
</td>
|
||||
</ng-container>
|
||||
</mat-menu>
|
||||
</td>
|
||||
</ng-container>
|
||||
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
</div>
|
||||
<tr mat-header-row *matHeaderRowDef="displayedColumns"></tr>
|
||||
<tr mat-row *matRowDef="let row; columns: displayedColumns;"></tr>
|
||||
</table>
|
||||
|
||||
<mat-paginator joyrideStep="paginationStep" text="{{ 'paginationStepText' | translate }}"
|
||||
[length]="length"
|
||||
<mat-paginator [length]="length"
|
||||
[pageSize]="itemsPerPage"
|
||||
[pageSizeOptions]="pageSizeOptions"
|
||||
(page)="onPageChange($event)">
|
||||
|
|
|
@ -1,53 +1,17 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { HttpClientTestingModule } from '@angular/common/http/testing';
|
||||
import { MatDialogModule } from '@angular/material/dialog';
|
||||
import { ToastrModule } from 'ngx-toastr';
|
||||
|
||||
import { CommandsTaskComponent } from './commands-task.component';
|
||||
import { MatDividerModule } from '@angular/material/divider';
|
||||
import { MatFormFieldModule } from '@angular/material/form-field';
|
||||
import { MatPaginatorModule } from '@angular/material/paginator';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { MatInputModule } from '@angular/material/input';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
||||
import { JoyrideModule, JoyrideService, JoyrideStepService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
describe('CommandsTaskComponent', () => {
|
||||
let component: CommandsTaskComponent;
|
||||
let fixture: ComponentFixture<CommandsTaskComponent>;
|
||||
let mockConfigService: jasmine.SpyObj<ConfigService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const configServiceSpy = jasmine.createSpyObj('ConfigService', [], { apiUrl: 'http://mock-api-url' });
|
||||
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [
|
||||
HttpClientTestingModule,
|
||||
MatDialogModule,
|
||||
ToastrModule.forRoot(),
|
||||
MatDividerModule,
|
||||
MatFormFieldModule,
|
||||
MatPaginatorModule,
|
||||
FormsModule,
|
||||
MatInputModule,
|
||||
BrowserAnimationsModule,
|
||||
TranslateModule.forRoot(),
|
||||
JoyrideModule.forRoot(),
|
||||
],
|
||||
declarations: [CommandsTaskComponent, LoadingComponent],
|
||||
providers: [
|
||||
{ provide: ConfigService, useValue: configServiceSpy }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
declarations: [CommandsTaskComponent]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
mockConfigService = TestBed.inject(ConfigService) as jasmine.SpyObj<ConfigService>;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(CommandsTaskComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
|
@ -56,6 +20,4 @@ describe('CommandsTaskComponent', () => {
|
|||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
|
|
|
@ -5,8 +5,6 @@ import { ToastrService } from 'ngx-toastr';
|
|||
import { CreateTaskComponent } from './create-task/create-task.component';
|
||||
import { DetailTaskComponent } from './detail-task/detail-task.component';
|
||||
import { DeleteModalComponent } from '../../../shared/delete_modal/delete-modal/delete-modal.component';
|
||||
import { JoyrideService } from 'ngx-joyride';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-commands-task',
|
||||
|
@ -14,44 +12,31 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./commands-task.component.css']
|
||||
})
|
||||
export class CommandsTaskComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
tasks: any[] = [];
|
||||
filters: { [key: string]: string | boolean } = {};
|
||||
length: number = 0;
|
||||
itemsPerPage: number = 10;
|
||||
page: number = 1;
|
||||
pageSizeOptions: number[] = [5, 10, 20, 40, 100];
|
||||
displayedColumns: string[] = ['taskid', 'notes', 'name', 'scheduledDate', 'enabled', 'actions'];
|
||||
loading: boolean = false;
|
||||
private apiUrl: string;
|
||||
displayedColumns: string[] = ['notes','name', 'scheduledDate', 'enabled', 'actions'];
|
||||
private apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService,
|
||||
private configService: ConfigService,
|
||||
private joyrideService: JoyrideService) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
}
|
||||
constructor(private http: HttpClient, private dialog: MatDialog, private toastService: ToastrService) {}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
search(): void {
|
||||
this.page = 1;
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
loadTasks(): void {
|
||||
this.loading = true;
|
||||
this.http.get<any>(`${this.apiUrl}?page=${this.page}&itemsPerPage=${this.itemsPerPage}`, { params: this.filters }).subscribe(
|
||||
(data) => {
|
||||
this.tasks = data['hydra:member'];
|
||||
this.length = data['hydra:totalItems'];
|
||||
this.loading = false;
|
||||
console.log('Tasks:', this.tasks);
|
||||
},
|
||||
(error) => {
|
||||
console.error('Error fetching tasks', error);
|
||||
this.loading = false;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
@ -59,19 +44,19 @@ export class CommandsTaskComponent implements OnInit {
|
|||
viewTaskDetails(task: any): void {
|
||||
this.dialog.open(DetailTaskComponent, {
|
||||
width: '800px',
|
||||
data: { task },
|
||||
data: task,
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}
|
||||
|
||||
openCreateTaskModal(): void {
|
||||
this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
width: '600px',
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}
|
||||
|
||||
editTask(task: any): void {
|
||||
this.dialog.open(CreateTaskComponent, {
|
||||
width: '800px',
|
||||
width: '600px',
|
||||
data: { task },
|
||||
}).afterClosed().subscribe(() => this.loadTasks());
|
||||
}
|
||||
|
@ -79,7 +64,7 @@ export class CommandsTaskComponent implements OnInit {
|
|||
deleteTask(task: any): void {
|
||||
this.dialog.open(DeleteModalComponent, {
|
||||
width: '300px',
|
||||
data: { name: task.createdBy },
|
||||
data: { name: task.createdBy },
|
||||
}).afterClosed().subscribe((result) => {
|
||||
if (result) {
|
||||
this.http.delete(`${this.apiUrl}/${task.uuid}`).subscribe({
|
||||
|
@ -96,24 +81,8 @@ export class CommandsTaskComponent implements OnInit {
|
|||
}
|
||||
|
||||
onPageChange(event: any): void {
|
||||
this.page = event.pageIndex + 1;
|
||||
this.page = event.pageIndex + 1;
|
||||
this.itemsPerPage = event.pageSize;
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
iniciarTour(): void {
|
||||
this.joyrideService.startTour({
|
||||
steps: [
|
||||
'titleStep',
|
||||
'addTaskStep',
|
||||
'searchStep',
|
||||
'tableStep',
|
||||
'actionsStep',
|
||||
'paginationStep'
|
||||
],
|
||||
showPrevButton: true,
|
||||
themeColor: '#3f51b5'
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,28 +1,86 @@
|
|||
.dialog-title {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.task-form {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.full-width {
|
||||
width: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.commands-list {
|
||||
margin-top: 10px;
|
||||
margin-bottom: 10px;
|
||||
padding: 10px;
|
||||
border: 1px solid #ccc;
|
||||
background-color: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.command-item {
|
||||
padding: 10px;
|
||||
background-color: #f4f4f4;
|
||||
margin-bottom: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.command-item:hover {
|
||||
background-color: #e9e9e9;
|
||||
}
|
||||
|
||||
/* Estilos generales para el dialogo */
|
||||
.mat-dialog-content {
|
||||
padding: 20px;
|
||||
max-width: 600px; /* Ancho máximo del dialogo */
|
||||
}
|
||||
|
||||
.button-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-top: 20px;
|
||||
padding: 1.5rem;
|
||||
/* Estilos para el stepper */
|
||||
.mat-horizontal-stepper {
|
||||
margin: 20px 0;
|
||||
}
|
||||
|
||||
mat-form-field {
|
||||
margin-bottom: 16px;
|
||||
/* Estilos para cada paso */
|
||||
.mat-step {
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||
background-color: #ffffff; /* Fondo blanco para los pasos */
|
||||
}
|
||||
|
||||
.section-title {
|
||||
margin-top: 24px;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 500;
|
||||
/* Estilos para el encabezado de los pasos */
|
||||
.mat-step-header {
|
||||
background-color: #f5f5f5; /* Fondo gris claro para el encabezado */
|
||||
padding: 10px;
|
||||
border-radius: 4px;
|
||||
font-weight: 600; /* Negrita para el texto del encabezado */
|
||||
}
|
||||
|
||||
/* Estilos para los botones */
|
||||
button.mat-button {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
/* Estilos para los campos de formulario */
|
||||
.mat-form-field {
|
||||
margin-bottom: 20px; /* Espaciado entre campos */
|
||||
}
|
||||
|
||||
/* Estilos para errores */
|
||||
.mat-error {
|
||||
font-size: 0.875em; /* Tamaño de fuente más pequeño para mensajes de error */
|
||||
color: #d32f2f; /* Color rojo para errores */
|
||||
}
|
||||
|
||||
/* Estilos para listas de comandos */
|
||||
.commands-list {
|
||||
margin: 20px 0;
|
||||
border: 1px solid #e0e0e0; /* Borde gris claro */
|
||||
border-radius: 4px;
|
||||
padding: 10px;
|
||||
background-color: #fafafa; /* Fondo gris muy claro */
|
||||
}
|
||||
|
||||
/* Estilos para elementos de comando */
|
||||
.command-item {
|
||||
padding: 5px 0;
|
||||
border-bottom: 1px solid #e0e0e0; /* Separación entre comandos */
|
||||
}
|
||||
|
||||
.command-item:last-child {
|
||||
border-bottom: none; /* Quitar borde del último elemento */
|
||||
}
|
||||
|
|
|
@ -1,106 +1,104 @@
|
|||
<h2 mat-dialog-title class="dialog-title">{{ editing ? ('editTask' | translate) : ('createTask' | translate) }}</h2>
|
||||
<h2 mat-dialog-title>Crear Tarea</h2>
|
||||
|
||||
<form [formGroup]="taskForm" class="task-form">
|
||||
<form [formGroup]="taskForm">
|
||||
<mat-dialog-content>
|
||||
<mat-horizontal-stepper>
|
||||
<!-- Paso 1: Información y Selecciona Comandos -->
|
||||
<mat-step label="Información y Selecciona Comandos">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Información</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="notas"></textarea>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">Información</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Información</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="Ingresa tus notas aquí"></textarea>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Comandos</mat-label>
|
||||
<mat-select formControlName="commandGroup" (selectionChange)="onCommandGroupChange()">
|
||||
<mat-option *ngFor="let group of availableCommandGroups" [value]="group.uuid">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('commandGroup')?.invalid">Este campo es obligatorio</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'informationSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'informationLabel' | translate }}</mat-label>
|
||||
<textarea matInput formControlName="notes" placeholder="{{ 'notesPlaceholder' | translate }}"></textarea>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperNext>Continuar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<h3 class="section-title">{{ 'commandSelectionSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectCommandsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="commandGroup" (selectionChange)="onCommandGroupChange()">
|
||||
<mat-option *ngFor="let group of availableCommandGroups" [value]="group.uuid">
|
||||
{{ group.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('commandGroup')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<!-- Paso 2: Selecciona Comandos Individuales -->
|
||||
<mat-step label="Selecciona Comandos Individuales">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Comandos Individuales (Opcional)</mat-label>
|
||||
<mat-select formControlName="extraCommands" multiple>
|
||||
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
|
||||
{{ command.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectIndividualCommandsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="extraCommands" multiple>
|
||||
<mat-option *ngFor="let command of availableIndividualCommands" [value]="command.uuid">
|
||||
{{ command.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>Atrás</button>
|
||||
<button mat-button matStepperNext>Continuar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<h3 class="section-title">{{ 'executionDateTimeSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'executionDateLabel' | translate }}</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="{{ 'selectDatePlaceholder' | translate }}">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="taskForm.get('date')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<!-- Paso 3: Fecha de Ejecución y Hora -->
|
||||
<mat-step label="Fecha de Ejecución y Hora">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Fecha de Ejecución</mat-label>
|
||||
<input matInput [matDatepicker]="picker" formControlName="date" placeholder="Selecciona una fecha">
|
||||
<mat-datepicker-toggle matSuffix [for]="picker"></mat-datepicker-toggle>
|
||||
<mat-datepicker #picker></mat-datepicker>
|
||||
<mat-error *ngIf="taskForm.get('date')?.invalid">Este campo es obligatorio</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'executionTimeLabel' | translate }}</mat-label>
|
||||
<input matInput type="time" formControlName="time" placeholder="{{ 'selectTimePlaceholder' | translate }}">
|
||||
<mat-error *ngIf="taskForm.get('time')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Hora de Ejecución</mat-label>
|
||||
<input matInput type="time" formControlName="time" placeholder="Selecciona una hora">
|
||||
<mat-error *ngIf="taskForm.get('time')?.invalid">Este campo es obligatorio</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<h3 class="section-title">{{ 'destinationSelectionSectionTitle' | translate }}</h3>
|
||||
<mat-divider></mat-divider>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectOrganizationalUnitLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">{{ 'requiredFieldError' | translate }}</mat-error>
|
||||
</mat-form-field>
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>Atrás</button>
|
||||
<button mat-button matStepperNext>Continuar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClassroomLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
|
||||
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
|
||||
{{ child.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<!-- Paso 4: Selecciona Unidad Organizacional, Aula y Clientes -->
|
||||
<mat-step label="Selecciona Unidad Organizacional, Aula y Clientes">
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Unidad Organizacional</mat-label>
|
||||
<mat-select formControlName="organizationalUnit" (selectionChange)="onOrganizationalUnitChange()">
|
||||
<mat-option *ngFor="let unit of availableOrganizationalUnits" [value]="unit['@id']">
|
||||
{{ unit.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
<mat-error *ngIf="taskForm.get('organizationalUnit')?.invalid">Este campo es obligatorio</mat-error>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>{{ 'selectClientsLabel' | translate }}</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
{{ 'selectAllClients' | translate }}
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona aula</mat-label>
|
||||
<mat-select formControlName="selectedChild" (selectionChange)="onChildChange()">
|
||||
<mat-option *ngFor="let child of selectedUnitChildren" [value]="child['@id']">
|
||||
{{ child.name }}
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Clientes</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option (click)="toggleSelectAll()" [selected]="areAllSelected()">
|
||||
Seleccionar todos
|
||||
</mat-option>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
<mat-form-field appearance="fill" class="full-width">
|
||||
<mat-label>Selecciona Clientes</mat-label>
|
||||
<mat-select formControlName="selectedClients" multiple>
|
||||
<mat-option *ngFor="let client of selectedClients" [value]="client.uuid">
|
||||
{{ client.name }} ({{ client.ip }})
|
||||
</mat-option>
|
||||
</mat-select>
|
||||
</mat-form-field>
|
||||
|
||||
<div>
|
||||
<button mat-button matStepperPrevious>Atrás</button>
|
||||
<button mat-flat-button color="primary" (click)="saveTask()">Guardar</button>
|
||||
</div>
|
||||
</mat-step>
|
||||
</mat-horizontal-stepper>
|
||||
</mat-dialog-content>
|
||||
</form>
|
||||
|
||||
<div class="button-container">
|
||||
<button class="submit-button" (click)="saveTask()">{{ 'buttonSave' | translate }}</button>
|
||||
</div>
|
||||
|
|
|
@ -0,0 +1,188 @@
|
|||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { CreateTaskComponent } from './create-task.component';
|
||||
import { ReactiveFormsModule } from '@angular/forms';
|
||||
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
|
||||
import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { of, throwError } from 'rxjs';
|
||||
|
||||
describe('CreateTaskComponent', () => {
|
||||
let component: CreateTaskComponent;
|
||||
let fixture: ComponentFixture<CreateTaskComponent>;
|
||||
let httpMock: HttpTestingController;
|
||||
let toastrService: jasmine.SpyObj<ToastrService>;
|
||||
|
||||
beforeEach(async () => {
|
||||
const toastrSpy = jasmine.createSpyObj('ToastrService', ['success', 'error']);
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ReactiveFormsModule, HttpClientTestingModule],
|
||||
declarations: [CreateTaskComponent],
|
||||
providers: [
|
||||
{ provide: MatDialogRef, useValue: { close: jasmine.createSpy('close') } },
|
||||
{ provide: MAT_DIALOG_DATA, useValue: {} },
|
||||
{ provide: ToastrService, useValue: toastrSpy },
|
||||
]
|
||||
})
|
||||
.compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(CreateTaskComponent);
|
||||
component = fixture.componentInstance;
|
||||
httpMock = TestBed.inject(HttpTestingController);
|
||||
toastrService = TestBed.inject(ToastrService) as jasmine.SpyObj<ToastrService>;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
httpMock.verify();
|
||||
});
|
||||
|
||||
it('should create', () => {
|
||||
expect(component).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should initialize the form with default values', () => {
|
||||
expect(component.taskForm).toBeTruthy();
|
||||
expect(component.taskForm.get('commandGroup')?.value).toBe('');
|
||||
expect(component.taskForm.get('extraCommands')?.value).toEqual([]);
|
||||
expect(component.taskForm.get('date')?.value).toBe('');
|
||||
expect(component.taskForm.get('time')?.value).toBe('');
|
||||
expect(component.taskForm.get('notes')?.value).toBe('');
|
||||
});
|
||||
|
||||
it('should load command groups and set availableCommandGroups', () => {
|
||||
const mockCommandGroups = { 'hydra:member': [{ uuid: '1', name: 'Group 1' }, { uuid: '2', name: 'Group 2' }] };
|
||||
|
||||
component.loadCommandGroups();
|
||||
|
||||
const req = httpMock.expectOne(`${component.baseUrl}/command-groups`);
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush(mockCommandGroups);
|
||||
|
||||
expect(component.availableCommandGroups.length).toBe(2);
|
||||
expect(component.availableCommandGroups).toEqual(mockCommandGroups['hydra:member']);
|
||||
});
|
||||
|
||||
it('should handle error when loading command groups', () => {
|
||||
component.loadCommandGroups();
|
||||
|
||||
const req = httpMock.expectOne(`${component.baseUrl}/command-groups`);
|
||||
req.flush('Error loading command groups', { status: 500, statusText: 'Server Error' });
|
||||
|
||||
expect(toastrService.error).toHaveBeenCalledWith('Error al cargar los grupos de comandos');
|
||||
});
|
||||
|
||||
it('should load individual commands and set availableIndividualCommands', () => {
|
||||
const mockCommands = { 'hydra:member': [{ uuid: '1', name: 'Command 1' }, { uuid: '2', name: 'Command 2' }] };
|
||||
|
||||
component.loadIndividualCommands();
|
||||
|
||||
const req = httpMock.expectOne(`${component.baseUrl}/commands`);
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush(mockCommands);
|
||||
|
||||
expect(component.availableIndividualCommands.length).toBe(2);
|
||||
expect(component.availableIndividualCommands).toEqual(mockCommands['hydra:member']);
|
||||
});
|
||||
|
||||
it('should handle error when loading individual commands', () => {
|
||||
component.loadIndividualCommands();
|
||||
|
||||
const req = httpMock.expectOne(`${component.baseUrl}/commands`);
|
||||
req.flush('Error loading individual commands', { status: 500, statusText: 'Server Error' });
|
||||
|
||||
expect(toastrService.error).toHaveBeenCalledWith('Error al cargar los comandos individuales');
|
||||
});
|
||||
|
||||
it('should fetch selected group commands on command group change', () => {
|
||||
component.taskForm.patchValue({ commandGroup: '1' });
|
||||
|
||||
component.onCommandGroupChange();
|
||||
|
||||
const mockGroupCommands = { commands: [{ uuid: '1', name: 'Group Command 1' }, { uuid: '2', name: 'Group Command 2' }] };
|
||||
|
||||
const req = httpMock.expectOne(`${component.baseUrl}/command-groups/1`);
|
||||
expect(req.request.method).toBe('GET');
|
||||
req.flush(mockGroupCommands);
|
||||
|
||||
expect(component.selectedGroupCommands.length).toBe(2);
|
||||
expect(component.selectedGroupCommands).toEqual(mockGroupCommands.commands);
|
||||
});
|
||||
|
||||
it('should handle error when fetching group commands', () => {
|
||||
component.taskForm.patchValue({ commandGroup: '1' });
|
||||
component.onCommandGroupChange();
|
||||
|
||||
const req = httpMock.expectOne(`${component.baseUrl}/command-groups/1`);
|
||||
req.flush('Error loading group commands', { status: 500, statusText: 'Server Error' });
|
||||
|
||||
expect(toastrService.error).toHaveBeenCalledWith('Error al cargar los comandos del grupo seleccionado');
|
||||
});
|
||||
|
||||
it('should save the task successfully', () => {
|
||||
const mockFormData = {
|
||||
commandGroup: '1',
|
||||
extraCommands: ['1', '2'],
|
||||
date: '2024-09-27',
|
||||
time: '12:00',
|
||||
notes: 'Test notes'
|
||||
};
|
||||
|
||||
component.taskForm.setValue(mockFormData);
|
||||
|
||||
component.saveTask();
|
||||
|
||||
const expectedPayload = {
|
||||
commandGroups: ['/command-groups/1'],
|
||||
commands: ['/commands/1', '/commands/2'],
|
||||
dateTime: jasmine.any(String),
|
||||
notes: 'Test notes'
|
||||
};
|
||||
|
||||
const req = httpMock.expectOne(component.apiUrl);
|
||||
expect(req.request.method).toBe('POST');
|
||||
req.flush({}); // Mock successful response
|
||||
|
||||
expect(toastrService.success).toHaveBeenCalledWith('Tarea creada con éxito');
|
||||
expect(component.dialogRef.close).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('should not save the task if form is invalid', () => {
|
||||
component.taskForm.setValue({
|
||||
commandGroup: '',
|
||||
extraCommands: [],
|
||||
date: '',
|
||||
time: '',
|
||||
notes: ''
|
||||
});
|
||||
|
||||
component.saveTask();
|
||||
|
||||
expect(toastrService.error).toHaveBeenCalledWith('Por favor, rellene todos los campos obligatorios');
|
||||
expect(component.dialogRef.close).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle error when saving the task', () => {
|
||||
const mockFormData = {
|
||||
commandGroup: '1',
|
||||
extraCommands: ['1'],
|
||||
date: '2024-09-27',
|
||||
time: '12:00',
|
||||
notes: 'Test notes'
|
||||
};
|
||||
|
||||
component.taskForm.setValue(mockFormData);
|
||||
component.saveTask();
|
||||
|
||||
const req = httpMock.expectOne(component.apiUrl);
|
||||
expect(req.request.method).toBe('POST');
|
||||
req.flush('Error creating task', { status: 500, statusText: 'Server Error' });
|
||||
|
||||
expect(toastrService.error).toHaveBeenCalledWith('Error al crear la tarea');
|
||||
});
|
||||
|
||||
it('should close the dialog', () => {
|
||||
component.close();
|
||||
expect(component.dialogRef.close).toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -3,7 +3,6 @@ import { HttpClient } from '@angular/common/http';
|
|||
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
|
||||
import { ToastrService } from 'ngx-toastr';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { ConfigService } from '@services/config.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-create-task',
|
||||
|
@ -11,30 +10,26 @@ import { ConfigService } from '@services/config.service';
|
|||
styleUrls: ['./create-task.component.css']
|
||||
})
|
||||
export class CreateTaskComponent implements OnInit {
|
||||
baseUrl: string;
|
||||
baseUrl: string = import.meta.env.NG_APP_BASE_API_URL;
|
||||
taskForm: FormGroup;
|
||||
availableCommandGroups: any[] = [];
|
||||
selectedGroupCommands: any[] = [];
|
||||
availableIndividualCommands: any[] = [];
|
||||
apiUrl: string;
|
||||
apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
editing: boolean = false;
|
||||
availableOrganizationalUnits: any[] = [];
|
||||
selectedUnitChildren: any[] = [];
|
||||
selectedClients: any[] = [];
|
||||
selectedClientIds: Set<string> = new Set();
|
||||
|
||||
constructor(
|
||||
private fb: FormBuilder,
|
||||
private http: HttpClient,
|
||||
private configService: ConfigService,
|
||||
private toastr: ToastrService,
|
||||
public dialogRef: MatDialogRef<CreateTaskComponent>,
|
||||
@Inject(MAT_DIALOG_DATA) public data: any
|
||||
) {
|
||||
this.baseUrl = this.configService.apiUrl;
|
||||
this.apiUrl = `${this.baseUrl}/command-tasks`;
|
||||
this.taskForm = this.fb.group({
|
||||
commandGroup: ['', Validators.required],
|
||||
commandGroup: [''],
|
||||
extraCommands: [[]],
|
||||
date: ['', Validators.required],
|
||||
time: ['', Validators.required],
|
||||
|
@ -78,7 +73,7 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
|
||||
loadOrganizationalUnits(): void {
|
||||
this.http.get<any>(`${this.baseUrl}/organizational-units?page=1&itemsPerPage=30`).subscribe(
|
||||
this.http.get<any>('http://127.0.0.1:8001/organizational-units?page=1&itemsPerPage=30').subscribe(
|
||||
(data) => {
|
||||
this.availableOrganizationalUnits = data['hydra:member'].filter((unit: any) => unit['type'] === 'organizational-unit');
|
||||
},
|
||||
|
@ -103,76 +98,6 @@ export class CreateTaskComponent implements OnInit {
|
|||
}
|
||||
}
|
||||
|
||||
private collectClassrooms(unit: any): any[] {
|
||||
let classrooms = [];
|
||||
if (unit.type === 'classroom') {
|
||||
classrooms.push(unit);
|
||||
}
|
||||
if (unit.children && unit.children.length > 0) {
|
||||
for (let child of unit.children) {
|
||||
classrooms = classrooms.concat(this.collectClassrooms(child));
|
||||
}
|
||||
}
|
||||
return classrooms;
|
||||
}
|
||||
|
||||
onOrganizationalUnitChange(): void {
|
||||
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
|
||||
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
|
||||
|
||||
if (selectedUnit) {
|
||||
this.selectedUnitChildren = this.collectClassrooms(selectedUnit);
|
||||
} else {
|
||||
this.selectedUnitChildren = [];
|
||||
}
|
||||
|
||||
this.taskForm.patchValue({ selectedChild: '', selectedClients: [] });
|
||||
this.selectedClients = [];
|
||||
this.selectedClientIds.clear();
|
||||
}
|
||||
|
||||
onChildChange(): void {
|
||||
const selectedChildId = this.taskForm.get('selectedChild')?.value;
|
||||
|
||||
if (!selectedChildId) {
|
||||
this.selectedClients = [];
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `${this.baseUrl}${selectedChildId}`.replace(/([^:]\/)\/+/g, '$1');
|
||||
|
||||
this.http.get<any>(url).subscribe(
|
||||
(data) => {
|
||||
if (Array.isArray(data.clients) && data.clients.length > 0) {
|
||||
this.selectedClients = data.clients;
|
||||
} else {
|
||||
this.selectedClients = [];
|
||||
this.toastr.warning('El aula seleccionada no tiene clientes.');
|
||||
}
|
||||
|
||||
this.taskForm.patchValue({ selectedClients: [] });
|
||||
this.selectedClientIds.clear();
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los detalles del aula seleccionada');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
toggleSelectAll() {
|
||||
const allSelected = this.areAllSelected();
|
||||
if (allSelected) {
|
||||
this.selectedClientIds.clear();
|
||||
} else {
|
||||
this.selectedClients.forEach(client => this.selectedClientIds.add(client.uuid));
|
||||
}
|
||||
this.taskForm.get('selectedClients')!.setValue(Array.from(this.selectedClientIds));
|
||||
}
|
||||
|
||||
areAllSelected(): boolean {
|
||||
return this.selectedClients.length > 0 && this.selectedClients.every(client => this.selectedClientIds.has(client.uuid));
|
||||
}
|
||||
|
||||
onCommandGroupChange(): void {
|
||||
const selectedGroupId = this.taskForm.get('commandGroup')?.value;
|
||||
this.http.get<any>(`${this.baseUrl}/command-groups/${selectedGroupId}`).subscribe(
|
||||
|
@ -185,6 +110,25 @@ export class CreateTaskComponent implements OnInit {
|
|||
);
|
||||
}
|
||||
|
||||
onOrganizationalUnitChange(): void {
|
||||
const selectedUnitId = this.taskForm.get('organizationalUnit')?.value;
|
||||
const selectedUnit = this.availableOrganizationalUnits.find(unit => unit['@id'] === selectedUnitId);
|
||||
this.selectedUnitChildren = selectedUnit ? selectedUnit.children : [];
|
||||
}
|
||||
|
||||
onChildChange(): void {
|
||||
const selectedChildId = this.taskForm.get('selectedChild')?.value;
|
||||
this.http.get<any>(`${this.baseUrl}${selectedChildId}`).subscribe(
|
||||
(data) => {
|
||||
this.selectedClients = data.clients;
|
||||
this.taskForm.patchValue({ selectedClients: [] });
|
||||
},
|
||||
(error) => {
|
||||
this.toastr.error('Error al cargar los detalles del aula seleccionada');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
saveTask(): void {
|
||||
if (this.taskForm.invalid) {
|
||||
this.toastr.error('Por favor, rellene todos los campos obligatorios');
|
||||
|
@ -193,21 +137,17 @@ export class CreateTaskComponent implements OnInit {
|
|||
|
||||
const formData = this.taskForm.value;
|
||||
const dateTime = this.combineDateAndTime(formData.date, formData.time);
|
||||
const selectedCommands = formData.extraCommands && formData.extraCommands.length > 0
|
||||
const selectedCommands = formData.extraCommands.length > 0
|
||||
? formData.extraCommands.map((id: any) => `/commands/${id}`)
|
||||
: null;
|
||||
: [""];
|
||||
|
||||
const payload: any = {
|
||||
commandGroups: formData.commandGroup ? [`/command-groups/${formData.commandGroup}`] : null,
|
||||
const payload = {
|
||||
commandGroups: [formData.commandGroup ? `/command-groups/${formData.commandGroup}` : [""]],
|
||||
commands: selectedCommands,
|
||||
dateTime: dateTime,
|
||||
notes: formData.notes || '',
|
||||
clients: Array.from(this.selectedClientIds).map((uuid: string) => `/clients/${uuid}`),
|
||||
notes: formData.notes || ''
|
||||
};
|
||||
|
||||
if (selectedCommands) {
|
||||
payload.commands = selectedCommands;
|
||||
}
|
||||
|
||||
if (this.editing) {
|
||||
const taskId = this.data.task.uuid;
|
||||
this.http.patch<any>(`${this.apiUrl}/${taskId}`, payload).subscribe({
|
||||
|
|
|
@ -1,60 +0,0 @@
|
|||
.detail-task-container {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.mat-card {
|
||||
margin: 20px 0;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #ccc;
|
||||
}
|
||||
|
||||
.mat-card-header {
|
||||
background-color: #f5f5f5;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
.mat-card-title {
|
||||
font-size: 20px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.mat-card-subtitle {
|
||||
font-size: 14px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.mat-card-content {
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
h3 {
|
||||
margin-top: 20px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
th.mat-header-cell {
|
||||
background-color: #f5f5f5;
|
||||
font-weight: bold;
|
||||
border-bottom: 2px solid #ccc;
|
||||
}
|
||||
|
||||
td.mat-cell {
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #ddd;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background-color: rgba(219, 219, 219, 0.5);
|
||||
}
|
||||
|
||||
.task-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue