Drowsito 3 settimane fa
commit
e2d7b63e40
59 ha cambiato i file con 8931 aggiunte e 0 eliminazioni
  1. 5 0
      .gitignore
  2. 14 0
      .vscode/launch.json
  3. 17 0
      .vscode/settings.json
  4. 286 0
      mvnw
  5. 161 0
      mvnw.cmd
  6. 208 0
      pom.xml
  7. 162 0
      src/main/java/META-INF/additional-spring-configuration-metadata.json
  8. 17 0
      src/main/java/es/uv/saic/SaicApplication.java
  9. 46 0
      src/main/java/es/uv/saic/config/ApplicationLocaleResolver.java
  10. 26 0
      src/main/java/es/uv/saic/config/AuthSuccessHandler.java
  11. 43 0
      src/main/java/es/uv/saic/config/Globals.java
  12. 135 0
      src/main/java/es/uv/saic/config/ScheduledTasks.java
  13. 22 0
      src/main/java/es/uv/saic/config/SchedulerConfig.java
  14. 108 0
      src/main/java/es/uv/saic/config/SecurityConfig.java
  15. 49 0
      src/main/java/es/uv/saic/config/WebConfig.java
  16. 54 0
      src/main/java/es/uv/saic/service/AcreditacioService.java
  17. 160 0
      src/main/java/es/uv/saic/service/AuthProvider.java
  18. 42 0
      src/main/java/es/uv/saic/service/CalendariService.java
  19. 62 0
      src/main/java/es/uv/saic/service/CategoriaService.java
  20. 132 0
      src/main/java/es/uv/saic/service/EmailService.java
  21. 27 0
      src/main/java/es/uv/saic/service/GraficaService.java
  22. 34 0
      src/main/java/es/uv/saic/service/InformeService.java
  23. 156 0
      src/main/java/es/uv/saic/service/InstanciaService.java
  24. 478 0
      src/main/java/es/uv/saic/service/InstanciaTascaService.java
  25. 39 0
      src/main/java/es/uv/saic/service/InstanciaTascaVerService.java
  26. 55 0
      src/main/java/es/uv/saic/service/LinkService.java
  27. 26 0
      src/main/java/es/uv/saic/service/NoticiaService.java
  28. 119 0
      src/main/java/es/uv/saic/service/OrganService.java
  29. 94 0
      src/main/java/es/uv/saic/service/ProcesService.java
  30. 29 0
      src/main/java/es/uv/saic/service/RolService.java
  31. 57 0
      src/main/java/es/uv/saic/service/SysStatusService.java
  32. 76 0
      src/main/java/es/uv/saic/service/TascaService.java
  33. 25 0
      src/main/java/es/uv/saic/service/TipusService.java
  34. 108 0
      src/main/java/es/uv/saic/service/UsuariService.java
  35. 176 0
      src/main/java/es/uv/saic/service/UsuarisRolService.java
  36. 22 0
      src/main/java/es/uv/saic/service/WikiService.java
  37. 844 0
      src/main/java/es/uv/saic/web/AdminController.java
  38. 253 0
      src/main/java/es/uv/saic/web/CalendarController.java
  39. 789 0
      src/main/java/es/uv/saic/web/DashboardController.java
  40. 42 0
      src/main/java/es/uv/saic/web/EmailController.java
  41. 214 0
      src/main/java/es/uv/saic/web/ManagersController.java
  42. 24 0
      src/main/java/es/uv/saic/web/NoticiaController.java
  43. 479 0
      src/main/java/es/uv/saic/web/OrganController.java
  44. 692 0
      src/main/java/es/uv/saic/web/ProceduresController.java
  45. 46 0
      src/main/java/es/uv/saic/web/StatsController.java
  46. 331 0
      src/main/java/es/uv/saic/web/SupervisionController.java
  47. 87 0
      src/main/java/es/uv/saic/web/TascaController.java
  48. 88 0
      src/main/java/es/uv/saic/web/TestController.java
  49. 67 0
      src/main/java/es/uv/saic/web/UsuariController.java
  50. 80 0
      src/main/java/es/uv/saic/web/WikiController.java
  51. 38 0
      src/main/resources/application-dev.properties
  52. 40 0
      src/main/resources/application-graal.properties
  53. 39 0
      src/main/resources/application-local.properties
  54. 38 0
      src/main/resources/application-prod.properties
  55. 63 0
      src/main/resources/application.properties
  56. 462 0
      src/main/resources/messages.properties
  57. 469 0
      src/main/resources/messages_ca.properties
  58. 463 0
      src/main/resources/messages_es.properties
  59. 13 0
      src/test/java/es/uv/docentia/SaicApplicationTests.java

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+/target/**
+.deployables
+.settings
+markdonwGen.py
+/target/

+ 14 - 0
.vscode/launch.json

@@ -0,0 +1,14 @@
+{
+    "configurations": [
+        {
+            "type": "java",
+            "name": "Spring Boot-SaicApplication<saic>",
+            "request": "launch",
+            "cwd": "${workspaceFolder}",
+            "mainClass": "es.uv.saic.SaicApplication",
+            "projectName": "saic",
+            "envFile": "${workspaceFolder}/.env",
+            "args": "--spring.profiles.active=local"
+        }
+    ]
+}

+ 17 - 0
.vscode/settings.json

@@ -0,0 +1,17 @@
+{
+    "sqltools.connections": [
+        {
+            "server": "coddono.uv.es",
+            "port": 9042,
+            "previewLimit": 50,
+            "driver": "Cassandra",
+            "name": "coddono",
+            "database": "acd425",
+            "username": "acd425",
+            "password": "acd425"
+        }
+    ],
+    "java.configuration.updateBuildConfiguration": "disabled",
+    "java.compile.nullAnalysis.mode": "automatic",
+    "java.dependency.syncWithFolderExplorer": false
+}

+ 286 - 0
mvnw

@@ -0,0 +1,286 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Mingw, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+  # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+##########################################################################################
+# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+# This allows using the maven wrapper in projects that prohibit checking in binary data.
+##########################################################################################
+if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Found .mvn/wrapper/maven-wrapper.jar"
+    fi
+else
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
+    fi
+    jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
+    while IFS="=" read key value; do
+      case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
+      esac
+    done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
+    if [ "$MVNW_VERBOSE" = true ]; then
+      echo "Downloading from: $jarUrl"
+    fi
+    wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
+
+    if command -v wget > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found wget ... using wget"
+        fi
+        wget "$jarUrl" -O "$wrapperJarPath"
+    elif command -v curl > /dev/null; then
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Found curl ... using curl"
+        fi
+        curl -o "$wrapperJarPath" "$jarUrl"
+    else
+        if [ "$MVNW_VERBOSE" = true ]; then
+          echo "Falling back to using Java to download"
+        fi
+        javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
+        if [ -e "$javaClass" ]; then
+            if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Compiling MavenWrapperDownloader.java ..."
+                fi
+                # Compiling the Java class
+                ("$JAVA_HOME/bin/javac" "$javaClass")
+            fi
+            if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
+                # Running the downloader
+                if [ "$MVNW_VERBOSE" = true ]; then
+                  echo " - Running MavenWrapperDownloader.java ..."
+                fi
+                ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
+            fi
+        fi
+    fi
+fi
+##########################################################################################
+# End of extension
+##########################################################################################
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+if [ "$MVNW_VERBOSE" = true ]; then
+  echo $MAVEN_PROJECTBASEDIR
+fi
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 161 - 0
mvnw.cmd

@@ -0,0 +1,161 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    https://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM set title of command window
+title %0
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar"
+FOR /F "tokens=1,2 delims==" %%A IN (%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties) DO (
+	IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 
+)
+
+@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
+@REM This allows using the maven wrapper in projects that prohibit checking in binary data.
+if exist %WRAPPER_JAR% (
+    echo Found %WRAPPER_JAR%
+) else (
+    echo Couldn't find %WRAPPER_JAR%, downloading it ...
+	echo Downloading from: %DOWNLOAD_URL%
+    powershell -Command "(New-Object Net.WebClient).DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"
+    echo Finished downloading %WRAPPER_JAR%
+)
+@REM End of extension
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%

+ 208 - 0
pom.xml

@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>4.0.2</version>
+		<relativePath/> <!-- lookup parent from repository -->
+	</parent>
+	<groupId>es.uv.saic</groupId>
+	<artifactId>saic</artifactId>
+	<version>3.0.0</version>
+	<name>saic</name>
+	<description>saic</description>
+
+	<properties>
+		<java.version>21</java.version>
+		<maven.compiler.release>21</maven.compiler.release>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<spring-cloud.version>2025.1.0</spring-cloud.version>
+	</properties>
+
+	<dependencyManagement>
+		<dependencies>
+			<dependency>
+				<groupId>org.springframework.cloud</groupId>
+				<artifactId>spring-cloud-dependencies</artifactId>
+				<version>${spring-cloud.version}</version>
+				<type>pom</type>
+				<scope>import</scope>
+			</dependency>
+		</dependencies>
+	</dependencyManagement>
+	
+	<dependencies>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-devtools</artifactId>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-actuator</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>io.micrometer</groupId>
+			<artifactId>micrometer-registry-prometheus</artifactId>
+		</dependency>
+		<!--<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-thymeleaf</artifactId>
+		</dependency>-->
+		<dependency>
+		    <groupId>org.springframework.boot</groupId>
+		    <artifactId>spring-boot-properties-migrator</artifactId>
+		    <scope>runtime</scope>
+		</dependency>
+		<!--
+		<dependency>
+		    <groupId>nz.net.ultraq.thymeleaf</groupId>
+		    <artifactId>thymeleaf-layout-dialect</artifactId>
+		</dependency>
+		<dependency>
+		    <groupId>org.thymeleaf.extras</groupId>
+		    <artifactId>thymeleaf-extras-springsecurity6</artifactId>
+		</dependency>
+		-->
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-security</artifactId>
+		</dependency>
+		<dependency>
+		    <groupId>org.springframework.security</groupId>
+		    <artifactId>spring-security-ldap</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>org.postgresql</groupId>
+			<artifactId>postgresql</artifactId>
+			<scope>runtime</scope>
+		</dependency>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-data-jpa</artifactId>
+		</dependency>
+		<dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-mail</artifactId>
+        </dependency>
+			
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-configuration-processor</artifactId>
+			<optional>true</optional>
+		</dependency>
+		
+		<dependency>
+		  <groupId>junit</groupId>
+		  <artifactId>junit</artifactId>
+		  <scope>test</scope>
+		</dependency>
+		
+		<dependency>
+		  <groupId>fr.opensagres.xdocreport</groupId>
+		  <artifactId>fr.opensagres.xdocreport.document.docx</artifactId>
+		  <version>2.0.0</version>
+		</dependency>
+				
+		<dependency>
+		  <groupId>fr.opensagres.xdocreport</groupId>
+		  <artifactId>fr.opensagres.xdocreport.template.velocity</artifactId>
+		  <version>2.0.0</version>
+		</dependency>	
+		
+		<dependency>
+		    <groupId>com.mysql</groupId>
+		    <artifactId>mysql-connector-j</artifactId>
+		</dependency>
+		
+		<dependency>
+			<groupId>org.jsoup</groupId>
+			<artifactId>jsoup</artifactId>
+			<version>1.15.3</version>
+		</dependency>
+				
+		<dependency>
+		    <groupId>org.apache.commons</groupId>
+		    <artifactId>commons-csv</artifactId>
+		    <version>1.11.0</version>
+		</dependency>
+		
+		<dependency>
+		    <groupId>org.apache.commons</groupId>
+		    <artifactId>commons-io</artifactId>
+		    <version>1.3.2</version>
+		</dependency>
+		
+		<dependency>
+		    <groupId>commons-io</groupId>
+		    <artifactId>commons-io</artifactId>
+		    <version>2.16.1</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-openfeign</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.cloud</groupId>
+			<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
+		</dependency>
+		<!-- 
+		<dependency>
+		    <groupId>org.jacoco</groupId> 
+		    <artifactId>jacoco-maven-plugin</artifactId>
+		    <version>0.8.12</version>
+		</dependency>
+	  	 -->
+
+		<dependency>
+			<groupId>es.uv.saic.shared</groupId>
+		    <artifactId>UV_SAIC_SHARED</artifactId>
+		    <version>0.0.1-SNAPSHOT</version>
+		</dependency>
+	</dependencies>
+	
+
+    <build>
+        <finalName>uv_saic_core</finalName>
+        <resources>
+            <resource>
+                <directory>src/main/resources</directory>
+                <filtering>true</filtering>
+            </resource>
+        </resources>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <configuration>
+                    <parameters>true</parameters>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.graalvm.buildtools</groupId>
+                <artifactId>native-maven-plugin</artifactId>
+                <configuration>
+                    <imageName>uv_saic_core</imageName>
+                    <buildArgs>
+                        <buildArg>--no-fallback</buildArg>
+                    </buildArgs>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+</project>

+ 162 - 0
src/main/java/META-INF/additional-spring-configuration-metadata.json

@@ -0,0 +1,162 @@
+{"properties": [
+  {
+    "name": "saic.data.filePath",
+    "type": "java.lang.String",
+    "description": "Path to store uploaded evidences"
+  },
+  {
+    "name": "saic.data.templates.filePath",
+    "type": "java.lang.String",
+    "description": "Path in where templates are stored"
+  },
+  {
+    "name": "saic.data.templates.logoPath",
+    "type": "java.lang.String",
+    "description": "Path in where logos are stored"
+  },
+  {
+    "name": "saic.mailer.reminder.enabled",
+    "type": "java.lang.String",
+    "description": "Enables or disables the mailer reminders (when assigned task is expired)"
+  },
+  {
+    "name": "saic.mailer.queue.enabled",
+    "type": "java.lang.String",
+    "description": "Enables or disables the mailer queue"
+  },
+  {
+    "name": "saic.mailer.calendar.enabled",
+    "type": "java.lang.String",
+    "description": "Enables or disables the email reminders related to the calendar (instance planifier)"
+  },
+  {
+    "name": "saic.mailer.maxMailsPerRound",
+    "type": "java.lang.String",
+    "description": "Defines de max number of emails that will be sended in a single round"
+  },
+  {
+    "name": "saic.parser.surveys.path",
+    "type": "java.lang.String",
+    "description": "A description for 'saic.parser.surveys.path'"
+  },
+  {
+    "name": "saic.uqserver.dbname.grau",
+    "type": "java.lang.String",
+    "description": "Defines the DB name for 'grau'"
+  },
+  {
+    "name": "saic.uqserver.dbname.master",
+    "type": "java.lang.String",
+    "description": "Defines the DB name for 'master'"
+  },
+  {
+    "name": "saic.uqserver.host",
+    "type": "java.lang.String",
+    "description": "A description for 'saic.uqserver.host'"
+  },
+  {
+    "name": "saic.uqserver.user",
+    "type": "java.lang.String",
+    "description": "Defines the final survey DB username"
+  },
+  {
+    "name": "saic.uqserver.passwd",
+    "type": "java.lang.String",
+    "description": "Defines the final survey DB password"
+  },
+  {
+    "name": "saic.uqserver2.host",
+    "type": "java.lang.String",
+    "description": "Defines the intermediate survey DB host"
+  },
+  {
+    "name": "saic.uqserver2.user",
+    "type": "java.lang.String",
+    "description": "Defines the intermediate survey DB username"
+  },
+  {
+    "name": "saic.uqserver2.passwd",
+    "type": "java.lang.String",
+    "description": "Defines the intermediate survey DB password"
+  },
+  {
+    "name": "saic.uqserver2.dbname.prof",
+    "type": "java.lang.String",
+    "description": "Defines de DB name for 'prof'"
+  },
+  {
+    "name": "saic.uqserver2.dbname.otros",
+    "type": "java.lang.String",
+    "description": "Defines de DB name for 'otros'"
+  },
+  {
+    "name": "saic.uqserver2.dbname.doct",
+    "type": "java.lang.String",
+    "description": "Defines the DB name for 'doctorado'"
+  },
+  {
+    "name": "saic.uqserver2.dbname.master",
+    "type": "java.lang.String",
+    "description": "Defines the DB name for 'master'"
+  },
+  {
+    "name": "saic.data.doctorado",
+    "type": "java.lang.String",
+    "description": "Defines the path to 'doctorado' data files"
+  },
+  {
+    "name": "saic.data.evdocente",
+    "type": "java.lang.String",
+    "description": "Defines the path to 'evdocente' data files"
+  },
+  {
+    "name": "saic.data.master",
+    "type": "java.lang.String",
+    "description": "Defines the path to 'master' data files"
+  },
+  {
+    "name": "saic.scheduler.expired.enabled",
+    "type": "java.lang.String",
+    "description": "Enables or disables the email reminders related to procedure tasks expiration"
+  },
+  {
+    "name": "saic.data.tmpPath",
+    "type": "java.lang.String",
+    "description": "Path to the temporal directory"
+  },
+  {
+    "name": "saic.data.templates.fileNotFound",
+    "type": "java.lang.String",
+    "description": "Path to the file that will be returned when the requested file does not exist"
+  },
+  {
+    "name": "saic.parser.surveys.enabled",
+    "type": "java.lang.String",
+    "description": "Enables or disables the surveys related scheduled tasks"
+  },
+  {
+    "name": "saic.actuator.validIp",
+    "type": "java.lang.String",
+    "description": "Actuator endpoints access will be restricted to the specified IP address"
+  },
+  {
+    "name": "saic.url.public",
+    "type": "java.lang.String",
+    "description": "A description for 'saic.url.public'"
+  },
+  {
+    "name": "saic.url.domain",
+    "type": "java.lang.String",
+    "description": "A description for 'saic.url.domain'"
+  },
+  {
+    "name": "saic.url.data.domain",
+    "type": "java.lang.String",
+    "description": "A description for 'saic.url.data.domain'"
+  },
+  {
+    "name": "saic.url.docs.domain",
+    "type": "java.lang.String",
+    "description": "A description for 'saic.url.docs.domain'"
+  }
+]}

+ 17 - 0
src/main/java/es/uv/saic/SaicApplication.java

@@ -0,0 +1,17 @@
+package es.uv.saic;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+@SpringBootApplication
+@EnableScheduling
+@EnableFeignClients
+public class SaicApplication {
+	
+	public static void main(String[] args) {
+		SpringApplication.run(SaicApplication.class, args);
+	}
+
+}

+ 46 - 0
src/main/java/es/uv/saic/config/ApplicationLocaleResolver.java

@@ -0,0 +1,46 @@
+package es.uv.saic.config;
+
+import java.util.Locale;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AnonymousAuthenticationToken;
+import org.springframework.security.core.context.SecurityContext;
+import org.springframework.security.core.context.SecurityContextHolder;
+import org.springframework.web.servlet.i18n.SessionLocaleResolver;
+
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.service.UsuariService;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+@Configuration
+public class ApplicationLocaleResolver extends SessionLocaleResolver {
+    @Autowired
+    UsuariService us;
+
+    @Override
+    public Locale resolveLocale(HttpServletRequest request) {
+        SecurityContext securityContext = SecurityContextHolder.getContext();
+        Locale userLocale = Locale.forLanguageTag("ca"); 
+        if(!(securityContext.getAuthentication() instanceof AnonymousAuthenticationToken)) {
+        	Usuari usuari = ((Usuari)securityContext.getAuthentication().getPrincipal());
+        	String locale = usuari.getLocale();
+        	userLocale = locale == null ? userLocale : Locale.forLanguageTag(locale);
+        }
+        
+        return userLocale;
+    }
+
+    @Override
+    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
+        super.setLocale(request, response, locale);
+
+        SecurityContext securityContext = SecurityContextHolder.getContext();
+        if(!(securityContext.getAuthentication() instanceof AnonymousAuthenticationToken)) {
+        	Usuari usuari = ((Usuari)securityContext.getAuthentication().getPrincipal());
+	        usuari.setLocale(locale.toLanguageTag());
+	        us.save(usuari);
+        }        
+    }
+}

+ 26 - 0
src/main/java/es/uv/saic/config/AuthSuccessHandler.java

@@ -0,0 +1,26 @@
+package es.uv.saic.config;
+
+import java.io.IOException;
+import jakarta.servlet.ServletException;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+
+import org.springframework.security.core.Authentication;
+import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
+import org.springframework.security.web.savedrequest.DefaultSavedRequest;
+
+public class AuthSuccessHandler implements AuthenticationSuccessHandler{
+	
+	@Override
+	public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth)
+	        throws IOException,  ServletException {
+	    request.getSession(false).setMaxInactiveInterval(3600); //3600
+		if(request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST") != null){
+			DefaultSavedRequest defaultSavedRequest = (DefaultSavedRequest) request.getSession().getAttribute("SPRING_SECURITY_SAVED_REQUEST");
+	    	response.sendRedirect(defaultSavedRequest.getRedirectUrl());
+		}
+		else{
+			response.sendRedirect(request.getContextPath()+"/procedures?_new=1");
+		}
+	}
+}

+ 43 - 0
src/main/java/es/uv/saic/config/Globals.java

@@ -0,0 +1,43 @@
+package es.uv.saic.config;
+
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.Date;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+
+
+@Configuration
+@ConfigurationProperties(prefix="globals")
+public class Globals {
+
+	private String filePath = "/tmp/uploads/";
+	//private String filePath = "/DATA/saic-data/files/";
+
+	public String getFilePath() {
+		return filePath;
+	}
+	
+	public String getFileName(String npi, int curs, int informe, String apartat, int item) {
+		return npi+"_"+Integer.toString(curs)+"_"+Integer.toString(informe)+"_"+apartat+Integer.toString(item);
+	}
+	
+	public int getCurrentYear() {
+
+		Date date = new Date();
+		LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
+		int month = localDate.getMonthValue();
+		int year = localDate.getYear();
+		
+		if(month < 9) {
+			return year-1;
+		}
+		else {
+			return year;
+		}
+	}
+	
+
+	
+}

+ 135 - 0
src/main/java/es/uv/saic/config/ScheduledTasks.java

@@ -0,0 +1,135 @@
+package es.uv.saic.config;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.sql.SQLException;
+import java.time.LocalDateTime;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+
+import jakarta.mail.MessagingException;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import es.uv.saic.shared.domain.Calendari;
+import es.uv.saic.shared.domain.Email;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.service.CalendariService;
+import es.uv.saic.service.EmailService;
+import es.uv.saic.service.InstanciaService;
+import es.uv.saic.service.InstanciaTascaService;
+import es.uv.saic.service.SysStatusService;
+import es.uv.saic.service.UsuariService;
+
+@Component
+@SuppressWarnings("unused")
+public class ScheduledTasks {
+	
+	@Autowired
+	private EmailService es;
+	@Autowired
+	private InstanciaService it;
+	@Autowired
+	private InstanciaTascaService its;
+	@Autowired
+	private UsuariService us;
+	@Autowired
+	private CalendariService cs;
+	@Autowired
+	private SysStatusService sss;
+	@Value("${saic.mailer.queue.enabled}")
+	private String queueEnabled;
+	@Value("${saic.mailer.reminder.enabled}")
+	private String reminderEnabled;
+	@Value("${saic.mailer.calendar.enabled}")
+	private String calendarEnabled;
+	@Value("${saic.mailer.maxMailsPerRound}")
+	private Integer maxMailsPerRound;
+	@Value("${saic.parser.surveys.enabled}")
+	private String parserEnabled;
+	@Value("${saic.scheduler.expired.enabled}")
+	private String expiredEnabled;
+	@Value("${saic.backups.database.enabled}")
+    private boolean backupDatabaseEnabled;
+    @Value("${saic.backups.database.exec}")
+    private String backupDatabaseExec;
+    @Value("${saic.backups.database.filePath}")
+    private String backupDatabasePath;
+
+
+	@Scheduled(fixedDelay = 300000)
+	public void sendInstanceMails() {
+		if(Boolean.parseBoolean(this.queueEnabled)) {
+			Integer counter = 0;
+			while(!es.pendingQueueIsEmpty() && counter < maxMailsPerRound) {
+				es.sendActiveTaskNext();
+				counter+=1;
+			}
+		}
+	}
+	
+	@Scheduled(cron="0 0 1 * * *")
+	public void sendReminderMails() {
+		if(Boolean.parseBoolean(this.reminderEnabled)) {
+			sss.log("Start sending mail reminders");
+			for(InstanciaTasca instanciaTasca : its.getPastTasks()) {
+				for(Usuari u : us.getInstanceAsignedUsers(instanciaTasca.getInstancia().getIdInstancia())) {
+					Email email = new Email();
+					email.setUsuari(u);
+					email.setInstanciaTasca(instanciaTasca);
+					es.addEmail(email);
+				}
+			}
+		}
+	}
+	
+	@Scheduled(cron="0 0 1 * * *")
+	public void sendCalendarMails() throws MessagingException {
+		if(Boolean.parseBoolean(this.calendarEnabled)) {
+			Integer counter = 0;
+			Usuari u = this.us.findByUsername("system");
+			List<Calendari> events = this.cs.getNextEvents();
+			for(Calendari e : events) {
+				this.es.sendCalendarMail(u, e);
+				counter+=1;
+			}
+		}
+	}
+	
+	@Scheduled(cron="0 0 1 * * *")
+	public void closeExpiredTasks() {
+		if(Boolean.parseBoolean(this.expiredEnabled)) {
+			// to do in future
+		}
+	}
+
+	@Scheduled(cron="0 0 3 * * *")
+    public void doBackups() throws InterruptedException {
+        if(this.backupDatabaseEnabled) { // do database backups if enabled
+            try {
+                List<File> sorted = Arrays.asList(new File(backupDatabasePath).listFiles());
+                sorted.sort(Comparator.comparing(File::lastModified));
+                if(sorted.size() == 3) {
+                    sorted.get(0).delete();
+                }
+                LocalDateTime now = LocalDateTime.now();
+                String fileName = "db_"+now.getYear()+"."+now.getDayOfMonth()+"."+now.getMonthValue()+"_"+now.getHour()+"."+now.getMinute()+"."+now.getSecond()+".sql";
+                ProcessBuilder pb = new ProcessBuilder("bash", "-c", backupDatabaseExec+" --output \""+backupDatabasePath+fileName+"\"");
+                //pb.inheritIO();
+                Process p = pb.start();
+                p.waitFor();
+            }
+            catch(IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+}

+ 22 - 0
src/main/java/es/uv/saic/config/SchedulerConfig.java

@@ -0,0 +1,22 @@
+package es.uv.saic.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.SchedulingConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
+import org.springframework.scheduling.config.ScheduledTaskRegistrar;
+
+@Configuration
+public class SchedulerConfig implements SchedulingConfigurer {
+    private final int POOL_SIZE = 10;
+
+    @Override
+    public void configureTasks(ScheduledTaskRegistrar scheduledTaskRegistrar) {
+        ThreadPoolTaskScheduler threadPoolTaskScheduler = new ThreadPoolTaskScheduler();
+
+        threadPoolTaskScheduler.setPoolSize(this.POOL_SIZE);
+        threadPoolTaskScheduler.setThreadNamePrefix("saic-mailer-");
+        threadPoolTaskScheduler.initialize();
+
+        scheduledTaskRegistrar.setTaskScheduler(threadPoolTaskScheduler);
+    }
+}

+ 108 - 0
src/main/java/es/uv/saic/config/SecurityConfig.java

@@ -0,0 +1,108 @@
+package es.uv.saic.config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
+import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
+import org.springframework.security.core.session.SessionRegistry;
+import org.springframework.security.core.session.SessionRegistryImpl;
+import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
+import org.springframework.security.crypto.password.PasswordEncoder;
+import org.springframework.security.ldap.DefaultSpringSecurityContextSource;
+import org.springframework.security.web.SecurityFilterChain;
+import org.springframework.security.web.access.expression.WebExpressionAuthorizationManager;
+import org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy;
+import org.springframework.security.web.authentication.session.ConcurrentSessionControlAuthenticationStrategy;
+import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy;
+import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy;
+import org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy;
+import org.springframework.security.web.session.HttpSessionEventPublisher;
+import es.uv.saic.service.AuthProvider;
+
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+public class SecurityConfig {
+
+	@Autowired
+	AuthProvider authProvider;
+	
+	@Value("${saic.actuator.validIp}")
+	private String validIp;
+	
+	@Bean
+	public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
+		http.authorizeHttpRequests((auth) -> auth
+	        	.requestMatchers("/", "/css/**", "/js/**", "/img/**", "/logos/*", "/logos/**").permitAll()
+	        	.requestMatchers("/login**").permitAll()
+	        	.requestMatchers("/keepalive").permitAll()
+	        	.requestMatchers("/actuator/**").access(new WebExpressionAuthorizationManager("hasIpAddress('" + this.validIp + "')"))
+				.requestMatchers("/actuator/**").access(new WebExpressionAuthorizationManager("hasIpAddress('127.0.0.1')")) 
+	        )
+	        .authorizeHttpRequests((auth)-> auth
+	            //.anyRequest().fullyAuthenticated()
+				.anyRequest().permitAll()
+	        )
+            .csrf((csrf) -> csrf.disable());
+	
+		http.headers((headers) -> headers
+				.frameOptions((options) -> options.sameOrigin())
+		    );
+	
+	    return http.build();
+	}
+	    
+    @Bean
+    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
+        return http.getSharedObject(AuthenticationManagerBuilder.class)
+        		   .authenticationProvider(authProvider)
+        		   .build();
+    }
+    
+    @Bean
+    public SessionRegistry sessionRegistry() {
+        return new SessionRegistryImpl();
+    }
+    
+    @Bean
+    public DefaultSpringSecurityContextSource contextSource() {
+        return  new DefaultSpringSecurityContextSource(
+                Collections.singletonList("ldap://ldap.uv.es"), "dc=uv,dc=es");
+    }
+    
+    @Bean
+    public PasswordEncoder passwordEncoder() {
+        return new BCryptPasswordEncoder();
+    }
+    
+    @Bean
+    public HttpSessionEventPublisher httpSessionEventPublisher() {
+        return new HttpSessionEventPublisher();
+    }
+     
+    @Bean
+    public CompositeSessionAuthenticationStrategy concurrentSession() {
+
+         ConcurrentSessionControlAuthenticationStrategy concurrentAuthenticationStrategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
+         concurrentAuthenticationStrategy.setMaximumSessions(1);
+         concurrentAuthenticationStrategy.setExceptionIfMaximumExceeded(true);
+         List<SessionAuthenticationStrategy> delegateStrategies = new ArrayList<SessionAuthenticationStrategy>();
+         delegateStrategies.add(concurrentAuthenticationStrategy);
+         delegateStrategies.add(new SessionFixationProtectionStrategy());
+         delegateStrategies.add(new RegisterSessionAuthenticationStrategy(sessionRegistry()));
+
+         CompositeSessionAuthenticationStrategy authenticationStrategy =  new CompositeSessionAuthenticationStrategy(delegateStrategies);
+         return authenticationStrategy;
+     }
+	
+}

+ 49 - 0
src/main/java/es/uv/saic/config/WebConfig.java

@@ -0,0 +1,49 @@
+package es.uv.saic.config;
+
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.Ordered;
+import org.springframework.web.filter.ForwardedHeaderFilter;
+import org.springframework.web.servlet.LocaleResolver;
+import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+import org.springframework.web.servlet.i18n.LocaleChangeInterceptor;
+
+import jakarta.servlet.DispatcherType;
+
+@Configuration
+public class WebConfig implements WebMvcConfigurer{
+	
+	@Bean(name = "localeResolver")
+	public LocaleResolver localeResolver() {
+	    return new ApplicationLocaleResolver();
+	}
+	
+	@Bean
+	public LocaleChangeInterceptor localeChangeInterceptor() {
+	    LocaleChangeInterceptor lci = new LocaleChangeInterceptor();
+	    lci.setParamName("lang");
+	    return lci;
+	}
+	
+	@Override
+	public void addInterceptors(InterceptorRegistry registry) {
+	    registry.addInterceptor(localeChangeInterceptor());
+	}
+	
+	@Bean
+	@ConditionalOnMissingBean(ForwardedHeaderFilter.class)
+	@ConditionalOnProperty(value = "server.forward-headers-strategy", havingValue = "native")
+	public FilterRegistrationBean<ForwardedHeaderFilter> forwardedHeaderFilter() {
+	    ForwardedHeaderFilter filter = new ForwardedHeaderFilter();
+	    FilterRegistrationBean<ForwardedHeaderFilter> registration = new FilterRegistrationBean<>(filter);
+	    registration.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.ASYNC, DispatcherType.ERROR);
+	    registration.setOrder(Ordered.HIGHEST_PRECEDENCE);
+	    return registration;
+	}
+	
+	
+}

+ 54 - 0
src/main/java/es/uv/saic/service/AcreditacioService.java

@@ -0,0 +1,54 @@
+package es.uv.saic.service;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Acreditacio;
+import es.uv.saic.shared.domain.AcreditacioPK;
+import es.uv.saic.shared.domain.AcreditacioRepository;
+import es.uv.saic.shared.domain.Organ;
+
+@Service
+public class AcreditacioService {
+
+	@Autowired
+	private AcreditacioRepository ar;
+	
+	public Acreditacio getById(String tlugar, Integer lugar) {
+		return this.ar.getReferenceById(new AcreditacioPK(tlugar, lugar));
+	}
+	
+	public List<Acreditacio> getAll(){
+		return this.ar.getAll();
+	}
+	
+	public List<Acreditacio> getNextsCurrentYear(){
+		return this.ar.getNextsCurrentYear();
+	}
+	
+	public List<Acreditacio> getByCursGrup(Integer curs, Integer grup){
+		return this.ar.getByCursGrup(curs, grup);
+	}
+	
+	public List<Acreditacio> getByCursGrupTambit(Integer curs, Integer grup, String tambit){
+		return this.ar.getByCursGrupTambit(curs, grup, tambit);
+	}
+	
+	public List<Acreditacio> getByCurs(Integer curs){
+		return this.ar.getByCurs(curs);
+	}
+	
+	public Acreditacio getByOrgan(Organ o) {
+		return this.ar.getByOrgan(o.getId().getTlugar(), o.getId().getLugar());
+	}
+	
+	public List<Acreditacio> getByCentre(Organ o){
+		return this.ar.getByCentre(o.getId().getTlugar(), o.getId().getLugar());
+	}
+	
+	public Acreditacio save(Acreditacio a) {
+		return this.ar.saveAndFlush(a);
+	}
+}

+ 160 - 0
src/main/java/es/uv/saic/service/AuthProvider.java

@@ -0,0 +1,160 @@
+package es.uv.saic.service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.Hashtable;
+import java.util.List;
+
+import javax.naming.Context;
+import javax.naming.NamingEnumeration;
+import javax.naming.NamingException;
+import javax.naming.directory.Attribute;
+import javax.naming.directory.Attributes;
+import javax.naming.directory.DirContext;
+import javax.naming.directory.InitialDirContext;
+import javax.naming.directory.SearchControls;
+import javax.naming.directory.SearchResult;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.authentication.AuthenticationProvider;
+import org.springframework.security.authentication.AuthenticationServiceException;
+import org.springframework.security.authentication.BadCredentialsException;
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
+import org.springframework.security.core.Authentication;
+import org.springframework.security.core.AuthenticationException;
+import org.springframework.security.core.authority.SimpleGrantedAuthority;
+import org.springframework.stereotype.Component;
+
+import es.uv.saic.SaicApplication;
+import es.uv.saic.shared.domain.Usuari;
+
+@Component
+public class AuthProvider implements AuthenticationProvider {
+
+	@Autowired
+	private UsuariService us;
+	@Autowired
+    private UsuarisRolService urs;
+		
+	private static final Logger logger = LoggerFactory.getLogger(SaicApplication.class);
+	
+	@Override
+	public Authentication authenticate(Authentication auth) throws AuthenticationException {
+		String username = auth.getName().toLowerCase().trim();
+        String password = auth.getCredentials().toString(); 
+        
+        List<SimpleGrantedAuthority> authorities = new ArrayList<SimpleGrantedAuthority>();
+    	authorities.add(new SimpleGrantedAuthority("ROLE_USER"));
+        
+        Usuari u = this.us.findByUsername(username);
+        
+        if(u != null) {
+        	u.setGranted(this.urs.isGrantedUser(u));
+            u.setAdmin(this.urs.isAdminUser(u));
+            u.setDataTest(this.urs.isDataTestUser(u));
+        	boolean vigent = this.us.hasActiveRol(u);
+        	
+	        if(!u.getLdap() && vigent) {
+	            if (u.getUsuari().equals(username) && u.getClau().equals(password)) {
+	            	logger.info("Autenticación LOCAL correcta: "+username);
+	            	if(u.isAdmin()) {
+	            		authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
+	            	}
+	            	if(u.isGranted()) {
+	            		authorities.add(new SimpleGrantedAuthority("ROLE_MANAGER"));
+	            	}
+	            	if(u.isDataTest()) {
+	            		authorities.add(new SimpleGrantedAuthority("ROLE_TESTER"));
+	            	}
+	            	UsernamePasswordAuthenticationToken authUser = new UsernamePasswordAuthenticationToken(u, password, authorities);
+	            	authUser.setDetails(u);
+	                return authUser ;
+	            } 
+	            else {
+	            	logger.info("Error de autenticación LOCAL ["+username+"]: el usuario o la contraseña no coinciden");
+	                throw new BadCredentialsException("Error de autenticación LOCAL ["+username+"]: el usuario o la contraseña no coinciden");
+	            }
+	        }
+	        else if(vigent) {
+	        	
+	        	Hashtable<String, String> env = new Hashtable<String, String>();
+
+                env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
+                env.put(Context.PROVIDER_URL, "ldap://ldap.uv.es/");
+                env.put(Context.SECURITY_AUTHENTICATION, "simple");
+                env.put(Context.SECURITY_PRINCIPAL, "uid=" + username.toLowerCase().trim() + ", dc=uv, dc=es ");
+                env.put(Context.SECURITY_CREDENTIALS, password);
+                
+                try {
+                    DirContext dc = new InitialDirContext(env);
+                    String base = "dc=uv,dc=es";
+                    String filter = "(&(uid=" + username + "))";
+                    int state = 0;
+
+                    SearchControls ctls = new SearchControls();
+                    ctls.setSearchScope(SearchControls.SUBTREE_SCOPE);
+                    ctls.setReturningAttributes(new String[] { "uid" });
+
+                    NamingEnumeration<SearchResult> resultEnum = dc.search(base, filter, ctls);
+
+                    while (resultEnum.hasMore() && state < 2) {
+                        SearchResult result = resultEnum.next();
+
+                        Attributes attrs = result.getAttributes();
+                        if (attrs.size() != 1) {
+                            logger.info("Error de autenticación LDAP ["+username+"]: el usuario o la contraseña no coinciden");
+                            throw new BadCredentialsException("Error de autenticación LDAP ["+username+"]: el usuario o la contraseña no coinciden");
+                        }
+                        NamingEnumeration<?> e = attrs.getAll();
+                        while (e.hasMore()) {
+                            Attribute attr = (Attribute) e.next();
+                            if (!((String) attr.get()).equals(username)) {
+                                logger.info("Error de autenticación LDAP ["+username+"]: el usuario no coincide con el devuelto por el servidor");
+                                throw new BadCredentialsException("Error de autenticación LDAP ["+username+"]: el usuario no coincide con el devuelto por el servidor");
+                            }
+                        }
+                        state++;
+                    }
+
+                    dc.close();
+
+                    if (state < 1 || state > 1) {
+                        logger.info("Error de autenticación LDAP ["+username+"]: -> el servidor LDAP ha devuelto un estado incorrecto.");
+                        throw new BadCredentialsException("Error de autenticación LDAP ["+username+"]: -> el servidor LDAP ha devuelto un estado incorrecto.");
+                    }
+                    logger.info("Autenticación LDAP correcta: " + u.getUsuari());
+                    u.setDataUltim(LocalDateTime.now());
+                    this.us.save(u);
+	            	if(u.isAdmin()) {
+	            		authorities.add(new SimpleGrantedAuthority("ROLE_ADMIN"));
+	            	}
+	            	if(u.isGranted()) {
+	            		authorities.add(new SimpleGrantedAuthority("ROLE_MANAGER"));
+	            	}
+	            	if(u.isDataTest()) {
+	            		authorities.add(new SimpleGrantedAuthority("ROLE_TESTER"));
+	            	}
+	            	UsernamePasswordAuthenticationToken authUser = new UsernamePasswordAuthenticationToken(u, password, authorities);
+	            	authUser.setDetails(u);
+	                return authUser ;
+                } 
+                catch (NamingException ex) {
+                    logger.info("Error de autenticación ["+username+"]: " + ex.getMessage());
+                    throw new AuthenticationServiceException("Error de autenticación ["+username+"]: " + ex.getMessage());
+                }
+	        }
+        }
+        
+        throw new BadCredentialsException("Error general en el sistema de autenticación");
+ 
+
+	}
+
+	@Override
+	public boolean supports(Class<?> auth) {
+		return auth.equals(UsernamePasswordAuthenticationToken.class);
+	}
+
+}

+ 42 - 0
src/main/java/es/uv/saic/service/CalendariService.java

@@ -0,0 +1,42 @@
+package es.uv.saic.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Calendari;
+import es.uv.saic.shared.domain.CalendariRepository;
+
+@Service
+public class CalendariService {
+	
+	@Autowired
+	private CalendariRepository cr;
+	
+	public List<Calendari> getAll() {
+		return this.cr.findAll();
+	}
+	
+	public Calendari findById(Integer id) {
+		Optional<Calendari> c = this.cr.findById(id);
+		if(!c.isEmpty()) {
+			return c.get();
+		}
+		return null;
+	}
+	
+	public List<Calendari> getNextEvents(){
+		return this.cr.getNextEvents();
+	}
+	
+	public Calendari save(Calendari c) {
+		return this.cr.saveAndFlush(c);
+	}
+	
+	public void delete(Calendari c) {
+		this.cr.delete(c);
+	}
+
+}

+ 62 - 0
src/main/java/es/uv/saic/service/CategoriaService.java

@@ -0,0 +1,62 @@
+package es.uv.saic.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Categoria;
+import es.uv.saic.shared.domain.CategoriaRepository;
+
+@Service
+public class CategoriaService {
+	
+	@Autowired
+	private CategoriaRepository cr;
+
+	
+	public List<Categoria> getAll() {
+		return this.cr.findAll();
+	}
+	
+	public Categoria findById(Integer id) {
+		Optional<Categoria> c = this.cr.findById(id);
+		if(!c.isEmpty()) {
+			return c.get();
+		}
+		return null;
+	}
+	
+	public List<Categoria> findFirstLevel(String ambit){
+		return this.cr.findFirstLevel(ambit);
+	}
+
+	public List<Categoria> findFirstLevelAndU(String ambit){
+		return this.cr.findFirstLevelAndU(ambit);
+	}
+	
+	public List<Categoria> findByTipusTambit(String tipus, String tambit){
+		return this.cr.findByTipusTambit(tipus, tambit);
+	}
+
+	public List<Categoria> findByTipusTambitAndU(String tipus, String tambit){
+		return this.cr.findByTipusTambitAndU(tipus, tambit);
+	}
+	
+	public List<Categoria> findByPareTambit(Integer pare, String tambit){
+		return this.cr.findByPareTambit(pare, tambit);
+	}
+
+	public List<Categoria> findByPareTambitAndU(Integer pare, String tambit){
+		return this.cr.findByPareTambitAndU(pare, tambit);
+	}
+	
+	public Categoria save(Categoria d) {
+		return this.cr.saveAndFlush(d);
+	}
+	
+	public void delete(Categoria d) {
+		this.cr.delete(d);
+	}
+}

+ 132 - 0
src/main/java/es/uv/saic/service/EmailService.java

@@ -0,0 +1,132 @@
+package es.uv.saic.service;
+
+import java.text.SimpleDateFormat;
+import java.util.LinkedList;
+import java.util.Queue;
+
+import jakarta.mail.MessagingException;
+
+import org.springframework.mail.MailSendException;
+import org.springframework.mail.SimpleMailMessage;
+import org.springframework.mail.javamail.JavaMailSender;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Calendari;
+import es.uv.saic.shared.domain.Email;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.domain.Usuari;
+
+@Service
+public class EmailService {
+	private JavaMailSender mailSender;
+	private Queue<Email> pendingQueue  = new LinkedList<Email>();
+
+    public EmailService(JavaMailSender javaMailSender) {
+        this.mailSender = javaMailSender;
+    }
+    
+    public boolean sendActiveTaskNext() {
+    	if(!this.pendingQueue.isEmpty()) {
+	    	Email e = this.getNextEmail();
+	    	try {
+	    		if(e.getInstanciaTasca() != null) {
+	    			this.sendNewTaskMail(e.getUsuari(), e.getInstanciaTasca());
+	    		}
+	    		else {
+	    			this.sendMail(e.getUsuari().getEmail(), e.getSubject(), e.getBody());
+	    		}
+			} 
+	    	catch (MessagingException err) {
+				err.printStackTrace();
+			}
+	    	catch (MailSendException err) {
+	    		err.printStackTrace();
+	    	}
+	    	return true;
+	    }
+    	else {
+    		return false;
+    	}
+    }
+    
+    public void addEmail(Email email) {
+    	pendingQueue.add(email);
+    }
+    
+    private Email getNextEmail() {
+    	return this.pendingQueue.poll();
+    }
+    
+    public Queue<Email> getPendingQueue() {
+    	return this.pendingQueue;
+    }
+    
+    public boolean pendingQueueIsEmpty() {
+    	return this.pendingQueue.isEmpty();
+    }
+
+    public void sendMail(String to, String subject, String msg) throws MessagingException{
+
+    	SimpleMailMessage mailMessage = new SimpleMailMessage();
+        mailMessage.setTo(to);
+        mailMessage.setSubject(subject);
+        mailMessage.setText(msg);
+        mailMessage.setFrom("saic@uv.es");
+
+        mailSender.send(mailMessage);
+    }
+    
+    public void sendNewTaskMail(Usuari u, InstanciaTasca it) throws MessagingException{
+    	String subject = "Notificació de tasca a realitzar en SAIC";
+    	String msg = "Estimat/da usuari/a:\n" + 
+			    	"\n" + 
+			    	"El Sistema d'Assegurament Intern de la Qualitat (SAIC) de la Universitat de València li informa que té vosté una tasca per realitzar\n" + 
+			    	"\n" + 
+			    	"Tasca: "+it.getTasca().getTitolVal()+"\n" + 
+			    	"Procediment: "+it.getTasca().getProces().getTitolVal()+"\n" + 
+			    	"Centre/Titulació: "+it.getInstancia().getOrgan().getOrgan().getNomVal()+" / "+it.getInstancia().getOrgan().getNomVal()+"\n" + 
+			    	"Curs al qual s'aplica: "+Integer.toString(it.getInstancia().getProces().getCursAvaluat()-1) + "-" + Integer.toString(it.getInstancia().getProces().getCursAvaluat())+"\n" + 
+			    	"\n" + 
+			    	"Per a realitzar-la ha de connectar-se al SAIC i seguir els passos que allà es descriuen.\n" + 
+			    	"\n" + 
+			    	"Faça clic ací https://saic.uv.es per a accedir al Sistema d'Assegurament Intern de la Qualitat (SAIC).\n" + 
+			    	"\n" + 
+			    	"Per a consultes sobre procediments o la documentació que s'ha d'aportar, envie un correu a unitat.qualitat@uv.es\n" + 
+			    	"Per a qualsevol consulta tècnica, envie un correu a saic@uv.es\n" + 
+			    	"\n" + 
+			    	"----------" +
+			    	"\n" + 
+			    	"Estimado/a usuario/a:\n" + 
+			    	"\n" + 
+			    	"El Sistema de Aseguramiento Interno de la Calidad (SAIC) de la Universitat de València le informa que tiene Vd. una tarea por realizar:\n" + 
+			    	"\n" + 
+			    	"Tarea: "+it.getTasca().getTitolCas()+"\n" + 
+			    	"Procedimiento: "+it.getTasca().getProces().getTitolCas()+"\n" + 
+			    	"Centro/Titulación: "+it.getInstancia().getOrgan().getOrgan().getNomCas()+" / "+it.getInstancia().getOrgan().getNomCas()+"\n" + 
+			    	"Curso al que se aplica: "+Integer.toString(it.getInstancia().getProces().getCursAvaluat())+"\n" + 
+			    	"\n" + 
+			    	"Para realizarla debe Vd. conectarse al SAIC y seguir los pasos allí descritos.\n" + 
+			    	"\n" + 
+			    	"Pinche aquí https://saic.uv.es para acceder al Sistema de Aseguramiento Interno de la Calidad (SAIC).\n" + 
+			    	"\n" + 
+			    	"Para consultas sobre los procedimientos o la documentación a aportar, envíe un correo a unitat.qualitat@uv.es \n" +
+    				"Para cualquier consulta técnica, envíe un correo a saic@uv.es\n";
+    	this.sendMail(u.getEmail(), subject, msg);
+    }
+    
+    public void sendCalendarMail(Usuari u, Calendari c) throws MessagingException{
+    	String subject = "[SYS] Notificación de evento planificado en SAIC";
+    	String msg = "Estimado/a administrador:\n" + 
+			    	"\n" + 
+			    	"El siguiente evento está planificado para hoy.\n" + 
+			    	"\n" + 
+			    	"Nombre: "+c.getTitolCas()+"\n" + 
+			    	"ID Proceso: "+c.getIdProces().toString()+"\n" + 
+			    	"Ámbito: "+c.getAmbit()+"\n" + 
+			    	"Planificado para el día "+new SimpleDateFormat("dd/MM/yyyy").format(c.getData())+"\n"+
+			    	"\n" + 
+			    	"Acceda a https://saic.uv.es y realice las acciones pertinentes. \n" + 
+			    	"\n";
+    	this.sendMail(u.getEmail(), subject, msg);
+    }
+}

+ 27 - 0
src/main/java/es/uv/saic/service/GraficaService.java

@@ -0,0 +1,27 @@
+package es.uv.saic.service;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Grafica;
+import es.uv.saic.shared.domain.GraficaRepository;
+
+@Service
+public class GraficaService {
+
+	@Autowired
+	private GraficaRepository gr;
+	
+	public List<Grafica> findLikeAmbit(String ambit) {
+		return this.gr.findLikeAmbit(ambit);
+	}
+	
+	public List<Grafica> findLikeAmbitAndEstudi(String ambit, String estudi) {
+		return this.gr.findLikeAmbitAndEstudi(ambit, estudi);
+	}
+	
+	
+	
+}

+ 34 - 0
src/main/java/es/uv/saic/service/InformeService.java

@@ -0,0 +1,34 @@
+package es.uv.saic.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Informe;
+import es.uv.saic.shared.domain.InformeRepository;
+
+@Service
+public class InformeService {
+	
+	@Autowired
+	private InformeRepository informeRepository;
+	
+	public List<Informe> findByGrupWeb(String grupWeb) {
+		return this.informeRepository.findByGrupWeb(grupWeb);
+	}
+	
+	public List<Informe> findByGrupWebTambit(String grupWeb, String tambit) {
+		return this.informeRepository.findByGrupWebTambit(grupWeb, tambit);
+	}
+	
+	public Informe findById(Integer idInforme) {
+		Optional<Informe> i = this.informeRepository.findById(idInforme);
+		if(i.isEmpty()) {
+			return null;
+		}
+		return i.get();
+	}
+
+}

+ 156 - 0
src/main/java/es/uv/saic/service/InstanciaService.java

@@ -0,0 +1,156 @@
+package es.uv.saic.service;
+
+import java.math.BigInteger;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Instancia;
+import es.uv.saic.shared.domain.InstanciaRepository;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.dto.InstanciaGanttDTO;
+
+
+@Service
+public class InstanciaService {
+
+	@Autowired
+	private InstanciaRepository instanciaRepository;
+	
+	@Autowired
+	@Lazy
+	private InstanciaTascaService instanciaTascaService;
+	
+	@SuppressWarnings("rawtypes")
+	public Instancia findByID(BigInteger p_id) {
+		Optional opt = this.instanciaRepository.findById(p_id);
+		if(opt.isEmpty()) {
+			return null;
+		}
+		else {
+			return (Instancia) opt.get();
+		}
+	}
+	
+	public InstanciaTasca getReportFromInstancia(BigInteger idInstancia) {
+		return this.getReportFromInstancia(idInstancia);
+	}
+	
+	public List<BigInteger> filterSupervisables(List<Integer> centres, List<Integer> titulacions, List<Integer> cursos, List<String> procediments) {
+		return this.instanciaRepository.filterSupervisables(centres, titulacions, cursos, procediments);
+	}
+	
+	public BigInteger filterSupervisable(Integer centre, Integer titulacio, Integer curs, String procediment) {
+		return this.instanciaRepository.filterSupervisable(centre, titulacio, curs, procediment);
+	}
+	
+	public List<BigInteger> filterSupervisablesByEvidencies(List<Integer> centres, List<Integer> titulacions, List<Integer> cursos, List<String> evidencies){
+		return this.instanciaRepository.filterSupervisablesByEvidencies(centres, titulacions, cursos, evidencies);
+	}
+	
+	public void save(Instancia i) {
+		this.instanciaRepository.save(i);
+		this.instanciaRepository.flush();
+	}
+	
+	public List<Instancia> findAll(){
+		return this.instanciaRepository.findAll();
+	}
+	
+	public List<BigInteger> findAllIds(){
+		return this.instanciaRepository.findAllIds();
+	}
+	
+	public List<Instancia> findByOrganCursNom(String tlugar, Integer lugar, Integer centre, Integer titulacio, Integer curs, List<String> nom){
+		return this.instanciaRepository.findByOrganCursNom(tlugar, lugar, centre, titulacio, curs, nom);
+	}
+
+	public List<Instancia> findByCentreCursNom(Integer lugar2, Integer curs, List<String> nom){
+		return this.instanciaRepository.findByCentreCursNom(lugar2, curs, nom);
+	}
+
+	public List<Instancia> findByIdProces(Integer idProces){
+		return this.instanciaRepository.findByIdProces(idProces);
+	}
+	
+	public List<InstanciaGanttDTO> findByOrganBetweenCurs(String tlugar, Integer lugar, Integer centre, List<Integer> titulacio, Integer cursIni, Integer cursFi){
+		List<InstanciaGanttDTO> a = this.instanciaRepository.findByOrganBetweenCurs(tlugar, lugar, centre, titulacio, cursIni, cursFi);
+		return a;
+	}
+	
+	public boolean exists(String proces, String tlugar, Integer lugar, Integer centre, Integer titulacio, Integer curs) {
+		List<Instancia> ins = this.instanciaRepository.findOlders(proces, tlugar, lugar, centre, titulacio, curs);
+		return ins.size() > 0;
+	}
+	
+	public boolean exists(String proces, String tlugar, Integer lugar, Integer centre, Integer titulacio, Integer curs, Integer idtascap, String estat) {
+		List<Instancia> ins = this.instanciaRepository.findOlders(proces, tlugar, lugar, centre, titulacio, curs, idtascap, estat);
+		return ins.size() > 0;
+	}
+	
+	public BigInteger instantiateT(Integer idProces, Integer titulacio) {
+		try {
+			BigInteger idInstancia = this.instanciaRepository.instantiateT(idProces, titulacio);
+			this.instanciaRepository.activateInstantiatedTask(idInstancia);
+			InstanciaTasca it = this.instanciaTascaService.findActiveByInstancia(idInstancia);
+			if(it.getTasca().getRol().getNomRol().equals("sys")) {
+				it = this.instanciaTascaService.system(it);
+			}
+			return idInstancia;
+		}
+		catch(Exception e){
+			e.printStackTrace();
+			return new BigInteger("0");
+		}
+	}
+	
+	public BigInteger instantiateC(Integer idProces, Integer centre, Integer titulacio) {
+		try {
+			BigInteger idInstancia = this.instanciaRepository.instantiateC(idProces, centre, titulacio);
+			this.instanciaRepository.activateInstantiatedTask(idInstancia);
+			InstanciaTasca it = this.instanciaTascaService.findActiveByInstancia(idInstancia);
+			if(it.getTasca().getRol().getNomRol().equals("sys")) {
+				it = this.instanciaTascaService.system(it);
+			}
+			return idInstancia;
+		}
+		catch(Exception e){
+			e.printStackTrace();
+			return new BigInteger("0");
+		}
+	}
+	
+	public BigInteger instantiateU(Integer idProces, Integer titulacio) {
+		try {
+			BigInteger idInstancia = this.instanciaRepository.instantiateU(idProces, titulacio);
+			this.instanciaRepository.activateInstantiatedTask(idInstancia);
+			InstanciaTasca it = this.instanciaTascaService.findActiveByInstancia(idInstancia);
+			if(it.getTasca().getRol().getNomRol().equals("sys")) {
+				it = this.instanciaTascaService.system(it);
+			}
+			return idInstancia;
+		}
+		catch(Exception e){
+			e.printStackTrace();
+			return new BigInteger("0");
+		}
+	}
+	
+	public void deleteInstance(Instancia instancia) {
+		if(this.instanciaRepository.clearInstance(instancia.getIdInstancia()) > 0) {
+			this.instanciaRepository.deleteInstance(instancia.getIdInstancia());
+		}
+	}
+	
+	public void clearInstance(Instancia instancia) {
+		if(this.instanciaRepository.clearInstance(instancia.getIdInstancia()) > 0) {
+			this.instanciaRepository.instantiateTasks(instancia.getIdInstancia(), instancia.getProces().getIdProces());
+			this.instanciaRepository.activateInstantiatedTask(instancia.getIdInstancia());
+		}
+	}
+	
+}
+

+ 478 - 0
src/main/java/es/uv/saic/service/InstanciaTascaService.java

@@ -0,0 +1,478 @@
+package es.uv.saic.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.io.FileUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import es.uv.saic.shared.domain.Document;
+import es.uv.saic.shared.domain.Email;
+import es.uv.saic.shared.domain.EvidenciaTransfer;
+import es.uv.saic.shared.domain.Instancia;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.domain.InstanciaTascaRepository;
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.Plantilla;
+import es.uv.saic.shared.domain.TascaVersioTransfer;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.dto.CategoriaDTO;
+import es.uv.saic.shared.dto.InstanciaTascaDTO;
+import es.uv.saic.shared.dto.TascaInformeTransferDTO;
+import es.uv.saic.shared.feign.DocumentClient;
+import es.uv.saic.shared.feign.PlantillaClient;
+
+@Service
+public class InstanciaTascaService {
+
+	@Autowired
+	private InstanciaTascaRepository instanciaTascaRepository;
+	@Autowired
+	private InstanciaTascaVerService instanciaTascaVerService;
+	@Autowired
+	private UsuarisRolService usuarisRolService;
+	@Autowired
+	private UsuariService usuariService;
+	@Autowired
+	private InstanciaService instanciaService;
+	@Autowired
+	private EmailService es;
+	@Autowired
+	private PlantillaClient plc;
+	@Value("${saic.data.filePath}")
+	private String filePath;
+	
+	@Autowired
+	private DocumentClient dc;
+	
+	public List<InstanciaTasca> findByInstancia(BigInteger i_id) {
+		return this.instanciaTascaRepository.findByInstancia(i_id);
+	}
+	
+	public InstanciaTasca findByInstanciaTascap(BigInteger i_id, Integer tascap) {
+		return this.instanciaTascaRepository.findByInstanciaTascap(i_id, tascap);
+	}
+	
+	public List<TascaVersioTransfer> findOlderVersions(BigInteger idInstancia, Integer idTasca, Integer version) {
+		List<InstanciaTasca> itlist = this.instanciaTascaRepository.findOlderVerions(idInstancia, idTasca, version);
+		List<TascaVersioTransfer> vtlist = new ArrayList<TascaVersioTransfer>();
+		if(itlist != null) {
+			if(itlist.size() > 0) {
+				for(InstanciaTasca it : itlist) {
+					TascaVersioTransfer tvt = new TascaVersioTransfer();
+					tvt.setIdInstanciaTasca(it.getIdInstanciaTasca());
+					tvt.setTipus(it.getTasca().getTipus().getTipus());
+					tvt.setVersion(it.getVersion());
+					tvt.setUsuariFet(it.getUsuari() == null ? "" : it.getUsuari().getNom() + " " + it.getUsuari().getCognoms());
+					tvt.setDataFet(it.getDataFet());
+					tvt.setEstat(it.getEstat());
+					if(it.getTasca().getTipus().getTipus() == 11 || it.getTasca().getTipus().getTipus() == 15) {
+						Plantilla p = plc.findByID(Integer.parseInt(it.getTasca().getCodiEvidencia()));
+						Plantilla p2 = null;
+						
+						if(it.getInstancia().getTitulacio() > 0) {
+							if(!p.getAmbit().equals(it.getInstancia().getOrgan().getTambit())) {
+								p2 = plc.findByVersioCodiAmbit(p.getVersio(), p.getCodi(), it.getInstancia().getOrgan().getTambit());
+							}
+						}			
+						if(p2 != null) { p = p2; }
+						tvt.setCodiEvidencia(p.getCodi());
+						tvt.setNomEvidenciaCas(p.getNomCas());
+						tvt.setNomEvidenciaVal(p.getNomVal());
+					}
+					else {
+						tvt.setCodiEvidencia(it.getTasca().getCodiEvidencia());
+						tvt.setNomEvidenciaCas(it.getTasca().getNomEvidenciaCas());
+						tvt.setNomEvidenciaVal(it.getTasca().getNomEvidenciaVal());
+					}
+					vtlist.add(tvt);
+				}
+			}
+		}
+		return vtlist;
+	}
+	
+	public InstanciaTasca findActiveByInstancia(BigInteger i_id) {
+		return this.instanciaTascaRepository.findActiveByInstancia(i_id);
+	}
+	
+	public List<InstanciaTasca> findActivesByType(Integer type) {
+		return this.instanciaTascaRepository.findActivesByType(type);
+	}
+	
+	public boolean isUserAuthorized(Usuari u, BigInteger idInstanciaTasca) {
+		if(this.instanciaTascaRepository.isUserAutorized(u.getUsuari(), idInstanciaTasca) != null) {
+			return true;
+		}
+		else if(this.usuarisRolService.hasActiveAmbit(u, "C")) {
+			return this.instanciaTascaRepository.isUserAutorizedByAmbitC(u.getUsuari(), idInstanciaTasca) != null;
+		}
+		else {
+			if((this.usuarisRolService.hasActiveRol(u, "u_uq") && 
+					this.instanciaTascaRepository.getReferenceById(idInstanciaTasca).getTasca().getRol().getNomRol().equals("u_uq"))
+				|| (this.usuarisRolService.hasActiveRol(u, "u_admi") && 
+						this.instanciaTascaRepository.getReferenceById(idInstanciaTasca).getTasca().getRol().getNomRol().equals("u_admi"))
+				|| (this.usuarisRolService.hasActiveRol(u, "adeit") && 
+						this.instanciaTascaRepository.getReferenceById(idInstanciaTasca).getTasca().getRol().getNomRol().equals("adeit"))){
+				return true;
+			}
+		}
+		
+		return false; 
+	}
+		
+	public InstanciaTasca getReportFromProcesOrgan(Integer idProces, String tlugar, Integer lugar,
+			 									   Integer centre, Integer titulacio) {
+		return this.instanciaTascaRepository.getReportFromProcesOrgan(idProces, tlugar, lugar, centre, titulacio);
+	}
+
+	public InstanciaTasca getReportFromNomProcesOrgan(String nomProces, String tlugar, Integer lugar,
+			 									      Integer centre, Integer titulacio) {
+		return this.instanciaTascaRepository.getReportFromNomProcesOrgan(nomProces, tlugar, lugar, centre, titulacio);
+	}
+	
+	public InstanciaTasca system(InstanciaTasca it) {
+		Integer tipus = it.getTasca().getTipus().getTipus();
+		Usuari user = this.usuariService.findByUsername("system");
+		if(tipus == 13) { /* Convert editable document to PDF */
+			Integer idTascap = Integer.parseInt(it.getTasca().getOpcions().replaceAll("[^0-9]", ""));
+			InstanciaTasca it2 = this.getNext(it.getInstancia().getIdInstancia(), it.getTasca().getProces().getIdProces(), idTascap);
+			String editableFile = this.filePath+it2.getEvidencia();
+			String tmpFile = this.filePath+"tmp/"+it2.getIdInstanciaTasca().toString()+".pdf";
+			String finalFile = this.filePath+it.getIdInstanciaTasca().toString()+".pdf";
+			String ext = StringUtils.getFilenameExtension(editableFile);
+			ext = ext == null ? "" : ext;
+			
+			if(!ext.equals("pdf")) {
+				ProcessBuilder builder = new ProcessBuilder();
+				builder.command("soffice", "--headless", "--convert-to", "pdf", "--outdir", this.filePath+"tmp", editableFile);
+				try {
+					Process process = builder.start();
+					process.waitFor();
+					builder.command("mv", tmpFile, finalFile);
+					process = builder.start();
+					process.waitFor();
+				} 
+				catch (IOException e) {
+					e.printStackTrace();
+				} 
+				catch (InterruptedException e) {
+					e.printStackTrace();
+					if (Thread.interrupted()) {
+						Thread.currentThread().interrupt();
+					}
+				}
+			}
+			else {
+				try {
+					ProcessBuilder builder = new ProcessBuilder();
+					builder.command("cp", editableFile, finalFile);
+					Process process = builder.start();
+					process.waitFor();
+					it.setEvidencia(it.getIdInstanciaTasca().toString()+".pdf");
+					it.setEstat("E");
+					this.saveChanges(user, it);
+				} 
+				catch (IOException e) {
+					e.printStackTrace();
+				} 
+				catch (InterruptedException e) {
+					e.printStackTrace();
+					if (Thread.interrupted()) {
+						Thread.currentThread().interrupt();
+					}
+				}
+			}
+		}
+		else if(tipus == 20) { /* Check if procedure was instanced in the past */
+			String [] options = it.getTasca().getOpcions().split(";");
+			boolean res = false;
+			if(options.length == 2) {
+				res = this.instanciaService.exists(options[0], it.getInstancia().getOrgan().getId().getTlugar(), 
+						   it.getInstancia().getOrgan().getId().getLugar(), it.getInstancia().getCentre(), 
+						   it.getInstancia().getTitulacio(), Integer.valueOf(options[1]));
+			}
+			else if(options.length == 4) {
+				res = this.instanciaService.exists(options[0], it.getInstancia().getOrgan().getId().getTlugar(), 
+						   it.getInstancia().getOrgan().getId().getLugar(), it.getInstancia().getCentre(), 
+						   it.getInstancia().getTitulacio(), Integer.valueOf(options[1]), Integer.valueOf(options[2]), options[3]);
+			}
+			else {
+				res = false;
+			}
+				
+			if(res) {
+				it.setEstat("S");
+			}
+			else {
+				it.setEstat("N");
+			}
+			this.saveChanges(user, it);
+		}
+		else if(tipus == 21) { /* Combine PDFs from each idtascap specified in opcions */
+			String [] options = it.getTasca().getOpcions().split(";");
+			String finalFile = this.filePath+it.getIdInstanciaTasca().toString()+".pdf";
+			
+			List<String> cmd = new ArrayList<String>();
+			List<HashMap<String, String>> evs = new ArrayList<HashMap<String, String>>();
+			for(String idtascap : options) {
+				InstanciaTasca i = this.findByInstanciaTascap(it.getInstancia().getIdInstancia(), Integer.valueOf(idtascap));
+				if(i.getEstat() == null) { /* Task was not submitted */
+					continue;
+				}
+				if(i.getEstat().equals("E")) {
+					HashMap<String, String> map = new HashMap<String, String>();
+					map.put("codi", i.getTasca().getCodiEvidencia());
+					map.put("idtascap", i.getTasca().getIdTascap().toString());
+					map.put("evidencia", this.filePath+i.getEvidencia());
+					boolean found = false;
+					for(HashMap<String, String> m : evs) {
+						if(m.get("codi").equals(i.getTasca().getCodiEvidencia())) {
+							if(Integer.parseInt(m.get("idtascap")) < i.getTasca().getIdTascap()) { /* Get only the last version */
+								m.replace("idtascap", map.get("idtascap"));
+								m.replace("evidencia", map.get("evidencia"));
+							}
+							found = true;
+						}
+					}
+					if(!found) {
+						evs.add(map);
+					}
+				}
+			}
+			
+			cmd.add("pdfunite");
+			for(HashMap<String, String> m : evs) {
+				cmd.add(m.get("evidencia"));
+			}
+			cmd.add(finalFile);
+			
+			ProcessBuilder builder = new ProcessBuilder();
+			builder.command(cmd.toArray(new String[]{}));
+			try {
+				Process process = builder.start();
+				process.waitFor();
+				it.setEvidencia(it.getIdInstanciaTasca().toString()+".pdf");
+				it.setEstat("E");
+				this.saveChanges(user, it);
+			}
+			catch (IOException e) {
+				e.printStackTrace();
+			} 
+			catch (InterruptedException e) {
+				e.printStackTrace();
+				if (Thread.interrupted()) {
+					Thread.currentThread().interrupt();
+				}
+			}
+		}
+		else if(tipus == 16){ /* Copy evidence from other procedure */
+			String [] options = it.getTasca().getOpcions().split(";");
+			if(options.length > 2){
+				/* Hay que cambiar esto para usar el id_tascap al seleccionar la evidencia */
+				InstanciaTasca itOld = this.getReportFromNomProcesOrgan(options[0], 
+																		it.getInstancia().getOrgan().getId().getTlugar(), 
+																		it.getInstancia().getOrgan().getId().getLugar(), 
+																		it.getInstancia().getCentre(), 
+																		it.getInstancia().getTitulacio());
+
+				String copiedPath = this.filePath+itOld.getEvidencia().replace(itOld.getIdInstanciaTasca().toString(), it.getIdInstanciaTasca().toString());
+				File copied = new File(copiedPath);
+				File original = new File(this.filePath+itOld.getIdInstanciaTasca().toString());
+
+				try {
+					FileUtils.copyFile(original, copied);
+				} catch (IOException e) {
+					e.printStackTrace();
+				}
+
+				it.setEvidencia(copied.getName());
+			}
+			it.setEstat("E");
+			this.saveChanges(user, it);
+		}
+		else if(tipus == 22){ /* Link document from documents as evidence */
+			String options = it.getTasca().getOpcions();
+			Organ o = it.getInstancia().getOrgan();
+			Document d = dc.findByCategoriaOrgan(new CategoriaDTO(Integer.parseInt(options), o.getId().getLugar(), o.getId().getTlugar()));
+			it.setEvidencia(d.getRuta());
+			it.setEstat("E");
+			this.saveChanges(user, it);
+		}
+		
+		return it;
+	}
+	
+	public void saveChanges(Usuari user, InstanciaTasca it) {
+
+		it.setUsuari(user);
+		it.setDataFet(LocalDate.now());
+		this.save(it);
+		
+		// Set next as active or close instance
+		Integer next = 9999;
+		if(it.getTasca().getTipus().getTipus() == 2 || it.getTasca().getTipus().getTipus() == 20) {
+			if(it.getEstat().equals("S")) {
+				next = it.getTasca().getIdTascaSeg();
+			}
+			else if(it.getEstat().equals("N")){
+				next = it.getTasca().getIdTascaSeg2();
+			}
+		}
+		else {
+			next = it.getTasca().getIdTascaSeg();
+		}
+				
+		if(next != 9999) {
+			InstanciaTasca it_next = this.getNext(it.getInstancia().getIdInstancia(), it.getTasca().getProces().getIdProces(), next);
+			if(it_next.getUsuari() == null) {
+				it_next.setEstat("A");
+				it_next.setDataFet(LocalDate.now());
+				if(it_next.getTasca().getTipus().getTipus() == 15) {
+					InstanciaTasca ta = this.findByInstanciaTascap(it.getInstancia().getIdInstancia(), Integer.parseInt(it_next.getTasca().getOpcions()));
+					it_next.setText(ta.getText());
+				}
+				this.save(it_next);
+			}
+			else {
+				InstanciaTasca it_nextNew = new InstanciaTasca();
+				it_nextNew.setDataFet(null);
+				it_nextNew.setEstat("A");
+				it_nextNew.setData(LocalDate.now());
+				it_nextNew.setEvidencia(null);
+				it_nextNew.setInstancia(it_next.getInstancia());
+				it_nextNew.setTasca(it_next.getTasca());
+				it_nextNew.setUsuari(null);
+				it_nextNew.setVersion(it_next.getVersion()+1);
+				it_nextNew.setIdInstanciaTasca(it_next.getIdInstanciaTasca().add(new BigInteger("1")));
+				if(it_next.getTasca().getTipus().getTipus() == 15) {
+					InstanciaTasca ta = this.findByInstanciaTascap(it.getInstancia().getIdInstancia(), Integer.parseInt(it_next.getTasca().getOpcions()));
+					it_nextNew.setText(ta.getText());
+				}
+				this.save(it_nextNew);
+			}
+			
+			for(Usuari u : this.usuariService.getInstanceAsignedUsers(it.getInstancia().getIdInstancia())) {
+				Email email = new Email();
+				email.setInstanciaTasca(this.findActiveByInstancia(it.getInstancia().getIdInstancia()));
+				email.setUsuari(u);
+				es.addEmail(email);
+			}
+			
+			if(this.findActiveByInstancia(it.getInstancia().getIdInstancia()).getTasca().getRol().getNomRol().equals("sys")) {
+				this.system(this.findActiveByInstancia(it.getInstancia().getIdInstancia()));
+			}
+		}
+		else {
+			Instancia inst = it.getInstancia();
+			inst.setEstat("F");
+			this.instanciaService.save(inst);
+			/* Delete editor drafts from any task associated to this instance */
+			this.instanciaTascaVerService.deleteByIdInstancia(inst.getIdInstancia());
+		}
+	}
+	
+	public List<EvidenciaTransfer> findOlderByProces(Integer centre, Integer titulacio, String nomProces, Integer minCurs){
+		List<EvidenciaTransfer> elist = new ArrayList<EvidenciaTransfer>();
+		List<InstanciaTasca> itlist = this.instanciaTascaRepository.findOlderByProces(centre, titulacio, nomProces, minCurs);
+		if(itlist != null) {
+			if(itlist.size() > 0) {
+				for(InstanciaTasca i : itlist) {
+					EvidenciaTransfer et = new EvidenciaTransfer();
+					et.setData(i.getDataFet());
+					et.setEvidencia(i.getEvidencia());
+					et.setCurs(i.getTasca().getProces().getCursAvaluat());
+					et.setNomProces(i.getTasca().getProces().getNomProces());
+					et.setIdTascai(i.getIdInstanciaTasca());
+					if(i.getTasca().getTipus().getTipus() == 11 || i.getTasca().getTipus().getTipus() == 15) {
+						Plantilla p = plc.findByID(Integer.parseInt(i.getTasca().getCodiEvidencia()));
+						Plantilla p2 = null;
+						
+						if(i.getInstancia().getTitulacio() > 0) {
+							if(!p.getAmbit().equals(i.getInstancia().getOrgan().getTambit())) {
+								p2 = plc.findByVersioCodiAmbit(p.getVersio(), p.getCodi(), i.getInstancia().getOrgan().getTambit());
+							}
+						}			
+						if(p2 != null) { p = p2; }
+								
+						et.setCodiEvidencia(p.getCodi());
+						et.setNomEvidenciaCas(p.getNomCas());
+						et.setNomEvidenciaVal(p.getNomVal());
+					}
+					else {
+						et.setCodiEvidencia(i.getTasca().getCodiEvidencia());
+						et.setNomEvidenciaCas(i.getTasca().getNomEvidenciaCas());
+						et.setNomEvidenciaVal(i.getTasca().getNomEvidenciaVal());
+					}
+					elist.add(et);
+				}
+				return elist;
+			}
+		}
+		
+		return null;
+	}
+	
+	public InstanciaTasca findById(BigInteger id) {
+		Optional<InstanciaTasca> i = this.instanciaTascaRepository.findById(id);
+		if(i.isEmpty()) {
+			return null;
+		}
+		else {
+			return i.get();
+		}
+	}
+
+	public InstanciaTascaDTO findByIdDTO(BigInteger id) {
+		Optional<InstanciaTasca> i = this.instanciaTascaRepository.findById(id);
+		
+		if(i.isEmpty()) {
+			return null;
+		}
+		else {
+			return new InstanciaTascaDTO(i.get());
+		}
+	}
+	
+	public List<InstanciaTasca> getPastTasks(){
+		return this.instanciaTascaRepository.getPastTasks();
+	}
+		
+	public InstanciaTasca getNext(BigInteger idInstancia, Integer idProces, Integer idTascap) {
+		return this.instanciaTascaRepository.findNext(idInstancia, idProces, idTascap);
+	}
+	
+	public List<Usuari> getUsers(BigInteger idInstancia){
+		return this.instanciaTascaRepository.getUsers(idInstancia);
+	}
+	
+	public TascaInformeTransferDTO getLastByProcName(String nomProces, Integer titulacio, Integer centre, String ambit) {
+		return this.instanciaTascaRepository.getLastByProcName(nomProces, titulacio, centre, ambit);
+	}
+	
+	public void deactivateAll(BigInteger idInstancia) {
+		this.instanciaTascaRepository.deactivateAll(idInstancia);
+	}
+	
+	public void remove(BigInteger idInstanciaTasca) {
+		this.instanciaTascaRepository.remove(idInstanciaTasca);
+	}
+	
+	public void removeAllVersions(BigInteger idInstancia, Integer idTasca) {
+		this.instanciaTascaRepository.removeAllVersions(idInstancia, idTasca);
+	}
+	
+	public void save(InstanciaTasca i) {
+		this.instanciaTascaRepository.save(i);
+		this.instanciaTascaRepository.flush();
+	}
+}

+ 39 - 0
src/main/java/es/uv/saic/service/InstanciaTascaVerService.java

@@ -0,0 +1,39 @@
+package es.uv.saic.service;
+
+import java.math.BigInteger;
+import java.util.Date;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.InstanciaTascaVer;
+import es.uv.saic.shared.domain.InstanciaTascaVerRepository;
+
+@Service
+public class InstanciaTascaVerService {
+
+	@Autowired
+	private InstanciaTascaVerRepository itverr;
+
+    public InstanciaTascaVer findById(BigInteger id, Date dataMod){
+        return this.itverr.findById(id, dataMod);
+    }
+
+    public List<InstanciaTascaVer> findByIdInstanciaTasca(BigInteger id){
+        return this.itverr.findByIdInstanciaTasca(id);
+    }
+
+    public InstanciaTascaVer save(InstanciaTascaVer i){
+        return this.itverr.save(i);
+    }
+
+    public void delete(InstanciaTascaVer i){
+        this.itverr.delete(i);
+    }
+
+    public void deleteByIdInstancia(BigInteger idInstancia){
+        this.itverr.deleteByIdInstancia(idInstancia);
+    }
+ 
+}

+ 55 - 0
src/main/java/es/uv/saic/service/LinkService.java

@@ -0,0 +1,55 @@
+package es.uv.saic.service;
+
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Link;
+import es.uv.saic.shared.domain.LinkRepository;
+
+@Service
+public class LinkService {
+
+    @Autowired
+	private LinkRepository lr;
+
+    public List<Link> findAll() {
+		return this.lr.findAll();
+	}
+
+    public Link findById(Integer id){
+        Optional<Link> l = this.lr.findById(id);
+        if(!l.isEmpty()){
+            return l.get();
+        }
+        return null;
+    }
+
+    public Link findByToken(String token){
+        return this.lr.findByToken(token);
+    }
+
+    public List<Link> findByRuct(Integer ruct){
+        return this.lr.findByRuct(ruct);
+    }
+
+    public Link increase(Link l){
+        Integer visites = l.getVisites();
+        l.setVisites(visites == null ? 1 : visites+1);
+        return this.save(l);
+    }
+
+    public Link save(Link l){
+        Link lnew = this.lr.save(l);
+        this.lr.flush();
+        return lnew;
+    }
+
+    public void delete(Link l){
+        this.lr.delete(l);
+        this.lr.flush();
+    }
+
+}

+ 26 - 0
src/main/java/es/uv/saic/service/NoticiaService.java

@@ -0,0 +1,26 @@
+package es.uv.saic.service;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Noticia;
+import es.uv.saic.shared.domain.NoticiaRepository;
+
+@Service
+public class NoticiaService {
+
+	@Autowired
+	private NoticiaRepository r;
+	
+	
+	public List<Noticia> findAll() {
+		return this.r.findAll();
+	}
+	
+	public List<Noticia> findVisibles(){
+		return this.r.findVisibles();
+	}
+	
+}

+ 119 - 0
src/main/java/es/uv/saic/service/OrganService.java

@@ -0,0 +1,119 @@
+package es.uv.saic.service;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.OrganRepository;
+import es.uv.saic.shared.dto.OrganDTO;
+import es.uv.saic.shared.dto.OrganRepositoryDTO;
+
+
+@Service
+public class OrganService {
+
+	@Autowired
+	private OrganRepository organRepository;
+	
+	public Organ findByID(String tlugar, Integer lugar) {
+		return this.organRepository.findByTlugarLugar(tlugar, lugar);
+	}
+	
+	public Organ findByRuct(Integer ruct) {
+		return this.organRepository.findByRuct(ruct);
+	}
+
+	public OrganRepositoryDTO findByRuctDTO(Integer ruct) {
+		return this.organRepository.findByRuctDTO(ruct);
+	}
+
+	public boolean exists(String tlugar, Integer lugar){
+		List<Integer> o = this.organRepository.exists(tlugar, lugar);
+		if(o == null){
+			return false;
+		}
+		if(o.isEmpty()){
+			return false;
+		}
+		return true;
+	}
+	 
+	public List<Organ> findAll(){
+		return this.organRepository.findAll();
+	}
+	
+	public List<Organ> findCurrentCentres(){
+		return this.organRepository.findCurrentCentres();
+	}
+	
+	public List<Organ> findTitulacionsByCentre(List<Integer> centres){
+		List<Integer> titulation_ids = this.organRepository.findTitulacionsByCentre(centres);
+		List<Organ> titulations = new ArrayList<Organ>();
+		for(Integer id : titulation_ids) {
+			Organ o = this.organRepository.findSupervisableByTitulacio(id);
+			if(o != null) {
+				titulations.add(o);
+			}
+		}
+		return titulations;
+	}
+	
+	public List<Organ> getTitulacions(){
+		return this.organRepository.findAllTitulacions();
+	}
+	
+	public List<Organ> getCentres(){
+		return this.organRepository.findAllCentres();
+	}
+	
+	public List<Organ> getActiveCentres(){
+		return this.organRepository.findAllActiveCentres();
+	}
+	
+	public List<Organ> getTitulacionsByCentre(Integer centre){
+		return this.organRepository.findActiveTitulacionsByCentre(centre);
+	}
+	
+	public List<Organ> getUsuariTitulacions(String usuari){
+		return this.organRepository.findTitulacionsByUsuari(usuari);
+	}
+	
+	public List<Organ> getUsuariCentres(String usuari){
+		return this.organRepository.findCentresByUsuari(usuari);
+	}
+	
+	public List<Organ> getTitulacionsByCentres(List<Organ> centres){
+		List<Integer> c_list = new ArrayList<Integer>();
+		for(Organ o : centres) {
+			c_list.add(o.getId().getLugar());
+		}
+		return this.organRepository.findAllTitulacionsByCentres(c_list);
+	}
+	
+	public List<Organ> getTitulacionsByTypeCentre(Integer centre, Integer type){
+		return this.organRepository.findActiveTitulacionsByTypeCentre(centre, type);
+	}
+	
+	public List<Organ> findActiveTitulacionsByCentreTambit(Integer centre, String tambit){
+		return this.organRepository.findActiveTitulacionsByCentreTambit(centre, tambit);
+	}
+	
+	public List<Integer> getEquivalents(Integer lugar, String tlugar){
+		return this.organRepository.getEquivalents(lugar, tlugar);
+	}
+
+	public List<OrganDTO> findAllTitulacionsWithCentre() {
+
+		return this.organRepository.findAllTitulacionsWithCentre()
+		.stream().map(OrganDTO::new).collect(Collectors.toList());
+	}
+
+	public void save(Organ organ){
+		this.organRepository.save(organ);
+	}
+	
+}

+ 94 - 0
src/main/java/es/uv/saic/service/ProcesService.java

@@ -0,0 +1,94 @@
+package es.uv.saic.service;
+
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Proces;
+import es.uv.saic.shared.domain.ProcesRepository;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.domain.UsuarisRol;
+
+@Service
+public class ProcesService {
+
+	@Autowired
+	private ProcesRepository procesRepository;
+	
+	@Autowired
+	private UsuarisRolService urs;
+
+	public Proces findByID(Integer id) {
+		Optional<Proces> p = this.procesRepository.findById(id);
+		if(p.isPresent()) {
+			return p.get();
+		}
+		return null;
+	}
+	
+	public List<Proces> findByName(String n) {
+		return this.procesRepository.findByName(n);
+	}
+	
+	public List<Proces> findByNameCurs(String n, Integer c) {
+		return this.procesRepository.findByNameCurs(n, c);
+	}
+	
+	public List<Proces> getAll(){
+		return this.procesRepository.findAll();
+	} 
+	
+	public List<Integer> getSupervisableCursos(List<Integer> centres, List<Integer> titulacions){
+		return this.procesRepository.findCursosByCentreTitulacio(centres, titulacions);
+	}
+	
+	public List<Integer> getSupervisableProcedures(Usuari usuari, List<Integer> cursos, List<Integer> centres, List<Integer> titulacions){
+		
+		List<String> a = new ArrayList<String>();
+		for(UsuarisRol urol : urs.findActiveRols(usuari)) {
+			if(this.urs.hasAssociatedProcs(urol.getRol().getNomRol())) { // rols especials
+				a = this.urs.findAssociatedProcs(urol.getRol().getNomRol());
+			}
+		}
+		
+		if(a.size() == 0) {
+			List<Integer> p_list = this.procesRepository.findProcsByCentreTitulacioCurs(cursos, centres, titulacions);
+			if(p_list != null) {
+				return p_list;
+			}
+		}
+		else {
+			List<Integer> p_list = new ArrayList<Integer>();
+			for(String n : a) {
+				List<Integer> ps = this.procesRepository.findProcsByCentreTitulacioCursNom(cursos, centres, titulacions, n);
+				p_list.addAll(ps);
+			}
+			return p_list;
+		}
+		
+		return new ArrayList<Integer>();
+		
+	}
+	
+	public void save(Proces p) {
+		this.procesRepository.save(p);
+		this.procesRepository.flush();
+	}
+	
+	public void deleteById(Integer idProces) {
+		this.procesRepository.deleteById(idProces);
+	}
+	
+	public void delete(Proces proces) {
+		this.procesRepository.delete(proces);
+	}
+
+	public List<String> getFlowDiagram(Proces proces){
+		return this.procesRepository.getFlowDiagram(proces.getNomProces());
+	}
+	
+}

+ 29 - 0
src/main/java/es/uv/saic/service/RolService.java

@@ -0,0 +1,29 @@
+package es.uv.saic.service;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.RolRepository;
+import es.uv.saic.shared.domain.Rol;
+
+@Service
+public class RolService {
+	
+	@Autowired
+	private RolRepository rolRepository;
+	
+	public List<Rol> findAll(){
+		return this.rolRepository.findAll();
+	}
+	
+	public Rol findOne(Integer idRol) {
+		return this.rolRepository.getReferenceById(idRol);
+	}
+	
+	public List<Rol> findAssignables(){
+		return this.rolRepository.findAssignables();
+	}
+	
+}

+ 57 - 0
src/main/java/es/uv/saic/service/SysStatusService.java

@@ -0,0 +1,57 @@
+package es.uv.saic.service;
+
+import java.sql.Timestamp;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.SysStatus;
+import es.uv.saic.shared.domain.SysStatusRepository;
+
+@Service
+public class SysStatusService {
+
+	@Autowired
+	private SysStatusRepository r;
+	
+	public List<SysStatus> findAll(){
+		return this.r.findAll();
+	}
+	
+	public List<SysStatus> findByType(String t){
+		return this.r.findByType(t);
+	}
+	
+	public void log(String msg) {
+		SysStatus s = new SysStatus();
+		s.setTimestamp(new Timestamp(System.currentTimeMillis()));
+		s.setType("I");
+		s.setMsg(msg);
+		this.save(s);
+	}
+	
+	public void err(String msg, String err) {
+		SysStatus s = new SysStatus();
+		s.setTimestamp(new Timestamp(System.currentTimeMillis()));
+		s.setType("E");
+		s.setMsg(msg);
+		s.setErr(err);
+		this.save(s);
+	}
+	
+	public void warn(String msg, String warn) {
+		SysStatus s = new SysStatus();
+		s.setTimestamp(new Timestamp(System.currentTimeMillis()));
+		s.setType("W");
+		s.setMsg(msg);
+		s.setErr(warn);
+		this.save(s);
+	}
+	
+	public void save(SysStatus s) {
+		this.r.save(s);
+		this.r.flush();
+	}
+	
+}

+ 76 - 0
src/main/java/es/uv/saic/service/TascaService.java

@@ -0,0 +1,76 @@
+package es.uv.saic.service;
+
+import java.util.Date;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Tasca;
+import es.uv.saic.shared.domain.TascaRepository;
+import es.uv.saic.shared.dto.EvidenciaBuscadorDTO;
+
+@Service
+public class TascaService {
+
+	@Autowired
+	private TascaRepository r;
+	
+	
+	public List<Tasca> findAll() {
+		return this.r.findAll();
+	}
+	
+	public List<Tasca> findByProces(Integer idProces) {
+		return this.r.findByProces(idProces);
+	}
+	
+	public Tasca getByID(Integer id) {
+		Optional<Tasca> t = this.r.findById(id);
+		if(t.isPresent()) {
+			return t.get();
+		}
+		return null;
+	}
+	
+	public Tasca getByProcesTascap(Integer idProces, Integer idTascap) {
+		return this.r.findByProcesTascap(idProces, idTascap);
+	}
+	
+	public List<EvidenciaBuscadorDTO> getAllEvidencies(){
+		return this.r.findAllEvidencies();
+	}
+	
+	public List<EvidenciaBuscadorDTO> getEvidenciesByCentreTitulacioCurs(List<Integer> centres, List<Integer> titulacions, List<Integer> cursos){
+		return this.r.findEvidenciesByCentreTitulacioCurs(centres, titulacions, cursos);
+	}
+	
+	public List<EvidenciaBuscadorDTO> getEvidencesByProcedure(Integer idProcedure){
+		return this.r.getEvidencesByProcedure(idProcedure);
+	}
+	
+	public List<String> getAllAvailableEvidences(){
+		return this.r.getAllAvailableEvidences();
+	}
+	
+	public Integer create(Integer idTasca, Integer idProces, Integer idTascap, String titolCas, String descripcioCas, String titolVal, 
+    		String descripcioVal, Date dataLim, Integer tipus, Integer idTascaSeg, Integer idTascaSeg2, Integer idRol, String codiEvidencia) {
+		return this.r.create(idTasca, idProces, idTascap, titolCas, descripcioCas, titolVal, descripcioVal, dataLim, tipus, idTascaSeg, idTascaSeg2, idRol, codiEvidencia);
+	}
+	
+	public void save(Tasca i) {
+		this.r.save(i);
+		this.r.flush();
+	}
+	
+	public void deleteById(Integer idTasca) {
+		this.r.deleteById(idTasca);
+	}
+	
+	public void delete(Tasca tasca) {
+		this.r.delete(tasca);
+	}
+	
+	
+}

+ 25 - 0
src/main/java/es/uv/saic/service/TipusService.java

@@ -0,0 +1,25 @@
+package es.uv.saic.service;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Tipus;
+import es.uv.saic.shared.domain.TipusRepository;
+
+@Service
+public class TipusService {
+	
+	@Autowired
+	private TipusRepository tipusRepository;
+	
+	public List<Tipus> findAll(){
+		return this.tipusRepository.findAll();
+	}
+	
+	public Tipus findOne(Integer idTipus) {
+		return this.tipusRepository.getReferenceById(idTipus);
+	}
+	
+}

+ 108 - 0
src/main/java/es/uv/saic/service/UsuariService.java

@@ -0,0 +1,108 @@
+package es.uv.saic.service;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.domain.UsuariRepository;
+import es.uv.saic.shared.domain.UsuarisRol;
+import es.uv.saic.shared.dto.TascaAsignadaDTO;
+
+@Service
+public class UsuariService {
+
+	@Autowired
+	private UsuariRepository usuariRepository;
+	@Autowired
+	private UsuarisRolService usuarisRolService;
+	
+	public List<Usuari> findAll() {
+		return this.usuariRepository.findAll();
+	}
+	
+	public Usuari findByUsername(String usuari) {
+		usuari = usuari.split(",")[0];
+		Usuari u = this.usuariRepository.findByUsername(usuari);
+		u.setAdmin(this.usuarisRolService.isAdminUser(u));
+		u.setGranted(this.usuarisRolService.isGrantedUser(u));
+		return u;
+	}
+	
+	public boolean hasActiveRol(Usuari usuari) {
+		return this.usuarisRolService.hasActiveRol(usuari);
+	}
+	
+	public List<TascaAsignadaDTO> getActiveInstanciaTasques(Usuari usuari){
+		return this.usuariRepository.findActiveInstanciaTasques(usuari.getUsuari());
+	}
+	
+	public List<BigInteger> getActiveInstancies(Usuari usuari){
+		
+		if(usuari.isGranted() || usuarisRolService.findActiveRols(usuari).stream().anyMatch(r -> "adeit".equals(r.getRol().getNomRol()))) {
+			return this.usuariRepository.findActiveInstanciesGranted(usuari.getUsuari());
+		}
+		else{
+			return this.usuariRepository.findActiveInstancies(usuari.getUsuari());
+		}
+	}
+	
+	public List<BigInteger> getSupervisableProcessos(List<UsuarisRol> usuarisRols){
+		List<BigInteger> a = new ArrayList<BigInteger>();
+		for(UsuarisRol urol : usuarisRols) {
+			if(urol.getRol().getNomRol().equals("c_resp")) {  // Responsable de centro
+				a.addAll(this.usuariRepository.findSupervisableProcessosResp(urol.getOrgan().getId().getLugar()));
+			}
+			else if(urol.getRol().getNomRol().equals("c_supe")) { // Supervisor de centro
+				a.addAll(this.usuariRepository.findSupervisableProcessosResp(urol.getOrgan().getId().getLugar()));
+			}
+			else if(urol.getRol().getNomRol().equals("e_supe")) { // Supervisor externo
+				a.addAll(this.usuariRepository.findSupervisableProcessosResp(urol.getOrgan().getId().getLugar()));
+			}
+			else if(this.usuarisRolService.hasAssociatedProcs(urol.getRol().getNomRol())) { // Roles especiales (asociados a proceso)
+				a.addAll(this.usuariRepository.findSupervisableProcessosSupervisor(this.usuarisRolService.findAssociatedProcs(urol.getRol().getNomRol())));
+			}
+			else { // Cualquier otro rol
+				a.addAll(this.usuariRepository.findSupervisableProcessos(urol.getOrgan().getId().getLugar(), urol.getOrgan().getId().getTlugar()));
+			}
+		}
+		
+		return a.stream()
+				 .distinct()
+				 .collect(Collectors.toList());
+	}
+	
+	public List<Integer> getSupervisableCentres(Usuari usuari){	
+		List<Integer> a = new ArrayList<Integer>();
+		for(UsuarisRol urol : usuarisRolService.findActiveRols(usuari)) {
+			if(this.usuarisRolService.hasAssociatedProcs(urol.getRol().getNomRol())) { // special roles
+				a.addAll(this.usuariRepository.findSupervisableCentresSupervisor(this.usuarisRolService.findAssociatedProcs(urol.getRol().getNomRol())));
+			}
+			else {
+				a.addAll(this.usuariRepository.findSupervisableCentres(usuari.getUsuari()));
+			}
+		}
+		
+		return a.stream()
+				 .distinct()
+				 .collect(Collectors.toList());
+	}
+	
+	public List<Usuari> getInstanceAsignedUsers(BigInteger idInstancia){
+		return this.usuariRepository.getInstanceAsignedUsers(idInstancia);
+	}
+	
+	public List<Usuari> findByRolCentre(Integer idRol, List<Integer> centres){
+		return this.usuariRepository.findByRolCentre(idRol, centres);
+	}
+	
+	public void save(Usuari u) {
+		this.usuariRepository.save(u);
+		this.usuariRepository.flush();
+	}
+	
+}

+ 176 - 0
src/main/java/es/uv/saic/service/UsuarisRolService.java

@@ -0,0 +1,176 @@
+package es.uv.saic.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.domain.UsuarisRol;
+import es.uv.saic.shared.domain.UsuarisRolRepository;
+
+@Service
+public class UsuarisRolService {
+
+	@Autowired
+	private UsuarisRolRepository usuarisRolRepository;
+	
+	
+	public List<UsuarisRol> findAll() {
+		return this.usuarisRolRepository.findAll();
+	}
+	
+	public boolean exists(String usuari, String tlugar, Integer lugar) {
+		if(this.usuarisRolRepository.findActiveByUsuariTlugarLugar(usuari, tlugar, lugar) != null) {
+			return true;
+		}
+		return false;
+	}
+	
+	public UsuarisRol find(Integer idRol, String usuari, Integer lugar, String tlugar) {
+		return this.usuarisRolRepository.find(idRol, usuari, lugar, tlugar);
+	}
+	
+	public UsuarisRol findActive(Integer idRol, String usuari, Integer lugar, String tlugar) {
+		return this.usuarisRolRepository.findActive(idRol, usuari, lugar, tlugar);
+	}
+	
+	public UsuarisRol findLast(Integer idRol, String usuari, Integer lugar, String tlugar) {
+		return this.usuarisRolRepository.findLast(idRol, usuari, lugar, tlugar);
+	}
+	
+	public Integer findLastNum(Integer idRol, Integer lugar, String tlugar) {
+		return this.usuarisRolRepository.findLastNum(idRol, lugar, tlugar);
+	}
+	
+	public boolean hasActiveRol(Usuari usuari) {
+		List<UsuarisRol> rols = this.usuarisRolRepository.findByUsernameActive(usuari);
+		if(rols != null) {
+			if(rols.size() > 0) {
+				return true;
+			}
+		}
+		return false;
+	}
+	
+	public List<UsuarisRol> findActiveRols(Usuari usuari){
+		return this.usuarisRolRepository.findByUsernameActive(usuari);
+	}
+	
+	public boolean isGrantedUser(Usuari usuari) {
+		if(this.usuarisRolRepository.findByUsernameActiveRol(usuari, "u_uq") != null || 
+		   this.usuarisRolRepository.findByUsernameActiveRol(usuari, "u_admi") != null) {
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public boolean isGrantedSupervisor(Usuari usuari) {
+		if(this.usuarisRolRepository.findByUsernameActiveRol(usuari, "adeit") != null ||
+		   this.usuarisRolRepository.findByUsernameActiveRol(usuari, "relint") != null ||
+		   this.usuarisRolRepository.findByUsernameActiveRol(usuari, "u_supe") != null) {
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public boolean isAdminUser(Usuari usuari) {
+		if(this.usuarisRolRepository.findByUsernameActiveRol(usuari, "u_admi") != null) {
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public boolean isDataTestUser(Usuari usuari) {
+		if(this.usuarisRolRepository.findByUsernameActiveRol(usuari, "u_data") != null) {
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public boolean hasActiveRol(Usuari usuari, String rol) {
+		if(this.usuarisRolRepository.findByUsernameActiveRol(usuari, rol) != null) {
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public boolean hasActiveRoles(Usuari usuari, ArrayList<String> rols) {
+		if(this.usuarisRolRepository.findByUsernameActiveRoles(usuari, rols) != null) {
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public boolean hasActiveAmbit(Usuari usuari, String ambit) {
+		if(this.usuarisRolRepository.findByUsernameActiveAmbit(usuari.getUsuari(), ambit) != null) {
+			return true;
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public boolean hasAssociatedProcs(String rol) {
+		List<String> procs = this.usuarisRolRepository.findAssociatedProcs(rol);
+		if(procs != null) {
+			if(procs.size() > 0) {
+				return true;
+			}
+			else {
+				return false;
+			}
+		}
+		else {
+			return false;
+		}
+	}
+	
+	public List<String> findAssociatedProcs(String rol) {
+		return this.usuarisRolRepository.findAssociatedProcs(rol);
+	}
+	
+	public List<UsuarisRol> findManagerByCentre(Integer centre){
+		return this.usuarisRolRepository.findManagerByCentre(centre);
+	}
+	
+	public List<UsuarisRol> findManagerByTitulacio(Integer titulacio){
+		return this.usuarisRolRepository.findManagerByTitulacio(titulacio);
+	}
+	
+	public List<UsuarisRol> findManagerByCentres(List<Integer> centre){
+		return this.usuarisRolRepository.findManagerByCentres(centre);
+	}
+	
+	public List<UsuarisRol> findManagerByTitulacions(List<Integer> titulacio){
+		return this.usuarisRolRepository.findManagerByTitulacions(titulacio);
+	}
+	
+	public ArrayList<String> getSpecialRoles(){
+		return new ArrayList<String>(Arrays.asList("adeit", "relint"));
+	}
+	
+	public void save(UsuarisRol u) {
+		this.usuarisRolRepository.saveAndFlush(u);
+	}
+	
+	public void delete(UsuarisRol ur) {
+		this.usuarisRolRepository.delete(ur);
+	}
+	
+	
+}

+ 22 - 0
src/main/java/es/uv/saic/service/WikiService.java

@@ -0,0 +1,22 @@
+package es.uv.saic.service;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import es.uv.saic.shared.domain.Wiki;
+import es.uv.saic.shared.domain.WikiRepository;
+
+@Service
+public class WikiService {
+	
+	@Autowired
+	private WikiRepository wr;
+
+	public Wiki findByCategoria(String categoria) {
+		return this.wr.findByCategoria(categoria);
+	}
+	
+	public Wiki save(Wiki w) {
+		return this.wr.saveAndFlush(w);
+	}
+}

+ 844 - 0
src/main/java/es/uv/saic/web/AdminController.java

@@ -0,0 +1,844 @@
+package es.uv.saic.web;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.Paths;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Calendar;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.PutMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.Acreditacio;
+import es.uv.saic.shared.domain.AcreditacioTransfer;
+import es.uv.saic.shared.domain.Email;
+import es.uv.saic.shared.domain.EvidenciaIndicadorEnquesta;
+import es.uv.saic.shared.domain.EvidenciaIndicadorEnquestaPK;
+import es.uv.saic.shared.domain.Instancia;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.Proces;
+import es.uv.saic.shared.domain.Rol;
+import es.uv.saic.shared.domain.Tasca;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.domain.UsuarisRol;
+import es.uv.saic.shared.dto.ProcedureRequestDTO;
+import es.uv.saic.shared.dto.ProcesDTO;
+import es.uv.saic.shared.dto.TascaDTO;
+import es.uv.saic.shared.feign.IndicadorClient;
+import es.uv.saic.service.AcreditacioService;
+import es.uv.saic.service.EmailService;
+import es.uv.saic.service.InstanciaService;
+import es.uv.saic.service.InstanciaTascaService;
+import es.uv.saic.service.InstanciaTascaVerService;
+import es.uv.saic.service.OrganService;
+import es.uv.saic.service.ProcesService;
+import es.uv.saic.service.RolService;
+import es.uv.saic.service.TascaService;
+import es.uv.saic.service.TipusService;
+import es.uv.saic.service.UsuariService;
+import es.uv.saic.service.UsuarisRolService;
+import tools.jackson.databind.ObjectMapper;
+
+@RestController
+@RequestMapping("/admin")
+public class AdminController {
+
+	@Autowired
+	private OrganService os;
+	@Autowired
+	private ProcesService ps;
+	@Autowired
+	private InstanciaService is;
+	@Autowired
+	private InstanciaTascaService its;
+	@Autowired
+	private InstanciaTascaVerService itsver;
+	@Autowired
+	private UsuariService us;
+	@Autowired
+	private RolService rs;
+	@Autowired
+	private UsuarisRolService urs;
+	@Autowired
+	private TascaService ts;
+	@Autowired
+	private TipusService tps;
+	@Autowired
+	private EmailService es;
+	@Autowired
+	private AcreditacioService as;
+	@Value("${saic.data.filePath}")
+	private String filePath;
+	@Value("${saic.data.templates.filePath}")
+	private String templatePath;
+	@Autowired
+	private IndicadorClient ic;
+
+	public static final String DDMMYYYY = "dd/MM/yyyy";
+
+	public static final Map<Integer, String> acreditaGroups =
+		Map.of(
+			1, "ENERO",   
+			4, "ABRIL",   
+			9, "SEPTIEMBRE",
+			10, "NOVIEMBRE"      
+		);
+
+	@PostMapping("/instance")
+	public HashMap<String, Object> instantiate(@RequestParam("procedure") Integer idProces,
+			@RequestParam("center") Integer idCentre,
+			@RequestParam("titulation") Integer idTitulacio) {
+		HashMap<String, Object> map = new HashMap<>();
+		Proces p = ps.findByID(idProces);
+		List<String> errorCas = new ArrayList<String>();
+		List<String> errorVal = new ArrayList<String>();
+		List<String> successCas = new ArrayList<String>();
+		List<String> successVal = new ArrayList<String>();
+
+		if (p.getAmbit().equals("U")) { // Type U procedure
+			Organ centre = os.findByID("C", idCentre);
+			BigInteger idInstancia = is.instantiateU(idProces, idTitulacio);
+			if (!idInstancia.equals(BigInteger.valueOf(0))) {
+				InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+				for (Usuari u : us.getInstanceAsignedUsers(idInstancia)) {
+					Email email = new Email();
+					email.setUsuari(u);
+					email.setInstanciaTasca(activa);
+					es.addEmail(email);
+				}
+				successCas.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomCas());
+				successVal.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomVal());
+				map.put("success", true);
+			} else {
+				errorCas.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomCas());
+				errorVal.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomVal());
+				map.put("error", true);
+			}
+		} else if (idTitulacio > 2 && p.getAmbit().equals("T")) { // Type T procedure for one place
+			Organ o = os.findByID("T", idTitulacio);
+			BigInteger idInstancia = is.instantiateT(idProces, idTitulacio);
+			if (!idInstancia.equals(BigInteger.valueOf(0))) {
+				InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+				for (Usuari u : us.getInstanceAsignedUsers(idInstancia)) {
+					Email email = new Email();
+					email.setUsuari(u);
+					email.setInstanciaTasca(activa);
+					es.addEmail(email);
+				}
+				successCas.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomCas()
+						+ ", Titulació: " + o.getNomCas());
+				successVal.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomVal()
+						+ ", Titulació: " + o.getNomVal());
+				map.put("success", true);
+			} else {
+				errorCas.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomCas()
+						+ ", Titulació: " + o.getNomCas());
+				errorVal.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomVal()
+						+ ", Titulació: " + o.getNomVal());
+				map.put("error", true);
+			}
+		} else if (idTitulacio <= 2 && p.getAmbit().equals("T")) {
+			List<Organ> titulacions;
+			if (idTitulacio == 0) { // Type T procedure for all active places
+				titulacions = os.getTitulacionsByCentre(idCentre);
+			} else { // Type T procedure for one whole category place (masters/grados)
+				titulacions = os.getTitulacionsByTypeCentre(idCentre, idTitulacio);
+			}
+			for (Organ o : titulacions) {
+				BigInteger idInstancia = is.instantiateT(idProces, o.getId().getLugar());
+				if (!idInstancia.equals(BigInteger.valueOf(0))) {
+					InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+					for (Usuari u : us.getInstanceAsignedUsers(idInstancia)) {
+						Email email = new Email();
+						email.setUsuari(u);
+						email.setInstanciaTasca(activa);
+						es.addEmail(email);
+					}
+					successCas.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomCas()
+							+ ", Titulació: " + o.getNomCas());
+					successVal.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomVal()
+							+ ", Titulació: " + o.getNomVal());
+				} else {
+					errorCas.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomCas()
+							+ ", Titulació: " + o.getNomCas());
+					errorVal.add("Procediment: " + p.getNomProces() + ", Centre: " + o.getOrgan().getNomVal()
+							+ ", Titulació: " + o.getNomVal());
+				}
+			}
+		} else if (p.getAmbit().equals("C")) {
+			if (idCentre == 0) { // Type C procedure for all active places
+				for (Organ centre : os.getActiveCentres()) {
+					Organ titulacio = os.findByID("T", idTitulacio);
+					BigInteger idInstancia = is.instantiateC(idProces, centre.getId().getLugar(), idTitulacio);
+					if (!idInstancia.equals(BigInteger.valueOf(0))) {
+						InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+						for (Usuari u : us.getInstanceAsignedUsers(idInstancia)) {
+							Email email = new Email();
+							email.setUsuari(u);
+							email.setInstanciaTasca(activa);
+							es.addEmail(email);
+						}
+						successCas.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomCas()
+								+ ", Titulació: " + titulacio.getNomCas());
+						successVal.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomVal()
+								+ ", Titulació: " + titulacio.getNomVal());
+						map.put("success", true);
+					} else {
+						errorCas.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomCas()
+								+ ", Titulació: " + titulacio.getNomCas());
+						errorVal.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomVal()
+								+ ", Titulació: " + titulacio.getNomVal());
+						map.put("error", true);
+					}
+				}
+			} else if (idCentre > 0) { // Type C procedure for one place
+				Organ titulacio = os.findByID("T", idTitulacio);
+				Organ centre = os.findByID("C", idCentre);
+				BigInteger idInstancia = is.instantiateC(idProces, idCentre, idTitulacio);
+				if (!idInstancia.equals(BigInteger.valueOf(0))) {
+					InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+					for (Usuari u : us.getInstanceAsignedUsers(idInstancia)) {
+						Email email = new Email();
+						email.setUsuari(u);
+						email.setInstanciaTasca(activa);
+						es.addEmail(email);
+					}
+					successCas.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomCas()
+							+ ", Titulació: " + titulacio.getNomCas());
+					successVal.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomVal()
+							+ ", Titulació: " + titulacio.getNomVal());
+					map.put("success", true);
+				} else {
+					errorCas.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomCas()
+							+ ", Titulació: " + titulacio.getNomCas());
+					errorVal.add("Procediment: " + p.getNomProces() + ", Centre: " + centre.getNomVal()
+							+ ", Titulació: " + titulacio.getNomVal());
+					map.put("error", true);
+				}
+			}
+		}
+
+		map.put("successCas", successCas);
+		map.put("successVal", successVal);
+		map.put("errorCas", errorCas);
+		map.put("errorVal", errorVal);
+		if (errorCas.isEmpty()) {
+			map.put("error", true);
+		}
+		if (successCas.isEmpty()) {
+			map.put("success", true);
+		}
+
+		return map;
+	}
+
+	// POST que elimina una instancia concreta
+	@DeleteMapping("/instance")
+	public void deleteInstance(@RequestParam BigInteger idInstancia) throws IOException {
+
+		Instancia instancia = is.findByID(idInstancia);
+		List<InstanciaTasca> instanciaTasques = its.findByInstancia(instancia.getIdInstancia());
+		for (InstanciaTasca instanciaTasca : instanciaTasques) {
+			Files.deleteIfExists(Paths.get(this.filePath + instanciaTasca.getEvidencia()));
+		}
+		is.deleteInstance(instancia);
+	}
+
+	// POST que reinicia una instancia por completo
+	@PostMapping("/instance/clear")
+	public String clearInstance(@RequestParam BigInteger idInstancia) throws IOException {
+
+		Instancia instancia = is.findByID(idInstancia);
+		List<InstanciaTasca> instanciaTasques = its.findByInstancia(instancia.getIdInstancia());
+		for (InstanciaTasca instanciaTasca : instanciaTasques) {
+			Files.deleteIfExists(Paths.get(this.filePath + instanciaTasca.getEvidencia()));
+		}
+		is.clearInstance(instancia);
+		instancia.setEstat("A");
+		is.save(instancia);
+
+		this.itsver.deleteByIdInstancia(idInstancia);
+
+		InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+		if (activa.getTasca().getRol().getNomRol().equals("sys")) {
+			activa = this.its.system(activa);
+		}
+
+		for (Usuari u : us.getInstanceAsignedUsers(idInstancia)) {
+			Email email = new Email();
+			email.setUsuari(u);
+			email.setInstanciaTasca(activa);
+			es.addEmail(email);
+		}
+
+		return instancia.getIdInstancia().toString();
+	}
+
+	// POST que establece que una instancia ha sido cerrada, cambiandole su estado
+	@PostMapping("/instance/close")
+	public String closeInstance(@RequestParam BigInteger idInstancia) throws IOException {
+
+		Instancia instancia = is.findByID(idInstancia);
+		instancia.setEstat("C");
+		InstanciaTasca instanciaTasca = its.findActiveByInstancia(instancia.getIdInstancia());
+		instanciaTasca.setEstat(null);
+		its.save(instanciaTasca);
+		is.save(instancia);
+
+		return instancia.getIdInstancia().toString();
+	}
+
+	// POST que se encarga de eliminar una tarea concreta de una instancia
+	@DeleteMapping("/instance/task")
+	public String removeTask(@RequestParam BigInteger idInstanciaTasca) throws IOException {
+
+		InstanciaTasca instanciaTasca = its.findById(idInstanciaTasca);
+		Files.deleteIfExists(Paths.get(this.filePath + instanciaTasca.getEvidencia()));
+		its.remove(instanciaTasca.getIdInstanciaTasca());
+		return instanciaTasca.getInstancia().getIdInstancia().toString();
+	}
+
+	// POST que se encarga de reiniciar por completo una tarea de una instancia
+	@PostMapping("/instance/task/clear")
+	public String clearTask(@RequestParam BigInteger idInstanciaTasca) throws IOException {
+
+		InstanciaTasca instanciaTasca = its.findById(idInstanciaTasca);
+		Files.deleteIfExists(Paths.get(this.filePath + instanciaTasca.getEvidencia()));
+		instanciaTasca.setDataFet(null);
+		instanciaTasca.setEvidencia(null);
+		instanciaTasca.setUsuari(null);
+		instanciaTasca.setEstat(null);
+		instanciaTasca.setText(null);
+		if (instanciaTasca.getTasca().getRol().getNomRol().equals("sys")) {
+			instanciaTasca = this.its.system(instanciaTasca);
+		}
+		its.save(instanciaTasca);
+
+		return instanciaTasca.getInstancia().getIdInstancia().toString();
+	}
+
+	// POST que reinicia una tarea del procedimiento, eliminando datos anteriores y
+	// creando una nueva vesión de esta
+	@PostMapping("/instance/task/reload")
+	public String reloadTask(@RequestParam BigInteger idInstanciaTasca) throws IOException {
+
+		InstanciaTasca instanciaTasca = its.findById(idInstanciaTasca);
+		Files.deleteIfExists(Paths.get(this.filePath + instanciaTasca.getEvidencia()));
+		InstanciaTasca instanciaTascaNew = new InstanciaTasca();
+		instanciaTascaNew.setDataFet(null);
+		instanciaTascaNew.setEstat("A");
+		instanciaTascaNew.setEvidencia(null);
+		instanciaTascaNew.setInstancia(instanciaTasca.getInstancia());
+		instanciaTascaNew.setTasca(instanciaTasca.getTasca());
+		instanciaTascaNew.setUsuari(null);
+		instanciaTascaNew.setText(null);
+		instanciaTascaNew.setVersion(0);
+		instanciaTascaNew.setIdInstanciaTasca(instanciaTasca.getInstancia().getIdInstancia()
+				.multiply(BigInteger.valueOf(10000))
+				.add(BigInteger.valueOf(instanciaTasca.getTasca().getIdTascap()))
+				.multiply(BigInteger.valueOf(100)));
+
+		/* Editable online revisión - inyectar de quien corresponda */
+		if (instanciaTasca.getTasca().getTipus().getTipus() == 15) {
+			InstanciaTasca itdone = its.findByInstanciaTascap(instanciaTasca.getInstancia().getIdInstancia(),
+					Integer.parseInt(instanciaTasca.getTasca().getOpcions()));
+			instanciaTascaNew.setText(itdone.getText());
+		}
+
+		its.deactivateAll(instanciaTasca.getInstancia().getIdInstancia());
+		its.removeAllVersions(instanciaTasca.getInstancia().getIdInstancia(), instanciaTasca.getTasca().getIdTasca());
+
+		if (instanciaTascaNew.getTasca().getRol().getNomRol().equals("sys")) {
+			instanciaTascaNew = this.its.system(instanciaTascaNew);
+		}
+		its.save(instanciaTascaNew);
+
+		if (!instanciaTasca.getInstancia().getEstat().equals("A")) {
+			instanciaTasca.getInstancia().setEstat("A");
+			is.save(instanciaTasca.getInstancia());
+		}
+
+		for (Usuari u : us.getInstanceAsignedUsers(instanciaTascaNew.getInstancia().getIdInstancia())) {
+			Email email = new Email();
+			email.setUsuari(u);
+			email.setInstanciaTasca(instanciaTascaNew);
+			es.addEmail(email);
+		}
+
+		return instanciaTasca.getInstancia().getIdInstancia().toString();
+	}
+
+	// POST que tiene como objetivo reactivar una tarea
+	@PostMapping("/instance/task/reactivate")
+	public String activateTask(@RequestParam BigInteger idInstanciaTasca) throws IOException {
+
+		InstanciaTasca instanciaTasca = its.findById(idInstanciaTasca);
+		InstanciaTasca instanciaTascaNew = new InstanciaTasca();
+		instanciaTascaNew.setDataFet(null);
+		instanciaTascaNew.setEstat("A");
+		instanciaTascaNew.setEvidencia(null);
+		instanciaTascaNew.setInstancia(instanciaTasca.getInstancia());
+		instanciaTascaNew.setTasca(instanciaTasca.getTasca());
+		instanciaTascaNew.setUsuari(null);
+		instanciaTascaNew.setVersion(instanciaTasca.getVersion() + 1);
+		instanciaTascaNew.setIdInstanciaTasca(instanciaTasca.getIdInstanciaTasca().add(BigInteger.valueOf(1)));
+
+		its.deactivateAll(instanciaTasca.getInstancia().getIdInstancia());
+
+		/* Editable online revisión - inyectar de quien corresponda */
+		if (instanciaTasca.getTasca().getTipus().getTipus() == 15) {
+			instanciaTascaNew.setText(instanciaTasca.getText());
+		}
+
+		its.save(instanciaTascaNew);
+
+		if (!instanciaTasca.getInstancia().getEstat().equals("A")) {
+			instanciaTasca.getInstancia().setEstat("A");
+			is.save(instanciaTasca.getInstancia());
+		}
+
+		for (Usuari u : us.getInstanceAsignedUsers(instanciaTascaNew.getInstancia().getIdInstancia())) {
+			Email email = new Email();
+			email.setUsuari(u);
+			email.setInstanciaTasca(instanciaTascaNew);
+			es.addEmail(email);
+		}
+
+		return instanciaTasca.getInstancia().getIdInstancia().toString();
+	}
+
+	// POST que edita la información de una tarea ya existente
+	@PostMapping("/instance/task/edit")
+	public String editTask(@RequestParam BigInteger idInstanciaTasca) throws IOException {
+
+		InstanciaTasca instanciaTasca = its.findById(idInstanciaTasca);
+		InstanciaTasca instanciaTascaNew = new InstanciaTasca();
+		instanciaTascaNew.setDataFet(null);
+		instanciaTascaNew.setEstat("A");
+		instanciaTascaNew.setEvidencia(null);
+		instanciaTascaNew.setInstancia(instanciaTasca.getInstancia());
+		instanciaTascaNew.setTasca(instanciaTasca.getTasca());
+		instanciaTascaNew.setUsuari(null);
+		instanciaTascaNew.setVersion(instanciaTasca.getVersion() + 1);
+		instanciaTascaNew.setIdInstanciaTasca(instanciaTasca.getIdInstanciaTasca().add(BigInteger.valueOf(1)));
+		instanciaTascaNew.setText(instanciaTasca.getText());
+
+		its.deactivateAll(instanciaTasca.getInstancia().getIdInstancia());
+		its.save(instanciaTascaNew);
+
+		if (!instanciaTasca.getInstancia().getEstat().equals("A")) {
+			instanciaTasca.getInstancia().setEstat("A");
+			is.save(instanciaTasca.getInstancia());
+		}
+
+		for (Usuari u : us.getInstanceAsignedUsers(instanciaTascaNew.getInstancia().getIdInstancia())) {
+			Email email = new Email();
+			email.setUsuari(u);
+			email.setInstanciaTasca(instanciaTascaNew);
+			es.addEmail(email);
+		}
+
+		return instanciaTasca.getInstancia().getIdInstancia().toString();
+	}
+
+	@PostMapping("/mailing")
+	public void sendMails(@RequestParam Integer idRol, @RequestParam("centres[]") List<Integer> centres,
+			@RequestParam String subject, @RequestParam String body) {
+		List<Usuari> usuaris = us.findByRolCentre(idRol, centres);
+		for (Usuari usuari : usuaris) {
+			Email email = new Email();
+			email.setUsuari(usuari);
+			email.setBody(body);
+			email.setSubject(subject);
+			es.addEmail(email);
+		}
+	}
+
+	// PUT para la creación de un procedimiento nuevo
+	@PutMapping("/procedure")
+	public void newProcedure(@RequestBody ProcedureRequestDTO request) {
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DDMMYYYY);
+		ProcesDTO procesDTO = request.getProces();
+		List<TascaDTO> tasquesDTO = request.getTasques();
+
+		Proces p = new Proces();
+		p.setIdProces(procesDTO.getIdProces());
+		p.setNomProces(procesDTO.getNomProces());
+		p.setVersio(procesDTO.getVersio());
+		p.setComentaris(procesDTO.getComentaris());
+		p.setSubTitol(procesDTO.getSubTitol());
+		p.setAmbit(procesDTO.getAmbit());
+		p.setTitolCas(procesDTO.getTitolCas());
+		p.setTitolVal(procesDTO.getTitolVal());
+		p.setDescripcioCas(procesDTO.getDescripcioCas());
+		p.setDescripcioVal(procesDTO.getDescripcioVal());
+		p.setCursActivacio(procesDTO.getCursActivacio());
+		p.setCursAvaluat(procesDTO.getCursAvaluat());
+
+		ps.save(p);
+
+		if (tasquesDTO != null && !tasquesDTO.isEmpty()) {
+			for (TascaDTO tDto : tasquesDTO) {
+				Tasca t = new Tasca();
+				t.setIdTasca((p.getIdProces() * 10000) + tDto.getIdTascap());
+				t.setProces(p);
+				t.setIdTascap(tDto.getIdTascap());
+				t.setIdTascaSeg(tDto.getIdTascaSeg());
+				t.setIdTascaSeg2(tDto.getIdTascaSeg2());
+				t.setOpcions(tDto.getOpcions());
+				t.setTitolCas(limpiarTexto(tDto.getTitolCas()));
+				t.setDescripcioCas(limpiarTexto(tDto.getDescripcioCas()));
+				t.setTitolVal(limpiarTexto(tDto.getTitolVal()));
+				t.setDescripcioVal(limpiarTexto(tDto.getDescripcioVal()));
+				t.setNomEvidenciaCas(limpiarTexto(tDto.getNomEvidenciaCas()));
+				t.setNomEvidenciaVal(limpiarTexto(tDto.getNomEvidenciaVal()));
+				if (tDto.getDataLim() != null && !tDto.getDataLim().isEmpty()) {
+					t.setDataLim(LocalDate.parse(tDto.getDataLim(), formatter));
+				}
+				if (tDto.getIdTipus() != null) {
+					t.setTipus(tps.findOne(tDto.getIdTipus()));
+					
+					if (t.getTipus().getTipus() == 11 || t.getTipus().getTipus() == 15) {
+						// Si es plantilla, usamos el idPlantilla del DTO
+						t.setCodiEvidencia(tDto.getIdPlantilla() != null ? tDto.getIdPlantilla().toString() : "");
+					} else {
+						t.setCodiEvidencia(limpiarTexto(tDto.getCodiEvidencia()));
+					}
+				}
+
+				if (tDto.getIdRol() != null) {
+					t.setRol(rs.findOne(tDto.getIdRol()));
+				}
+            	t.setInforme(tDto.getInforme() != null ? tDto.getInforme() : false);
+
+				ts.save(t);
+			}
+		}
+	}
+
+	@PostMapping("/procedure")
+	public void editProcedure(@RequestBody ProcedureRequestDTO request) {
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DDMMYYYY);
+		ProcesDTO procesDTO = request.getProces();
+		List<TascaDTO> tasquesDTO = request.getTasques();
+
+		Proces p = ps.findByID(procesDTO.getIdProces());
+		p.setNomProces(procesDTO.getNomProces());
+		p.setVersio(procesDTO.getVersio());
+		p.setComentaris(procesDTO.getComentaris());
+		p.setSubTitol(procesDTO.getSubTitol());
+		p.setAmbit(procesDTO.getAmbit());
+		p.setTitolCas(procesDTO.getTitolCas());
+		p.setTitolVal(procesDTO.getTitolVal());
+		p.setDescripcioCas(procesDTO.getDescripcioCas());
+		p.setDescripcioVal(procesDTO.getDescripcioVal());
+		p.setCursActivacio(procesDTO.getCursActivacio());
+		p.setCursAvaluat(procesDTO.getCursAvaluat());
+		ps.save(p);
+
+		List<Tasca> tasquesActuals = ts.findByProces(p.getIdProces());
+		List<Integer> idsTasquesNoves = tasquesDTO.stream()
+				.map(TascaDTO::getIdTascap)
+				.toList();
+
+		for (Tasca t2 : tasquesActuals) {
+			if (!idsTasquesNoves.contains(t2.getIdTascap())) {
+				ts.delete(t2);
+			}
+		}
+
+		for (TascaDTO tDto : tasquesDTO) {
+			if (tDto.getIdTascaSeg() != null) {
+				int idCalculat = (int) (p.getIdProces() * 10000) + tDto.getIdTascap();
+				
+				Tasca tasca = ts.getByID(idCalculat);
+				if (tasca == null) {
+					tasca = new Tasca();
+					tasca.setIdTasca(idCalculat);
+					tasca.setProces(p);
+					tasca.setIdTascap(tDto.getIdTascap());
+				}
+				tasca.setIdTascaSeg(tDto.getIdTascaSeg());
+				tasca.setIdTascaSeg2(tDto.getIdTascaSeg2());
+				tasca.setOpcions(tDto.getOpcions());
+				tasca.setTitolCas(limpiarTexto(tDto.getTitolCas()));
+				tasca.setTitolVal(limpiarTexto(tDto.getTitolVal()));
+				tasca.setDescripcioCas(limpiarTexto(tDto.getDescripcioCas()));
+				tasca.setDescripcioVal(limpiarTexto(tDto.getDescripcioVal()));
+				tasca.setNomEvidenciaCas(limpiarTexto(tDto.getNomEvidenciaCas()));
+				tasca.setNomEvidenciaVal(limpiarTexto(tDto.getNomEvidenciaVal()));
+
+				if (tDto.getDataLim() != null && !tDto.getDataLim().isEmpty()) {
+					tasca.setDataLim(LocalDate.parse(tDto.getDataLim(), formatter));
+				}
+
+				if (tDto.getIdTipus() != null) {
+					tasca.setTipus(tps.findOne(tDto.getIdTipus()));
+				}
+
+				if (tasca.getTipus() != null && (tasca.getTipus().getTipus() == 11 || tasca.getTipus().getTipus() == 15)) {
+					tasca.setCodiEvidencia(tDto.getIdPlantilla() != null ? tDto.getIdPlantilla().toString() : "");
+				} else {
+					tasca.setCodiEvidencia(limpiarTexto(tDto.getCodiEvidencia()));
+				}
+
+				tasca.setRol(rs.findOne(tDto.getIdRol()));
+				tasca.setInforme(tDto.getInforme() != null ? tDto.getInforme() : false);
+				
+				ts.save(tasca);
+			}
+		}
+	}
+
+	private String limpiarTexto(String texto) {
+		if (texto == null || texto.equals(".")) {
+			return "";
+		}
+		return texto.replace("[comma]", ",");
+	}
+
+	// DELETE que elimina un procedimiento
+	@DeleteMapping("/procedure")
+	public void removeProcedure(@RequestParam("idProces") Integer idProces) {
+		Proces p = ps.findByID(idProces);
+		for (Tasca t : ts.findByProces(p.getIdProces())) {
+			ts.delete(t);
+		}
+		ps.delete(p);
+	}
+
+	// POST que le eliminar un usuario concreto del sitema
+	@DeleteMapping("/userrole")
+	public boolean removeUserrole(@RequestParam("idRol") Integer idRol,
+			@RequestParam("usuari") String usuari,
+			@RequestParam("lugar") Integer lugar, @RequestParam("tlugar") String tlugar) throws IOException {
+		
+		usuari = usuari.split(",")[0];
+
+		UsuarisRol u = urs.findActive(idRol, usuari, lugar, tlugar);
+		if (u != null) {
+			u.setVigent(false);
+			u.setFin(Calendar.getInstance().getTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+			urs.save(u);	
+		}
+
+		return urs.findActive(idRol, usuari, lugar, tlugar) == null ? true : false;
+	}
+
+	// PUT para añadir un nuevo usuario al sistema
+	@PutMapping("/userrole")
+	public boolean newUserrole(@RequestParam("idRol") Integer idRol,
+			@RequestParam(name = "usuari", required = false) String usuari, @RequestParam("centre") Integer idCentre,
+			@RequestParam(name = "titulacio", required = false) Integer idTitulacio,
+			@RequestParam Map<String, String> params) {
+		
+		usuari = usuari.split(",")[0];
+
+		if (usuari == null && params.get("username") != null) {
+			Usuari x = new Usuari();
+			x.setCognoms(params.get("lastname"));
+			x.setNom(params.get("firstname"));
+			x.setUsuari(params.get("username"));
+			x.setEmail(params.get("email"));
+			x.setLdap(true);
+			this.us.save(x);
+			usuari = x.getUsuari();
+		}
+
+		Rol rol = rs.findOne(idRol);
+		Integer lugar = null;
+		String tlugar = null;
+		if (rol.getAmbit().equals("U") || (rol.getAmbit().equals("C"))) {
+			lugar = idCentre;
+			tlugar = "C";
+			UsuarisRol u = urs.findLast(idRol, usuari, lugar, tlugar);
+			Integer num = urs.findLastNum(idRol, lugar, tlugar);
+			num = num == null ? 0 : num;
+			if (u != null) {
+				if (!u.getVigent()) {
+					UsuarisRol r = new UsuarisRol();
+					r.setFin(null);
+					r.setInici(Calendar.getInstance().getTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+					r.setUsuari(u.getUsuari());
+					r.setOrgan(u.getOrgan());
+					r.setRol(u.getRol());
+					r.setVigent(true);
+					r.setSupervisor(0);
+					r.setNum(num + 1);
+					urs.save(r);
+				}
+			} else {
+				UsuarisRol r = new UsuarisRol();
+				r.setFin(null);
+				r.setInici(Calendar.getInstance().getTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+				r.setUsuari(us.findByUsername(usuari));
+				r.setOrgan(os.findByID(tlugar, lugar));
+				r.setRol(rs.findOne(idRol));
+				r.setVigent(true);
+				r.setSupervisor(0);
+				r.setNum(num + 1);
+				urs.save(r);
+			}	
+		} else {
+			lugar = idTitulacio;
+			tlugar = "T";
+			UsuarisRol u = urs.findLast(idRol, usuari, idTitulacio, tlugar);
+			Integer num = urs.findLastNum(idRol, idTitulacio, tlugar);
+			num = num == null ? 0 : num;
+			if (u != null) {
+				if (!u.getVigent()) {
+					UsuarisRol r = new UsuarisRol();
+					r.setFin(null);
+					r.setInici(Calendar.getInstance().getTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+					r.setUsuari(u.getUsuari());
+					r.setOrgan(u.getOrgan());
+					r.setRol(u.getRol());
+					r.setVigent(true);
+					r.setSupervisor(0);
+					r.setNum(num + 1);
+					urs.save(r);
+				}
+			} else {
+				UsuarisRol r = new UsuarisRol();
+				r.setFin(null);
+				r.setInici(Calendar.getInstance().getTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+				r.setUsuari(us.findByUsername(usuari));
+				r.setOrgan(os.findByID(tlugar, idTitulacio));
+				r.setRol(rs.findOne(idRol));
+				r.setVigent(true);
+				r.setSupervisor(0);
+				r.setNum(num + 1);
+				urs.save(r);
+			}
+		};
+		return urs.findActive(idRol, usuari, lugar, tlugar) != null ? true : false;
+	}
+
+	// GET que recoge todas las plantillas actuales del sistema
+	@GetMapping("/templates")
+	public List<String> getTemplates2() {
+
+		File[] files = Stream.concat(Arrays.stream(new File(this.templatePath).listFiles()),
+				Arrays.stream(new File(this.templatePath + "/T1/").listFiles()))
+				.toArray(File[]::new);
+		files = Stream.concat(Arrays.stream(files),
+				Arrays.stream(new File(this.templatePath + "/T2/").listFiles()))
+				.distinct()
+				.toArray(File[]::new);
+
+		List<String> evs = new ArrayList<String>();
+		for (File f : files) {
+
+			String e = f.getName().replace(".docx", "");
+			if (f.getPath().contains("/T1/")) {
+				e = e + " (G)";
+			} else if (f.getPath().contains("/T2/")) {
+				e = e + " (M)";
+			}
+
+			if (!e.endsWith("_") && !e.contains(".old")) {
+				evs.add(e.replace("_", "."));
+			}
+
+		}
+
+		return evs;
+	}
+
+	// POST que redefine que indicadores están asociados a una evidencia dentro de
+	// un proceso
+	@PostMapping("/templates/inds/update")
+	public void updateTemplateInds(@RequestParam(name = "indicador[]", required = true) List<String> indicador,
+			@RequestParam(name = "enquesta[]", required = true) List<String> enquesta,
+			@RequestParam(name = "media[]", required = true) List<String> media,
+			@RequestParam(name = "evidencia", required = true) String evidencia,
+			@RequestParam(name = "proces", required = true) String proces) {
+
+		ic.deleteByProcesEnquesta(proces, evidencia);
+
+		for (Integer i = 0; i < indicador.size(); i++) {
+			EvidenciaIndicadorEnquesta e = new EvidenciaIndicadorEnquesta();
+			EvidenciaIndicadorEnquestaPK eid = new EvidenciaIndicadorEnquestaPK();
+			eid.setEnquesta(enquesta.get(i));
+			eid.setEvidencia(evidencia);
+			eid.setIndicador(indicador.get(i));
+			eid.setProces(proces);
+			e.setId(eid);
+			e.setMedia(media.get(i));
+			ic.save(e);
+		}
+	}
+
+	@GetMapping("/acredita/{curs}/{grup}/{tambit}")
+	public List<AcreditacioTransfer> acreditacionsByCurs(@PathVariable Integer curs, @PathVariable Integer grup,
+			@PathVariable String tambit) {
+
+		List<AcreditacioTransfer> alist = new ArrayList<AcreditacioTransfer>();
+
+		if (grup == 0) {
+			for (Acreditacio a : this.as.getByCurs(curs)) {
+				Organ o = os.findByID(a.getTlugar(), a.getLugar());
+				alist.add(new AcreditacioTransfer(a, o));
+			}
+			return alist;
+		} else {
+			if (tambit.equals("A")) {
+				for (Acreditacio a : this.as.getByCursGrup(curs, grup)) {
+					Organ o = os.findByID(a.getTlugar(), a.getLugar());
+					alist.add(new AcreditacioTransfer(a, o));
+				}
+			} else {
+				for (Acreditacio a : this.as.getByCursGrupTambit(curs, grup, tambit)) {
+					Organ o = os.findByID(a.getTlugar(), a.getLugar());
+					alist.add(new AcreditacioTransfer(a, o));
+				}
+			}
+			return alist;
+		}
+	}
+
+	@PostMapping("/acredita")
+	public Acreditacio UpdateAcreditacio(@RequestParam String tlugar, @RequestParam Integer lugar,
+			@RequestParam Integer grupCurs, @RequestParam Integer grupNum, @RequestParam Integer cursImpla,
+			@RequestParam String dataAcred, @RequestParam String dataRenov, @RequestParam String dataSegui,
+			@RequestParam String dataVerif, @RequestParam Boolean recom, @RequestParam Boolean segui)
+			throws ParseException {
+
+		Acreditacio a = this.as.getById(tlugar, lugar);
+		a.setGrupCurs(grupCurs);
+		a.setGrupNum(grupNum);
+		a.setGrup(acreditaGroups.get(grupNum));
+		a.setDataAcred(new SimpleDateFormat(DDMMYYYY).parse(dataAcred).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
+		a.setDataRenov(new SimpleDateFormat(DDMMYYYY).parse(dataRenov).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
+		a.setDataSegui(new SimpleDateFormat(DDMMYYYY).parse(dataSegui).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
+		a.setDataVerif(new SimpleDateFormat(DDMMYYYY).parse(dataVerif).toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
+		a.setCursImpla(cursImpla);
+		a.setRecom(recom);
+		a.setSegui(segui);
+		a = as.save(a);
+
+		return a;
+	}
+}

+ 253 - 0
src/main/java/es/uv/saic/web/CalendarController.java

@@ -0,0 +1,253 @@
+package es.uv.saic.web;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.text.ParseException;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.Calendari;
+import es.uv.saic.shared.domain.Email;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.Proces;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.service.CalendariService;
+import es.uv.saic.service.EmailService;
+import es.uv.saic.service.InstanciaService;
+import es.uv.saic.service.InstanciaTascaService;
+import es.uv.saic.service.OrganService;
+import es.uv.saic.service.ProcesService;
+import es.uv.saic.service.UsuariService;
+
+@RestController
+@RequestMapping("/calendar")
+public class CalendarController {
+    @Autowired
+	private OrganService os;
+	@Autowired
+	private UsuariService us;
+	@Autowired
+	private ProcesService ps;
+	@Autowired
+	private CalendariService cs;
+	@Autowired
+	private InstanciaService is;
+	@Autowired
+	private InstanciaTascaService its;
+	@Autowired
+	private EmailService es;
+	
+	// POST para añadir un evento al calendario
+	@ResponseBody
+	@PostMapping
+	public Calendari calendarAddEvent(@RequestParam("idProces") Integer idProces, 
+			@RequestParam("titulacions") List<String> titulacions, @RequestParam("centres") List<String> centres, 
+			@RequestParam("data") String data, @RequestParam("instancia") Optional<Integer> instancia) throws IOException, ParseException {	
+		
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
+		LocalDate d = LocalDate.parse(data, formatter);
+		String dstr = d.toString();
+		Proces p = this.ps.findByID(idProces);
+		Calendari c = new Calendari();
+		c.setIdProces(p.getIdProces());
+		c.setData(d);
+		c.setDataStr(dstr);
+		c.setTitolCas(p.getNomProces() + " " + p.getTitolCas());
+		c.setTitolVal(p.getNomProces() + " " + p.getTitolVal());
+		c.setAmbit(p.getAmbit());
+		c.setTipus(1);
+		c.setColor(p.getAmbit().equals("C")  ? "aquamarine" : "dodgerblue");
+		c.setStatus("P");
+		if(instancia.isPresent()) {
+			Calendari cold = cs.findById(instancia.get());
+			c.setCentres(cold.getCentres());
+			c.setTitulacions(cold.getTitulacions());
+		}
+		else {
+			c.setCentres(centres.toString());
+			c.setTitulacions(titulacions.toString());
+		}
+		
+		c = cs.save(c);
+		
+		return c;
+	}
+	
+	// POST que actualiza el evento ddel calendario
+	@ResponseBody
+	@PostMapping("/{id}")
+	public Calendari calendarAddEvent(@PathVariable("id") Integer id, 
+			@RequestParam("idProces") Integer idProces, @RequestParam("titulacions") List<String> titulacions, 
+			@RequestParam("centres") List<String> centres, @RequestParam("data") String data) throws IOException, ParseException {	
+		
+		Calendari c = cs.findById(id);
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd/MM/yyyy");
+		LocalDate d = LocalDate.parse(data, formatter);
+		String dstr = d.toString();
+		Proces p = this.ps.findByID(idProces);
+		c.setIdProces(p.getIdProces());
+		c.setData(d);
+		c.setDataStr(dstr);
+		c.setCentres(centres.toString());
+		c.setTitulacions(titulacions.toString());
+		c.setTitolCas(p.getNomProces() + " " + p.getTitolCas());
+		c.setTitolVal(p.getNomProces() + " " + p.getTitolVal());
+		c.setAmbit(p.getAmbit());
+		c.setColor(p.getAmbit().equals("C") ? "aquamarine" : "dodgerblue");
+		c.setStatus("P");
+		
+		c = cs.save(c);
+		
+		return c;
+	}
+	
+	// GET que recoge todos los eventos del calendario
+	@ResponseBody
+	@GetMapping
+	public List<Calendari> calendarGetEvents() throws IOException, ParseException {	
+		return cs.getAll();
+	}
+	
+	// DELETE que elimina un evento del calendario
+	@ResponseBody
+	@DeleteMapping
+	public Integer calendarDeleteEvent(@RequestParam("id") Integer id) throws IOException, ParseException {	
+		Calendari c = this.cs.findById(id);
+		this.cs.delete(c);
+		return id;
+	}
+	
+
+	// POST para instanciar un proceso desde el calendario
+	@ResponseBody
+	@PostMapping("/instantiate")
+	public List<String> instantiate(@RequestParam("id") Integer id,
+			@RequestParam("idProces") Integer idProces, @RequestParam("centres") List<Integer> centres,
+			@RequestParam("titulacions") List<Integer> titulacions, @RequestParam String usuari) throws IOException, ParseException {	
+		
+        Usuari u = us.findByUsername(usuari);
+        if(!u.isAdmin()) {
+			return null;
+		}
+		
+		Calendari cal = cs.findById(id);
+		if(!cal.getIdProces().equals(idProces) || !cal.getCentres().equals(centres.toString()) 
+				|| !cal.getTitulacions().equals(titulacions.toString())) {
+			cal.setCentres(centres.toString());
+			cal.setTitulacions(titulacions.toString());
+			cal.setIdProces(idProces);
+			cs.save(cal);
+		}
+		
+		List<String> log = new ArrayList<String>();
+		boolean hasErrors = false;
+		
+		Proces pr = ps.findByID(idProces);
+		
+		if(pr.getAmbit().equals("U")) {  // ambit == universitat (U)
+			for(Integer idTitulacio : titulacions) {
+				BigInteger idInstancia = is.instantiateU(idProces, idTitulacio);
+				if(!idInstancia.equals(new BigInteger("0"))) {
+					log.add("[OK] "+idInstancia.toString()+" - "+groupedTitToText(idTitulacio));
+					InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+					for(Usuari usr : us.getInstanceAsignedUsers(idInstancia)) {
+						Email email = new Email();
+						email.setUsuari(usr);
+						email.setInstanciaTasca(activa);
+						es.addEmail(email);
+					}
+				}
+				else {
+					log.add("[ERROR] "+groupedTitToText(idTitulacio));
+					hasErrors = true;
+				}		
+			}
+		}
+		else if(pr.getAmbit().equals("C")) { // ambit == centre (C)
+			for(Integer idCentre : centres) {
+				Organ c = os.findByID("C", idCentre);
+				for(Integer idTitulacio : titulacions) {
+					BigInteger idInstancia = is.instantiateC(idProces, c.getId().getLugar(), idTitulacio);
+					if(!idInstancia.equals(new BigInteger("0"))) {
+						log.add("[OK] "+idInstancia.toString()+" - "+c.getNomCas() + " -> " + groupedTitToText(idTitulacio));
+						InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+						for(Usuari usr : us.getInstanceAsignedUsers(idInstancia)) {
+							Email email = new Email();
+							email.setUsuari(usr);
+							email.setInstanciaTasca(activa);
+							es.addEmail(email);
+						}
+					}
+					else {
+						hasErrors = true;
+						log.add("[ERROR] "+c.getNomCas() + " -> " + groupedTitToText(idTitulacio));
+					}
+				}
+			}
+		}
+		else if(pr.getAmbit().equals("T")) { // ambit == titulacio (T)
+			for(Integer idTitulacio : titulacions) {
+				Organ t = os.findByID("T", idTitulacio);
+				BigInteger idInstancia = is.instantiateT(idProces, t.getId().getLugar());
+				if(!idInstancia.equals(new BigInteger("0"))) {
+					log.add("[OK] "+idInstancia.toString()+" - "+t.getNomCas());
+					InstanciaTasca activa = its.findActiveByInstancia(idInstancia);
+					for(Usuari usr : us.getInstanceAsignedUsers(idInstancia)) {
+						Email email = new Email();
+						email.setUsuari(usr);
+						email.setInstanciaTasca(activa);
+						es.addEmail(email);
+					}
+				}
+				else {
+					hasErrors = true;
+					log.add("[ERROR] "+idInstancia.toString()+" - "+t.getNomCas());
+				}
+			}
+		}
+		
+		if(hasErrors) {
+			cal.setStatus("E");
+			cal.setColor("indianred");
+			cal.setData(LocalDate.now());
+			cs.save(cal);
+		}
+		else {
+			cal.setStatus("F");
+			cal.setColor("lightgreen");
+			cal.setData(LocalDate.now());
+			cs.save(cal);
+		}
+		
+		return log;
+	}
+	
+	private String groupedTitToText(Integer t) {
+		if(t == 1) {
+			return "Grados";
+		}
+		else if(t == 2) {
+			return "Masters";
+		}
+		else if(t == 3) {
+			return "Doctorados";
+		}
+		else {
+			return "Todas Titulaciones";
+		}
+	}
+}

+ 789 - 0
src/main/java/es/uv/saic/web/DashboardController.java

@@ -0,0 +1,789 @@
+package es.uv.saic.web;
+
+import java.io.IOException;
+import java.text.CharacterIterator;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.text.StringCharacterIterator;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.UUID;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import javax.xml.parsers.ParserConfigurationException;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import es.uv.saic.shared.domain.Categoria;
+import es.uv.saic.shared.domain.CursoValor;
+import es.uv.saic.shared.domain.Document;
+import es.uv.saic.shared.domain.Grafica;
+import es.uv.saic.shared.domain.Indicador;
+import es.uv.saic.shared.domain.Informe;
+import es.uv.saic.shared.domain.InformeProcessos;
+import es.uv.saic.shared.domain.Instancia;
+import es.uv.saic.shared.domain.Link;
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.domain.UsuarisRol;
+import es.uv.saic.shared.dto.AnyDimensioDTO;
+import es.uv.saic.shared.dto.ArchiveOrganDTO;
+import es.uv.saic.shared.dto.CategoriaDTO;
+import es.uv.saic.shared.dto.CategoriaDocumentDTO;
+import es.uv.saic.shared.dto.DimensioInstanciesDTO;
+import es.uv.saic.shared.dto.DocumentDTO;
+import es.uv.saic.shared.dto.DocumentTmpDTO;
+import es.uv.saic.shared.dto.IndicadorDTO;
+import es.uv.saic.shared.dto.IndicadorDTOimp;
+import es.uv.saic.shared.dto.InstanciaDTO;
+import es.uv.saic.shared.dto.InstanciaGanttDTO;
+import es.uv.saic.shared.dto.InstanciaGanttDTOImp;
+import es.uv.saic.shared.dto.OrganDTO;
+import es.uv.saic.shared.dto.TreeDTOAny;
+import es.uv.saic.shared.dto.TreeDTODimensio;
+import es.uv.saic.shared.dto.TreeDTOInstancia;
+import es.uv.saic.shared.dto.TreeDTOOrgan;
+import es.uv.saic.shared.dto.UsuarisRolDTO;
+import es.uv.saic.shared.feign.DocumentClient;
+import es.uv.saic.shared.feign.IndicadorClient;
+import es.uv.saic.service.CategoriaService;
+import es.uv.saic.service.GraficaService;
+import es.uv.saic.service.InformeService;
+import es.uv.saic.service.InstanciaService;
+import es.uv.saic.service.LinkService;
+import es.uv.saic.service.OrganService;
+import es.uv.saic.service.UsuariService;
+import es.uv.saic.service.UsuarisRolService;
+
+@RestController
+@RequestMapping("/dashboard")
+public class DashboardController {
+
+	@Autowired 
+	private OrganService os;
+	@Autowired
+	private UsuarisRolService urs;
+
+	@Autowired
+	private UsuariService us;
+	@Autowired
+	private InformeService infs;
+	@Autowired
+	private InstanciaService is;
+	@Autowired
+	private CategoriaService cs;
+	@Autowired
+	private GraficaService gs;
+	@Autowired
+	private LinkService ls;
+
+	@Value("${saic.data.filePath}")
+	private String filePath;
+
+	@Autowired
+	private DocumentClient dc;
+	@Autowired
+	private IndicadorClient ic;
+
+	@Value("${saic.url.public}")
+	private String publicUrl;
+
+	@Value("${saic.data.templates.fileNotFound}")
+	private String fileNotFound;
+	
+	// POST para cargar el dashboard con toda su información
+	@PostMapping
+	@ResponseBody
+	public HashMap<String, Object> getOrganList(@RequestBody String usuari) {
+		HashMap<String, Object> model = new HashMap<>();
+		List<Organ> titulacions = new ArrayList<Organ>();
+		List<Organ> centres = new ArrayList<Organ>();
+		Usuari u = us.findByUsername(usuari);
+		boolean showCentres = false;
+		
+		if(urs.isGrantedUser(u)){
+			Organ o = os.findByID("C", 0);
+			model.put("organ", o.getRuct());
+			return model;
+		}		
+		else if(urs.isGrantedSupervisor(u)) {
+			titulacions = os.getTitulacions().stream().filter(o -> o.getOrgan().getId().getLugar() != 99).collect(Collectors.toList());
+		}
+		else {
+			List<UsuarisRol> rols = this.urs.findActiveRols(u);
+			for(UsuarisRol rol : rols) {
+				if(rol.getOrgan().getId().getTlugar().equals("C")) {
+					titulacions.addAll(this.os.findTitulacionsByCentre(Arrays.asList(rol.getOrgan().getId().getLugar())));
+					centres.add(rol.getOrgan());
+					showCentres = true;
+				}
+				else if(rol.getOrgan().getId().getTlugar().equals("T")) {
+					titulacions.add(rol.getOrgan());
+				}
+			}
+			
+			titulacions.sort(Comparator.comparing(a -> a.getNomCas()));
+			titulacions = titulacions.stream()
+									 .distinct()
+									 .collect(Collectors.toList());
+			centres = centres.stream()
+							 .distinct()
+							 .collect(Collectors.toList());
+		}
+		if(centres.size() == 1) {
+			model.put("organ", centres.get(0).getRuct());
+			return model;
+		}
+		else if(titulacions.size() == 1) {
+			model.put("organ", titulacions.get(0).getRuct());
+			return model;
+		}
+		else {
+			model.put("showCentres", showCentres);
+			model.put("titulacions", titulacions.stream().map(OrganDTO::new).toList());
+			model.put("organ", 0);
+			return model;
+		}
+	}
+	
+	//POST para mostrar el dashboard a partir de un numero ruct
+	@PostMapping("/{ruct}")
+	@ResponseBody
+	public HashMap<String, Object> getDashboardOrgan(@PathVariable Integer ruct, @RequestBody String usuari) {
+		HashMap<String, Object> model = new HashMap<>();
+		Organ o = os.findByRuct(ruct);
+		Usuari u = us.findByUsername(usuari);
+		o.setCodis();
+
+		if(!this.isSuitable(o, u)) {
+			model.put("redirect", "401");
+				return model;
+		}
+		
+		if(o.getTambit().equals("C") && o.getId().getTlugar().equals("C") && o.getId().getLugar() == 0) {
+			List<Organ> tits = os.getTitulacions();
+			tits.forEach( (t) -> t.setCodis() );
+			model.put("titulacions", tits.stream().map(OrganDTO::new).toList());
+			this.loadManagers(model, o.getId().getLugar(), o.getId().getLugar());
+		}
+		else if(o.getTambit().equals("C")){
+			List<Organ> tits = this.os.getTitulacionsByCentre(o.getId().getLugar());
+			model.put("titulacions", tits.stream().map(OrganDTO::new).toList());
+			this.loadManagers(model, o.getId().getLugar(), o.getId().getLugar());	
+		}
+		else {
+			this.loadManagers(model, o.getOrgan().getId().getLugar(), o.getId().getLugar());
+		}
+		
+		List<Organ> centers = os.getActiveCentres();
+		model.put("centers", centers.stream().map(OrganDTO::new).toList());
+
+		List<Categoria> c = cs.findFirstLevel(o.getId().getTlugar());
+		model.put("organ", new OrganDTO(o));
+		model.put("categories", c);		
+		model.put("editable", (u.isAdmin() || (u.isGranted())));
+		model.put("showCentres", false);
+		if(o.getTambit().equals("C") && o.getId().getTlugar().equals("C") && o.getId().getLugar() == 0) {
+			model.put("showCentres", true);
+			c = cs.findFirstLevelAndU(o.getId().getTlugar());
+			model.put("categories", c);
+			model.put("redirect", "U");
+			return model;
+		}
+		else if(o.getTambit().equals("C")) {
+			model.put("redirect", "C");
+			return model;
+		}
+		model.put("redirect", "T");
+		return model;
+	}
+	
+	// GET para conseguir todos los procedimiento a partir del idTitulacio
+	@PostMapping("/procedures/{idTitulacio}")
+	@ResponseBody
+	public List<?> loadReports(@PathVariable Integer idTitulacio) {
+		String locale = LocaleContextHolder.getLocale().getLanguage();
+		Organ o = os.findByRuct(idTitulacio);
+		List<Informe> informes = this.infs.findByGrupWebTambit("D", o.getTambit());
+		List<AnyDimensioDTO> treeByTitulacio = new ArrayList<AnyDimensioDTO>();
+		List<TreeDTOAny> treeByCentre = new ArrayList<TreeDTOAny>();
+		
+		int maxYear = Year.now().getValue();
+		int minYear = maxYear-5;
+		LocalDate currentdate = LocalDate.now();
+		if(currentdate.getMonthValue() >= 9){ // Si cambiamos de curso académico incrementamos el año
+			maxYear+=1;
+		}
+
+		for(int i=maxYear; i>=minYear; i--) {
+			
+			AnyDimensioDTO treeAnyT = new AnyDimensioDTO();
+			TreeDTOAny treeAnyC = new TreeDTOAny();
+			if(o.getTambit().equals("C")){
+				treeAnyC.setText(Integer.toString(i-1)+" - "+Integer.toString(i));
+				treeAnyC.setChildren(new ArrayList<TreeDTODimensio>());
+			}
+			else{
+				treeAnyT.setText(Integer.toString(i-1)+" - "+Integer.toString(i));
+				treeAnyT.setChildren(new ArrayList<DimensioInstanciesDTO>());
+			}
+			
+			for(Informe dim : informes) {
+				List<String> processos = new ArrayList<String>();
+				for(InformeProcessos ip : dim.getProcessos()) {
+					processos.add(ip.getNomProces());
+				}
+				// Crear Dimensión
+				List<TreeDTOOrgan> treeOrgans = new ArrayList<TreeDTOOrgan>();
+				
+				List<Instancia> instanciesT = new ArrayList<Instancia>();
+				List<Instancia> instanciesC = new ArrayList<Instancia>();
+				List<Instancia> instanciesC0 = new ArrayList<Instancia>();
+				List<Instancia> instanciesU = new ArrayList<Instancia>();
+				if(o.getTambit().equals("C")) {
+					instanciesC = is.findByOrganCursNom(o.getId().getTlugar(), o.getId().getLugar(), 
+														o.getId().getLugar(), 1, i, processos);
+					instanciesC.addAll(is.findByOrganCursNom(o.getId().getTlugar(), o.getId().getLugar(), 
+															 o.getId().getLugar(), 2, i, processos));
+					instanciesC.addAll(is.findByOrganCursNom(o.getId().getTlugar(), o.getId().getLugar(), 
+															 o.getId().getLugar(), 3, i, processos));
+					instanciesC0 = is.findByOrganCursNom(o.getId().getTlugar(), o.getId().getLugar(), 
+														 o.getId().getLugar(), 0, i, processos);
+
+					instanciesU = is.findByOrganCursNom("C", 0, 0, 0, i, processos);
+
+					instanciesC.addAll(instanciesC0);
+
+					TreeDTOOrgan treeOrgC = new TreeDTOOrgan();
+					TreeDTOOrgan treeOrgU = new TreeDTOOrgan();
+					treeOrgC.setText("Centro");
+					treeOrgC.setChildren(instanciesC.stream().map(x -> new TreeDTOInstancia(x)).collect(Collectors.toList()));
+					treeOrgU.setText("Universidad");
+					treeOrgU.setChildren(instanciesU.stream().map(x -> new TreeDTOInstancia(x)).collect(Collectors.toList()));
+
+					treeOrgans.add(treeOrgU);
+					treeOrgans.add(treeOrgC);
+
+					List<Organ> orgChilds = os.findTitulacionsByCentre(Stream.of(o.getId().getLugar()).toList());
+					Collections.sort(orgChilds, new Comparator<Organ>(){
+						public int compare(Organ s1, Organ s2) {
+							return s1.getNomCas().compareToIgnoreCase(s2.getNomCas());
+						}
+					});
+
+					for(Organ tit : orgChilds){
+						instanciesT = is.findByOrganCursNom(tit.getId().getTlugar(), tit.getId().getLugar(), 
+														    tit.getOrgan().getId().getLugar(), 
+														    tit.getId().getLugar(), i, processos);
+
+						if(instanciesT.size() > 0){
+							TreeDTOOrgan treeOrg = new TreeDTOOrgan();
+							treeOrg.setText(tit.getNomCas());
+							treeOrg.setChildren(instanciesT.stream().map(x -> new TreeDTOInstancia(x)).collect(Collectors.toList()));
+							treeOrgans.add(treeOrg);
+						}
+					}
+				}
+				else {
+					instanciesT = is.findByOrganCursNom(o.getId().getTlugar(), o.getId().getLugar(), 
+														o.getOrgan().getId().getLugar(), 
+														o.getId().getLugar(), i, processos);
+					Integer g = 1;
+					if(o.getTambit().equals("M")) {
+						g = 2;
+					}
+					else if(o.getTambit().equals("D")) {
+						g = 3;
+					}
+					instanciesC = is.findByOrganCursNom(o.getOrgan().getId().getTlugar(), 
+														o.getOrgan().getId().getLugar(), 
+														o.getOrgan().getId().getLugar(), 
+														g, i, processos);
+					instanciesC0 = is.findByOrganCursNom(o.getOrgan().getId().getTlugar(), 
+														 o.getOrgan().getId().getLugar(), 
+														 o.getOrgan().getId().getLugar(), 
+														 0, i, processos);
+				}
+
+				if(o.getTambit().equals("C")){
+					TreeDTODimensio treeDim = new TreeDTODimensio();
+					treeDim.setText("["+dim.getNom()+"]  "+(locale.equals("es") ? dim.getTitolCas() : dim.getTitolVal()));
+					treeDim.setChildren(treeOrgans);
+					treeAnyC.getChildren().add(treeDim);
+				}
+				else {
+					instanciesT.addAll(instanciesC);
+					instanciesT.addAll(instanciesC0);
+					List<InstanciaDTO> ints = instanciesT.stream().map(x -> new InstanciaDTO(x)).collect(Collectors.toList());
+					DimensioInstanciesDTO dimension = new DimensioInstanciesDTO();
+					dimension.setText("["+dim.getNom()+"]  "+(locale.equals("es") ? dim.getTitolCas() : dim.getTitolVal()));
+					dimension.setChildren(ints);
+					treeAnyT.getChildren().add(dimension);
+				}
+
+				
+			}
+			if(o.getTambit().equals("C")){
+				treeByCentre.add(treeAnyC);
+			}
+			else {
+				treeByTitulacio.add(treeAnyT);
+			}
+			
+		}
+		if(o.getTambit().equals("C")){
+			return treeByCentre;
+		}
+		else {
+			return treeByTitulacio;
+		}
+	}
+	
+	// GET para conseguir la documentación ya a aportada a partir del id de la tituación
+	@GetMapping("/documents/{idTitulacio}")
+	@ResponseBody
+	public List<CategoriaDocumentDTO> loadDocuments(@PathVariable Integer idTitulacio) {
+		String locale = LocaleContextHolder.getLocale().getLanguage();
+		Organ o = os.findByRuct(idTitulacio);
+		List<Categoria> parents = cs.findFirstLevelAndU(o.getId().getTlugar());
+		List<CategoriaDocumentDTO> data = new ArrayList<CategoriaDocumentDTO>();
+		for(Categoria c : parents) {
+			CategoriaDocumentDTO cat = new CategoriaDocumentDTO();
+			cat.setText(locale.equals("es") ? c.getNomCas() : c.getNomVal());
+			cat.setChildren(new ArrayList<DocumentDTO>());
+			
+			List<Categoria> catChilds = cs.findByPareTambitAndU(c.getIdCategoria(), o.getTambit());
+			for(Categoria cc : catChilds) {
+				Document doc = findByCategoriaOrgan(cc.getIdCategoria(), o.getId().getLugar(), o.getId().getTlugar());
+				if(doc != null) {
+					DocumentDTO docdto = new DocumentDTO(locale.equals("es") ? cc.getNomCas() : cc.getNomVal(), 
+															doc.getIdDocument().toString(),
+															String.format("%td-%<tm-%<tY", doc.getData()) + "(" + this.getSize(doc.getRuta()) + ")"
+														);
+					cat.getChildren().add(docdto);
+				}
+			}
+			if(cat.getChildren().size() == 0) {
+				DocumentDTO docdto = new DocumentDTO();
+				docdto.setText(locale.equals("es") ? "No hay documentos subidos en esta categoría" : "No hi ha documents pujats en aquesta categoria");
+				docdto.setRuta("");
+				cat.getChildren().add(docdto);
+			}
+			data.add(cat);
+		}
+				
+		return data;
+	}
+
+	// GET para conseguir el diagrama de gantt a partir del ruct
+	@GetMapping("/gantt/{ruct}")
+	@ResponseBody
+	public List<InstanciaGanttDTOImp> loadGantt(@PathVariable Integer ruct) {
+		Organ o = os.findByRuct(ruct);
+		List<Integer> ambits = Arrays.asList(1, 2, 3, 0);
+		if(!o.getTambit().equals("C")) {
+			if(o.getTambit().equals("G")) {
+				ambits = Arrays.asList(1, 0);
+			}
+			else if(o.getTambit().equals("M")) {
+				ambits = Arrays.asList(2, 0);
+			}
+			else {
+				ambits = Arrays.asList(3, 0);
+			}
+		}
+		
+		int currentYear = Year.now().getValue();
+		List<InstanciaGanttDTO> data = new ArrayList<InstanciaGanttDTO>();
+		List<InstanciaGanttDTO> instancies = new ArrayList<InstanciaGanttDTO>();
+		
+		if(o.getTambit().equals("C")) {
+			instancies = this.is.findByOrganBetweenCurs("C", o.getId().getLugar(), o.getId().getLugar(), ambits, currentYear-5, currentYear);
+		}
+		else {
+			instancies = this.is.findByOrganBetweenCurs("T", o.getId().getLugar(), o.getOrgan().getId().getLugar(), Arrays.asList(o.getId().getLugar()), currentYear-5, currentYear);
+			data.addAll(instancies);
+			instancies = this.is.findByOrganBetweenCurs("C", o.getOrgan().getId().getLugar(), o.getOrgan().getId().getLugar(), ambits, currentYear-5, currentYear);
+		}
+		data.addAll(instancies);
+		
+		//TODO Esto peta
+		Collections.sort(data, new Comparator<InstanciaGanttDTO>() {  
+			@Override  
+			public int compare(InstanciaGanttDTO a, InstanciaGanttDTO b) {  
+				DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
+
+				LocalDateTime datea = LocalDateTime.parse(a.getStart(), f);
+				LocalDateTime dateb = LocalDateTime.parse(b.getStart(), f);
+				boolean isBefore = datea.isBefore(dateb);
+				return isBefore ? -1 : 1;
+			}  
+		});
+						
+		return data.stream().map(InstanciaGanttDTOImp::new).toList();
+	}
+		
+	@GetMapping("/documents/cats/{idCategoria}/{tambit}")
+	@ResponseBody
+	public List<Categoria> getDocumentChildCats(@PathVariable Integer idCategoria, @PathVariable String tambit) {
+		return cs.findByPareTambit(idCategoria, tambit);
+	}
+
+	@GetMapping("/documents/catsu/{idCategoria}/{tambit}")
+	@ResponseBody
+	public List<Categoria> getDocumentChildCatsU(@PathVariable Integer idCategoria, @PathVariable String tambit) {
+		return cs.findByPareTambitAndU(idCategoria, tambit);
+	}
+	
+	// POST para guardar un documento en el sistema
+	@PostMapping("/documents")
+	@ResponseBody
+	public void uploadDocument(@RequestParam MultipartFile file, @RequestParam Integer idCategoria, 
+							   @RequestParam Integer lugar, @RequestParam String tlugar) throws IllegalStateException, IOException {
+		
+		Categoria cat = this.cs.findById(idCategoria);
+		Organ org = this.os.findByID(tlugar, lugar);
+		Document doc = findByCategoriaOrgan(idCategoria, lugar, tlugar);
+		if(doc != null) {
+			this.archive(doc);
+		}
+		doc = new Document();
+		doc.setCategoria(cat);
+		doc.setOrgan(org);
+		doc.setData(new Date(System.currentTimeMillis()));
+		doc.setNom(cat.getNomCas());
+		doc.setVisible(true);
+		doc = save(doc);
+		String path = upload(doc.getIdDocument(), cat.getIdCategoria(), lugar, tlugar, file);
+		doc.setRuta(path);
+		save(doc);
+	}
+
+	// POST para añadir un documento a un centro cocncreto
+	@PostMapping("/documents/archive")
+	@ResponseBody
+	public void archiveDocuments(Model model, @RequestParam Integer lugar, @RequestParam String tlugar) {
+		ArchiveOrganDTO organ = new ArchiveOrganDTO(lugar, tlugar);
+		dc.archiveByOrgan(organ);
+	}
+
+	// GET para conseguir todos los graficos a partir de un RUCT
+	@GetMapping("/graphs/list/{ruct}")
+	@ResponseBody
+	public List<Grafica> getGraphDataList(@PathVariable Integer ruct) throws ParserConfigurationException {
+		Organ o = os.findByRuct(ruct);
+		if(o.getId().getTlugar().equals("C")) {
+			return this.gs.findLikeAmbit("%"+o.getId().getTlugar()+"%");
+		}
+		else {
+			return this.gs.findLikeAmbitAndEstudi("%"+o.getId().getTlugar()+"%", o.getTambit());
+		}	
+	}
+	
+	//GET para conseguir todos los datos concretos de tasas para las tablas
+	@GetMapping("/graphs/rates/{lugar}")
+	@ResponseBody
+	public List<Indicador> getGraphDataTaxes(@PathVariable Integer lugar) throws ParserConfigurationException {
+		return this.ic.getGraphData(lugar);
+	}
+
+	//GET para conseguir todos los datos concretos de cada gráfico
+	@GetMapping("/graphs/inds/{ruct}/{tambit}")
+	@ResponseBody
+	public List<Indicador> getGraphDataByRuctTambit(@PathVariable Integer ruct, @PathVariable String tambit) throws ParserConfigurationException {
+		Organ o = os.findByRuct(ruct);
+		int year = Year.now().getValue();
+		List<IndicadorDTOimp> data = new ArrayList<IndicadorDTOimp>();
+
+		if(o.getTambit().equals("C")) {
+			Integer centre = o.getId().getLugar();
+			data = this.ic.getGraphData(centre, tambit, year-10);
+		}
+		else {
+			List<Integer> tits = this.os.getEquivalents(o.getId().getLugar(), "T");
+			tits.add(o.getId().getLugar());
+			Integer centre = o.getOrgan().getId().getLugar();
+			data = this.ic.getGraphData(tits, centre,	o.getTambit(), year-10);
+		}
+		List<Indicador> inds = new ArrayList<Indicador>();
+		Indicador in = new Indicador();
+		String inAux = null;
+		Integer count = 0;
+		for(IndicadorDTOimp i : data) {
+			count++;
+			if(inAux == null) { 
+				inAux = i.getDimensio()+"_"+i.getIndicador()+"_"+i.getAmbit();
+				in.setIndicador(i.getIndicador());
+				in.setDimension(i.getDimensio());
+				in.setAmbit(i.getAmbit());
+				in.getValores().add(new CursoValor(Integer.toString(Integer.parseInt(i.getCurs())-1)+"-"+i.getCurs(), i.getValor()));
+			}
+			else if(inAux.equals(i.getDimensio()+"_"+i.getIndicador()+"_"+i.getAmbit())) {
+				in.getValores().add(new CursoValor(Integer.toString(Integer.parseInt(i.getCurs())-1)+"-"+i.getCurs(), i.getValor()));
+			}
+			else {
+				inds.add(in);
+				inAux = i.getDimensio()+"_"+i.getIndicador()+"_"+i.getAmbit();
+				in = new Indicador();
+				in.setIndicador(i.getIndicador());
+				in.setDimension(i.getDimensio());
+				in.setAmbit(i.getAmbit());
+				in.getValores().add(new CursoValor(Integer.toString(Integer.parseInt(i.getCurs())-1)+"-"+i.getCurs(), i.getValor()));
+			}
+			
+			if(count == data.size()) {
+				inds.add(in);
+			}
+		}
+		return inds;
+	}
+
+	@GetMapping("/graphs/inds/{ruct}")
+	@ResponseBody
+	public List<Indicador> getGraphDataByRuct(@PathVariable Integer ruct) throws ParserConfigurationException {
+		return getGraphDataByRuctTambit(ruct, null);
+	}
+
+	@GetMapping("/links/{ruct}")
+	@ResponseBody
+	public List<Link> getLinks(@PathVariable Integer ruct) {
+		List<Link> links = this.ls.findByRuct(ruct);
+		for(Link l : links){
+			l.setLink(publicUrl+l.getLink());
+		}
+		return this.ls.findByRuct(ruct);
+	}
+
+	@PostMapping("/links/{ruct}")
+	@ResponseBody
+	public Link createLink(@PathVariable Integer ruct, @RequestParam String dataExp) throws ParseException {
+		
+		String token = UUID.randomUUID().toString();
+		SimpleDateFormat formatter = new SimpleDateFormat("dd/MM/yyyy");
+		Date date = formatter.parse(dataExp);
+
+		Link l = new Link();
+		l.setRuct(ruct);
+		l.setData(new Date(System.currentTimeMillis()));
+		l.setDataExp(date);
+		l.setToken(token);
+		l.setLink("/shared/"+token);
+		l.setVisites(0);
+		this.ls.save(l);
+
+		return l;
+	}
+
+	@DeleteMapping("/links/{id}")
+	@ResponseBody
+	public Integer deleteLink(@PathVariable Integer id) {
+		Link l = this.ls.findById(id);
+		if(l != null){
+			this.ls.delete(l);
+			return id;
+		}
+		return 0;
+	}
+	
+	// Función encargada de cargar a los managers a partir del centro y titulación
+	public void loadManagers(Model model, Integer centre, Integer titulacio) {
+		List<Integer> centres = Arrays.asList(centre);
+		List<Integer> titulacions = Arrays.asList(titulacio);
+		List<UsuarisRol> resp_centre = urs.findManagerByCentres(centres);
+		Collections.sort(resp_centre, new Comparator<UsuarisRol>() {  
+		    @Override  
+		    public int compare(UsuarisRol a, UsuarisRol b) {  
+		          
+		        int OrgnomComp = a.getOrgan().getNomVal().compareTo(b.getOrgan().getNomVal()); 
+		        if (OrgnomComp != 0) {  
+		            return OrgnomComp;  
+		        }  
+		        int rolComp = a.getRol().getNomRol().compareTo(b.getRol().getNomRol());
+		        if (rolComp != 0) {  
+		            return rolComp;  
+		        }
+		        return a.getUsuari().getNom().concat(a.getUsuari().getCognoms()).compareTo(
+		        		b.getUsuari().getNom().concat(b.getUsuari().getCognoms()));  
+		    }  
+		});
+		
+		List<UsuarisRol> resp_titulacio = new ArrayList<UsuarisRol>();
+		if(titulacions.size() > 0) {
+			resp_titulacio = urs.findManagerByTitulacions(titulacions);
+			Collections.sort(resp_titulacio, new Comparator<UsuarisRol>() {  
+			    @Override 
+			    public int compare(UsuarisRol a, UsuarisRol b) {  
+			          
+			        int OrgnomComp = a.getOrgan().getNomVal().compareTo(b.getOrgan().getNomVal()); 
+			        if (OrgnomComp != 0) {  
+			            return OrgnomComp;  
+			        }  
+			        int rolComp = a.getRol().getNomRol().compareTo(b.getRol().getNomRol());
+			        if (rolComp != 0) {  
+			            return rolComp;  
+			        }
+			        return a.getUsuari().getNom().concat(a.getUsuari().getCognoms()).compareTo(
+			        		b.getUsuari().getNom().concat(b.getUsuari().getCognoms()));  
+			    }  
+			});
+		}
+						
+		if(resp_centre == null) {
+			model.addAttribute("results", false);
+		}
+		else{
+			model.addAttribute("results", true);
+			model.addAttribute("resp_centres", resp_centre.stream().map(UsuarisRolDTO::new).toList());
+			model.addAttribute("resp_titulacions", resp_titulacio.stream().map(UsuarisRolDTO::new).toList());
+		}
+	}
+	
+	// Función para comprobar si un usario esta permitido para hacer una acción
+	public boolean isSuitable(Organ o, Usuari u) {
+		if(u.isAdmin() || u.isGranted()) {
+			return true;
+		}
+		else if(this.urs.exists(u.getUsuari(), o.getId().getTlugar(), o.getId().getLugar())) {
+			return true;
+		}
+		
+		List<Organ> titulacions = new ArrayList<Organ>();
+		List<Organ> centres = new ArrayList<Organ>();
+		List<UsuarisRol> rols = this.urs.findActiveRols(u);
+		for(UsuarisRol rol : rols) {
+			if(rol.getOrgan().getId().getTlugar().equals("C")) {
+				titulacions.addAll(this.os.findTitulacionsByCentre(Arrays.asList(rol.getOrgan().getId().getLugar())));
+				centres.add(rol.getOrgan());
+			}
+			else if(rol.getOrgan().getId().getTlugar().equals("T")) {
+				titulacions.add(rol.getOrgan());
+			}
+		}
+		
+		if(centres.contains(o) || titulacions.contains(o)) {
+			return true;
+		}
+		
+		return false;
+	}
+
+	// Función para saber el tamaño de un documento concreto
+	private String getSize(String path){
+		FileSystemResource f = new FileSystemResource(path); 
+		if(f.exists() && f.isFile()) {
+			return this.bytesToHuman(f.getFile().length());
+		}
+		else {
+			return "0 KiB";
+		}
+	}
+	
+	private String bytesToHuman(long bytes) {
+	    long absB = bytes == Long.MIN_VALUE ? Long.MAX_VALUE : Math.abs(bytes);
+	    if (absB < 1024) {
+	        return bytes + " B";
+	    }
+	    long value = absB;
+	    CharacterIterator ci = new StringCharacterIterator("KMGTPE");
+	    for (int i = 40; i >= 0 && absB > 0xfffccccccccccccL >> i; i -= 10) {
+	        value >>= 10;
+	        ci.next();
+	    }
+	    value *= Long.signum(bytes);
+	    return String.format("%.1f %ciB", value / 1024.0, ci.current());
+	}
+	
+	private Document findByCategoriaOrgan(Integer idCategoria, Integer lugar, String tlugar) {
+		CategoriaDTO categoriaDTO = new CategoriaDTO(idCategoria, lugar, tlugar);
+		return dc.findByCategoriaOrgan(categoriaDTO);
+	}
+
+	private Document save(Document doc) {
+		return dc.save(doc);
+	}
+
+	private Document archive(Document doc){
+		return dc.archive(doc);
+	}
+
+	private String upload(Integer idDocument, Integer idCategoria, Integer lugar, String tlugar, MultipartFile file) {
+			DocumentTmpDTO doc = new DocumentTmpDTO(idDocument, idCategoria, lugar, tlugar, file);
+			return dc.upload(doc);
+	}
+
+	public void loadManagers(HashMap<String, Object> model, Integer centre, Integer titulacio) {
+		List<Integer> centres = Arrays.asList(centre);
+		List<Integer> titulacions = Arrays.asList(titulacio);
+		List<UsuarisRol> resp_centre = urs.findManagerByCentres(centres);
+		Collections.sort(resp_centre, new Comparator<UsuarisRol>() {  
+		    @Override  
+		    public int compare(UsuarisRol a, UsuarisRol b) {  
+		          
+		        int OrgnomComp = a.getOrgan().getNomVal().compareTo(b.getOrgan().getNomVal()); 
+		        if (OrgnomComp != 0) {  
+		            return OrgnomComp;  
+		        }  
+		        int rolComp = a.getRol().getNomRol().compareTo(b.getRol().getNomRol());
+		        if (rolComp != 0) {  
+		            return rolComp;  
+		        }
+		        return a.getUsuari().getNom().concat(a.getUsuari().getCognoms()).compareTo(
+		        		b.getUsuari().getNom().concat(b.getUsuari().getCognoms()));  
+		    }  
+		});
+		
+		List<UsuarisRol> resp_titulacio = new ArrayList<UsuarisRol>();
+		if(titulacions.size() > 0) {
+			resp_titulacio = urs.findManagerByTitulacions(titulacions);
+			Collections.sort(resp_titulacio, new Comparator<UsuarisRol>() {  
+			    @Override 
+			    public int compare(UsuarisRol a, UsuarisRol b) {  
+			          
+			        int OrgnomComp = a.getOrgan().getNomVal().compareTo(b.getOrgan().getNomVal()); 
+			        if (OrgnomComp != 0) {  
+			            return OrgnomComp;  
+			        }  
+			        int rolComp = a.getRol().getNomRol().compareTo(b.getRol().getNomRol());
+			        if (rolComp != 0) {  
+			            return rolComp;  
+			        }
+			        return a.getUsuari().getNom().concat(a.getUsuari().getCognoms()).compareTo(
+			        		b.getUsuari().getNom().concat(b.getUsuari().getCognoms()));  
+			    }  
+			});
+		}
+						
+		if(resp_centre == null) {
+			model.put("results", false);
+		}
+		else{
+			model.put("results", true);
+			model.put("resp_centres", resp_centre.stream().map(UsuarisRolDTO::new).toList());
+			model.put("resp_titulacions", resp_titulacio.stream().map(UsuarisRolDTO::new).toList());
+		}
+	}
+}

+ 42 - 0
src/main/java/es/uv/saic/web/EmailController.java

@@ -0,0 +1,42 @@
+package es.uv.saic.web;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.Email;
+import es.uv.saic.shared.dto.EmailDTO;
+import es.uv.saic.service.EmailService;
+import es.uv.saic.service.InstanciaTascaService;
+import es.uv.saic.service.UsuariService;
+import jakarta.mail.MessagingException;
+
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@RestController
+@RequestMapping("/email")
+public class EmailController {
+    @Autowired
+    EmailService es;
+    @Autowired
+    UsuariService us;
+    @Autowired
+    InstanciaTascaService its;
+
+    @PostMapping("/send")
+    public void sendEmail(@RequestBody EmailDTO email) {
+        try {
+            es.sendMail(email.getTo(), email.getSubject(), email.getMessage());
+        } catch (MessagingException e) {
+            e.printStackTrace();
+        }
+
+    }
+
+    @PostMapping("/add")
+    public void addEmail(@RequestBody EmailDTO email) {
+        es.addEmail(new Email(us.findByUsername(email.getUsuari()), its.findById(email.getIdInstanciaTasca()),
+            email.getTo(), email.getSubject(), email.getMessage()));
+    }
+}

+ 214 - 0
src/main/java/es/uv/saic/web/ManagersController.java

@@ -0,0 +1,214 @@
+package es.uv.saic.web;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.Rol;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.domain.UsuarisRol;
+import es.uv.saic.shared.dto.OrganDTO;
+import es.uv.saic.shared.dto.RolDTO;
+import es.uv.saic.shared.dto.UsuariDTO;
+import es.uv.saic.shared.dto.UsuarisRolDTO;
+import es.uv.saic.service.OrganService;
+import es.uv.saic.service.RolService;
+import es.uv.saic.service.UsuariService;
+import es.uv.saic.service.UsuarisRolService;
+
+// Controller for the managers administration page 
+@RestController
+@RequestMapping("/manager")
+public class ManagersController {
+	
+	@Autowired
+	private UsuarisRolService urs;
+	@Autowired
+	private OrganService ors;
+	@Autowired
+	private UsuariService us;
+	@Autowired
+	private RolService rs;
+	
+	/*
+	 * Load the managers administration page data into the model
+	 * - List of centres and titulacions the logged user can manage
+	 * - List of all users
+	 * - List of assignable roles
+	 * - If there are search results in the session, load them too
+	 * - If there is a roleExists attribute in the session, load it too
+	 * - The view is managers.html
+	 */
+	@PostMapping("/form")
+	public HashMap<String, Object> managersForm(@RequestBody String usuari) {
+		HashMap<String, Object> model = new HashMap<>();
+		Usuari u = us.findByUsername(usuari);
+		
+		List<OrganDTO> sup_centres = new ArrayList<>();
+		List<OrganDTO> sup_titulacions = new ArrayList<>();
+		List<Organ> centres;
+		List<Organ> titulacions;
+		if(u.isGranted()){
+			centres = this.ors.getCentres();
+			titulacions = this.ors.getTitulacions();
+		}
+		else {
+			centres = this.ors.getUsuariCentres(usuari);
+			if(centres != null) {
+				if(centres.size() > 0) {
+					titulacions = this.ors.getTitulacionsByCentres(centres);
+				}
+				else {
+					titulacions = this.ors.getUsuariTitulacions(usuari);
+					for(Organ o : titulacions) {
+						centres.add(o.getOrgan());
+					}
+				}
+			}
+			else {
+				titulacions = this.ors.getUsuariTitulacions(usuari);
+			}
+		}
+
+		for(Organ o : centres) {
+			sup_centres.add(new OrganDTO(o));
+		}
+
+		for(Organ o : titulacions) {
+			sup_titulacions.add(new OrganDTO(o));
+		}
+
+		model.put("sup_centres", sup_centres);
+		model.put("sup_titulacions", sup_titulacions);
+		
+		model.putAll(managers(null));
+	
+		return model;
+	}
+	
+	/*
+	 * Process the search form submission and load the results into the model
+	 * @Param centres List of centre ids to search managers in
+	 * @Param titulacions List of titulacion ids to search managers in
+	 * @Return The view component list_managers
+	 */
+	@PostMapping("/search")
+	public HashMap<String, Object> managersSearch(@RequestParam("center") List<Integer> centres,
+			@RequestParam(name="titulation[]", required=false) List<Integer> titulacions) {
+		HashMap<String, Object> map = this.loadManagers(centres, titulacions);
+		
+		return map;
+	}
+	
+	/*
+	 * Load the search results into the model
+	 * @Param model The model to load the data into
+	 * @Param centres List of centre ids to search managers in
+	 * @Param titulacions List of titulacion ids to search managers in
+	 */
+	public HashMap<String, Object> loadManagers(List<Integer> centres, List<Integer> titulacions) {
+		HashMap<String, Object> model = new HashMap<>();
+		List<UsuarisRol> centre = urs.findManagerByCentres(centres);
+		Collections.sort(centre, new Comparator<UsuarisRol>() {  
+		    @Override  
+		    public int compare(UsuarisRol a, UsuarisRol b) {  
+		          
+		        int OrgnomComp = a.getOrgan().getNomVal().compareTo(b.getOrgan().getNomVal()); 
+		        if (OrgnomComp != 0) {  
+		            return OrgnomComp;  
+		        }  
+		        int rolComp = a.getRol().getNomRol().compareTo(b.getRol().getNomRol());
+		        if (rolComp != 0) {  
+		            return rolComp;  
+		        }
+		        return a.getUsuari().getNom().concat(a.getUsuari().getCognoms()).compareTo(
+		        		b.getUsuari().getNom().concat(b.getUsuari().getCognoms()));  
+		    }  
+		});
+		
+		List<UsuarisRol> titulacio = new ArrayList<UsuarisRol>();
+		if(titulacions.size() > 0) {
+			titulacio = urs.findManagerByTitulacions(titulacions);
+			Collections.sort(titulacio, new Comparator<UsuarisRol>() {  
+			    @Override  
+			    public int compare(UsuarisRol a, UsuarisRol b) {  
+			          
+			        int OrgnomComp = a.getOrgan().getNomVal().compareTo(b.getOrgan().getNomVal()); 
+			        if (OrgnomComp != 0) {  
+			            return OrgnomComp;  
+			        }  
+			        int rolComp = a.getRol().getNomRol().compareTo(b.getRol().getNomRol());
+			        if (rolComp != 0) {  
+			            return rolComp;  
+			        }
+			        return a.getUsuari().getNom().concat(a.getUsuari().getCognoms()).compareTo(
+			        		b.getUsuari().getNom().concat(b.getUsuari().getCognoms()));  
+			    }  
+			});
+		}
+
+		List<UsuarisRolDTO> resp_centre = new ArrayList<>();
+		List<UsuarisRolDTO> resp_titulacio = new ArrayList<>();
+
+		for(UsuarisRol ur : centre) {
+			resp_centre.add(new UsuarisRolDTO(ur));
+		}
+		for(UsuarisRol ur : titulacio) {
+			resp_titulacio.add(new UsuarisRolDTO(ur));
+		}
+				
+		if(centre == null) {
+			model.put("results", false);
+		}
+		else{
+			
+			model.put("results", true);
+			model.put("resp_centres", resp_centre);
+			model.put("resp_titulacions", resp_titulacio);
+		}
+
+		return model;
+	}
+	
+	@PostMapping("/list")
+	HashMap<String, Object> managers(@RequestParam(name="ambit", required=false) String ambit) {
+		HashMap<String, Object> model = new HashMap<>();
+		
+		List<Usuari> sup_users = us.findAll();
+		List<Rol> sup_roles = rs.findAssignables();
+		List<UsuariDTO> users = new ArrayList<>();
+		List<RolDTO> roles = new ArrayList<>();
+
+		for(Usuari usu : sup_users) {
+			users.add(new UsuariDTO(usu));
+		}
+
+		for(Rol r : sup_roles) {
+			roles.add(new RolDTO(r));
+		}
+		
+		if(ambit == null) {
+			model.put("roles", roles);
+		} else if (ambit.equals("U")) { // Solo usuarios y roles de Universidad
+			model.put("roles", roles.stream().filter(r -> r.getAmbit().equals("U")));
+		} else if (ambit.equals("C")) { // Solo usuarios y roles de Centro
+			model.put("roles", roles.stream().filter(r -> r.getAmbit().equals("C")));
+		} else if (ambit.equals("T")) { // Solo usuarios y roles de Titulación
+			model.put("roles", roles.stream().filter(r -> r.getAmbit().equals("T")));
+		}
+
+		model.put("users", users);
+		return model;
+	}
+}

+ 24 - 0
src/main/java/es/uv/saic/web/NoticiaController.java

@@ -0,0 +1,24 @@
+package es.uv.saic.web;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.Noticia;
+import es.uv.saic.service.NoticiaService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+
+@RestController
+@RequestMapping("/noticia")
+public class NoticiaController {
+    @Autowired
+    private NoticiaService ns;
+    
+    @GetMapping
+    public List<Noticia> findVisibles() {
+        return ns.findVisibles();
+    }
+    
+}

+ 479 - 0
src/main/java/es/uv/saic/web/OrganController.java

@@ -0,0 +1,479 @@
+package es.uv.saic.web;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.DummyDataTransfer;
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.OrganPK;
+import es.uv.saic.shared.domain.Proces;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.dto.OrganDTO;
+import es.uv.saic.shared.dto.OrganRepositoryDTO;
+import es.uv.saic.service.OrganService;
+import es.uv.saic.service.ProcesService;
+import es.uv.saic.service.UsuariService;
+
+@RestController
+@RequestMapping("/organ")
+public class OrganController {
+    @Autowired
+    private UsuariController uc;
+
+    @Autowired
+    private OrganService os;
+
+    @Autowired
+    private UsuariService us;
+
+    @Autowired
+    private ProcesService ps;
+
+    /*
+     * Endpoint for getTitulacions
+     */
+    @GetMapping("/titulacions")
+    public List<OrganDTO> getTitulacionsWithCentre() {
+        return os.findAllTitulacionsWithCentre();
+    }
+
+    @GetMapping("/{ruct}")
+    public OrganDTO getOrgByRuct(@PathVariable Integer ruct) {
+
+		OrganRepositoryDTO organ = os.findByRuctDTO(ruct);
+		OrganDTO organDto = new OrganDTO(organ);
+
+        return organDto;
+    }
+
+    /*
+     * Endpoint para findById
+     */
+    @GetMapping("/find/{tlugar}/{idTitulacio}")
+    public OrganDTO findByID(@PathVariable String tlugar,  @PathVariable Integer idTitulacio) {
+        Organ organ = os.findByID(tlugar, idTitulacio);
+
+        return new OrganDTO(organ);
+
+    }
+
+    /*
+     * Endpoint para getTitulacionsByTypeCentre
+     */
+    @GetMapping("/titulacions/{lugar}/{type}")
+    public List<OrganDTO> getTitulacionsByTypeCentre(@PathVariable Integer lugar, @PathVariable Integer type) {
+        return os.getTitulacionsByTypeCentre(lugar, type).stream()
+			.map(OrganDTO::new)
+			.collect(Collectors.toList());
+    }
+
+    @GetMapping("/exist/{tlugar}/{idTitulacio}")
+    public boolean existsOrg(@PathVariable String tlugar,  @PathVariable Integer idTitulacio) {
+        return os.exists(tlugar, idTitulacio);
+    }
+
+    @GetMapping("/titulacions/{centre}/{tambit}")
+    public List<OrganDTO> getTitulacionsByCentreTambit(@PathVariable Integer centre, @PathVariable String tambit) {
+        return os.findActiveTitulacionsByCentreTambit(centre, tambit).stream()
+			.map(OrganDTO::new)
+			.collect(Collectors.toList());
+    }
+
+    @GetMapping("/supervisor")
+    public List<OrganDTO> getTitulacionsSupervisor() {
+        return os.getTitulacions().stream()
+            .filter(o -> o.getOrgan().getId().getLugar() != 99)
+            .map(OrganDTO::new)
+            .collect(Collectors.toList());
+    }
+    
+    @GetMapping("/titulacions/{centre}")
+    public List<OrganDTO> getTitulacionsByCentre(@PathVariable Integer centre) {
+        return os.getTitulacionsByCentre(centre).stream()
+            .map(OrganDTO::new)
+            .distinct()
+            .collect(Collectors.toList());
+    }
+
+    @PostMapping("/usuari")
+    public List<OrganDTO> findOrgansByUsuari(@RequestBody Usuari usuari) {
+        return uc.findActiveRols(usuari).stream()
+            .map(ur -> new OrganDTO(ur.getOrgan()))
+            .distinct()
+            .collect(Collectors.toList());
+    }
+
+    @GetMapping
+    public List<OrganDTO> getCentres() {
+        return os.getActiveCentres().stream()
+			.map(OrganDTO::new)
+			.collect(Collectors.toList());
+    }
+
+    @GetMapping("/active")
+    public List<OrganDTO> getActiveCentres() {
+        return os.getActiveCentres().stream()
+			.map(OrganDTO::new)
+			.collect(Collectors.toList());
+    }
+
+    // Funciones ajax
+    @PostMapping("/titulacions/usuari")
+	public HashMap<String, Object> getTitulationsByCenter(@RequestParam(name="centers[]", required=false) List<Integer> centres,
+        @RequestParam String usuari ) throws IOException {	
+        
+        HashMap<String, Object> model = new HashMap<>();
+        Usuari u = us.findByUsername(usuari);
+
+		if(centres == null) { centres = us.getSupervisableCentres(u); }
+		List<Organ> org_list = this.os.findTitulacionsByCentre(centres);
+
+		List<DummyDataTransfer> supervisable_cents = new ArrayList<DummyDataTransfer>();
+		for(Organ or : org_list) {
+			DummyDataTransfer c = new DummyDataTransfer();
+			c.setText(or.getNomCas());
+			c.setText2(or.getNomVal());
+			c.setValue(or.getId().getLugar());
+			supervisable_cents.add(c);
+		}
+		
+		DummyDataTransfer o = new DummyDataTransfer();
+		o = new DummyDataTransfer();
+		Organ org = os.findByID("T", 1);
+		o.setText(org.getNomCas());
+		o.setText2(org.getNomVal());
+		o.setValue(1);
+		supervisable_cents.add(o);
+		o = new DummyDataTransfer();
+		org = os.findByID("T", 2);
+		o.setText(org.getNomCas());
+		o.setText2(org.getNomVal());
+		o.setValue(2);
+		supervisable_cents.add(o);
+		o = new DummyDataTransfer();
+		org = os.findByID("T", 0);
+		o.setText(org.getNomCas());
+		o.setText2(org.getNomVal());
+		o.setValue(0);
+		supervisable_cents.add(o);
+
+		Collections.sort(supervisable_cents, (a, b) -> {
+			return a.getText2().compareTo(b.getText2());
+		});
+
+		model.put("sup_orgs", supervisable_cents);
+		return model;
+	}
+
+    // POST que recoge toda las titulaciones que contiene un centro para el manager
+	@PostMapping("/titulacions/managers")
+	public HashMap<String, Object> getCenterTitulations(
+			@RequestParam("center") Integer centre) {
+        
+        HashMap<String, Object> model = new HashMap<>();
+		List<Organ> sup_titulacions;
+		sup_titulacions = this.os.getTitulacionsByCentre(centre);
+		
+		List<OrganDTO> titulacionDto = sup_titulacions.stream()
+            .map(organ -> new OrganDTO(organ)) 
+            .collect(Collectors.toList());
+		
+		model.put("resp_titulacions", titulacionDto);
+
+		return model;
+	}
+
+    @PostMapping("/titulacions/calendar")
+	public HashMap<String, Object> getTitulationsByCenters(@RequestParam("centers[]") List<Integer> centres, @RequestParam("procedure") Integer idProces) throws IOException {	
+		
+        HashMap<String, Object> model = new HashMap<>();
+        List<DummyDataTransfer> titulations = new ArrayList<DummyDataTransfer>();
+		DummyDataTransfer o = new DummyDataTransfer();
+		
+		if(idProces > 0) {
+			Proces p = ps.findByID(idProces);
+			if(p.getAmbit().equals("U")) {
+				o = new DummyDataTransfer();
+				Organ org = os.findByID("T", 0);
+				o.setText(org.getNomCas());
+				o.setText2(org.getNomVal());
+				titulations.add(o);
+			}
+			else if(p.getAmbit().equals("C")) {
+				o = new DummyDataTransfer();
+				Organ org = os.findByID("T", 1);
+				o.setText(org.getNomCas());
+				o.setText2(org.getNomVal());
+				o.setValue(1);
+				titulations.add(o);
+				o = new DummyDataTransfer();
+				org = os.findByID("T", 2);
+				o.setText(org.getNomCas());
+				o.setText2(org.getNomVal());
+				o.setValue(2);
+				titulations.add(o);
+				o = new DummyDataTransfer();
+				org = os.findByID("T", 0);
+				o.setText(org.getNomCas());
+				o.setText2(org.getNomVal());
+				o.setValue(0);
+				titulations.add(o);
+			}
+			else if(p.getAmbit().equals("T")) {
+				for(Integer centre : centres) {
+					List<Organ> org_list = this.os.getTitulacionsByCentre(centre);
+					for(Organ or : org_list) {
+						DummyDataTransfer c = new DummyDataTransfer();
+						c.setText(or.getNomCas());
+						c.setText2(or.getNomVal());
+						c.setValue(or.getId().getLugar());
+						titulations.add(c);
+					}
+				}	
+				Collections.sort(titulations, 
+	                    (o1, o2) -> Integer.valueOf(o1.getValue()).compareTo(Integer.valueOf(o2.getValue())));
+			}
+		}
+		else {
+			for(Integer centre : centres) {
+				List<Organ> org_list = this.os.getTitulacionsByCentre(centre);
+				for(Organ or : org_list) {
+					DummyDataTransfer c = new DummyDataTransfer();
+					c.setText(or.getNomCas());
+					c.setText2(or.getNomVal());
+					c.setValue(or.getId().getLugar());
+					titulations.add(c);
+				}
+			}	
+			Collections.sort(titulations, 
+                    (o1, o2) -> Integer.valueOf(o1.getValue()).compareTo(Integer.valueOf(o2.getValue())));
+		}
+		
+		model.put("sup_titulacions", titulations);
+		return model;
+	}
+	
+	@PostMapping("/titulacions/admin")
+	public HashMap<String, Object> getTitulationsByCenter(@RequestParam("center") Integer centre, @RequestParam("procedure") Integer idProces) throws IOException {	
+		HashMap<String, Object> model = new HashMap<>();
+        Proces p = ps.findByID(idProces);
+		List<DummyDataTransfer> titulations = new ArrayList<DummyDataTransfer>();
+		DummyDataTransfer o = new DummyDataTransfer();
+		
+		if(p.getAmbit().equals("U")) {
+			o = new DummyDataTransfer();
+			Organ org = os.findByID("T", 0);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			titulations.add(o);
+		}
+		else if(p.getAmbit().equals("C")) {
+			o = new DummyDataTransfer();
+			Organ org = os.findByID("T", 1);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(1);
+			titulations.add(o);
+			o = new DummyDataTransfer();
+			org = os.findByID("T", 2);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(2);
+			titulations.add(o);
+			o = new DummyDataTransfer();
+			org = os.findByID("T", 0);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(0);
+			titulations.add(o);
+		}
+		else if(p.getAmbit().equals("T")) {
+			o = new DummyDataTransfer();
+			o.setText("Todos los grados");
+			o.setText2("Tots els graus");
+			o.setValue(1);
+			titulations.add(o);
+			o = new DummyDataTransfer();
+			o.setText("Todos los másters");
+			o.setText2("Tots els màsters");
+			o.setValue(2);
+			titulations.add(o);
+			o = new DummyDataTransfer();
+			o.setText("Todas las titulaciones");
+			o.setText2("Totes les titulacions");
+			o.setValue(0);
+			titulations.add(o);
+			List<Organ> org_list = this.os.getTitulacionsByCentre(centre);
+			for(Organ or : org_list) {
+				DummyDataTransfer c = new DummyDataTransfer();
+				c.setText(or.getNomCas());
+				c.setText2(or.getNomVal());
+				c.setValue(or.getId().getLugar());
+				titulations.add(c);
+			}
+		}
+		model.put("sup_orgs", titulations);
+        return model;
+	}
+
+    @PostMapping("/titulacions")
+	public HashMap<String, Object> getAllTitulationsByCenter(@RequestParam("center") Integer centre) throws IOException {	
+		List<DummyDataTransfer> titulations = new ArrayList<DummyDataTransfer>();
+		DummyDataTransfer o = new DummyDataTransfer();
+		HashMap<String, Object> model = new HashMap<>();
+
+		o = new DummyDataTransfer();
+		o.setText("Todos los grados");
+		o.setText2("Tots els graus");
+		o.setValue(1);
+		titulations.add(o);
+		o = new DummyDataTransfer();
+		o.setText("Todos los másters");
+		o.setText2("Tots els màsters");
+		o.setValue(2);
+		titulations.add(o);
+		o = new DummyDataTransfer();
+		o.setText("Todas las titulaciones");
+		o.setText2("Totes les titulacions");
+		o.setValue(0);
+		titulations.add(o);
+		List<Organ> org_list = this.os.getTitulacionsByCentre(centre);
+		for(Organ or : org_list) {
+			DummyDataTransfer c = new DummyDataTransfer();
+			c.setText(or.getNomCas());
+			c.setText2(or.getNomVal());
+			c.setValue(or.getId().getLugar());
+			titulations.add(c);
+		}
+		model.put("sup_orgs", titulations);
+        return model;
+	}
+
+    @PostMapping("/centres")
+	public HashMap<String, Object> getAllCentresByAmbit(@RequestParam("procedure") Integer idProces) throws IOException {	
+		HashMap<String, Object> model = new HashMap<>();
+        List<DummyDataTransfer> centres = new ArrayList<DummyDataTransfer>();
+		if(idProces > 0) {
+			Proces p = ps.findByID(idProces);
+			if(p.getAmbit().equals("C")) {
+				DummyDataTransfer o = new DummyDataTransfer();
+				o.setText("Todos los centros");
+				o.setText2("Tots els centres");
+				o.setValue(0);
+				centres.add(o);
+			}
+		}
+		List<Organ> org_list = this.os.getActiveCentres();
+		for(Organ or : org_list) {
+			DummyDataTransfer c = new DummyDataTransfer();
+			c.setText(or.getNomCas());
+			c.setText2(or.getNomVal());
+			c.setValue(or.getId().getLugar());
+			centres.add(c);
+		}
+		model.put("sup_centers", centres);
+        return model;
+	}
+
+	@PostMapping("/equivalents")
+	public List<Integer> getEquivalents(@RequestParam Integer lugar, @RequestParam String tlugar) {
+		return os.getEquivalents(lugar, tlugar);
+	}
+
+	@PostMapping("/new/centre")
+    public void createNewCentre(@RequestParam("codiCentro") Integer codigo,
+			@RequestParam("nomCasCentro") String nomCas,
+            @RequestParam("nomValCentro") String nomVal,
+            @RequestParam("ructCentro") Integer ruct){
+		OrganPK orgPK = new OrganPK();
+		orgPK.setTlugar("C");
+		orgPK.setLugar(codigo);
+
+		Organ org = new Organ();
+		org.setId(orgPK);
+		org.setNomCas(nomCas);
+		org.setNomVal(nomVal);
+		org.setRuct(ruct);
+		org.setActiu(true);
+		org.setTambit("C");
+		org.setOrgan(org);
+
+		os.save(org);
+	}
+
+	@PostMapping("/new/titulacion")
+    public void createNewTitulacion(@RequestParam("codiTit") Integer codigo,
+			@RequestParam("centre") Integer idCentro,
+            @RequestParam("nomCasTit") String nomCas,
+            @RequestParam("nomValTit") String nomVal,
+            @RequestParam("ructTit") Integer ruct,
+            @RequestParam("tambit") String ambit){
+		OrganPK orgPK = new OrganPK();
+		orgPK.setTlugar("T");
+		orgPK.setLugar(codigo);	
+		
+		Organ center = new Organ();
+		center = os.findByID("C", idCentro);
+
+		Organ org = new Organ();
+		org.setId(orgPK);
+		org.setOrgan(center);
+		org.setNomCas(nomCas);
+		org.setNomVal(nomVal);
+		org.setRuct(ruct);
+		org.setActiu(true);
+		org.setTambit(ambit);
+
+		this.os.save(org);
+	}
+
+	@PostMapping("/update/centre")
+	public void updateCentre(@RequestParam("lugar") Integer lugar,
+			@RequestParam("tlugar") String tlugar,
+			@RequestParam("nomCasTit") String nomCas,
+			@RequestParam("nomValTit") String nomVal,
+			@RequestParam("ructTit") Integer ruct) {
+		Organ oldOrgan = os.findByID(tlugar, lugar);
+
+		oldOrgan.setNomCas(nomCas);
+		oldOrgan.setNomVal(nomVal);
+		oldOrgan.setRuct(ruct);
+		
+		this.os.save(oldOrgan);
+	}
+
+	@PostMapping("/update/titulacion")
+	public void updateTitulacion(@RequestParam("lugar") Integer lugar,
+			@RequestParam("tlugar") String tlugar,
+			@RequestParam("nomCasTit") String nomCas,
+			@RequestParam("nomValTit") String nomVal,
+			@RequestParam("ructTit") Integer ruct,
+		    @RequestParam("centre") Integer idCentro,
+			@RequestParam("tambit") String ambit) {
+		Organ oldOrgan = os.findByID(tlugar, lugar);
+
+		oldOrgan.setNomCas(nomCas);
+		oldOrgan.setNomVal(nomVal);
+		oldOrgan.setRuct(ruct);
+		
+		Organ centre = os.findByID("C", idCentro);
+		oldOrgan.setOrgan(centre);
+		oldOrgan.setTambit(ambit);
+		
+		this.os.save(oldOrgan);
+	}
+	
+}

+ 692 - 0
src/main/java/es/uv/saic/web/ProceduresController.java

@@ -0,0 +1,692 @@
+package es.uv.saic.web;
+
+import static java.util.Comparator.comparing;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.nio.file.StandardCopyOption;
+import java.sql.Timestamp;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.Year;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import es.uv.saic.shared.domain.DummyDataTransfer;
+import es.uv.saic.shared.domain.Instancia;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.domain.InstanciaTascaTransfer;
+import es.uv.saic.shared.domain.InstanciaTascaVer;
+import es.uv.saic.shared.domain.InstanciaTransfer;
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.Plantilla;
+import es.uv.saic.shared.domain.Proces;
+import es.uv.saic.shared.domain.Rol;
+import es.uv.saic.shared.domain.Tipus;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.dto.InstanciaTascaDTO;
+import es.uv.saic.shared.dto.PdfDTO;
+import es.uv.saic.shared.dto.ProcesDTO;
+import es.uv.saic.shared.dto.RolDTO;
+import es.uv.saic.shared.dto.TascaDTO;
+import es.uv.saic.shared.dto.TemplateDataDTO;
+import es.uv.saic.shared.feign.PlantillaClient;
+import es.uv.saic.service.InstanciaService;
+import es.uv.saic.service.InstanciaTascaService;
+import es.uv.saic.service.InstanciaTascaVerService;
+import es.uv.saic.service.OrganService;
+import es.uv.saic.service.ProcesService;
+import es.uv.saic.service.RolService;
+import es.uv.saic.service.TascaService;
+import es.uv.saic.service.TipusService;
+import es.uv.saic.service.UsuariService;
+
+
+@RestController
+public class ProceduresController {
+
+    private final ProcesService procesService;
+	 
+	@Autowired
+	private UsuariService us;
+	@Autowired
+	private InstanciaService is;
+	@Autowired
+	private InstanciaTascaService its;
+	@Autowired
+	private InstanciaTascaVerService itsver;
+	@Autowired
+	private OrganService ors;
+	@Autowired
+    private ProcesService ps;
+    @Autowired
+    private TipusService tps;
+    @Autowired
+    private RolService rs;
+    @Autowired
+    private TascaService tas;
+	@Autowired
+	private PlantillaClient plc;
+	@Value("${saic.data.filePath}")
+	private String filePath;
+
+    ProceduresController(ProcesService procesService) {
+        this.procesService = procesService;
+    }
+
+	/*
+	 * Load the list of active procedure instances for the logged-in user
+	 * 
+	 * @param model 
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param _new Optional parameter to indicate a new access
+	 * @return The name of the view to render
+	 */
+
+	@GetMapping("/tipus/findAll")
+	public List<Tipus> findAll() {
+		return tps.findAll();
+	}
+	
+
+	@PostMapping("/procedures")
+	public HashMap<String, Object> getActiveInstances(@RequestParam(required = false) String _new, @RequestParam String usuari) {
+		HashMap<String, Object> model = new HashMap<>();
+
+		if(_new != null) {
+			model.put("new_access", true);
+		}
+		
+		Usuari u = us.findByUsername(usuari);
+		List<InstanciaTransfer> plist = new ArrayList<InstanciaTransfer>();
+		Instancia i;
+		InstanciaTransfer it;
+		InstanciaTasca itt;
+		for(BigInteger p_id: this.us.getActiveInstancies(u)) {
+			i = this.is.findByID(p_id);
+			it = new InstanciaTransfer();
+			it.setIdInstancia(i.getIdInstancia());
+			it.setIdProces(i.getProces().getIdProces());
+			it.setNomProces(i.getProces().getNomProces());
+			it.setCursAvaluat(i.getProces().getCursAvaluat());
+			it.setCursActivacio(i.getProces().getCursActivacio());
+			it.setTitolCas(i.getProces().getTitolCas());
+			it.setTitolVal(i.getProces().getTitolVal());
+			it.setDescripcioCas(i.getProces().getDescripcioCas());
+			it.setDescripcioVal(i.getProces().getDescripcioVal());
+			it.setEstat(i.getEstat());
+			if(i.getOrgan().getId().getTlugar().equals("C")) {
+				Organ o = ors.findByID("T", i.getTitulacio());
+				it.setTitulacioCas(o.getNomCas());
+				it.setTitulacioVal(o.getNomVal());
+				it.setCentreCas(i.getOrgan().getNomCas());
+				it.setCentreVal(i.getOrgan().getNomVal());
+			}
+			else {
+				it.setRuct(i.getOrgan().getRuct());
+				it.setTitulacioCas(i.getOrgan().getNomCas());
+				it.setTitulacioVal(i.getOrgan().getNomVal());
+				it.setCentreCas(i.getOrgan().getOrgan().getNomCas());
+				it.setCentreVal(i.getOrgan().getOrgan().getNomVal());
+			}
+			itt = its.findActiveByInstancia(i.getIdInstancia());
+			it.setDataLimTascaActiva(itt.getTasca().getDataLim());
+			it.setDataTascaActiva(itt.getData());
+			it.setNomTascaActivaCas(itt.getTasca().getTitolCas());
+			it.setNomTascaActivaVal(itt.getTasca().getTitolVal());
+			it.setDescTascaActivaCas(itt.getTasca().getDescripcioCas());
+			it.setDescTascaActivaVal(itt.getTasca().getDescripcioVal());
+			it.setIdTascapActiva(itt.getTasca().getIdTascap());
+			it.setTascaActivaExpired(itt.getTasca().isExpired());
+			it.setTascaActivaAssignedToUser(its.isUserAuthorized(u, itt.getIdInstanciaTasca()));
+			plist.add(it);
+		}
+		model.put("procedure_list", plist);
+		return model;
+	}
+	
+	/*
+	 * Loads a procedure instance and its tasks
+	 * 
+	 * @param model 
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param id Instancia ID Instance to load
+	 * @return The name of the view to render
+	 */
+	@GetMapping("/procedure/{id}/{usuari}")
+	public HashMap<String, Object> getInstance(@PathVariable BigInteger id, @PathVariable String usuari) {
+		HashMap<String, Object> model = new HashMap<>();
+		Usuari u = us.findByUsername(usuari);
+
+		this.loadProcedure(model, u, id);
+		return model;
+	}
+	
+
+	/*
+	 * Updates a task instance with evidence files (for specific task types)
+	 * 
+	 * @param model
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param id Instancia ID Instance to load
+	 * @param params Form parameters
+	 * @param evidencias List of evidence files (if any)
+	 * @return The number of files uploaded (as a String)
+	 * @throws IllegalStateException
+	 * @throws IOException
+	 * @throws InterruptedException
+	 */
+	@PostMapping("/procedure/files/{id}")
+	@ResponseBody 
+	public HashMap<String, Object> updateInstanciaTascaEvidencia(@PathVariable BigInteger id, @RequestParam Map<String,String> params, 
+			@RequestBody(required = true) List<File> evidencias, @RequestParam String usuari) throws IllegalStateException, IOException {
+		HashMap<String, Object> model = new HashMap<>();
+		Usuari u = us.findByUsername(usuari);
+		
+		InstanciaTasca it = its.findById(new BigInteger(params.get("taskid")));
+		
+		/* Tipos de tarea permitidas */
+		final Set<Integer> suitable = Set.of(1, 12, 14);
+		
+		boolean newTask = (suitable.contains(it.getTasca().getTipus().getTipus())) && it.getEstat().equals("E") && 
+						   (u.isAdmin() || u.isGranted());
+		
+		if(suitable.contains(it.getTasca().getTipus().getTipus())) {   // Evidencia iterable
+			String fileName = "";
+			
+			if(evidencias.size() > 1) {
+				fileName = (newTask ? it.getIdInstanciaTasca().add(new BigInteger("1")).toString() : it.getIdInstanciaTasca().toString()) + ".zip";
+				try (ZipOutputStream zipOut = new ZipOutputStream(new FileOutputStream(filePath+fileName))) {
+				    for (File f : evidencias) {
+						zipOut.putNextEntry(new ZipEntry(f.getName()));
+						Files.copy(f.toPath(), zipOut);
+						zipOut.closeEntry();
+					}
+				}		
+			}
+			else if(evidencias.size() == 1){
+				File evidencia = evidencias.get(0);
+				fileName = (newTask ? it.getIdInstanciaTasca().add(new BigInteger("1")).toString() : it.getIdInstanciaTasca().toString()) + "." +
+						FilenameUtils.getExtension(evidencia.getName());
+				File destino = new File(filePath, fileName);
+				Files.copy(evidencia.toPath(), destino.toPath(), StandardCopyOption.REPLACE_EXISTING);
+			}
+			else {
+				model.put("ammount", "0");
+				return model;
+			}
+			
+			if(newTask){
+				InstanciaTasca itNew = new InstanciaTasca();
+				itNew.setUsuari(u);
+				itNew.setDataFet(LocalDate.now());
+				itNew.setData(LocalDate.now());
+				itNew.setEstat("E");
+				itNew.setEvidencia(fileName);
+				itNew.setInstancia(it.getInstancia());
+				itNew.setTasca(it.getTasca());
+				itNew.setVersion(it.getVersion()+1);
+				itNew.setIdInstanciaTasca(it.getIdInstanciaTasca().add(new BigInteger("1")));
+				its.save(itNew);
+			}
+			else {
+				it.setEvidencia(fileName);
+				it.setEstat("E");
+				this.saveChanges(u, it);
+			}	
+		}
+		model.put("ammount", Integer.toString(evidencias.size()));
+		return model; 
+	}
+	
+	/*
+	 * Updates a task instance
+	 * 
+	 * @param model 
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param id Instancia ID Instance to load
+	 * @param params Form parameters
+	 * @param evidencias List of evidence files (if any)
+	 * @return The name of the view to render
+	 */
+	@PostMapping("/procedure/{id}")
+	public HashMap<String, Object> updateInstanciaTasca(@PathVariable BigInteger id, @RequestBody Map<String,String> params,
+			@RequestParam(required = false) List<MultipartFile> evidencias, @RequestParam String usuari) throws IllegalStateException, IOException, InterruptedException {
+		HashMap<String, Object> model = new HashMap<>();
+		Usuari u = us.findByUsername(usuari);
+	
+		// Update task instance 
+		InstanciaTasca it = its.findById(new BigInteger(params.get("taskid")));
+		if(it.getTasca().getTipus().getTipus() == 4) { // Avanzar
+			it.setEstat("F");
+		}
+		else if(it.getTasca().getTipus().getTipus() == 2) { // SI/NO
+			if(params.get("response").equals("0")) {
+				it.setEstat("N");
+			}
+			else {
+				it.setEstat("S");
+			}
+		}
+		else if(it.getTasca().getTipus().getTipus() == 11 || it.getTasca().getTipus().getTipus() == 15) { // Evidencias online
+			it.setText(params.get("evidencia_text"));
+			PdfDTO pdf = new PdfDTO(params.get("evidencia_text"), it.getIdInstanciaTasca());
+			it.setEvidencia(this.plc.savePDF(pdf));
+			it.setEstat("E");
+		}
+
+		this.saveChanges(u, it);
+		this.loadProcedure(model, u, id);
+		return model; 
+	}
+	
+	/*
+	 * Saves changes to a task instance
+	 * 
+	 * @param model 
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param it InstanciaTasca to save
+	 */
+	public void saveChanges(Usuari u, InstanciaTasca it) {	
+		this.its.saveChanges(u, it);
+	}
+	
+
+	/*
+	 * Saves a draft of a task instance (for specific task types)
+	 * 
+	 * @param model
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param id Instancia ID Instance to load
+	 * @param text Text content of the draft
+	 * @param manual Whether the save was manually triggered by the user
+	 * @return The timestamp of the save operation formatted as a String
+	 */
+	@PostMapping("/procedure/save/{id}")
+	@ResponseBody
+	public HashMap<String, Object> saveDraft(@PathVariable BigInteger id, @RequestBody String text, 
+							@RequestParam boolean manual, @RequestParam String usuari) {
+		HashMap<String, Object> model = new HashMap<>();
+		Usuari u = us.findByUsername(usuari);
+
+		InstanciaTasca it = its.findById(id);
+		it.setText(text);
+		it.setUsuari(u);
+		it.setDataMod(LocalDateTime.now());
+		its.save(it);
+		if(manual){
+			List<InstanciaTascaVer> itvs = this.itsver.findByIdInstanciaTasca(id);
+			InstanciaTascaVer itv = new InstanciaTascaVer(id, it.getUsuari().getUsuari(), it.getText(), Timestamp.valueOf(it.getDataMod()));
+			if(itvs != null){
+				if(itvs.size() == 10){
+					this.itsver.delete(itvs.get(itvs.size()-1));
+				}
+			}
+			this.itsver.save(itv);
+		}
+		
+		model.put("date", String.format("%1$td/%1$tm/%1$tY %1$tH:%1$tM:%1$tS", it.getDataMod()));
+		return model;
+	}
+	
+	/*
+	 * Loads a procedure instance and its tasks into the model
+	 * @param model
+	 * @param auth Authentication
+	 * @param id Instancia ID Instance to load
+	 * @return The name of the view to render
+	 */
+	public void loadProcedure(HashMap<String, Object> model, Usuari usuari, BigInteger id) {
+		Instancia i = is.findByID(id);
+		InstanciaTransfer inst = new InstanciaTransfer();
+		inst.setIdInstancia(i.getIdInstancia());
+		inst.setIdProces(i.getProces().getIdProces());
+		inst.setNomProces(i.getProces().getNomProces());
+		inst.setCursAvaluat(i.getProces().getCursAvaluat());
+		inst.setCursActivacio(i.getProces().getCursActivacio());
+		inst.setTitolCas(i.getProces().getTitolCas());
+		inst.setTitolVal(i.getProces().getTitolVal());
+		inst.setDescripcioCas(i.getProces().getDescripcioCas());
+		inst.setDescripcioVal(i.getProces().getDescripcioVal());
+		inst.setEstat(i.getEstat());
+		if(i.getOrgan().getId().getTlugar().equals("C")) {
+			Organ o = ors.findByID("T", i.getTitulacio());
+			inst.setTitulacioCas(o.getNomCas());
+			inst.setTitulacioVal(o.getNomVal());
+			inst.setCentreCas(i.getOrgan().getNomCas());
+			inst.setCentreVal(i.getOrgan().getNomVal());
+		}
+		else {
+			inst.setTitulacioCas(i.getOrgan().getNomCas());
+			inst.setTitulacioVal(i.getOrgan().getNomVal());
+			inst.setCentreCas(i.getOrgan().getOrgan().getNomCas());
+			inst.setCentreVal(i.getOrgan().getOrgan().getNomVal());
+			inst.setRuct(i.getOrgan().getRuct());
+		}
+		model.put("instance", inst);
+		
+		List<InstanciaTascaTransfer> tasks = new ArrayList<InstanciaTascaTransfer>();
+		Boolean active;
+		for(InstanciaTasca it : its.findByInstancia(i.getIdInstancia())) {
+			active = false;
+			InstanciaTascaTransfer itt = new InstanciaTascaTransfer();
+			itt.setIdInstanciaTasca(it.getIdInstanciaTasca());
+			itt.setIdTasca(it.getTasca().getIdTasca());
+			itt.setIdProces(it.getTasca().getProces().getIdProces());
+			itt.setTitolCas(it.getTasca().getTitolCas());
+			itt.setTitolVal(it.getTasca().getTitolVal());
+			itt.setDescripcioCas(it.getTasca().getDescripcioCas());
+			itt.setDescripcioVal(it.getTasca().getDescripcioVal());
+			itt.setTipus(it.getTasca().getTipus().getTipus());
+			itt.setDataLim(formatDate(it.getTasca().getDataLim()));
+			itt.setEstat(it.getEstat());
+			itt.setIsExpired(it.getTasca().isExpired());
+			itt.setIdTascap(it.getTasca().getIdTascap());
+			itt.setDescripcioRolCas(it.getTasca().getRol().getDescripcioCas());
+			itt.setDescripcioRolVal(it.getTasca().getRol().getDescripcioVal());
+			itt.setDataFet(formatDate(it.getDataFet()));
+			itt.setEstatInstancia(it.getInstancia().getEstat());
+			itt.setOpcions(it.getTasca().getOpcions());
+			itt.setDataMod((it.getDataMod() != null) ? formatDate(it.getDataMod().toLocalDate()) : null);
+			
+			if(itt.getEstat() != null) {
+				if(itt.getEstat().equals("A")) {
+					active = true;
+				}
+			}
+			
+			if(it.getTasca().getTipus().getTipus() == 11 || it.getTasca().getTipus().getTipus() == 15) {
+				Plantilla p = plc.findByID(Integer.parseInt(it.getTasca().getCodiEvidencia()));
+				Plantilla p2 = null;
+				
+				if(i.getTitulacio() > 0) {
+					if(!p.getAmbit().equals(i.getOrgan().getTambit())) {
+						p2 = plc.findByVersioCodiAmbit(p.getVersio(), p.getCodi(), i.getOrgan().getTambit());
+					}
+				}			
+				
+				if(p2 != null) {
+					p = p2;
+				}
+						
+				itt.setCodiEvidencia(p.getCodi());
+				itt.setNomEvidenciaCas(p.getNomCas());
+				itt.setNomEvidenciaVal(p.getNomVal());
+				/* Comprobar estado evidencia, si vacío inyectar contenido de plantilla asociada */
+				if(it.getText() == null && active) {
+					InstanciaTascaDTO itDTO = new InstanciaTascaDTO(it);
+					TemplateDataDTO td = new TemplateDataDTO(itDTO, p.getText());
+					itt.setText(plc.addTemplateData(td));
+				}
+				else if(active){
+					itt.setText(it.getText());
+				}
+			}
+			else {
+				itt.setCodiEvidencia(it.getTasca().getCodiEvidencia());
+				itt.setNomEvidenciaCas(it.getTasca().getNomEvidenciaCas());
+				itt.setNomEvidenciaVal(it.getTasca().getNomEvidenciaVal());
+				itt.setEvidencia(it.getEvidencia());
+			}
+			
+			if(it.getUsuari() != null) {
+				itt.setUsuariFet(it.getUsuari().getNom() + " " + it.getUsuari().getCognoms());
+			}
+			else {
+				itt.setUsuariFet("");
+			}			
+			itt.setAssignedToUser(its.isUserAuthorized(usuari, itt.getIdInstanciaTasca()));
+			itt.setOldEvidences(its.findOlderByProces(it.getInstancia().getCentre(), it.getInstancia().getTitulacio(), it.getInstancia().getProces().getNomProces(), i.getProces().getCursActivacio()));
+			itt.setVersions(its.findOlderVersions(it.getInstancia().getIdInstancia(), it.getTasca().getIdTasca(), it.getVersion()));
+			tasks.add(itt);
+		}
+		model.put("tasks", tasks);
+		model.put("flow", this.getFlowDiagram(i.getProces()));
+	}
+
+	private String formatDate(LocalDate date) {
+		if (date == null) return "";
+		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("dd-MM-yyyy");
+		return date.format(formatter);
+	}
+
+
+	/*
+	 * Get all drafts for a given task instance
+	 * @param model
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param id InstanciaTasca ID to load drafts for
+	 * @return The name of the view to render
+	 */
+	@GetMapping("/procedure/drafts/{id}")
+	public HashMap<String, Object> getDrafts(@PathVariable BigInteger id) {
+		HashMap<String, Object> model = new HashMap<>();
+		List<InstanciaTascaVer> itvs = this.itsver.findByIdInstanciaTasca(id);
+		model.put("versions", itvs);
+		model.put("idInstanciaTasca", id);
+		return model;
+	}
+
+	/*
+	 * Get a specific draft for a given task instance
+	 * @param model
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param id InstanciaTasca ID to load drafts for
+	 * @param dataMod Timestamp of the draft to load
+	 * @return The InstanciaTascaVer object representing the draft
+	 */
+	@GetMapping("/procedure/draft/{id}")
+	@ResponseBody
+	public InstanciaTascaVer getDraft(@PathVariable BigInteger id, @RequestParam Timestamp dataMod) {
+		return this.itsver.findById(id, dataMod);
+	}
+
+	/*
+	 * Restore a specific draft for a given task instance
+	 * @param model
+	 * @param auth Authentication
+	 * @param session HttpSession
+	 * @param id InstanciaTasca ID to restore draft for
+	 * @param dataMod Timestamp of the draft to restore
+	 * @return "1" if successful, "0" otherwise
+	 */
+	@PostMapping("/procedure/draft/{id}")
+	@ResponseBody
+	public String restoreDraft(@PathVariable BigInteger id, @RequestParam Timestamp dataMod) {
+		try{
+			InstanciaTascaVer itv = this.itsver.findById(id, dataMod);
+			InstanciaTasca it = this.its.findById(id);
+			it.setText(itv.getText());
+			this.its.save(it);
+		}
+		catch(Exception e){
+			return "0";
+		}
+
+		return "1";
+	}
+
+	/*
+	 * Generate a flow diagram for a given process
+	 * @param proces The process to generate the flow diagram for
+	 * @return A String containing the flow diagram
+	 */
+	private String getFlowDiagram(Proces proces){
+		List<String> flow = this.procesService.getFlowDiagram(proces);
+		return "graph TD \n" + String.join("\n", flow);
+	}
+
+    @PostMapping("/proces/{id}")
+    public ProcesDTO findById(@PathVariable Integer id) {
+    	Proces proces = ps.findByID(id);
+		return proces != null ? new ProcesDTO(proces) : null;
+    }
+    
+    @GetMapping("/proces/getAll")
+    public List<ProcesDTO> getAll() {
+        return ps.getAll().stream()
+			.map(ProcesDTO::new)
+			.toList();
+    }
+    
+    @PostMapping("/procedure/search/years")
+	public HashMap<String, Object> getYearsByCenterTitulation(
+			@RequestParam(name="centers[]", required=false) List<Integer> centres,
+			@RequestParam("titulations[]") List<Integer> titulacions, @RequestParam String usuari) throws IOException {
+            
+            HashMap<String, Object> model = new HashMap<>();
+            Usuari u = us.findByUsername(usuari);
+
+			List<DummyDataTransfer> supervisable_years = new ArrayList<DummyDataTransfer>();
+			if(centres == null) { centres = us.getSupervisableCentres(u); }
+			for(Integer c : ps.getSupervisableCursos(centres, titulacions)) {
+				DummyDataTransfer d = new DummyDataTransfer();
+				d.setValue(c);
+				d.setText(Integer.toString(c-1) + " - " + Integer.toString(c));
+				supervisable_years.add(d);
+			}
+
+			Collections.sort(supervisable_years, (a, b) -> {
+				return a.getText().compareTo(b.getText());
+			});
+
+			model.put("sup_years", supervisable_years);
+		return model;
+	}
+
+    @PostMapping("/procedure/search")
+	public HashMap<String, Object> getProceduresByCenterTitulationYear(
+            @RequestParam(name="centers[]", required=false) List<Integer> centres,
+			@RequestParam("years[]") List<Integer> cursos,
+			@RequestParam("titulations[]") List<Integer> titulacions,
+            @RequestParam String usuari) throws IOException {	
+		
+        HashMap<String, Object> model = new HashMap<>();
+        Usuari u = us.findByUsername(usuari);
+
+        List<DummyDataTransfer> supervisable_procedures = new ArrayList<DummyDataTransfer>();
+		if(centres == null) { centres = us.getSupervisableCentres(u); }
+		for(Integer p_id : ps.getSupervisableProcedures(u, cursos, centres, titulacions)) {
+			DummyDataTransfer d = new DummyDataTransfer();
+			Proces p = ps.findByID(p_id);
+			d.setText(p.getNomProces());
+			d.setText2(p.getNomProces() + " " + p.getTitolCas());
+			d.setText3(p.getNomProces() + " " + p.getTitolVal());
+			supervisable_procedures.add(d);
+		}
+		
+		Set<String> set = new HashSet<>(supervisable_procedures.size());
+		supervisable_procedures.removeIf(p -> !set.add(p.getText()));
+		Collections.sort(supervisable_procedures, comparing(DummyDataTransfer::getText));
+		model.put("sup_procs", supervisable_procedures);
+
+		return model;
+	}
+
+    @PostMapping("/procedure/find")
+	public HashMap<String, Object> findProcedure(@RequestParam("procedure") String idProces, 
+			@RequestParam("action") String action) throws IOException {	
+		
+		HashMap<String, Object> model = new HashMap<>();
+		Proces p = new Proces();
+		model.put("action", action);
+		List<Tipus> tipus = tps.findAll();
+		List<Plantilla> templates = plc.findAll();
+		model.put("tipus", tipus);
+		model.put("templates", templates);
+		System.out.println(idProces);
+		
+		if(!idProces.equals("0") && (action.equals("duplicate") || action.equals("edit"))) {
+			p = ps.findByID(Integer.parseInt(idProces));
+			if(action.equals("duplicate")) {
+				p.setVersio(Integer.toString(Integer.parseInt(p.getVersio())+1));
+				p.setIdProces(p.getIdProces()+1);
+				p.setCursActivacio(Year.now().getValue());
+				p.setCursAvaluat(null);
+			}
+			model.put("tasks", this.tas.findByProces(Integer.parseInt(idProces)).stream().map(TascaDTO::new).toList());
+			ProcesDTO pDto = new ProcesDTO(p);
+			model.put("procedure", pDto);
+			List<Rol> roles = rs.findAssignables();
+			model.put("roles", roles.stream().map(RolDTO::new).toList());
+            return model;
+		}
+		else if(!idProces.equals("0") && action.equals("remove")) {
+			p = ps.findByID(Integer.parseInt(idProces));
+			ProcesDTO pDto = new ProcesDTO(p);
+			model.put("procedure", pDto);
+			return model;
+		}
+		else {
+			ProcesDTO pDto = new ProcesDTO(p);
+			model.put("procedure", pDto);
+			List<Rol> roles = rs.findAssignables();
+			model.put("roles", roles.stream().map(RolDTO::new).toList());
+			return model;
+		}	
+	}
+
+	@PostMapping("/procedure/form/template")
+	public HashMap<String, Object> formTemplate(@RequestParam("id") Integer idPlantilla,
+			@RequestParam("action") String action) throws IOException {	
+		
+		HashMap<String, Object> model = new HashMap<>();
+
+		if(action.equals("new")) {
+			Plantilla p = new Plantilla();
+				model.put("editable", true);
+			model.put("template", p);
+			model.put("redirect", true);
+			return model;
+		}
+		else if(action.equals("edit")) {
+			Plantilla p = plc.findByID(idPlantilla);
+			model.put("editable", false);
+			model.put("template", p);
+			model.put("redirect", true);
+			return model;
+		}
+		else if(action.equals("duplicate")) {
+			Plantilla p = plc.findByID(idPlantilla);
+			p.setVersio(p.getVersio()+1);
+			model.put("editable", true);
+			model.put("template", p);
+			model.put("redirect", true);
+			return model;
+		}
+		model.put("redirect", false);
+		return model;
+	}
+
+}

+ 46 - 0
src/main/java/es/uv/saic/web/StatsController.java

@@ -0,0 +1,46 @@
+package es.uv.saic.web;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.Email;
+import es.uv.saic.service.EmailService;
+
+// Controller to handle admin statistics page
+@RestController
+public class StatsController {
+	
+	@Autowired
+    private EmailService emailService;
+
+	public static class PendingEmail{
+		private String username;
+		private String fullName;
+		private String email;
+		public void setUsername(String username) {this.username = username;}
+		public void setFullName(String fullName) {this.fullName = fullName;}
+		public void setEmail(String email) {this.email = email;}
+		public String getUsername() {return this.username;}
+		public String getFullName() {return this.fullName;}
+		public String getEmail() {return this.email;}
+		
+	}
+
+	@GetMapping("/admin/get/pendingEmails")
+	public  List<PendingEmail> getPendingEmails() {
+		List<PendingEmail> pendingEmails = new ArrayList<PendingEmail>();
+		for(Email e : emailService.getPendingQueue()) {
+			PendingEmail p = new PendingEmail();
+			p.setUsername(e.getUsuari().getUsuari());
+			p.setFullName(e.getUsuari().getNom()+ " " +e.getUsuari().getCognoms());
+			p.setEmail(e.getUsuari().getEmail());
+			pendingEmails.add(p);
+		}
+		
+		return pendingEmails;
+	}
+}

+ 331 - 0
src/main/java/es/uv/saic/web/SupervisionController.java

@@ -0,0 +1,331 @@
+package es.uv.saic.web;
+
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.DummyDataTransfer;
+import es.uv.saic.shared.domain.Instancia;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.domain.InstanciaTransfer;
+import es.uv.saic.shared.domain.Organ;
+import es.uv.saic.shared.domain.SupervisionSearchParams;
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.dto.EvidenciaBuscadorDTO;
+import es.uv.saic.service.InstanciaService;
+import es.uv.saic.service.InstanciaTascaService;
+import es.uv.saic.service.OrganService;
+import es.uv.saic.service.TascaService;
+import es.uv.saic.service.UsuariService;
+import es.uv.saic.service.UsuarisRolService;
+
+// Render the supervision page and handle supervision searches
+@RestController
+@RequestMapping("/supervision")
+public class SupervisionController {
+	
+	@Autowired
+	private UsuariService us;
+	@Autowired
+	private InstanciaService is;
+	@Autowired
+	private UsuarisRolService urs;
+	@Autowired
+	private OrganService ors;
+	@Autowired
+	private InstanciaTascaService its;
+	@Autowired
+	private TascaService tas;
+
+	/*
+	 * Renders the supervision page
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @param session The HTTP session
+	 * @return The name of the view to render
+	 */
+	@PostMapping
+	public HashMap<String, Object> supervisionForm(@RequestParam String usuari, @RequestBody Optional<SupervisionSearchParams> ssp) {
+		HashMap<String, Object> model = new HashMap<>();
+
+		if(usuari.equals("procedures") || usuari.equals("evidences"))
+			usuari = "admin";
+
+		Usuari u = us.findByUsername(usuari);
+
+		List<DummyDataTransfer> supervisable_procs = new ArrayList<DummyDataTransfer>();
+		List<DummyDataTransfer> supervisable_years = new ArrayList<DummyDataTransfer>();
+		List<DummyDataTransfer> supervisable_orgs = new ArrayList<DummyDataTransfer>();
+		List<DummyDataTransfer> supervisable_cents = new ArrayList<DummyDataTransfer>();
+		
+		if(u.isGranted()){ // Admin o UQ
+			List<Instancia> i_list = is.findAll();
+			for(Instancia inst : i_list) {
+				DummyDataTransfer p = new DummyDataTransfer();
+				DummyDataTransfer y = new DummyDataTransfer();
+				DummyDataTransfer o = new DummyDataTransfer();
+				p.setText(inst.getProces().getNomProces());
+				p.setValue(inst.getProces().getIdProces());
+				y.setText(Integer.toString(inst.getProces().getCursAvaluat()-1) + " - " + Integer.toString(inst.getProces().getCursAvaluat()));
+				y.setValue(inst.getProces().getCursAvaluat());
+				o.setText(inst.getOrgan().getNomCas());
+				o.setText2(inst.getOrgan().getNomVal());
+				o.setValue(inst.getOrgan().getId().getLugar());
+				o.setText3(inst.getOrgan().getId().getTlugar());
+				if(!supervisable_procs.contains(p)) {
+					supervisable_procs.add(p);
+				}
+				if(!supervisable_years.contains(y)) {
+					supervisable_years.add(y);
+				}
+				if(!supervisable_orgs.contains(o) && inst.getOrgan().getId().getTlugar().equals("T") && inst.getOrgan().getId().getLugar() > 2) {
+					supervisable_orgs.add(o);
+				}
+			}
+			DummyDataTransfer o = new DummyDataTransfer();
+			o = new DummyDataTransfer();
+			Organ org = ors.findByID("T", 1);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(1);
+			supervisable_orgs.add(o);
+			o = new DummyDataTransfer();
+			org = ors.findByID("T", 2);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(2);
+			supervisable_orgs.add(o);
+			o = new DummyDataTransfer();
+			org = ors.findByID("T", 0);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(0);
+			supervisable_orgs.add(o);
+			List<Organ> org_list = ors.findCurrentCentres();
+			for(Organ or : org_list) {
+				DummyDataTransfer c = new DummyDataTransfer();
+				c.setText(or.getNomCas());
+				c.setText2(or.getNomVal());
+				c.setValue(or.getId().getLugar());
+				supervisable_cents.add(c);
+			}
+			List<DummyDataTransfer> supervisable_evs = new ArrayList<DummyDataTransfer>();
+			List<EvidenciaBuscadorDTO> evs_list = tas.getAllEvidencies();
+			for(EvidenciaBuscadorDTO e : evs_list) {
+				DummyDataTransfer c = new DummyDataTransfer();
+				c.setText(e.getCodiEvidencia());
+				c.setText2(e.getNomEvidenciaCas());
+				c.setText3(e.getNomEvidenciaVal());
+				supervisable_evs.add(c);
+			}
+			supervisable_cents.sort(Comparator.comparing(a -> a.getText()));
+			model.put("granted", true);
+			model.put("sup_evs", supervisable_evs);
+			model.put("sup_cents", supervisable_cents);
+		}
+		else { // Otros     
+			List<BigInteger> i_list = this.us.getSupervisableProcessos(urs.findActiveRols((u)));
+			for(BigInteger id : i_list) {
+				Instancia inst = is.findByID(id);
+				DummyDataTransfer p = new DummyDataTransfer();
+				DummyDataTransfer y = new DummyDataTransfer();
+				DummyDataTransfer o = new DummyDataTransfer();
+				p.setText(inst.getProces().getNomProces());
+				p.setText2(inst.getProces().getNomProces() + " " + inst.getProces().getTitolCas());
+				p.setText3(inst.getProces().getNomProces() + " " + inst.getProces().getTitolVal());
+				y.setText(Integer.toString(inst.getProces().getCursAvaluat()-1) + " - " + Integer.toString(inst.getProces().getCursAvaluat()));
+				y.setValue(inst.getProces().getCursAvaluat());
+				o.setText(inst.getOrgan().getNomCas());
+				o.setText2(inst.getOrgan().getNomVal());
+				o.setValue(inst.getOrgan().getId().getLugar());
+				o.setText3(inst.getOrgan().getId().getTlugar());
+				if(!supervisable_procs.contains(p)) {
+					supervisable_procs.add(p);
+				}
+				if(!supervisable_years.contains(y)) {
+					supervisable_years.add(y);
+				}
+				if(!supervisable_orgs.contains(o) && inst.getOrgan().getId().getTlugar().equals("T") && inst.getOrgan().getId().getLugar() > 2) {
+					supervisable_orgs.add(o);
+				}
+			}
+			DummyDataTransfer o = new DummyDataTransfer();
+			o = new DummyDataTransfer();
+			Organ org = ors.findByID("T", 1);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(1);
+			supervisable_orgs.add(o);
+			o = new DummyDataTransfer();
+			org = ors.findByID("T", 2);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(2);
+			supervisable_orgs.add(o);
+			o = new DummyDataTransfer();
+			org = ors.findByID("T", 0);
+			o.setText(org.getNomCas());
+			o.setText2(org.getNomVal());
+			o.setValue(0);
+			supervisable_orgs.add(o);
+			model.put("granted", false);
+		}
+		
+		supervisable_orgs.sort(Comparator.comparing(a -> a.getText()));
+		
+		model.put("sup_procs", supervisable_procs);
+		model.put("sup_years", supervisable_years);
+		model.put("sup_orgs", supervisable_orgs);
+		
+		if(ssp != null) {
+			model.put("restore", ssp);
+			model.put("removeAttribute", "searchParams");;
+		}
+		else {
+			model.put("restore", null);
+		}
+		
+		return model;
+	}
+	
+	/*
+	 * Handles the supervision search form submission
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @param centres The selected centers
+	 * @param cursos The selected years
+	 * @param titulacions The selected titulacions
+	 * @param procediments The selected procedures
+	 * @param evidencies The selected evidences
+	 * @param searchType The type of search (by procedures or evidences)
+	 * @param session The HTTP session
+	 * @return The name of the view to render
+	 */
+	@PostMapping("/search")
+	public HashMap<String, Object> supervisionSearch(
+			@RequestParam(name="centers[]", required=false) List<Integer> centres,
+			@RequestParam("years[]") List<Integer> cursos,
+			@RequestParam("titulations[]") List<Integer> titulacions,
+			@RequestParam(name="procedures[]", required=false) List<String> procediments,
+			@RequestParam(name="evidences[]", required=false) List<String> evidencies,
+			@RequestParam(name="searchType", required=false) String searchType,
+			@RequestParam String usuari) {
+		HashMap<String, Object> model = new HashMap<>();
+
+		if(usuari.equals("procedures") || usuari.equals("evidences"))
+			usuari = "admin";
+
+		Usuari u = us.findByUsername(usuari);
+
+		if(centres == null) { centres = us.getSupervisableCentres(u); }
+		this.loadResults(model, u, centres, titulacions, cursos, procediments, evidencies, searchType);
+		model.put("searchParams", new SupervisionSearchParams(centres, titulacions, cursos, procediments, evidencies, searchType));
+		return model;
+	}
+	
+	/*
+	 * Restores the previous search results from the session
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @param session The HTTP session
+	 * @return The name of the view to render
+	 */
+	@PostMapping("/restore")
+	public HashMap<String, Object> restoreSearch(@RequestBody String usuari, @RequestBody SupervisionSearchParams ssp) {
+		HashMap<String, Object> model = new HashMap<>();
+		Usuari u = us.findByUsername(usuari);
+
+		if(ssp != null) {
+			this.loadResults(model, u, ssp.getCentres(), ssp.getTitulacions(), ssp.getCursos(), ssp.getProcediments(), ssp.getEvidencies(), ssp.getSearchType());
+		}
+
+		return model; 
+	}
+	
+	/*
+	 * Loads the search results based on the provided filters
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @param centres The selected centers
+	 * @param titulacions The selected titulacions
+	 * @param cursos The selected years
+	 * @param procediments The selected procedures
+	 * @param evidencies The selected evidences
+	 * @param searchType The type of search (by procedures or evidences)
+	 */
+	public void loadResults(HashMap<String, Object> model, Usuari u, List<Integer> centres, List<Integer> titulacions, 
+			List<Integer> cursos, List<String> procediments, List<String> evidencies, String searchType) {
+		List<InstanciaTransfer> plist = new ArrayList<InstanciaTransfer>();
+		List<BigInteger> supervisables;
+		if(searchType != null) {
+			if(searchType.equals("evidences")) {
+				supervisables = this.is.filterSupervisablesByEvidencies(centres, titulacions, cursos, evidencies);
+			}
+			else {
+				supervisables = this.is.filterSupervisables(centres, titulacions, cursos, procediments);
+			}
+		}
+		else {
+			supervisables = this.is.filterSupervisables(centres, titulacions, cursos, procediments);
+		}
+		Instancia i;
+		InstanciaTransfer it;
+		InstanciaTasca itt;
+		for(BigInteger p_id: supervisables) {
+			i = this.is.findByID(p_id);
+			it = new InstanciaTransfer();
+			it.setIdInstancia(i.getIdInstancia());
+			it.setIdProces(i.getProces().getIdProces());
+			it.setNomProces(i.getProces().getNomProces());
+			it.setCursAvaluat(i.getProces().getCursAvaluat());
+			it.setCursActivacio(i.getProces().getCursActivacio());
+			it.setTitolCas(i.getProces().getTitolCas());
+			it.setTitolVal(i.getProces().getTitolVal());
+			it.setDescripcioCas(i.getProces().getDescripcioCas());
+			it.setDescripcioVal(i.getProces().getDescripcioVal());
+			it.setEstat(i.getEstat());
+			if(i.getOrgan().getId().getTlugar().equals("C")) {
+				Organ o = ors.findByID("T", i.getTitulacio());
+				it.setTitulacioCas(o.getNomCas());
+				it.setTitulacioVal(o.getNomVal());
+				it.setCentreCas(i.getOrgan().getNomCas());
+				it.setCentreVal(i.getOrgan().getNomVal());
+			}
+			else {
+				it.setTitulacioCas(i.getOrgan().getNomCas());
+				it.setTitulacioVal(i.getOrgan().getNomVal());
+				it.setCentreCas(i.getOrgan().getOrgan().getNomCas());
+				it.setCentreVal(i.getOrgan().getOrgan().getNomVal());
+			} 
+			if(i.getEstat().equals("A")) {
+				itt = its.findActiveByInstancia(i.getIdInstancia());
+				it.setDataLimTascaActiva(itt.getTasca().getDataLim());
+				it.setDataTascaActiva(itt.getData());
+				it.setNomTascaActivaCas(itt.getTasca().getTitolCas());
+				it.setNomTascaActivaVal(itt.getTasca().getTitolVal());
+				it.setDescTascaActivaCas(itt.getTasca().getDescripcioCas());
+				it.setDescTascaActivaVal(itt.getTasca().getDescripcioVal());
+				it.setIdTascapActiva(itt.getTasca().getIdTascap());
+				it.setTascaActivaExpired(itt.getTasca().isExpired());
+				it.setTascaActivaAssignedToUser(its.isUserAuthorized(u, itt.getIdInstanciaTasca()));
+			}
+			plist.add(it);
+		}
+		model.put("procedure_list", plist);
+	}
+	
+}

+ 87 - 0
src/main/java/es/uv/saic/web/TascaController.java

@@ -0,0 +1,87 @@
+package es.uv.saic.web;
+
+import java.io.IOException;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.DummyDataTransfer;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.dto.EvidenciaBuscadorDTO;
+import es.uv.saic.shared.dto.InstanciaTascaDTO;
+import es.uv.saic.shared.dto.NomProcesOrganDTO;
+import es.uv.saic.shared.dto.ProcesDTO;
+import es.uv.saic.shared.dto.TascaDTO;
+import es.uv.saic.shared.dto.TascaInformeTransferDTO;
+import es.uv.saic.service.InstanciaTascaService;
+import es.uv.saic.service.TascaService;
+
+
+@RestController
+public class TascaController {
+    @Autowired
+    TascaService ts;
+
+    @Autowired
+    InstanciaTascaService its;
+
+    @GetMapping("/instanciaTasca/{id}")
+    public InstanciaTascaDTO findInstanciaTascaById(@PathVariable("id") BigInteger id) {
+        return its.findByIdDTO(id);
+    }
+
+    @PostMapping("/tasca/informe")
+    public TascaInformeTransferDTO getLastByProcName(@RequestBody ProcesDTO tascaDTO) {
+        return its.getLastByProcName(tascaDTO.getNomProces(),
+            tascaDTO.getLugar(), tascaDTO.getLugar2(), tascaDTO.getTambit());
+    }
+    
+    @GetMapping("/tasca/{idProces}/{idTascap}")
+    public TascaDTO getByProcesTascap(@PathVariable Integer idProces, @PathVariable Integer idTascap) {
+        return new TascaDTO(ts.getByProcesTascap(idProces, idTascap));
+    }
+    
+    @PostMapping("/instanciaTasca/report")
+    InstanciaTascaDTO getReportFromNomProcesOrgan(@RequestBody NomProcesOrganDTO nomProcesOrgan) {
+            InstanciaTasca itasca = its.getReportFromNomProcesOrgan(nomProcesOrgan.getNomProces(), nomProcesOrgan.getTlugar(), nomProcesOrgan.getLugar(), nomProcesOrgan.getCentre(), nomProcesOrgan.getTitulacio());
+            return new InstanciaTascaDTO(itasca);
+    }
+
+    // POST para buscar pas evidencias a partir del año, centro y titulación
+	@PostMapping("/tasca/evidences")
+	public HashMap<String, Object> getEvidencesByCenterTitulationYear(
+			@RequestParam(name="centers[]", required=false) List<Integer> centres,
+			@RequestParam("years[]") List<Integer> cursos,
+			@RequestParam("titulations[]") List<Integer> titulacions) throws IOException {	
+		
+        HashMap<String, Object> model = new HashMap<>();
+
+        List<DummyDataTransfer> supervisable_evs = new ArrayList<DummyDataTransfer>();
+		List<EvidenciaBuscadorDTO> evs_list = ts.getEvidenciesByCentreTitulacioCurs(centres, titulacions, cursos);		
+		for(EvidenciaBuscadorDTO e : evs_list) {
+			DummyDataTransfer c = new DummyDataTransfer();
+			c.setText(e.getCodiEvidencia());
+			c.setText2(e.getNomEvidenciaCas());
+			c.setText3(e.getNomEvidenciaVal());
+			supervisable_evs.add(c);
+		}
+        
+		model.put("sup_evs", supervisable_evs);
+		return model;
+	}
+
+    @GetMapping("/tasca/evidences/{idProces}")
+    public List<EvidenciaBuscadorDTO> getEvidencesByProcedure(@PathVariable Integer idProces) {
+        return ts.getEvidencesByProcedure(idProces);
+    }
+    
+}

+ 88 - 0
src/main/java/es/uv/saic/web/TestController.java

@@ -0,0 +1,88 @@
+package es.uv.saic.web;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.security.access.annotation.Secured;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import es.uv.saic.shared.domain.Instancia;
+import es.uv.saic.shared.domain.InstanciaTasca;
+import es.uv.saic.shared.dto.PdfDTO;
+import es.uv.saic.shared.feign.PlantillaClient;
+import es.uv.saic.service.InstanciaService;
+import es.uv.saic.service.InstanciaTascaService;
+
+//	Generates PDFs for all instances of a given process and task for admin user 
+@Controller
+public class TestController {
+		
+	@Autowired 
+	private InstanciaService ins;
+	@Autowired 
+	private InstanciaTascaService its;
+	@Autowired
+	private PlantillaClient plc;
+	
+	@Value("${saic.data.filePath}")
+	private String filePath;
+	@Value("${saic.data.templates.fileNotFound}")
+	private String fileNotFound;
+	@Value("${saic.data.templates.filePath}")
+	private String templatePath;
+	@Value("${saic.data.templates.logoPath}")
+	private String logoPath;	
+	
+	/*
+	 * Generates PDFs for all instances of a given process and task for admin user
+	 * 
+	 * @param auth The authentication object
+	 * @return A string indicating the result of the operation
+	 * @throws FileNotFoundException If a required file is not found
+	 * @throws IOException If an I/O error occurs
+	 * @throws InterruptedException If the thread is interrupted
+	 */
+	@GetMapping("/admin/pdf")
+	@Secured({"ROLE_ADMIN"})
+	@ResponseBody
+	public String generatePDF(Authentication auth) throws FileNotFoundException, IOException, InterruptedException {
+		
+		// 94231 y 94232    tascap = 300
+		List<Instancia> instancies = ins.findByIdProces(94231);
+		for(Instancia i : instancies) {
+			InstanciaTasca it = this.its.findByInstanciaTascap(i.getIdInstancia(), 300);
+			if(it.getEstat() != null) {
+				if(it.getEstat().equals("E")) {
+					PdfDTO pdf = new PdfDTO(it.getText(), it.getIdInstanciaTasca());
+					String dst = plc.savePDF(pdf);
+					it.setEvidencia(dst);
+					its.save(it);
+				}
+			}	
+		}
+		
+		instancies = new ArrayList<Instancia>();
+		instancies = ins.findByIdProces(94231);
+		for(Instancia i : instancies) {
+			InstanciaTasca it = this.its.findByInstanciaTascap(i.getIdInstancia(), 300);
+			if(it.getEstat() != null) {
+				if(it.getEstat().equals("E")) {
+					PdfDTO pdf = new PdfDTO(it.getText(), it.getIdInstanciaTasca());
+					String dst = plc.savePDF(pdf);
+					it.setEvidencia(dst);
+					its.save(it);
+				}
+			}	
+		}
+		
+		return "OK";
+	}
+	
+}

+ 67 - 0
src/main/java/es/uv/saic/web/UsuariController.java

@@ -0,0 +1,67 @@
+package es.uv.saic.web;
+
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.shared.domain.Usuari;
+import es.uv.saic.shared.domain.UsuarisRol;
+import es.uv.saic.shared.dto.RolDTO;
+import es.uv.saic.shared.dto.UsuariDTO;
+import es.uv.saic.service.RolService;
+import es.uv.saic.service.UsuariService;
+import es.uv.saic.service.UsuarisRolService;
+
+
+
+@RestController
+public class UsuariController {
+    @Autowired
+    UsuariService us;
+    
+    @Autowired
+    UsuarisRolService urs;
+
+    @Autowired
+    RolService rs;
+
+    @PostMapping("/user/granted")
+    public boolean isGrantedUser(@RequestBody Usuari usuari) {
+        return urs.isGrantedUser(usuari);
+    }
+    
+    @PostMapping("/user/supervisor")
+    public boolean isGrantedSupervisor(@RequestBody Usuari usuari) {
+        return urs.isGrantedSupervisor(usuari);
+    }
+
+    @PostMapping("/findActiveRols")
+    public List<UsuarisRol> findActiveRols(@RequestBody Usuari usuari) {
+        return urs.findActiveRols(usuari);
+    }
+
+	public boolean exists(String usuari, String tlugar, Integer lugar) {
+		return urs.exists(usuari, tlugar, lugar);
+	}
+
+    @GetMapping("/user/rols")
+    public List<RolDTO> findAllRols() {
+        return rs.findAll().stream().map(RolDTO::new).toList();
+    }
+
+    @GetMapping("/user")
+    public List<UsuariDTO> findAllUsers() {
+        return us.findAll().stream().map(UsuariDTO::new).toList();
+    }
+    
+    @GetMapping("/user/assignables")
+    public List<RolDTO> findAssignables() {
+        return rs.findAssignables().stream().map(RolDTO::new).toList();
+    }
+    
+
+}

+ 80 - 0
src/main/java/es/uv/saic/web/WikiController.java

@@ -0,0 +1,80 @@
+package es.uv.saic.web;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.core.Authentication;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import es.uv.saic.shared.domain.Wiki;
+import es.uv.saic.service.WikiService;
+
+// This class is meant to fill/edit the wiki and wikiEditor html
+@Controller
+@RequestMapping("/wiki")
+public class WikiController {
+	
+	@Autowired
+	WikiService ws;
+
+	/*
+	 * Renders the wiki page
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @return The name of the view to render
+	 */
+	@GetMapping
+	public String renderWiki(Model model, Authentication auth) {
+		return "wiki";
+	}
+	
+	/*
+	 * Renders the wiki editor page
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @return The name of the view to render
+	 */
+	@GetMapping("/editor")
+	public String renderWikiEditor(Model model, Authentication auth) {
+		return "wikiEditor";
+	}
+	
+	/*
+	 * Returns the wiki text for a given category
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @param categoria The category of the wiki
+	 * @return The wiki text
+	 */
+	@GetMapping("/text/{categoria}")
+	@ResponseBody
+	public Wiki renderWiki(Model model, Authentication auth, @PathVariable String categoria) {
+		return this.ws.findByCategoria(categoria);
+	}
+	
+	/*
+	 * Updates the wiki text for a given category
+	 * 
+	 * @param model The model to pass data to the view
+	 * @param auth The authentication object
+	 * @param cat The category of the wiki
+	 * @param text The new wiki text
+	 * @return The updated wiki object
+	 */
+	@PostMapping("/editor")
+	@ResponseBody
+	public Wiki updateWiki(@RequestParam String cat, @RequestParam String text) {
+		Wiki w = this.ws.findByCategoria(cat);
+		w.setText(text);
+		return this.ws.save(w);
+	}
+	
+}

+ 38 - 0
src/main/resources/application-dev.properties

@@ -0,0 +1,38 @@
+# Urls
+saic.url.domain = https://saicd.uv.es:4443
+saic.url.public = ${saic.url.domain}/public
+
+# Email config
+saic.mailer.queue.enabled = true
+saic.mailer.reminder.enabled = false
+saic.mailer.calendar.enabled = false
+saic.mailer.maxMailsPerRound = 20
+
+# Data parser config
+saic.parser.surveys.path = /DATA/saic-data/databases/
+saic.parser.surveys.enabled = false
+
+# Database config
+spring.datasource.url=jdbc:postgresql://127.0.0.1/saic_v2
+spring.datasource.password=docent1ia2.l6
+
+# Files and log config
+logging.file.path=/DATA/saic-data/logs
+logging.file.name=/DATA/saic-data/logs/saicd.log
+saic.data.filePath=/DATA/saic-data/files/
+saic.data.tmpPath=/DATA/saic-data/tmp/
+saic.data.docsPath=/DATA/saic-data/documents/
+saic.data.templates.filePath=/DATA/saic-data/templates/
+saic.data.templates.logoPath=/DATA/saic-data/templates/logos/
+saic.data.templates.fileNotFound=/DATA/saic-data/utils/filenotfound.pdf
+saic.data.master = /DATA/saic-data/databases/MasterOficial/
+saic.data.doctorado = /DATA/saic-data/databases/doctorat/
+saic.data.evdocente = /DATA/saic-data/databases/ev_docente/latest/
+
+# Scheduler
+saic.scheduler.expired.enabled=false
+
+# Desactivar RefreshScope en Eureka Client
+eureka.client.refresh.enable=false
+# Desactivar RefreshScope en SpringCloud
+spring.cloud.refresh.enabled=false

+ 40 - 0
src/main/resources/application-graal.properties

@@ -0,0 +1,40 @@
+# Urls
+saic.url.domain = https://saicd.uv.es:4443
+saic.url.public = ${saic.url.domain}/public
+
+# Email config
+saic.mailer.queue.enabled = true
+saic.mailer.reminder.enabled = false
+saic.mailer.calendar.enabled = false
+saic.mailer.maxMailsPerRound = 20
+
+# Data parser config
+saic.parser.surveys.path = /DATA/saic-data/databases/
+saic.parser.surveys.enabled = false
+
+# Database config
+spring.datasource.url=jdbc:postgresql://saicd.uv.es:5432/saic_v2
+spring.datasource.password=docent1ia2.l6
+
+# Files and log config
+logging.file.path = saic-data/logs
+logging.file.name = saic-data/logs/saic.log
+
+# Dummy
+saic.data.filePath = saic-data/files/
+saic.data.tmpPath = saic-data/tmp/
+saic.data.docsPath = saic-data/documents/
+saic.data.templates.filePath = saic-data/templates/
+saic.data.templates.logoPath = saic-data/templates/logos/
+saic.data.templates.fileNotFound = saic-data/utils/filenotfound.pdf
+saic.data.master = saic-data/databases/MasterOficial/
+saic.data.doctorado = saic-data/databases/doctorat/
+saic.data.evdocente = saic-data/databases/ev_docente/latest/
+
+# Scheduler
+saic.scheduler.expired.enabled=false
+
+# Desactivar RefreshScope en Eureka Client
+eureka.client.refresh.enable=false
+# Desactivar RefreshScope en SpringCloud
+spring.cloud.refresh.enabled=false

+ 39 - 0
src/main/resources/application-local.properties

@@ -0,0 +1,39 @@
+# Urls
+saic.url.domain = http://127.0.0.1
+saic.url.public = ${saic.url.domain}/public
+
+# JPA Debug
+#spring.jpa.show-sql=true 
+#spring.jackson.serialization.FAIL_ON_SELF_REFERENCES=false
+
+# Email config
+saic.mailer.queue.enabled = false
+saic.mailer.reminder.enabled = false
+saic.mailer.calendar.enabled = false
+saic.mailer.maxMailsPerRound = 20
+
+# Data parser config
+saic.parser.surveys.path = /home/dagarcos/DATA_SYNC/UV_APPS/SAIC/DATA/saic-data/databases/
+saic.parser.surveys.enabled = false
+
+# Database config
+spring.datasource.url=jdbc:postgresql://saicd.uv.es:5432/saic_v2
+spring.datasource.password=docent1ia2.l6
+
+# Files and log config
+saic.data.filePath=/home/mariomh/Documentos/SAIC/docs/files/
+saic.data.tmpPath=/home/mariomh/Documentos/SAIC/docs/tmp/
+saic.data.docsPath=/home/mariomh/Documentos/SAIC/docs/documents/
+saic.data.templates.filePath = /home/mariomh/Documentos/SAIC/docs/templates/
+saic.data.templates.logoPath = /home/mariomh/Documentos/SAIC/docs/templates/logos/
+saic.data.templates.fileNotFound = /home/mariomh/Documentos/SAIC/docs/filenotfound.pdf
+saic.data.master = /home/mariomh/Documentos/SAIC/docs/databases/MasterOficial/
+saic.data.doctorado = /home/mariomh/Documentos/SAIC/docs/databases/doctorat/
+saic.data.evdocente = /home/mariomh/Documentos/SAIC/docs/databases/ev_docente/latest/
+logging.file.path=/home/mariomh/Documentos/SAIC/docs/logs
+logging.file.name=/home/mariomh/Documentos/SAIC/docs/logs/saic.log
+
+# Scheduler
+saic.scheduler.expired.enabled=false
+
+

+ 38 - 0
src/main/resources/application-prod.properties

@@ -0,0 +1,38 @@
+# Urls
+saic.url.domain = https://saic.uv.es:4443
+saic.url.public = ${saic.url.domain}/public
+
+# Email config
+saic.mailer.queue.enabled = true
+saic.mailer.reminder.enabled = false
+saic.mailer.calendar.enabled = true
+saic.mailer.maxMailsPerRound = 20
+
+# Data parser config
+saic.parser.surveys.path = /DATA/saic-data/databases/
+saic.parser.surveys.enabled = false
+
+# Database config
+spring.datasource.url=jdbc:postgresql://127.0.0.1/saic_v2
+spring.datasource.password=docent1ia2.l6
+
+# Files and log config
+logging.file.path=/DATA/saic-data/logs
+logging.file.name=/DATA/saic-data/logs/saic.log
+saic.data.filePath=/DATA/saic-data/files/
+saic.data.tmpPath=/DATA/saic-data/tmp/
+saic.data.docsPath=/DATA/saic-data/documents/
+saic.data.templates.filePath=/DATA/saic-data/templates/
+saic.data.templates.logoPath=/DATA/saic-data/templates/logos/
+saic.data.templates.fileNotFound=/DATA/saic-data/utils/filenotfound.pdf
+saic.data.master = /DATA/saic-data/databases/MasterOficial/
+saic.data.doctorado = /DATA/saic-data/databases/doctorat/
+saic.data.evdocente = /DATA/saic-data/databases/ev_docente/latest/
+
+# Scheduler
+saic.scheduler.expired.enabled=false
+
+# Desactivar RefreshScope en Eureka Client
+eureka.client.refresh.enable=false
+# Desactivar RefreshScope en SpringCloud
+spring.cloud.refresh.enabled=false

+ 63 - 0
src/main/resources/application.properties

@@ -0,0 +1,63 @@
+spring.application.name = saic-core-service
+
+# EUREKA
+eureka.client.service-url.defaultZone=http://127.0.0.1:8761/eureka
+eureka.instance.prefer-ip-address=false
+eureka.instance.status-page-url-path=/
+
+# Actuator
+management.endpoints.web.exposure.include=*
+management.endpoint.health.show-details=always
+saic.actuator.validIp=147.156.0.0/16
+
+# Activate spring profiles
+spring.profiles.active=@activatedProperties@
+
+# Multilang config
+spring.messages.encoding=UTF-8
+
+# JPA config
+spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
+spring.jpa.properties.hibernate.jdbc.lob.non_contextual_creation=true
+spring.jpa.hibernate.ddl-auto=none
+#spring.jpa.properties.hibernate.show_sql=true
+
+
+# Logs config
+logging.logback.rollingpolicy.max-file-size=10MB
+logging.level.root=INFO
+
+# Max request and file size
+spring.servlet.multipart.max-file-size=128MB
+spring.servlet.multipart.max-request-size=128MB
+
+# Datasource config
+spring.datasource.driver-class-name=org.postgresql.Driver
+spring.datasource.username=postgres
+spring.sql.init.platform=postgres
+
+#spring.jpa.show-sql=true 
+
+# Email config
+spring.main.banner-mode=off
+spring.mail.protocol=smtp
+spring.mail.host=post.uv.es
+spring.mail.port=25
+spring.mail.properties.mail.smtp.auth = false
+spring.mail.properties.mail.smtp.starttls.enable = false
+
+# Server
+server.port=0
+server.forward-headers-strategy=native
+server.tomcat.remoteip.remote-ip-header=X-Forwarded-For
+server.tomcat.remoteip.protocol-header=X-Forwarded-Proto
+server.tomcat.remoteip.port-header=X-Forwarded-Port
+server.tomcat.use-relative-redirects=true
+server.tomcat.remoteip.internal-proxies=.*
+
+# Nombre de la cookie de sesion
+server.servlet.session.cookie.name=SAICSESSIONID
+
+saic.backups.database.enabled=true
+saic.backups.database.exec=/DATA/saic-data/backups/database_backup.sh
+saic.backups.database.filePath=/DATA/saic-data/backups/saic/

+ 462 - 0
src/main/resources/messages.properties

@@ -0,0 +1,462 @@
+
+##########################################
+#	GLOBAL LOCALES 
+##########################################
+global.lang.va = Valencià
+global.lang.es = Castellano
+global.exit = Eixir
+global.login = Accedir
+global.help = Instruccions
+global.lang = Idioma
+global.footer.span1 = Consultes procedimentals
+global.footer.span2 = Consultes tècniques
+global.footer.span3 = Unitat de Qualitat
+global.menu.tits = Panell de control
+global.menu.procedures = Tasques assignades
+global.menu.supervision = Supervisió
+global.menu.managers = Responsables
+global.menu.admin = Administració
+global.menu.admin.stats = Informació
+global.menu.admin.instances = Instanciació
+global.menu.admin.procedures = Procediments
+global.menu.admin.mailing = Correus
+global.menu.admin.calendar = Planificador
+global.menu.admin.acredita = Acreditacions
+global.menu.data = Dades i plantilles
+global.menu.data.templates = Plantilles (word)
+global.menu.data.editor = Plantilles (online)
+global.menu.data.import = Importar dades
+global.menu.data.data = Dades consolidades
+global.menu.data.parse = Consolidar dades
+global.menu.contact = Contacte
+global.menu.contact.title = Contacte
+global.menu.contact.title = Adreces de correu electrònic de contacte
+global.accept = Acceptar
+global.edit = Editar
+global.new = Afegir
+global.titleNew = Afegir element
+global.delete = Esborrar 
+global.cancel = Cancel·lar
+global.close = Tancar
+global.close.confirm = Es perdran les dades no guardades
+global.save = Guardar
+global.confirm = Confirmar
+global.return = Tornar
+global.loading = Carregant...
+global.download = Descarregar
+global.titleDownload = Descarregar evidència
+global.deleteWarning = Atenció, aquesta acció no es podrà desfer
+global.error.msg.i = Hi ha hagut un error, per favor, torneu a intentar-ho o contacteu amb 
+global.error.msg.f = reportant el següent error
+global.editEvidence = Eliminar/Editar evidència
+global.uploadLimits = Només es permet pujar un fitxer, per a pujar-ne més utilitzi un fitxer comprimit
+global.uploadSelectFile = Seleccione un fitxer
+global.noData = No hi ha dades disponibles
+global.noTasks = No té cap tasca assignada
+global.expire.notice = En 5 minuts es tancarà automàticament la sessió per inactivitat.
+global.expire.keepalive = Tanque aquesta alerta per seguir fent servir l'aplicació
+global.error.msg = Hi ha hagut un error, per favor, torneu a intentar-ho. Si l'error persisteix contacteu amb
+global.unautorized.msg = El seu usuari no esta autoritzat a accedir a aquest apartat
+global.selectors.noData = No hi ha elements marcats
+
+##########################################
+#	LOGIN FORM LOCALES
+##########################################
+login.h2 = Identificació UV
+login.button = Iniciar Sessió
+login.placeholder.username = Usuari
+login.placeholder.passwd = Contrasenya
+login.error = L'usuari o contrasenya introduïts no són correctes o el seu usuari no està autoritzat.
+login.expired = La seua sessió s'ha tancat per inactivitat o per accedir des d'un altre dispositiu/navegador, per favor, torneu a identificar-vos.
+login.session.info1 = La sessió es tancarà automàticament després de 60 minuts d'inactivitat.
+login.session.info2 = Per favor, deseu els canvis periòdicament.
+
+##########################################
+#	PROCEDURES MAIN PAGE LOCALES
+##########################################
+procedures.title = Tasques assignades
+procedures.subtitle = Faça clic sobre el procediment per a realizar la tasca
+procedures.title.tasks = Tasques associades al procediment
+procedures.title.flow = veure flux
+procedures.center = Centre
+procedures.titulation = Titulació
+procedures.year.eval = Curs avaluat
+procedures.year.act = activat el curs
+procedures.dateLimit = Data límit
+procedures.dateInstance = Data darrera acció
+procedures.status = Estat
+procedures.btn.continue = Acceptar i avançar
+procedures.btn.yes = Si
+procedures.btn.no = No
+procedures.btn.selectFile = Seleccione l'evidència
+procedures.btn.draft = Guardar esborrany
+procedures.done.user = Realitzada per
+procedures.done.date = Data de realització
+procedures.done.evidence = Evidència aportada
+procedures.done.response = Resposta
+procedures.done.response.s = SI
+procedures.done.response.n = NO
+procedures.status.a = Activa
+procedures.status.n = Realitzada (Resposta NO)
+procedures.status.s = Realitzada (Resposta SI)
+procedures.status.f = Realitzada
+procedures.status.e = Realitzada (Evidència adjunta)
+procedures.status.i = Inactiva
+procedures.status.title.a = Tasca activa
+procedures.status.title.p = Tasca inactiva
+procedures.status.title.o = No li correspon realitzar aquesta tasca
+procedures.status.title.r = Tasca realitzada
+procedures.activeTask = Tasca activa
+procedures.olderEvidences = Evidències generades en el procés anteriorment
+procedures.closed = Procediment finalitzat
+procedures.cancelled = Procediment tancat
+procedures.extensionAlert = Només es permet pujar fitxers amb extensió PDF
+procedures.assignedTo = Assignada a
+procedures.back = Tornar
+procedures.template = Descarregue la plantilla
+procedures.legend.active = Activa
+procedures.legend.other = No li correspon
+procedures.legend.blocked = Inactiva
+procedures.legend.done = Realitzada
+procedures.legend.expired = Endarrerida
+procedures.list.legend.active = Actiu
+procedures.list.legend.other = No li correspon
+procedures.list.legend.done = Finalitzat
+procedures.list.legend.expired = Endarrerit
+procedures.otherActions = Accions realitzades anteriorment en aquesta tasca
+procedures.editor.confirm = Esta segur que desitja continuar? En enviar l'evidència ja no podrà fer canvis sobre aquesta tasca.
+procedures.editor.msgsave = Esborrany guardat correctament
+procedures.session.expired = La seua sessió s'ha tancat per inactivitat o per accedir des d'un altre dispositiu/navegador. Tanque aquest diàleg per tornar a entrar a SAIC.
+procedures.nosupported = El seu navegador no suporta les funcionalitats necessàries per treballar amb SAIC. Es recomana utilitzar una versió actualitzada de Chrome o Firefox.
+procedures.autosave.lastdate = Última modificació
+procedures.autosave.none = Encara no s'ha guardat cap esborrany
+
+##########################################
+#	SUPERVISION PAGE LOCALES
+##########################################
+supervision.title = Supervisió dels procediments
+supervision.subtitle = Seleccione els criteris de filtrat dels procediments que desitja supervisar i faça clic en "Consultar".
+supervision.searchBy = Consultar per 
+supervision.years = Curs(os) d'avaluació del procediment
+supervision.titulations = Titulació(ns)
+supervision.centers = Centre(s)
+supervision.titulations.all = Totes
+supervision.titulations.all.g = Tots els graus del centre
+supervision.titulations.all.m = Tots els masters del centre
+supervision.procedures = Procediment(s)
+supervision.evidences = Evidència(es)
+supervision.search = Consultar
+supervision.procedures.title = Procediments que coincideixen amb els criteris seleccionats
+supervision.filter.apha = Ordenar alfabèticament
+supervision.filter.date = Ordenar per data de darrera acció
+supervision.filter.none = Eliminar filtres
+supervision.filter.download1 = Exportar llistat
+supervision.filter.download2 = Exportar per titulació
+
+##########################################
+#	MANAGERS PAGE LOCALES
+##########################################
+managers.title = Cerca de responsables
+managers.title2 = Responsables del seu centre
+managers.subtitle = Seleccione els criteris de filtrat dels responsables que desitja cercar i faça clic en "Cercar".
+managers.centers = Centre
+managers.titulations = Titulació(ns)
+managers.search = Cercar
+managers.search.title = Resultats de la cerca
+managers.noResults = No s'ha trobar cap responsable amb els criteris de cerca seleccionats
+managers.since = des del
+
+##########################################
+#	ADMIN PAGE LOCALES
+##########################################
+admin.instances.title = Instanciació de procediments
+admin.instances.subtitle = Seleccione un procediment junt amb el centre i la titulació sobre els que vol instanciar-lo
+admin.instances.error = Els següents procediments no s'han pogut instanciar
+admin.instances.done = S'han instanciat correctament els següents procediments
+admin.instances.selector.titulation = Titulació
+admin.instances.selector.center = Centre
+admin.instances.selector.procedure = Procediment
+admin.instances.selector.curs = Curs
+admin.instances.selector.taskType = Tipus de tasca
+admin.actions.instance = Instanciar
+admin.actions.load = Carregar
+admin.templates.title = Cerca i prova de plantilles
+admin.templates.subtitle = Seleccione un procediment junt amb el centre i la titulació sobre els que vol provar una plantilla
+admin.templates.info = Faça click sobre la plantilla que vol provar per a descarregar-la i veure els associats que s'han carregat correctament o faça click en editar per a modificar la llista d'indicadors
+admin.templates.indicators = Indicadors associats a la plantilla
+admin.templates.table.enq = Dades de enquestes
+admin.templates.table.serv = Dades del web service
+admin.procedures.title = Administració de procediments
+admin.procedures.subtitle = Seleccione el procediment i feu click en l'opció desitjada
+admin.procedures.action.new = Nou
+admin.procedures.action.duplicate = Duplicar
+admin.procedures.action.edit = Editar
+admin.procedures.action.remove = Esborrar
+admin.procedures.selector.procedure = Procediment
+admin.procedures.selector.years = Curs d' avaluació del procediment
+admin.procedures.selector.titulations = Titulació
+admin.procedures.selector.centers = Centre
+admin.procedures.confirm = Confirmar
+admin.procedures.info.procedure = Informació del procediment
+admin.procedures.info.task = Tasca
+admin.procedures.form.id = ID
+admin.procedures.form.nameProcedure = Codi
+admin.procedures.form.subtitleProcedure = Dimensió abreujada
+admin.procedures.form.context = Àmbit
+admin.procedures.form.version = Versió
+admin.procedures.form.activationYear = Curs d'activació
+admin.procedures.form.evalYear = Curs avaluat
+admin.procedures.form.titleCas = Titol en castellà
+admin.procedures.form.titleVal = Titol en valencià
+admin.procedures.form.descriptionCas = Descripció en castellà
+admin.procedures.form.descriptionVal = Descripció en valencià
+admin.procedures.form.comments = Informació adicional (ocult als usuaris) 
+admin.procedures.form.nameEvCas = Nom de la evidència en castellà
+admin.procedures.form.nameEvVal = Nom de la evidència en valencià
+admin.procedures.form.taskp = ID Tascap
+admin.procedures.form.limitDate = Data límit
+admin.procedures.form.options = Opcions
+admin.procedures.form.report = ¿Genera informe?
+admin.procedures.form.type = Tipus
+admin.procedures.form.type.1 = Evidència
+admin.procedures.form.type.2 = Si/No
+admin.procedures.form.type.3 = Continuar
+admin.procedures.form.evCode = Codi de l'evidència
+admin.procedures.form.next = ID tasca següent
+admin.procedures.form.next2 = ID tasca següent alternativa
+admin.procedures.form.role = Rol
+admin.procedures.form.addTask = Afegir tasca
+admin.procedures.form.removeConfirm = Si esborra aquest procediment les dades no es podràn recuperar
+admin.action.delete = Esborrar
+admin.action.deleteInstance = Esborrar procediment
+admin.action.deleteTask = Esborrar acció
+admin.action.reloadInstance = Esborrar i reiniciar procediment
+admin.action.closeInstance = Tancar procediment
+admin.action.reloadTask = Esborrar i repetir tasca
+admin.action.reactivate = Repetir tasca
+admin.action.edit = Editar tasca
+admin.action.attach = Adjuntar nova evidència
+admin.managers.add = Afegir responsable
+admin.managers.center = Centre
+admin.managers.titulation = Titulació
+admin.managers.user = Usuari
+admin.managers.firstname = Nom
+admin.managers.lastname = Cognoms
+admin.managers.email = Email
+admin.managers.role = Rol
+admin.managers.exists = Ja hi ha una entrada amb les dades seleccionades
+admin.managers.newRole = Afegir reponsable
+admin.managers.newUser = Nou usuari
+admin.stats.pending.title = Correus pendents d'enviar
+admin.stats.pending.total = Total de correus pendents d'enviar
+admin.stats.pending.username = Usuari
+admin.stats.pending.fullname = Nom
+admin.stats.pending.email = Correu
+admin.stats.sessions.title = Sesions actives en aquest moment
+admin.stats.sessions.id = ID de sessió
+admin.stats.sessions.username = Usuari
+admin.stats.sessions.last = Darrera acció
+admin.stats.sessions.expired = Expirada
+admin.stats.sessions.active.total = Total de sessions actives
+admin.stats.sessions.expired.total = Total de sessions expirades
+admin.stats.table.lengthMenu = Files
+admin.stats.table.zeroRecords = No s'han trobat resultats
+admin.stats.table.info = Pàgina _PAGE_ de _PAGES_
+admin.stats.table.infoEmpty = No hi ha dades per a mostrar
+admin.stats.table.infoFiltered = (filtrats de un total de _MAX_)
+admin.stats.table.next = Següent
+admin.stats.table.previous = Anterior
+admin.mailing.title = Correus electrònics
+admin.mailing.subtitle = Seleccione un centre i un rol al que enviar el correu
+admin.mailing.send = Enviar
+admin.mailing.center = Centre
+admin.mailing.role = Rol
+admin.mailing.subject = Assumpte
+admin.mailing.body = Missatge
+admin.mailing.sent.success = El missatge s'ha processat i serà enviat al més aviat possible 
+admin.mailing.sent.error = No s'han trobat usuaris per al rol i centres especificats
+admin.calendar.form.titleAdd = Planificar nova instància
+admin.calendar.form.titleEdit = Editar instància planificada
+admin.calendar.form.titleCopy = Copiar instancia existent
+admin.calendar.form.delete = Esborrar
+admin.calendar.form.instanciar = Instanciar
+admin.calendar.form.add = Afegir
+admin.calendar.form.date = Data
+admin.calendar.form.load = Carregar
+admin.calendar.form.changeDate = Canviar la data al
+admin.calendar.form.acredSel = Selecció per acreditació
+admin.calendar.form.input.grup.1 = Gener 
+admin.calendar.form.input.grup.4 = Abril
+admin.calendar.form.input.grup.9 = Septembre
+admin.calendar.form.input.grup.11 = Novembre
+admin.calendar.form.input.tambit.g = Graus
+admin.calendar.form.input.tambit.m = Másters
+admin.calendar.form.input.tambit.d = Doctorats
+admin.acredita.title = Resumen d'acreditacions 
+admin.acredita.form.view = Carregar
+admin.acredita.form.year = Any
+admin.acredita.form.group = Grup
+admin.acredita.form.groupnum.0 = Tots 
+admin.acredita.form.groupnum.1 = Gener 
+admin.acredita.form.groupnum.4 = Abril
+admin.acredita.form.groupnum.9 = Septembre
+admin.acredita.form.groupnum.11 = Novembre
+
+##########################################
+#	DASHBOARD PAGE LOCALES
+##########################################
+dashboard.header.published = Informes publicats
+dashboard.menu.summary = Resum
+dashboard.menu.procedures = Procediments SAIC
+dashboard.menu.docs = Documentació
+dashboard.menu.data = Indicadors
+dashboard.menu.tits = Titulacions
+dashboard.menu.managers = Responsables
+dashboard.summary.legend.active = Finalitzat
+dashboard.summary.legend.ontime = En curs
+dashboard.summary.legend.delayed = Endarrerit
+dashboard.summary.legend.closed = Tancat
+dashboard.data.sel.g = Dades de Grau
+dashboard.data.sel.m = Dades de Màster
+dashboard.data.sel.d = Dades de Doctorat
+dashboard.data.sel.rates = Taxes
+dashboard.data.sel.estud = Satisfacció estudiantat
+dashboard.data.sel.estud1 = Satisfacció estudiantat de 1er
+dashboard.data.sel.estud3 = Satisfacció estudiantat de 3er
+dashboard.data.sel.gradu = Satisfacció graduats/des
+dashboard.data.sel.prof = Satisfacció professorat
+dashboard.data.sel.eval = Evaluació docent
+dashboard.data.sel.ptgas = Satisfacció PTGAS
+dashboard.data.legend.1 = Titulació
+dashboard.data.legend.2 = Centre
+dashboard.data.legend.3 = Universitat
+dashboard.data.table.hr1 = Enquesta
+dashboard.data.table.hr2 = Indicador
+dashboard.data.table.hr3 = Ámbit
+dashboard.data.table.hr4 = Estudi
+dashboard.data.table.hr5 = Valor
+dashboard.data.table.hr6 = Curs
+dashboard.data.table.hr7 = CursD
+dashboard.data.table.hr8 = Tipus
+dashboard.data.table.hr9 = NºEnq
+dashboard.data.table.hr10 = Curs Enq
+dashboard.data.table.hr11 = Centre Orig
+dashboard.data.table.hr12 = Tit. Orig
+dashboard.data.table.hr13 = RUCT
+dashboard.data.table.hr14 = Data
+dashboard.data.table.btn1 = Dades del curs
+dashboard.data.table.btn2 = Exportar a Excel
+dashboard.data.table.btn3 = Exportar a CSV
+dashboard.data.table.btn4 = Exportar a PDF
+dashboard.data.table.btn5 = Mostrar/Ocultar columnes
+dashboard.data.table.btn6 = Veure gràfiques
+dashboard.data.table.btn7 = Veure totes les dades en format taula
+dashboard.acred.title.edit = Canviar dates
+dashboard.acred.date.segui = Seguiment
+dashboard.acred.date.acred = Pròxima acreditació
+dashboard.acred.date.renov = Última acreditació
+dashboard.acred.date.verif = Verificació
+dashboard.acred.date.impla = Implantació
+dashboard.acred.group.year = Curs pròx. acred.
+dashboard.acred.group.name = Grup pròx. acred.
+dashboard.acred.recom.title = Recommanacions *
+dashboard.acred.recom.msg = Hi ha recomanacions d'obligat compliment derivades de la darrera renovació.
+dashboard.acred.recom.info = Indicar si hi ha recomanacions d'obligat compliment pendents
+dashboard.acred.segui.title = Seguimiento **
+dashboard.acred.segui.info = Indicar si la titulació ha de passar seguiment obligatòriament
+dashboard.acred.segui.msg = La titulació ha de passar obligatòriament un seguiment en la data indicada al camp seguiment.
+dashboard.acred.codes = Codis interns
+dashboard.acred.table.title = Titulacions del centre
+dashboard.acred.table.ruct = RUCT
+dashboard.acred.table.tit = Titulació
+dashboard.acred.table.type = Tipus
+dashboard.acred.table.next = Pròx Accr.
+dashboard.acred.table.acred = Fecha Accr.
+dashboard.acred.table.renov = Renovació
+dashboard.acred.table.verif = Verificació
+dashboard.acred.table.impla = Implantació
+dashboard.acred.table.segui = Seguiment
+dashboard.acred.table.inter = InterUniv.
+dashboard.acred.docs.title = Documentació
+dashboard.gantt.selector = Veure procediments de
+dashboard.tree.selector.title = Veure procediments de
+dashboard.tree.selector.centre = Centre i totes les titulacions
+dashboard.tits.ruct = RUCT
+dashboard.tits.codes = Codis
+dashboard.tits.centre = Centre
+dashboard.tits.tit = Titulació
+dashboard.tits.type = Tipus 
+
+##########################################
+#	DATA PAGE LOCALES
+##########################################
+data.import.title = Importador de dades
+data.import.subtitle = Les dades que importi es quedaran en estat pendent de consolidar. De la mateixa manera, només podreu consultar les dades que es trobin en aquest mateix estat.
+data.import.note1 = El fitxer CSV ha de contenir, com a mínim, les columnes "curs", "titulacio", "centre" i "tipus"
+data.import.note2 = La columna "tipus" pot pendre els valors "avg", "min" o "max"
+data.import.note3 = * Només per a importació 
+data.import.note4 = * Només per a consulta
+data.import.note5 = S'han detectat les següents columnes en el fitxer seleccionat. Si desitja que s'ignore alguna columna, desmarque-la de la llista.
+data.show.title = Consultar dades consolidades
+data.consolide.title = Consolidar dades
+data.consolide.subtitle = Seleccione el conjunt de dades que desitja consolidar
+data.consolide.err1 = S'han detectat dades duplicades en el conjunt pendent de consolidar, si us plau, corregiu-lo abans de consolidar.
+data.consolide.err2 = S'han detectat errors d'integritat entre el conjunt seleccionat i les dades ja consolidades, si us plau, corregiu-les abans de consolidar.
+data.consolide.err3 = No s'ha pogut executar l'acció, comproveu de nou la integritat de les dades.
+data.consolide.count = Nombre de registres pendents de consolidar per al conjunt seleccionat
+data.consolide.tabdesc = Es mostren els registres per als quals s'han detectat problemes
+data.consolide.nodata = No hi ha dades pendents de consolidar per al conjunt seleccionat
+data.consolide.noerr = No s'han detectat errors
+data.consolide.ok = Acció realitzada correctament
+data.consolide.actions.info = Accions automàtiques disponibles
+data.consolide.actions.action1 = Esborrar duplicats
+data.consolide.actions.action2 = Esborrar consolidades
+data.consolide.actions.action3 = Esborrar pendents
+data.consolide.actions.msg = Nombre de registres esborrats
+
+data.input.source = Tipus de dades
+data.input.survey = Grup de dades
+data.input.year = Curs
+data.input.dstyear = Curs a imputar
+data.input.scope = Àmbit
+data.input.scope.t = Titulació
+data.input.scope.c = Centre
+data.input.scope.u = Universitat
+data.input.type = Tipus Titulació
+data.input.type.g = Grau
+data.input.type.m = Màster
+data.input.type.d = Doctorat
+data.input.type.u = Global
+data.input.tit = Titulació
+data.input.cen = Centre
+data.input.file = Seleccioneu o arrossegueu ací el fitxer CSV
+data.input.select = Trieu un conjunt de dades
+data.input.option = Trieu una opció
+data.input.delim = Delimitador
+data.input.dbsource = Origen
+data.input.importType = Tipus d'importació
+data.input.importType.file = Fitxer
+data.input.importType.db = Base de dades
+data.input.view = Vista/Taula a importar (nom_esquema.nom_vista)
+data.input.key = Clau
+
+data.btn.import = Importar
+data.btn.connect = Comprovar connexió
+data.btn.show = Consultar
+data.btn.delete = Eliminar
+data.btn.query = Consultar
+data.btn.check = Comprobar
+data.btn.run = Executar
+data.table.0 = Curs
+data.table.1 = Titulació
+data.table.2 = Centre
+data.table.3 = Àmbit
+data.table.4 = Tipus
+data.table.5 = Indicador
+data.table.6 = Valor
+data.table.7 = Num enq.
+data.table.8 = Titulació Orig.
+data.table.9 = Centre Orig.
+data.table.10 = RUCT
+data.table.11 = Usuari
+data.alert.connect = No s'ha pogut establir la connexió amb la vista indicada

+ 469 - 0
src/main/resources/messages_ca.properties

@@ -0,0 +1,469 @@
+
+##########################################
+#	GLOBAL LOCALES 
+##########################################
+global.lang.va = Valencià
+global.lang.es = Castellano
+global.exit = Eixir
+global.login = Accedir
+global.help = Instruccions
+global.lang = Idioma
+global.footer.span1 = Consultes procedimentals
+global.footer.span2 = Consultes tècniques
+global.footer.span3 = Unitat de Qualitat
+global.menu.tits = Panell de control
+global.menu.procedures = Tasques assignades
+global.menu.supervision = Supervisió
+global.menu.managers = Responsables
+global.menu.admin = Administració
+global.menu.admin.stats = Informació
+global.menu.admin.instances = Instanciació
+global.menu.admin.procedures = Procediments
+global.menu.admin.mailing = Correus
+global.menu.admin.calendar = Planificador
+global.menu.admin.acredita = Acreditacions
+global.menu.data = Dades i plantilles
+global.menu.data.templates = Plantilles (word)
+global.menu.data.editor = Plantilles (online)
+global.menu.data.import = Importar dades
+global.menu.data.data = Dades consolidades
+global.menu.data.parse = Consolidar dades
+global.menu.contact = Contacte
+global.menu.contact.title = Adreces de correu electrònic de contacte
+global.accept = Acceptar
+global.edit = Editar
+global.new = Afegir
+global.titleNew = Afegir element
+global.delete = Esborrar 
+global.cancel = Cancel·lar
+global.close = Tancar
+global.close.confirm = Es perdran les dades no guardades
+global.save = Guardar
+global.confirm = Confirmar
+global.return = Tornar
+global.loading = Carregant...
+global.download = Descarregar
+global.titleDownload = Descarregar evidència
+global.deleteWarning = Atenció, aquesta acció no es podrà desfer
+global.error.msg.i = Hi ha hagut un error, per favor, torneu a intentar-ho o contacteu amb 
+global.error.msg.f = reportant el següent error
+global.editEvidence = Eliminar/Editar evidència
+global.uploadLimits = Només es permet pujar un fitxer, per a pujar-ne més utilitzi un fitxer comprimit
+global.uploadSelectFile = Seleccione un fitxer
+global.noData = No hi ha dades disponibles
+global.noTasks = No té cap tasca assignada
+global.expire.notice = En 5 minuts es tancarà automàticament la sessió per inactivitat.
+global.expire.keepalive = Tanque aquesta alerta per seguir fent servir l'aplicació
+global.error.msg = Hi ha hagut un error, per favor, torneu a intentar-ho. Si l'error persisteix contacteu amb
+global.unautorized.msg = El seu usuari no esta autoritzat a accedir a aquest apartat
+global.selectors.noData = No hi ha elements marcats
+
+##########################################
+#	LOGIN FORM LOCALES
+##########################################
+login.h2 = Identificació UV
+login.button = Iniciar Sessió
+login.placeholder.username = Usuari
+login.placeholder.passwd = Contrasenya
+login.error = L'usuari o contrasenya introduïts no són correctes o el seu usuari no està autoritzat.
+login.expired = La seua sessió s'ha tancat per inactivitat o per accedir des d'un altre dispositiu/navegador, per favor, torneu a identificar-vos.
+login.session.info1 = Aquesta sessió es tancarà automàticament després de 60 minuts d'inactivitat o en accedir des d'un altre dispositiu/navegador.
+login.session.info2 = Per favor, deseu els canvis periòdicament.
+
+##########################################
+#	PROCEDURES MAIN PAGE LOCALES
+##########################################
+procedures.title = Tasques assignades
+procedures.subtitle = Faça clic sobre el procediment per a realizar la tasca
+procedures.title.tasks = Tasques associades al procediment
+procedures.title.flow = veure flux
+procedures.center = Centre
+procedures.titulation = Titulació
+procedures.year.eval = Curs avaluat
+procedures.year.act = activat el curs
+procedures.dateLimit = Data límit
+procedures.dateInstance = Data darrera acció
+procedures.status = Estat
+procedures.btn.continue = Acceptar i avançar
+procedures.btn.yes = Si
+procedures.btn.no = No
+procedures.btn.selectFile = Seleccione l'evidència
+procedures.btn.draft = Guardar esborrany
+procedures.done.user = Realitzada per
+procedures.done.date = Data de realització
+procedures.done.evidence = Evidència aportada
+procedures.done.response = Resposta
+procedures.done.response.s = SI
+procedures.done.response.n = NO
+procedures.status.a = Activa
+procedures.status.n = Realitzada (Resposta NO)
+procedures.status.s = Realitzada (Resposta SI)
+procedures.status.f = Realitzada
+procedures.status.e = Realitzada (Evidència adjunta)
+procedures.status.i = Inactiva
+procedures.status.title.a = Tasca activa
+procedures.status.title.p = Tasca inactiva
+procedures.status.title.o = No li correspon realitzar aquesta tasca
+procedures.status.title.r = Tasca realitzada
+procedures.activeTask = Tasca activa
+procedures.olderEvidences = Evidències generades en el procés anteriorment
+procedures.closed = Procediment finalitzat
+procedures.cancelled = Procediment tancat
+procedures.extensionAlert = Només es permet pujar fitxers amb extensió PDF
+procedures.assignedTo = Assignada a
+procedures.back = Tornar
+procedures.template = Descarregue la plantilla
+procedures.legend.active = Activa
+procedures.legend.other = No li correspon
+procedures.legend.blocked = Inactiva
+procedures.legend.done = Realitzada
+procedures.legend.expired = Endarrerida
+procedures.list.legend.active = Actiu
+procedures.list.legend.other = No li correspon
+procedures.list.legend.done = Finalitzat
+procedures.list.legend.expired = Endarrerit
+procedures.otherActions = Accions realitzades anteriorment en aquesta tasca
+procedures.editor.confirm = Esta segur que desitja continuar? En enviar l'evidència ja no podrà fer canvis sobre aquesta tasca.
+procedures.editor.msgsave = Esborrany guardat correctament
+procedures.session.expired = La seua sessió s'ha tancat per inactivitat o per accedir des d'un altre dispositiu/navegador. Tanque aquest diàleg per tornar a entrar a SAIC.
+procedures.nosupported = El seu navegador no suporta les funcionalitats necessàries per treballar amb SAIC. Es recomana utilitzar una versió actualitzada de Chrome o Firefox.
+procedures.autosave.lastdate = Última modificació
+procedures.autosave.none = Encara no s'ha guardat cap esborrany
+
+##########################################
+#	SUPERVISION PAGE LOCALES
+##########################################
+supervision.title = Supervisió dels procediments
+supervision.subtitle = Seleccione els criteris de filtrat dels procediments que desitja supervisar i faça clic en "Consultar".
+supervision.searchBy = Consultar per 
+supervision.years = Curs(os) d'avaluació del procediment
+supervision.titulations = Titulació(ns)
+supervision.centers = Centre(s)
+supervision.titulations.all = Totes
+supervision.titulations.all.g = Tots els graus del centre
+supervision.titulations.all.m = Tots els masters del centre
+supervision.procedures = Procediment(s)
+supervision.evidences = Evidència(es)
+supervision.search = Consultar
+supervision.procedures.title = Procediments que coincideixen amb els criteris seleccionats
+supervision.filter.apha = Ordenar alfabèticament
+supervision.filter.date = Ordenar per data de darrera acció
+supervision.filter.none = Eliminar filtres
+supervision.filter.download1 = Exportar llistat
+supervision.filter.download2 = Exportar per titulació
+
+##########################################
+#	MANAGERS PAGE LOCALES
+##########################################
+managers.title = Cerca de responsables
+managers.title2 = Responsables del seu centre
+managers.subtitle = Seleccione els criteris de filtrat dels responsables que desitja cercar i faça clic en "Cercar".
+managers.centers = Centre
+managers.titulations = Titulació(ns)
+managers.search = Cercar
+managers.search.title = Resultats de la cerca
+managers.noResults = No s'ha trobar cap responsable amb els criteris de cerca seleccionats
+managers.since = des del
+
+##########################################
+#	ADMIN PAGE LOCALES
+##########################################
+admin.instances.title = Instanciació de procediments
+admin.instances.subtitle = Seleccione un procediment junt amb el centre i la titulació sobre els que vol instanciar-lo
+admin.instances.error = Els següents procediments no s'han pogut instanciar
+admin.instances.done = S'han instanciat correctament els següents procediments
+admin.instances.selector.titulation = Titulació
+admin.instances.selector.center = Centre
+admin.instances.selector.procedure = Procediment
+admin.instances.selector.curs = Curs
+admin.instances.selector.taskType = Tipus de tasca
+admin.templates.title = Cerca i prova de plantilles
+admin.templates.subtitle = Seleccione un procediment junt amb el centre i la titulació sobre els que vol provar una plantilla
+admin.templates.info = Faça click sobre la plantilla que vol provar per a descarregar-la i veure els associats que s'han carregat correctament o faça click en editar per a modificar la llista d'indicadors
+admin.templates.indicators = Indicadors associats a la plantilla
+admin.templates.table.enq = Dades de enquestes
+admin.templates.table.serv = Dades del web service
+admin.actions.instance = Instanciar
+admin.actions.load = Carregar
+admin.procedures.title = Administració de procediments
+admin.procedures.subtitle = Seleccione el procediment i feu click en l'opció desitjada
+admin.procedures.action.new = Nou
+admin.procedures.action.duplicate = Duplicar
+admin.procedures.action.edit = Editar
+admin.procedures.action.remove = Esborrar
+admin.procedures.selector.procedure = Procediment
+admin.procedures.selector.years = Curs d' avaluació del procediment
+admin.procedures.selector.titulations = Titulació
+admin.procedures.selector.centers = Centre
+admin.procedures.confirm = Confirmar
+admin.procedures.info.procedure = Informació del procediment
+admin.procedures.info.task = Tasca
+admin.procedures.form.id = ID
+admin.procedures.form.nameProcedure = Codi
+admin.procedures.form.subtitleProcedure = Dimensió abreujada
+admin.procedures.form.context = Àmbit
+admin.procedures.form.version = Versió
+admin.procedures.form.activationYear = Curs d'activació
+admin.procedures.form.evalYear = Curs avaluat
+admin.procedures.form.titleCas = Titol en castellà
+admin.procedures.form.titleVal = Titol en valencià
+admin.procedures.form.descriptionCas = Descripció en castellà
+admin.procedures.form.descriptionVal = Descripció en valencià
+admin.procedures.form.nameEvCas = Nom de la evidència en castellà
+admin.procedures.form.nameEvVal = Nom de la evidència en valencià
+admin.procedures.form.comments = Informació adicional (ocult als usuaris) 
+admin.procedures.form.taskp = ID Tascap
+admin.procedures.form.limitDate = Data límit
+admin.procedures.form.options = Opcions
+admin.procedures.form.report = ¿Genera informe?
+admin.procedures.form.type = Tipus
+admin.procedures.form.type.1 = Evidència
+admin.procedures.form.type.2 = Si/No
+admin.procedures.form.type.3 = Continuar
+admin.procedures.form.evCode = Codi de l'evidència
+admin.procedures.form.next = ID tasca següent
+admin.procedures.form.next2 = ID tasca següent alternativa
+admin.procedures.form.role = Rol
+admin.procedures.form.addTask = Afegir tasca
+admin.procedures.form.removeConfirm = Si esborra aquest procediment les dades no es podràn recuperar
+admin.action.delete = Esborrar
+admin.action.deleteInstance = Esborrar procediment
+admin.action.deleteTask = Esborrar acció
+admin.action.reloadInstance = Esborrar i reiniciar procediment
+admin.action.closeInstance = Tancar procediment
+admin.action.reloadTask = Esborrar i repetir tasca
+admin.action.reactivate = Repetir tasca
+admin.action.edit = Editar tasca
+admin.action.attach = Adjuntar nova evidència
+admin.managers.add = Afegir responsable
+admin.managers.center = Centre
+admin.managers.titulation = Titulació
+admin.managers.user = Usuari
+admin.managers.firstname = Nom
+admin.managers.lastname = Cognoms
+admin.managers.email = Email
+admin.managers.role = Rol
+admin.managers.exists = Ja hi ha una entrada amb les dades seleccionades
+admin.managers.newRole = Afegir reponsable
+admin.managers.newUser = Nou usuari
+admin.stats.pending.title = Correus pendents d'enviar
+admin.stats.pending.total = Total de correus pendents d'enviar
+admin.stats.pending.username = Usuari
+admin.stats.pending.fullname = Nom
+admin.stats.pending.email = Correu
+admin.stats.sessions.title = Sesions actives en aquest moment
+admin.stats.sessions.id = ID de sessió
+admin.stats.sessions.username = Usuari
+admin.stats.sessions.last = Darrera acció
+admin.stats.sessions.expired = Expirada
+admin.stats.sessions.active.total = Total de sessions actives
+admin.stats.sessions.expired.total = Total de sessions expirades
+admin.stats.table.lengthMenu = Files
+admin.stats.table.zeroRecords = No s'han trobat resultats
+admin.stats.table.info = Pàgina _PAGE_ de _PAGES_
+admin.stats.table.infoEmpty = No hi ha dades per a mostrar
+admin.stats.table.infoFiltered = (filtrats de un total de _MAX_)
+admin.stats.table.next = Següent
+admin.stats.table.previous = Anterior
+admin.mailing.title = Correus electrònics
+admin.mailing.subtitle = Seleccione un centre i un rol al que enviar el correu
+admin.mailing.send = Enviar
+admin.mailing.center = Centre
+admin.mailing.role = Rol
+admin.mailing.subject = Assumpte
+admin.mailing.body = Missatge
+admin.mailing.sent.success = El missatge s'ha processat i serà enviat al més aviat possible 
+admin.mailing.sent.error = No s'han trobat usuaris per al rol i centres especificats
+admin.calendar.form.titleAdd = Planificar nova instància
+admin.calendar.form.titleEdit = Editar instància planificada
+admin.calendar.form.titleCopy = Copiar instancia existent
+admin.calendar.form.delete = Esborrar
+admin.calendar.form.instanciar = Instanciar
+admin.calendar.form.add = Afegir
+admin.calendar.form.date = Data
+admin.calendar.form.load = Carregar
+admin.calendar.form.changeDate = Canviar la data al
+admin.calendar.form.acredSel = Selecció per acreditació
+admin.calendar.form.input.grup.1 = Gener 
+admin.calendar.form.input.grup.4 = Abril
+admin.calendar.form.input.grup.9 = Septembre
+admin.calendar.form.input.grup.11 = Novembre
+admin.calendar.form.input.tambit.g = Graus
+admin.calendar.form.input.tambit.m = Másters
+admin.calendar.form.input.tambit.d = Doctorats
+admin.acredita.title = Resumen d'acreditacions 
+admin.acredita.form.view = Carregar
+admin.acredita.form.year = Any
+admin.acredita.form.group = Grup
+admin.acredita.form.groupnum.0 = Tots 
+admin.acredita.form.groupnum.1 = Gener 
+admin.acredita.form.groupnum.4 = Abril
+admin.acredita.form.groupnum.9 = Septembre
+admin.acredita.form.groupnum.11 = Novembre
+
+
+##########################################
+#	DASHBOARD PAGE LOCALES
+##########################################
+dashboard.header.published = Informes publicats
+dashboard.menu.summary = Resum
+dashboard.menu.procedures = Procediments SAIC
+dashboard.menu.docs = Documentació
+dashboard.menu.data = Indicadors
+dashboard.menu.tits = Titulacions
+dashboard.menu.managers = Responsables
+dashboard.summary.legend.active = Finalitzat
+dashboard.summary.legend.ontime = En curs
+dashboard.summary.legend.delayed = Endarrerit
+dashboard.summary.legend.closed = Tancat
+dashboard.data.sel.g = Dades de Grau
+dashboard.data.sel.m = Dades de Màster
+dashboard.data.sel.d = Dades de Doctorat
+dashboard.data.sel.rates = Taxes
+dashboard.data.sel.estud = Satisfacció estudiantat
+dashboard.data.sel.estud1 = Satisfacció estudiantat de 1er
+dashboard.data.sel.estud3 = Satisfacció estudiantat de 3er
+dashboard.data.sel.gradu = Satisfacció graduats/des
+dashboard.data.sel.prof = Satisfacció professorat
+dashboard.data.sel.eval = Evaluació docent
+dashboard.data.sel.ptgas = Satisfacció PTGAS
+dashboard.data.legend.1 = Titulació
+dashboard.data.legend.2 = Centre
+dashboard.data.legend.3 = Universitat
+dashboard.data.table.hr1 = Enquesta
+dashboard.data.table.hr2 = Indicador
+dashboard.data.table.hr3 = Ámbit
+dashboard.data.table.hr4 = Estudi
+dashboard.data.table.hr5 = Valor
+dashboard.data.table.hr6 = Curs
+dashboard.data.table.hr7 = CursD
+dashboard.data.table.hr8 = Tipus
+dashboard.data.table.hr9 = NºEnq
+dashboard.data.table.hr10 = Curs Enq
+dashboard.data.table.hr11 = Centre Orig
+dashboard.data.table.hr12 = Tit. Orig
+dashboard.data.table.hr13 = RUCT
+dashboard.data.table.hr14 = Data
+dashboard.data.table.btn1 = Dades del curs
+dashboard.data.table.btn2 = Exportar a Excel
+dashboard.data.table.btn3 = Exportar a CSV
+dashboard.data.table.btn4 = Exportar a PDF
+dashboard.data.table.btn5 = Mostrar/Ocultar columnes
+dashboard.data.table.btn6 = Veure gràfiques
+dashboard.data.table.btn7 = Veure totes les dades en format taula
+dashboard.acred.title.edit = Canviar dates
+dashboard.acred.date.segui = Seguiment
+dashboard.acred.date.acred = Pròxima acreditació
+dashboard.acred.date.renov = Última acreditació
+dashboard.acred.date.renov = Renovació
+dashboard.acred.date.verif = Verificació
+dashboard.acred.date.impla = Implantació
+dashboard.acred.group.year = Curs pròx. acred.
+dashboard.acred.group.name = Grup pròx. acred.
+dashboard.acred.recom.title = Recommanacions *
+dashboard.acred.recom.msg = Hi ha recomanacions d'obligat compliment derivades de la darrera renovació.
+dashboard.acred.recom.info = Indicar si hi ha recomanacions d'obligat compliment pendents
+dashboard.acred.segui.title = Seguimiento **
+dashboard.acred.segui.info = Indicar si la titulació ha de passar seguiment obligatòriament
+dashboard.acred.segui.msg = La titulació ha de passar obligatòriament un seguiment en la data indicada al camp seguiment.
+dashboard.acred.codes = Codis interns
+dashboard.acred.table.title = Titulacions del centre
+dashboard.acred.table.ruct = RUCT
+dashboard.acred.table.tit = Titulació
+dashboard.acred.table.type = Tipus
+dashboard.acred.table.next = Pròx Accr.
+dashboard.acred.table.acred = Fecha Accr.
+dashboard.acred.table.renov = Renovació
+dashboard.acred.table.verif = Verificació
+dashboard.acred.table.impla = Implantació
+dashboard.acred.table.segui = Seguiment
+dashboard.acred.table.inter = InterUniv.
+dashboard.acred.table.date = Data acreditació
+dashboard.acred.docs.title = Documentació
+dashboard.gantt.selector = Veure procediments de
+dashboard.tree.selector.title = Veure procediments de
+dashboard.tree.selector.centre = Centre i totes les titulacions
+dashboard.tits.ruct = RUCT
+dashboard.tits.codes = Codis
+dashboard.tits.centre = Centre
+dashboard.tits.tit = Titulació
+dashboard.tits.type = Tipus 
+
+##########################################
+#	DATA PAGE LOCALES
+##########################################
+data.import.title = Importador de dades
+data.import.subtitle = Les dades que importi es quedaran en estat pendent de consolidar. De la mateixa manera, només podreu consultar les dades que es trobin en aquest mateix estat.
+data.import.note1 = El fitxer CSV ha de contenir, com a mínim, les columnes "curs", "titulacio", "centre" i "tipus"
+data.import.note2 = La columna "tipus" pot pendre els valors "avg", "min" o "max"
+data.import.note3 = * Només per a importació 
+data.import.note4 = * Només per a consulta
+data.import.note5 = S'han detectat les següents columnes en el fitxer seleccionat. Si desitja que s'ignore alguna columna, desmarque-la de la llista.
+data.show.title = Consultar dades consolidades
+data.consolide.title = Consolidar dades
+data.consolide.subtitle = Seleccione el conjunt de dades que desitja consolidar
+data.consolide.err1 = S'han detectat dades duplicades en el conjunt pendent de consolidar, si us plau, corregiu-lo abans de consolidar.
+data.consolide.err2 = S'han detectat errors d'integritat entre el conjunt seleccionat i les dades ja consolidades, si us plau, corregiu-les abans de consolidar.
+data.consolide.err3 = No s'ha pogut executar l'acció, comproveu de nou la integritat de les dades.
+data.consolide.count = Nombre de registres pendents de consolidar per al conjunt seleccionat
+data.consolide.tabdesc = Es mostren els registres per als quals s'han detectat problemes
+data.consolide.nodata = No hi ha dades pendents de consolidar per al conjunt seleccionat
+data.consolide.noerr = No s'han detectat errors
+data.consolide.ok = Acció realitzada correctament
+data.consolide.actions.info = Accions automàtiques disponibles
+data.consolide.actions.action1 = Esborrar duplicats
+data.consolide.actions.action2 = Esborrar consolidades
+data.consolide.actions.action3 = Esborrar pendents
+data.consolide.actions.msg = Nombre de registres esborrats
+
+data.input.source = Tipus de dades
+data.input.survey = Grup de dades
+data.input.year = Curs
+data.input.dstyear = Curs a imputar
+data.input.scope = Àmbit
+data.input.scope.t = Titulació
+data.input.scope.c = Centre
+data.input.scope.u = Universitat
+data.input.type = Tipus Titulació
+data.input.type.g = Grau
+data.input.type.m = Màster
+data.input.type.d = Doctorat
+data.input.type.u = Global
+data.input.tit = Titulació
+data.input.cen = Centre
+data.input.file = Seleccioneu o arrossegueu ací el fitxer CSV
+data.input.select = Trieu un conjunt de dades
+data.input.option = Trieu una opció
+data.input.delim = Delimitador
+data.input.dbsource = DB d'origen
+data.input.importType = Tipus d'importació
+data.input.importType.file = Fitxer
+data.input.importType.db = Base de dades
+data.input.view = Vista/Taula a importar (nom_esquema.nom_vista)
+data.input.key = Clau
+
+data.btn.import = Importar
+data.btn.connect = Comprovar connexió
+data.btn.show = Consultar
+data.btn.delete = Eliminar
+data.btn.query = Consultar
+data.btn.check = Comprobar
+data.btn.run = Executar
+data.table.0 = Curs
+data.table.1 = Titulació
+data.table.2 = Centre
+data.table.3 = Àmbit
+data.table.4 = Tipus
+data.table.5 = Indicador
+data.table.6 = Valor
+data.table.7 = Num enq.
+data.table.8 = Titulació Orig.
+data.table.9 = Centre Orig.
+data.table.10 = RUCT
+data.table.11 = Usuari
+data.alert.connect = No s'ha pogut establir la connexió amb la vista indicada
+
+
+
+
+

+ 463 - 0
src/main/resources/messages_es.properties

@@ -0,0 +1,463 @@
+
+##########################################
+#	GLOBAL LOCALES 
+##########################################
+global.lang.va = Valencià
+global.lang.es = Castellano
+global.exit = Salir
+global.login = Acceder
+global.help = Instrucciones
+global.lang = Idioma
+global.footer.span1 = Consultas procedimentales
+global.footer.span2 = Consultas técnicas
+global.footer.span3 = Unidad de Calidad
+global.menu.tits = Panel de control
+global.menu.procedures = Tareas asignadas
+global.menu.supervision = Supervisión
+global.menu.managers = Responsables
+global.menu.admin = Administración
+global.menu.admin.stats = Información
+global.menu.admin.instances = Instanciación
+global.menu.admin.procedures = Procedimientos
+global.menu.admin.parse = Consolidar datos
+global.menu.admin.mailing = Correos
+global.menu.admin.calendar = Planificador
+global.menu.admin.acredita = Acreditaciones
+global.menu.data = Datos y plantillas
+global.menu.admin.templates = Plantillas (word)
+global.menu.admin.editor = Plantillas (online)
+global.menu.admin.import = Importar datos
+global.menu.admin.data = Datos consolidados
+global.menu.contact = Contacto
+global.menu.contact.title = Direcciones de correo electrónico de contacto
+global.accept = Aceptar
+global.edit = Editar
+global.new = Nuevo
+global.titleNew = Nuevo elemento
+global.delete = Borrar 
+global.cancel = Cancelar
+global.close = Cerrar
+global.close.confirm = Se perderán los datos no guardados
+global.save = Guardar
+global.confirm = Confirmar
+global.return = Volver
+global.loading = Cargando...
+global.titleDownload = Descargar evidencia
+global.download = Descargar
+global.deleteWarning = Atenció, esta acción no se podrá deshacer
+global.error.msg.i = Ha ocurrido un error, por favor, vuelva a intentarlo o contacte con
+global.error.msg.f = reportando el siguiente error
+global.editEvidence = Eliminar/Editar evidencia
+global.uploadLimits = Solo se permite subir un fichero, para subir más utilize un fichero comprimido
+global.uploadSelectFile = Seleccione un fichero
+global.noData = No hay datos disponibles
+global.noTasks = No tiene ninguna tarea asignada
+global.expire.notice = En 5 minutos se cerrará automaticamente la sesión por inactividad.
+global.expire.keepalive = Cierre esta alerta para seguir usando la aplicación
+global.error.msg = Ha habido un error, por favor, vuelva a intentarlo. Si el error persiste contacte con
+global.unautorized.msg = Su usuario no está autorizado a acceder a este apartado
+global.selectors.noData = No hay elementos marcados
+
+##########################################
+#	LOGIN FORM LOCALES
+##########################################
+login.h2 = Identificación UV
+login.button = Iniciar Sesión
+login.placeholder.username = Usuario
+login.placeholder.passwd = Contraseña
+login.error = El usuario o contraseña introducidos no son correctos o su usuario no está autorizado.
+login.expired = Su sesión se ha cerrado por inactividad o por acceder desde otro dispositivo/navegador, por favor, vuelva a identificarse.
+login.session.info1 = La sesión se cerrará automaticamente tras 60 minutos de inactividad.
+login.session.info2 = Por favor, guarde los cambios periodicamente.
+
+##########################################
+#	PROCEDURES MAIN PAGE LOCALES
+##########################################
+procedures.title = Tareas asignadas
+procedures.subtitle = Haga clic sobre el procedimiento para realizar la tarea
+procedures.title.tasks = Tareas asociadas al procedimiento
+procedures.title.flow = ver flujo
+procedures.center = Centro
+procedures.titulation = Titulación
+procedures.year.eval = Curso evaluado
+procedures.year.act = activado el curso
+procedures.dateLimit = Fecha límite
+procedures.dateInstance = Fecha última acción
+procedures.status = Estado
+procedures.btn.continue = Aceptar y avanzar
+procedures.btn.yes = Si
+procedures.btn.no = No
+procedures.btn.selectFile = Seleccione la evidencia
+procedures.btn.draft = Guardar borrador
+procedures.done.user = Realizada por
+procedures.done.date = Fecha de realización
+procedures.done.evidence = Evidencia aportada
+procedures.done.response = Respuesta
+procedures.done.response.s = SI
+procedures.done.response.n = NO
+procedures.status.a = Activa
+procedures.status.n = Realizada (Respuesta NO)
+procedures.status.s = Realizada (Respuesta SI)
+procedures.status.f = Realizada
+procedures.status.e = Realizada (Evidencia adjunta)
+procedures.status.i = Inactiva
+procedures.status.title.a = Tarea activa
+procedures.status.title.p = Tarea inactiva
+procedures.status.title.o = No le corresponde realizar está tarea
+procedures.status.title.r = Tarea realizada
+procedures.activeTask = Tarea activa
+procedures.olderEvidences = Evidencias generadas en el proceso anteriormente
+procedures.closed = Procedimiento finalizado
+procedures.cancelled = Procedimiento cerrado
+procedures.extensionAlert = Solo se permite subir archivos con extensión PDF
+procedures.assignedTo = Asignada a
+procedures.back = Volver
+procedures.template = Descargue la plantilla
+procedures.legend.active = Activa
+procedures.legend.other = No le corresponde
+procedures.legend.blocked = Inactiva
+procedures.legend.done = Realizada
+procedures.legend.expired = Atrasada
+procedures.list.legend.active = Activo
+procedures.list.legend.other = No le corresponde
+procedures.list.legend.done = Finalizado
+procedures.list.legend.expired = Atrasado
+procedures.otherActions = Acciones realizadas anteriormente en esta tarea
+procedures.editor.confirm = ¿Esta seguro de que desea continuar? Al enviar la evidencia ya no podrá realizar cambios sobre esta tarea.
+procedures.editor.msgsave = Borrador guardado correctamente
+procedures.session.expired = Su sesión se ha cerrado por inactividad o por acceder desde otro dispositivo/navegador. Cierre este diálogo para volver a entrar a SAIC.
+procedures.nosupported = Su navegador no soporta las funcionalidades necesarias para trabajar con SAIC. Se recomienda utilizar una versión actualizada de Chrome o Firefox.
+procedures.autosave.lastdate = Última modificación
+procedures.autosave.none = Aun no se ha guardado ningún borrador
+
+##########################################
+#	SUPERVISION PAGE LOCALES
+##########################################
+supervision.title = Supervisión de los procedimientos
+supervision.subtitle = Seleccione los criterios de filtrado de los procedimentos que desea supervisar y pulse "Consultar".
+supervision.searchBy = Buscar por
+supervision.years = Curs(os) de evaluación del procedimiento
+supervision.titulations = Titulación(es)
+supervision.titulations.all = Todos
+supervision.centers = Centro(s)
+supervision.titulations.all.g = Todos los grados del centro
+supervision.titulations.all.m = Todos los másters del centro
+supervision.procedures = Procedimiento(s)
+supervision.evidences = Evidencia(s)
+supervision.search = Consultar
+supervision.procedures.title = Procedimientos que coinciden con los criterios seleccionados
+supervision.filter.apha = Ordenar alfabéticamente
+supervision.filter.date = Ordenar por fecha de última acción
+supervision.filter.none = Eliminar filtros
+supervision.filter.download1 = Exportar listado
+supervision.filter.download2 = Exportar por titulación
+
+
+##########################################
+#	MANAGERS PAGE LOCALES
+##########################################
+managers.title = Búsqueda de responsables
+managers.title2 = Responsables de su centro
+managers.subtitle = Seleccione los criterios de filtrado de los responsables que desea buscar y pulse "Buscar".
+managers.centers = Centro
+managers.titulations = Titulación(es)
+managers.search = Buscar
+managers.search.title = Resultados de la búsqueda
+managers.noResults = No se ha encontrado ningun responsable con los criterios seleccionados
+managers.since = desde del
+
+##########################################
+#	ADMIN PAGE LOCALES
+##########################################
+admin.instances.title = Instanciación de procedimientos
+admin.instances.subtitle = Seleccione un procedimiento junto con el centro y la titulación sobre los que quiere instanciarlo
+admin.instances.error = Los siguientes procedimentos no se han podido instanciar
+admin.instances.done = Se han instanciado correctamente los siguientes procedimientos
+admin.instances.selector.titulation = Titulación
+admin.instances.selector.center = Centro
+admin.instances.selector.procedure = Procedimiento
+admin.instances.selector.curs = Curso
+admin.instances.selector.taskType = Tipo de tarea
+admin.templates.title = Búsqueda y prueba de plantillas
+admin.templates.subtitle = Seleccione un procedimiento junto con el centro y la titulación sobre los que quiere probar una plantilla
+admin.templates.info = Haga click sobre la plantilla que desea probar
+admin.templates.info = Faça click sobre la plantilla que vol provar per a descarregar-la i veure els associats que s'han carregat correctament o faça click en editar per a modificar la llista d'indicadors
+admin.templates.indicators = Indicadores asociados a la plantilla
+admin.templates.table.enq = Datos de encuestas
+admin.templates.table.serv = Datos del web service
+admin.actions.instance = Instanciar
+admin.actions.load = Cargar
+admin.procedures.title = Administración de procedimientos
+admin.procedures.subtitle = Selccione el procedimiento y haga click en la opción deseada
+admin.procedures.action.new = Nuevo
+admin.procedures.action.duplicate = Duplicar
+admin.procedures.action.edit = Editar
+admin.procedures.action.remove = Eliminar
+admin.procedures.selector.procedure = Procedimiento
+admin.procedures.selector.years = Curso de evaluación del procedimiento
+admin.procedures.selector.titulations = Titulación
+admin.procedures.selector.centers = Centro
+admin.procedures.confirm = Confirmar
+admin.procedures.info.procedure = Información del procedimiento
+admin.procedures.info.task = Tarea
+admin.procedures.form.id = ID
+admin.procedures.form.nameProcedure = Código
+admin.procedures.form.subtitleProcedure = Dimensión abreviada
+admin.procedures.form.context = Ámbito
+admin.procedures.form.version = Versión
+admin.procedures.form.activationYear = Curso de activación
+admin.procedures.form.evalYear = Curso evaluado
+admin.procedures.form.titleCas = Titulo en castellano
+admin.procedures.form.titleVal = Titulo en valenciano
+admin.procedures.form.descriptionCas = Descripción en castellano
+admin.procedures.form.descriptionVal = Descripción en valenciano
+admin.procedures.form.comments = Información adicional (oculto a los usuaris) 
+admin.procedures.form.nameEvCas = Nombre de la evidencia en castellano
+admin.procedures.form.nameEvVal = Nombre de la evidencia en valenciano
+admin.procedures.form.taskp = ID Tascap
+admin.procedures.form.limitDate = Fecha límite
+admin.procedures.form.options = Opciones
+admin.procedures.form.report = ¿Genera informe?
+admin.procedures.form.type = Tipo
+admin.procedures.form.type.1 = Evidencia
+admin.procedures.form.type.2 = Si/No
+admin.procedures.form.type.3 = Continuar
+admin.procedures.form.evCode = Código de la evidencia
+admin.procedures.form.next = ID tarea siguiente
+admin.procedures.form.next2 = ID tarea siguiente alternativa
+admin.procedures.form.role = Rol
+admin.procedures.form.addTask = Añadir tarea
+admin.procedures.form.removeConfirm = Si elimina este procedimiento los datos no se podrán recuperar
+admin.action.delete = Eliminar
+admin.action.deleteInstance = Eliminar procedimiento
+admin.action.deleteTask = Eliminar acción
+admin.action.reloadInstance = Borrar y reiniciar procedimiento
+admin.action.closeInstance = Cerrar procedimiento
+admin.action.reloadTask = Borrar y repetir tarea
+admin.action.reactivate = Repetir tarea
+admin.action.edit = Editar tarea
+admin.action.attach = Adjuntar nueva evidencia
+admin.managers.add = Añadir responsable
+admin.managers.center = Centro
+admin.managers.titulation = Titulación
+admin.managers.user = Usuario
+admin.managers.firstname = Nombre
+admin.managers.lastname = Apellidos
+admin.managers.email = Email
+admin.managers.role = Rol
+admin.managers.exists = Ya existe una entrada con los datos seleccionados
+admin.managers.newRole = Añadir responsable
+admin.managers.newUser = Nuevo usuario
+admin.stats.pending.title = Correos pendientes de enviar
+admin.stats.pending.total = Total de correos pendientes de enviar
+admin.stats.pending.username = Usuario
+admin.stats.pending.fullname = Nombre
+admin.stats.pending.email = Correo
+admin.stats.sessions.title = Sesiones activas en este instante
+admin.stats.sessions.id = ID de sesión
+admin.stats.sessions.username = Usuario
+admin.stats.sessions.last = Última acción
+admin.stats.sessions.expired = Expirada
+admin.stats.sessions.active.total = Total de sesiones activas
+admin.stats.sessions.expired.total = Total de sesionrs expiradas
+admin.stats.table.lengthMenu = Filas
+admin.stats.table.zeroRecords = No se han encontrado resultados
+admin.stats.table.info = Página _PAGE_ de _PAGES_
+admin.stats.table.infoEmpty = No hay datos para mostrar
+admin.stats.table.infoFiltered = (filtradas de un total de _MAX_)
+admin.stats.table.next = Siguiente
+admin.stats.table.previous = Anterior
+admin.mailing.title = Correos electronicos
+admin.mailing.subtitle = Seleccione un centro y un rol al que enviar el correo
+admin.mailing.send = Enviar
+admin.mailing.center = Centro
+admin.mailing.role = Rol
+admin.mailing.subject = Asunto
+admin.mailing.body = Mensaje
+admin.mailing.sent.success = El mensaje se ha procesado correctamente y será enviado lo antes posible
+admin.mailing.sent.error = No se han encontrado usuarios para el rol y centros especificados
+admin.calendar.form.titleAdd = Planificar nueva instancia
+admin.calendar.form.titleEdit = Editar instancia planificada
+admin.calendar.form.titleCopy = Copiar instancia existente
+admin.calendar.form.delete = Eliminar
+admin.calendar.form.instanciar = Instanciar
+admin.calendar.form.add = Añadir
+admin.calendar.form.date = Fecha
+admin.calendar.form.load = Cargar
+admin.calendar.form.changeDate = ¿Cambiar la fecha al
+admin.calendar.form.acredSel = Selección por acreditación
+admin.calendar.form.input.grup.1 = Enero 
+admin.calendar.form.input.grup.4 = Abril
+admin.calendar.form.input.grup.9 = Septiembre
+admin.calendar.form.input.grup.11 = Noviembre
+admin.calendar.form.input.tambit.g = Grados
+admin.calendar.form.input.tambit.m = Másters
+admin.calendar.form.input.tambit.d = Doctorados
+admin.acredita.title = Resumen de acreditaciones 
+admin.acredita.form.view = Cargar
+admin.acredita.form.year = Año
+admin.acredita.form.group = Grupo
+admin.acredita.form.groupnum.0 = Todos 
+admin.acredita.form.groupnum.1 = Enero 
+admin.acredita.form.groupnum.4 = Abril
+admin.acredita.form.groupnum.9 = Septiembre
+admin.acredita.form.groupnum.11 = Noviembre
+
+##########################################
+#	DASHBOARD PAGE LOCALES
+##########################################
+dashboard.header.published = Informes publicados
+dashboard.menu.summary = Resumen
+dashboard.menu.procedures = Procedimientos SAIC
+dashboard.menu.docs = Documentación
+dashboard.menu.data = Indicadores
+dashboard.menu.tits = Titulaciones
+dashboard.menu.managers = Responsables
+dashboard.summary.legend.active = Finalizado
+dashboard.summary.legend.ontime = En curso
+dashboard.summary.legend.delayed = Atrasado
+dashboard.summary.legend.closed = Cerrado
+dashboard.data.sel.g = Datos de Grado
+dashboard.data.sel.m = Datos de Máster
+dashboard.data.sel.d = Datos de Doctorado
+dashboard.data.sel.rates = Tasas
+dashboard.data.sel.estud = Satisfacción estudiantado
+dashboard.data.sel.estud1 = Satisfacción estudiantado de 1º
+dashboard.data.sel.estud3 = Satisfacción estudiantado de 3º
+dashboard.data.sel.gradu = Satisfacción graduados/as
+dashboard.data.sel.prof = Satisfacción profesorado
+dashboard.data.sel.eval = Evaluación docente
+dashboard.data.sel.ptgas = Satisfacción del PTGAS
+dashboard.data.legend.1 = Titulación
+dashboard.data.legend.2 = Centro
+dashboard.data.legend.3 = Universidad
+dashboard.data.table.hr1 = Encuesta
+dashboard.data.table.hr2 = Indicador
+dashboard.data.table.hr3 = Ámbito
+dashboard.data.table.hr4 = Estudio
+dashboard.data.table.hr5 = Valor
+dashboard.data.table.hr6 = Curso
+dashboard.data.table.hr7 = CursoD
+dashboard.data.table.hr8 = Tipo
+dashboard.data.table.hr9 = NºEnc
+dashboard.data.table.hr10 = Curso Enc
+dashboard.data.table.hr11 = Centro Orig
+dashboard.data.table.hr12 = Tit. Orig
+dashboard.data.table.hr13 = RUCT
+dashboard.data.table.hr14 = Fecha
+dashboard.data.table.btn1 = Datos del curso
+dashboard.data.table.btn2 = Exportar a Excel
+dashboard.data.table.btn3 = Exportar a CSV
+dashboard.data.table.btn4 = Exportar a PDF
+dashboard.data.table.btn5 = Mostrar/Ocultar columnas
+dashboard.data.table.btn6 = Ver gráficas
+dashboard.data.table.btn7 = Ver todos los datos en formato tabla
+dashboard.acred.title.edit = Cambiar fechas
+dashboard.acred.date.segui = Seguimiento
+dashboard.acred.date.acred = Próxima acreditación
+dashboard.acred.date.renov = Última acreditación
+dashboard.acred.date.verif = Verificación
+dashboard.acred.date.impla = Implantación
+dashboard.acred.group.year = Curso próx. acred.
+dashboard.acred.group.name = Grupo próx. acred.
+dashboard.acred.recom.title = Recomendaciones *
+dashboard.acred.recom.msg = Hay recomendaciones de obligado cumplimiento derivadas de la última renovación.
+dashboard.acred.recom.info = Indicar si hay recomendaciones de obligado cumplimiento pendientes
+dashboard.acred.segui.title = Seguimiento **
+dashboard.acred.segui.info = Indicar si la titulación tiene que hacer especial seguimiento
+dashboard.acred.segui.msg = La titulación tiene que pasar obligatoriamente un seguimiento en la fecha indicada en el campo seguimiento.
+dashboard.acred.codes = Códigos internos
+dashboard.acred.table.title = Titulaciones del centro
+dashboard.acred.table.ruct = RUCT
+dashboard.acred.table.tit = Titulación
+dashboard.acred.table.type = Tipo
+dashboard.acred.table.next = Próx Accr.
+dashboard.acred.table.acred = Fecha Accr.
+dashboard.acred.table.renov = Renovación
+dashboard.acred.table.verif = Verificación
+dashboard.acred.table.impla = Implantación
+dashboard.acred.table.segui = Seguimiento
+dashboard.acred.table.inter = InterUniv.
+dashboard.acred.docs.title = Documentación
+dashboard.gantt.selector = Ver procedimientos de
+dashboard.tree.selector.title = Ver procedimientos de
+dashboard.tree.selector.centre = Centro y todas sus titulaciones
+dashboard.tits.ruct = RUCT
+dashboard.tits.codes = Códigos
+dashboard.tits.centre = Centro
+dashboard.tits.tit = Titulación
+dashboard.tits.type = Tipo
+
+##########################################
+#	DATA PAGE LOCALES
+##########################################
+data.import.title = Importador de datos
+data.import.subtitle = Los datos que importe se quedarán en estado pendiente de consolidar. Del mismo modo, solo podrá consultar los datos que se encuentren en ese mismo estado.
+data.import.note1 = El fichero CSV debe de contener, como mínimo, las columnas "curs", "titulacio", "centre" y "tipus".
+data.import.note2 = La columna "tipus" puede tomar los valores "avg", "min" o "max"
+data.import.note3 = * Solo para importación 
+data.import.note4 = * Solo para consulta
+data.import.note5 = Se han detectado las siguientes columnas en el fichero seleccionado. Si desea que se ignore alguna columna, desmárquela de la lista.
+data.show.title = Consultar datos consolidados
+data.consolide.title = Consultar datos consolidados
+data.consolide.subtitle = Seleccione el conjunto de datos que desea consolidar
+data.consolide.err1 = Se han detectado datos duplicados en el conjunto pendiente de consolidar, por favor, corríjalo antes de consolidar.
+data.consolide.err2 = Se han detectado errores de integridad entre el conjunto seleccionado y los datos ya consolidados, por favor, corríjalos antes de consolidar.
+data.consolide.err3 = No se ha podido ejecutar la acción, compruebe de nuevo la integridad de los datos.
+data.consolide.count = Número de registros pendientes de consolidar para el conjunto seleccionado
+data.consolide.tabdesc = Se muestran los registros para los que se han detectado problemas
+data.consolide.nodata = No hay datos pendientes de consolidar para el conjunto seleccionado
+data.consolide.noerr = No se han detectado errores
+data.consolide.ok = Acción realizada correctamente
+data.consolide.actions.info = Acciones automáticas disponibles
+data.consolide.actions.action1 = Eliminar duplicados
+data.consolide.actions.action2 = Eliminar consolidados
+data.consolide.actions.action3 = Eliminar pendientes
+data.consolide.actions.msg = Número de registros eliminados
+
+data.input.source = Tipo de datos
+data.input.survey = Grupo de datos
+data.input.year = Curso
+data.input.dstyear = Curso a imputar
+data.input.scope = Ámbito
+data.input.scope.t = Titulación
+data.input.scope.c = Centro
+data.input.scope.u = Universidad
+data.input.type = Tipo Titulación
+data.input.type.g = Grado
+data.input.type.m = Máster
+data.input.type.d = Doctorado
+data.input.type.u = Global
+data.input.tit = Titulación
+data.input.cen = Centro
+data.input.file = Seleccione o arrastre aquí el archivo CSV
+data.input.select = Seleccione un conjunto de datos
+data.input.option = Seleccione una opción
+data.input.delim = Delimitador
+data.input.dbsource = DB de origen
+data.input.importType = Tipo de importación
+data.input.importType.file = Fichero CSV
+data.input.importType.db = Base de datos
+data.input.view = Vista/Tabla a importar (nombre_esquema.nombre_vista)
+data.input.key = Clau
+
+data.btn.import = Importar
+data.btn.connect = Comprobar conexión
+data.btn.show = Consultar
+data.btn.delete = Eliminar
+data.btn.query = Consultar
+data.btn.check = Comprobar
+data.btn.run = Ejecutar
+data.table.0 = Curso
+data.table.1 = Titulación
+data.table.2 = Centro
+data.table.3 = Ámbito
+data.table.4 = Tipo
+data.table.5 = Indicador
+data.table.6 = Valor
+data.table.7 = Num enc.
+data.table.8 = Titulacion Orig.
+data.table.9 = Centro Orig.
+data.table.10 = RUCT
+data.table.11 = Usuario
+data.alert.connect = No se ha podido establecer la conexión con la vista indicada

+ 13 - 0
src/test/java/es/uv/docentia/SaicApplicationTests.java

@@ -0,0 +1,13 @@
+package es.uv.docentia;
+
+//import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class SaicApplicationTests {
+
+	//@Test
+	void contextLoads() {
+	}
+
+}