Drowsito 3 недель назад
Сommit
de18bcdb37
29 измененных файлов с 4381 добавлено и 0 удалено
  1. 8 0
      .gitignore
  2. 14 0
      .vscode/launch.json
  3. 17 0
      .vscode/settings.json
  4. 286 0
      mvnw
  5. 161 0
      mvnw.cmd
  6. 194 0
      pom.xml
  7. 167 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. 93 0
      src/main/java/es/uv/saic/config/SecurityConfig.java
  13. 49 0
      src/main/java/es/uv/saic/config/WebConfig.java
  14. 87 0
      src/main/java/es/uv/saic/service/DocumentService.java
  15. 640 0
      src/main/java/es/uv/saic/service/PlantillaService.java
  16. 104 0
      src/main/java/es/uv/saic/service/UsuariService.java
  17. 177 0
      src/main/java/es/uv/saic/service/UsuarisRolService.java
  18. 67 0
      src/main/java/es/uv/saic/web/DocumentController.java
  19. 496 0
      src/main/java/es/uv/saic/web/DownloadController.java
  20. 89 0
      src/main/java/es/uv/saic/web/PlantillaController.java
  21. 38 0
      src/main/resources/application-dev.properties
  22. 40 0
      src/main/resources/application-graal.properties
  23. 29 0
      src/main/resources/application-local.properties
  24. 38 0
      src/main/resources/application-prod.properties
  25. 48 0
      src/main/resources/application.properties
  26. 462 0
      src/main/resources/messages.properties
  27. 469 0
      src/main/resources/messages_ca.properties
  28. 463 0
      src/main/resources/messages_es.properties
  29. 13 0
      src/test/java/es/uv/docentia/SaicApplicationTests.java

+ 8 - 0
.gitignore

@@ -0,0 +1,8 @@
+/target/**
+.deployables
+.settings
+markdonwGen.py
+/target/
+.project
+.classpath
+/docs/

+ 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": "automatic",
+    "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%

+ 194 - 0
pom.xml

@@ -0,0 +1,194 @@
+<?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>
+		<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-properties-migrator</artifactId>
+		    <scope>runtime</scope>
+		</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_docs</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_docs</imageName>
+                    <buildArgs>
+                        <buildArg>--no-fallback</buildArg>
+                    </buildArgs>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+
+</project>

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

@@ -0,0 +1,167 @@
+{"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.core.domain",
+    "type": "java.lang.String",
+    "description": "A description for 'saic.url.core.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;
+		}
+	}
+	
+
+	
+}

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

@@ -0,0 +1,93 @@
+package es.uv.saic.config;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+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;
+
+@Configuration
+@EnableWebSecurity
+@EnableMethodSecurity
+public class SecurityConfig {
+	
+	@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 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;
+	}
+	
+	
+}

+ 87 - 0
src/main/java/es/uv/saic/service/DocumentService.java

@@ -0,0 +1,87 @@
+package es.uv.saic.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+import org.springframework.web.multipart.MultipartFile;
+
+import es.uv.saic.shared.domain.Document;
+import es.uv.saic.shared.domain.DocumentRepository;
+
+@Service
+public class DocumentService {
+	
+	@Autowired
+	private DocumentRepository dr;
+	
+	@Value("${saic.data.docsPath}")
+	private String docsPath;
+
+	
+	public List<Document> getAll() {
+		return this.dr.findAll();
+	}
+	
+	public Document findById(Integer id) {
+		Optional<Document> c = this.dr.findById(id);
+		if(!c.isEmpty()) {
+			return c.get();
+		}
+		return null;
+	}
+	
+	public List<Document> findAllByCategoriaOrgan(Integer categoria, Integer lugar, String tlugar){
+		return this.dr.findAllByCategoriaOrgan(categoria, lugar, tlugar);
+	}
+	
+	public Document findByCategoriaOrgan(Integer categoria, Integer lugar, String tlugar){
+		return this.dr.findByCategoriaOrgan(categoria, lugar, tlugar);
+	}
+	
+	public List<Document> findByCategoriaNom(String nom){
+		return this.dr.findByCategoriaNom(nom);
+	}
+	
+	public List<Document> findByCategoriaTipus(String tipus){
+		return this.dr.findByCategoriaTipus(tipus);
+	}
+	
+	public Document archive(Document d) {
+		d.setVisible(false);
+		d = this.dr.save(d);
+		return d;
+	}
+	
+	public void archiveByOrgan(Integer lugar, String tlugar) {
+		this.dr.archiveByOrgan(lugar, tlugar);
+	}
+	
+	public boolean exists(Integer idCategoria, Integer lugar, String tlugar) {
+		Document d = this.dr.findByCategoriaOrgan(idCategoria, lugar, tlugar);
+		if(d == null) {
+			return false;
+		}
+		return true;
+	}
+	
+	public Document save(Document d) {
+		return this.dr.saveAndFlush(d);
+	}
+	
+	public void delete(Document d) {
+		this.dr.delete(d);
+	}
+	
+	public String upload(Integer idDocument, Integer idCategoria, Integer lugar, String tlugar, MultipartFile file) throws IllegalStateException, IOException {
+		String ext = FilenameUtils.getExtension(file.getOriginalFilename());
+		String fname = lugar.toString()+"_"+tlugar+"_"+idCategoria.toString()+"_"+idDocument.toString()+"."+ext;
+		file.transferTo(new File(docsPath+fname));
+		return docsPath+fname;
+	}
+}

+ 640 - 0
src/main/java/es/uv/saic/service/PlantillaService.java

@@ -0,0 +1,640 @@
+package es.uv.saic.service;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.PrintWriter;
+import java.io.Reader;
+import java.io.UncheckedIOException;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.text.DecimalFormat;
+import java.util.Base64;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.jsoup.Jsoup;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+import org.jsoup.select.Elements;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.ResourceLoader;
+import org.springframework.stereotype.Service;
+import org.springframework.util.FileCopyUtils;
+
+import com.fasterxml.jackson.core.JsonParseException;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.JsonMappingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+import es.uv.saic.shared.domain.Plantilla;
+import es.uv.saic.shared.domain.PlantillaComentario;
+import es.uv.saic.shared.domain.PlantillaConversation;
+import es.uv.saic.shared.domain.PlantillaRepository;
+import es.uv.saic.shared.dto.InstanciaTascaDTO;
+import es.uv.saic.shared.dto.NomProcesOrganDTO;
+import es.uv.saic.shared.dto.OrganDTO;
+import es.uv.saic.shared.feign.IndicadorClient;
+import es.uv.saic.shared.feign.OrganClient;
+import es.uv.saic.shared.feign.ProceduresClient;
+import es.uv.saic.shared.feign.TascaClient;
+
+@Service
+public class PlantillaService {
+
+	@Autowired
+	private PlantillaRepository r;
+	
+	@Value("${saic.data.filePath}")
+	private String filePath;
+	@Value("${saic.data.tmpPath}")
+	private String tmpPath;
+	@Value("${saic.data.templates.fileNotFound}")
+	private String fileNotFound;
+	@Value("${saic.data.templates.filePath}")
+	private String templatePath;
+	@Value("${saic.data.templates.logoPath}")
+	private String logoPath;
+	
+	@Autowired
+	private TascaClient tc;
+	
+	@Autowired
+	private OrganClient oc;
+
+	@Autowired
+	private ProceduresClient pc;
+
+	@Autowired
+	private IndicadorClient ic;
+	
+	private static final DecimalFormat df = new DecimalFormat("0.00");
+	
+	public List<Plantilla> findAll(){
+		return this.r.findAll();
+	}
+	
+	public Plantilla findByID(Integer id) {
+		return this.r.findByIdPlantilla(id);
+	}
+	
+	public Plantilla findByVersioCodiAmbit(Integer versio, String codi, String ambit) {
+		return this.r.findByVersioCodiAmbit(versio, codi, ambit);
+	}
+	
+	public void save(Plantilla p) {
+		this.r.save(p);
+		this.r.flush();
+	}
+	
+	public void delete(Plantilla p) {
+		this.r.delete(p);
+	}
+	
+	public boolean isUsed(Integer idPlantilla) {
+		List<Plantilla> l = this.r.findUsedByIdPlantilla(idPlantilla);
+		if(l.size() > 0) {
+			return true;
+		}
+		return false;
+	}
+	
+	public String addTemplateData(InstanciaTascaDTO it, String template){
+		HashMap<String, String> context = new HashMap<String, String>();
+		HashMap<String, String> header = new HashMap<String, String>();
+		HashMap<String, String> img = new HashMap<String, String>();
+		 
+		Integer idTitulacio = it.getInstancia().getTitulacio();
+		Integer cursAvaluat = it.getInstancia().getCursAvaluat();
+		Integer idCentre = it.getInstancia().getCentre();
+		String ambit = "G";
+		String opcionsStr = it.getTasca().getOpcions().replaceAll("^\\.$", "");
+		String [] opcions = new String[0];
+		if(!opcionsStr.isEmpty()) {
+			opcions = opcionsStr.split(";");
+		}
+		if(it.getInstancia().getTlugar().equals("C")) {
+			if(it.getInstancia().getTitulacio() == 2) {
+				ambit = "M";
+			}
+		}
+		
+		/** Add header information **/
+		addHeaderData(it.getInstancia().getNomval(), it.getInstancia().getLugar(), it.getInstancia().getNomValTitulacio(), it.getInstancia().getCursAvaluat(), header, img);
+		
+		/** Iterate elements inside {[loop]} ... {[endloop]} **/
+		template = this.iterateLoopTag(template, idCentre, ambit, cursAvaluat, context);
+		
+		/** Iterate elements with data-loop attribute **/
+		Document doc = Jsoup.parse(template);
+		doc = iterateLoopAttr(idCentre, cursAvaluat, doc, context);
+		
+		/** Add template data (non iterated data)**/
+		ic.getTemplateData(idTitulacio, idCentre, cursAvaluat, context);	
+		this.replaceValues(doc, context, header, img);
+		
+		/** Replace sections if specified in options **/
+		this.replaceSection(opcions, it, doc);
+		
+		return doc.html();	
+	}
+	
+	public String addTemplateData(Integer idTitulacio, Integer idCentre, Integer curs, String template){
+		
+		HashMap<String, String> context = new HashMap<String, String>();
+		HashMap<String, String> header = new HashMap<String, String>();
+		HashMap<String, String> img = new HashMap<String, String>();
+		 
+		OrganDTO centre = oc.findByID("C", idCentre);
+		OrganDTO titulacio = oc.findByID("T", idTitulacio);
+		
+		/** Add header information **/
+		addHeaderData(centre.getNomVal(), centre.getLugar2(), titulacio.getNomVal(), curs, header, img);
+							
+		/** Iterate elements inside {[loop]} ... {[endloop]} **/
+		template = this.iterateLoopTag(template, idCentre, titulacio.getTambit(), curs, context);
+		
+		/** Iterate elements with data-loop attribute **/
+		Document doc = Jsoup.parse(template);
+		doc = iterateLoopAttr(idCentre, curs, doc, context);
+		
+		/** Add template data (non iterated data)**/
+		ic.getTemplateData(idTitulacio, idCentre, curs, context);	
+		this.replaceValues(doc, context, header, img);
+		
+		return doc.html();	
+	}
+	
+	private void addHeaderData(String nomVal, Integer lugar, String nomValTitulacion, Integer curs, HashMap<String, String> header, HashMap<String, String> img) {
+		header.put("centre", nomVal);
+		header.put("titulacio", nomValTitulacion);
+		header.put("curs", "CURSO "+Integer.toString(curs-1)+" - "+Integer.toString(curs));
+		header.put("curs_anterior", Integer.toString(curs-2)+" - "+Integer.toString(curs-1));
+		header.put("conv-ant1", "CONVOCATORIA "+Integer.toString(curs-1)+" - "+Integer.toString(curs));
+		header.put("conv-ant2", "CONVOCATORIA "+Integer.toString(curs-2)+" - "+Integer.toString(curs-1));
+		header.put("periode-ant1", Integer.toString(curs-6)+" - "+Integer.toString(curs-1));
+		header.put("periode-ant2", Integer.toString(curs-7)+" - "+Integer.toString(curs-2));
+		ClassPathResource fuv = new ClassPathResource("/static/logos/UV.png");
+		ClassPathResource fc = new ClassPathResource("/static/logos/C"+Integer.toString(lugar)+".png");
+		String logouv_b64;
+		String logoc_b64;
+		try {
+			logouv_b64 = "data:image/png;base64, "+Base64.getEncoder().encodeToString(fuv.getInputStream().readAllBytes());
+		} catch (IOException e) {
+			logouv_b64 = "https://saic.uv.es/public/logos/UV.png";
+		}
+		try {
+			logoc_b64 = "data:image/png;base64, "+Base64.getEncoder().encodeToString(fc.getInputStream().readAllBytes());
+		} catch (IOException e) {
+			logoc_b64 = "https://saic.uv.es/public/logos/C"+Integer.toString(lugar)+".png";
+		}
+		img.put("logo_centre", logoc_b64);
+		img.put("logo_uv", logouv_b64);
+	}
+	
+	private Document iterateLoopAttr(Integer idCentre, Integer curs, Document doc, HashMap<String, String> context) {
+		Elements loop_elements = doc.select("*[data-loop]");
+		for(Element e : loop_elements) {
+			String tambit = e.attr("data-loop");
+			List<OrganDTO> tits = oc.getTitulacionsByCentreTambit(idCentre, tambit);
+			for(OrganDTO org : tits) {
+				context.clear();
+				Element e_copy = e.clone();
+				ic.getTemplateData(org.getLugar(), org.getLugar2(), curs, context);
+				context.put("titulacio_loop", org.getNomVal());
+				Elements ielements = e_copy.select("*:matchesWholeOwnText(\\{\\{([^\\}]*)\\}\\})");
+				for(Element ielem : ielements) {
+					String t = ielem.html().replace("{", "").replace("}", "");
+					String i = this.formatValue(context.get(t));
+					if(i != null) {
+						if(ielem.tagName().equals("td")) {
+							ielem.attr("class", "mceEditable");
+							ielem.attr("id", "ind_"+t);
+							ielem.html(i);
+						}
+						else {
+							ielem.html(i);
+						}
+					}
+					else {
+						if(ielem.tagName().equals("td")) {
+							ielem.attr("class", "mceEditable");
+							ielem.attr("id", "ind_"+t);
+							ielem.html("");
+						}
+						else {
+							ielem.html("");
+						}
+					}
+				}		
+				e.after(e_copy);
+			}
+			e.remove();
+		}
+		context.clear();
+		return doc;
+	}
+	
+	private String iterateLoopTag(String template, Integer idCentre, String Tambit, Integer curs, HashMap<String, String> context) {
+		Pattern pattern = Pattern.compile(".*<p>\\{\\[loop\\]\\}</p>([\\S\\s]*)<p>\\{\\[endloop\\]\\}</p>.*");
+		Matcher matcher = pattern.matcher(template);
+		String replaced = "";
+		
+		while(matcher.find()) {
+			String group = matcher.group(1);
+			Document tab = Jsoup.parse(group);
+			List<OrganDTO> tits = oc.getTitulacionsByCentreTambit(idCentre, Tambit);
+			for(OrganDTO t : tits) {
+				Document tabcopy = Jsoup.parse(tab.html());
+				context.clear();
+				ic.getTemplateData(t.getLugar(), t.getLugar2(), curs, context);
+				context.put("titulacio_loop", t.getNomVal());
+				tabcopy = this.replaceValuesLoop(tabcopy, context);
+				replaced += tabcopy.html()+"<p></p>";
+			}
+			template = template.replace("<p>{[loop]}</p>"+group+"<p>{[endloop]}</p>", replaced);
+		}
+		context.clear();
+		return template;
+	}
+	
+	private Document replaceValues(Document doc, HashMap<String, String> context, HashMap<String, String> header, HashMap<String, String> img) {
+		Elements elems = doc.select("*:matchesWholeOwnText(\\{\\{([^\\}]*)\\}\\})");
+		for(Element e : elems) {
+			String t = e.html().replace("{", "").replace("}", "");
+			if(header.containsKey(t)) {
+				e.html(header.get(t));
+			}
+			else if(img.containsKey(t)) {
+				Element x = new Element("img");
+				x.attr("src", img.get(t));
+				x.attr("style", "display:block; margin-left:auto; margin-right:auto; width:100px; max-width:100px;");
+				x.attr("class", "logo");
+				e.html("");
+				e.appendChild(x);	
+			}
+			else {
+				String i = this.formatValue(context.get(t));
+				if(i != null) {
+					if(e.parent().tagName().equals("td")) {
+						e.parent().attr("class", "mceEditable");
+						e.parent().attr("id", "ind_"+t);
+						e.parent().attr("style", e.attr("style"));
+						e.parent().html(i);
+					}
+					else {
+						e.attr("class", "mceEditable");
+						e.attr("ind", "ind_"+t);
+						e.html(i);
+					}
+				}
+				else {
+					if(e.parent().tagName().equals("td")) {
+						e.parent().attr("class", "mceEditable");
+						e.parent().attr("id", "ind_"+t);
+						e.parent().attr("style", e.attr("style"));
+						e.parent().html("");
+					}
+					else {
+						e.attr("class", "mceEditable");
+						e.attr("ind", "ind_"+t);
+						e.html("");
+					}
+				}
+			}
+		}
+		return doc;
+	}
+	
+	private Document replaceValuesLoop(Document doc, HashMap<String, String> context) {
+		Elements elems = doc.select("*:matchesWholeOwnText(\\{\\{([^\\}]*)\\}\\})");
+		for(Element e : elems) {
+			String t = e.html().replace("{", "").replace("}", "");
+			String i = this.formatValue(context.get(t));
+			if(i != null) {
+				if(e.parent().tagName().equals("td")) {
+					e.parent().attr("class", "mceEditable");
+					e.parent().attr("id", "ind_"+t);
+					e.parent().attr("style", e.attr("style"));
+					e.parent().html(i);
+				}
+				else {
+					e.attr("class", "mceEditable");
+					e.attr("ind", "ind_"+t);
+					e.html(i);
+				}
+			}
+			else {
+				if(e.parent().tagName().equals("td")) {
+					e.parent().attr("class", "mceEditable");
+					e.parent().attr("id", "ind_"+t);
+					e.parent().attr("style", e.attr("style"));
+					e.parent().html("");
+				}
+				else {
+					e.attr("class", "mceEditable");
+					e.attr("ind", "ind_"+t);
+					e.html("");
+				}
+			}
+		}
+		return doc;
+	}
+	
+	private String formatValue(String v) {	
+		if(v == null) return ""; 
+		if(v.isEmpty() | v.isBlank()) return "";
+		if(v.equals("NP")) return "NP";
+				
+		try {
+		    double d = Double.parseDouble(v);
+		    return (Integer.toString((int)d).equals(v) ? v : df.format(d).replace(",", "."));
+		} 
+		catch (NumberFormatException e) { }
+		
+		if(v.endsWith("%") && v.startsWith(".")) {
+			return "0"+v;
+		}
+		
+		return v;
+		
+	}
+	
+	public String savePDF(String content, BigInteger idtascai) throws IOException, InterruptedException {
+		content = content.replace("<p><!-- pagebreak --></p>", "<p class=\"pagebreak\"><!-- pagebreak --></p>");
+		Document d = Jsoup.parse(content, "UTF8");
+		d.head().append("<style>"
+				+ "@page{size: 297mm 210mm;} "
+				+ "html{ max-width:297mm; max-height:210mm; margin:0; padding:0; transform:scale(0.925); transform-origin:left top; } "
+				+ "table { padding:2px !important; }"
+				+ "th, td { padding:2px !important; }"
+				+ ".pagebreak { page-break-before:always; }"
+				+ "</style>");
+		List<Element> trs = d.getElementsByTag("tr");
+		for(Element t : trs) {
+			if(t.hasAttr("height")) {
+				t.removeAttr("height");
+			}
+			if(t.hasAttr("style")) {
+				String style = t.attr("style");
+				style = style.replaceAll("height:[ \\d.pxcmin]{1,};", "");
+				t.attr("style", style);
+			}
+		}
+		List<Element> tds = d.getElementsByTag("td");
+		for(Element t : tds) {
+			if(t.hasAttr("height")) {
+				t.removeAttr("height");
+			}
+			if(t.hasAttr("style")) {
+				String style = t.attr("style");
+				style = style.replaceAll("height:[ \\d.pxcmin]{1,};", "");
+				t.attr("style", style);
+			}
+		}
+		List<Element> ignore = d.getElementsByClass("pdfignore");
+		for(Element e: ignore) {
+			e.remove();
+		}
+		
+		InstanciaTascaDTO ita = tc.findInstanciaTascaById(idtascai);
+		if(ita.getTasca().getNomRol().equals("u_uq")) {
+			d.body().append(this.parseComments(d.html()));
+		}
+		
+		String basecommand = "google-chrome --headless --disable-gpu --no-pdf-header-footer --run-all-compositor-stages-before-draw --no-sandbox";
+		String dst = idtascai.toString()+".pdf";		
+		File src = File.createTempFile("saic-pdfexport-", ".tmp.html", new File(tmpPath));
+		src.deleteOnExit();
+		PrintWriter out = new PrintWriter(src.getAbsolutePath());
+		out.println(d.html());
+		out.flush();
+		out.close();
+		System.out.println(basecommand+" --print-to-pdf='"+filePath+dst+"' "+src.getAbsolutePath());
+		
+		ProcessBuilder pb = new ProcessBuilder("bash", "-c", basecommand+" --print-to-pdf='"+filePath+dst+"' "+src.getAbsolutePath());
+		Process pr = pb.start();
+		pr.waitFor();
+		src.delete();
+		
+		return dst;
+	}
+	
+	public byte[] toPDF(String content, Optional<BigInteger> idtascai) throws IOException, InterruptedException {
+		content = content.replace("<p><!-- pagebreak --></p>", "<p class=\"pagebreak\"><!-- pagebreak --></p>");
+		Document d = Jsoup.parse(content, "UTF8");
+		d.head().append("<style>"
+				+ "@page{size: 297mm 210mm;} "
+				+ "html{ max-width:297mm; max-height:210mm; margin:0; padding:0; transform:scale(0.925); transform-origin:left top; } "
+				+ "table { padding:2px !important; }"
+				+ "th, td { padding:2px !important; }"
+				+ ".pagebreak { page-break-before:always; }"
+				+ "</style>");
+		List<Element> trs = d.getElementsByTag("tr");
+		for(Element t : trs) {
+			if(t.hasAttr("height")) {
+				t.removeAttr("height");
+			}
+			if(t.hasAttr("style")) {
+				String style = t.attr("style");
+				style = style.replaceAll("height:[ \\d.pxcmin]{1,};", "");
+				t.attr("style", style);
+			}
+		}
+		List<Element> tds = d.getElementsByTag("td");
+		for(Element t : tds) {
+			if(t.hasAttr("height")) {
+				t.removeAttr("height");
+			}
+			if(t.hasAttr("style")) {
+				String style = t.attr("style");
+				style = style.replaceAll("height:[ \\d.pxcmin]{1,};", "");
+				t.attr("style", style);
+			}
+		}
+		List<Element> ignore = d.getElementsByClass("pdfignore");
+		for(Element e: ignore) {
+			e.remove();
+		}
+		
+		if(idtascai.isPresent()) {
+			InstanciaTascaDTO ita = tc.findInstanciaTascaById(idtascai.get());
+			if(ita.getTasca().getNomRol().equals("u_uq")) {
+				d.body().append(this.parseComments(d.html()));
+			}
+		}
+		
+		d.body().append(this.parseComments(d.html()));
+				
+		String basecommand = "google-chrome --headless --disable-gpu --no-pdf-header-footer --run-all-compositor-stages-before-draw --no-sandbox";	
+		File dst = File.createTempFile("saic-pdfpreview-", ".tmp.pdf", new File(tmpPath));
+		File src = File.createTempFile("saic-pdfpreview-", ".tmp.html", new File(tmpPath));
+		src.deleteOnExit();
+		dst.deleteOnExit();
+		
+		PrintWriter out = new PrintWriter(src.getAbsolutePath());
+		out.println(d.html());
+		out.flush();
+		out.close();
+		ProcessBuilder pb = new ProcessBuilder("bash", "-c", basecommand+" --print-to-pdf='"+dst+"' "+src.getAbsolutePath());
+		Process pr = pb.start();
+		pr.waitFor();
+				
+		byte[] bytes = Files.readAllBytes(dst.toPath());
+		src.delete();
+		dst.delete();
+		
+		return bytes;
+	}
+	
+	private String parseComments(String content) throws JsonParseException, JsonMappingException, IOException {
+		// <!--tinycomments\|2\.1\|data:application\/json;base64,([A-Za-z0-9]*)=-->
+		Pattern pattern = Pattern.compile("\\<\\!\\-\\-tinycomments\\|2\\.1\\|data\\:application\\/json\\;base64\\,([A-Za-z0-9\\/\\+\\\\]*)\\=*\\-\\-\\>");
+		Matcher matcher = pattern.matcher(content);
+		String rawComments = "";
+		if(matcher.find()) {
+			byte[] decoded = Base64.getDecoder().decode(matcher.group(1));
+			rawComments = new String(decoded, "UTF-8");
+			
+			if(rawComments.length() < 10) {
+				return "";
+			}
+			
+			rawComments = rawComments.replaceAll("^.", "[");
+			rawComments = rawComments.replaceAll(".$", "]");
+			rawComments = rawComments.replaceAll("\"mce-conversation\\_\\d*\":", "");
+			
+			ObjectMapper mapper = new ObjectMapper();
+			List<PlantillaConversation> comments = mapper.readValue(rawComments, new TypeReference<List<PlantillaConversation>>(){});
+			
+			String tabComments =  "<p class=\"pagebreak\"><!-- pagebreak --></p>"
+								+ "<h3>COMENTARIOS GENERADOS DURANTE LA REVISIÓN DEL DOCUMENTO</h3>"
+								+ "<h3><small>Nota: Esta página no será visible en la versión final publicada del documento.</small></h3>"
+								+ "<table style=\"border-collapse: collapse; width: 297mm; border-width: 1px; border-spacing: 0px; border-color: rgb(149, 165, 166); margin-left: 0px; margin-right: auto;\" border=\"1\" width=\"297mm\" cellspacing=\"0\" cellpadding=\"8\">"
+								+ "<thead>"
+								+ "    <tr style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166);\" bgcolor=\"#153d63\">"
+								+ "      <th style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166); color:white;\" scope=\"col\">Usuario</th>"
+								+ "      <th style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166); color:white;\" scope=\"col\">Nombre</th>"
+								+ "      <th style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166); color:white;\" scope=\"col\">Comentario</th>"
+								+ "    </tr>"
+								+ "  </thead>"
+								+ "<tbody>";
+			for(PlantillaConversation conv: comments) {
+				for(PlantillaComentario c: conv.getComments()) {
+					tabComments += ("<tr style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166);\"> "
+								  + "  <td style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166);\">"+c.getAuthor()+"</td>"
+								  + "  <td style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166);\">"+c.getAuthorName()+"</td>"
+								  + "  <td style=\"padding: 0px; border-width: 1px; border-color: rgb(149, 165, 166);\">"+c.getContent()+"</td>"
+								  + "</tr> ");
+				}
+			}
+			tabComments += "</tbody></table>";
+			return tabComments;
+		}
+		
+		return "";
+	}
+	
+	public Document replaceSection(String[] opcions, InstanciaTascaDTO it, Document doc) {
+		if(opcions.length > 0) {
+			NomProcesOrganDTO nomProcesOrganDTO = new NomProcesOrganDTO(opcions[0], 
+																		it.getInstancia().getTlugar(), 
+																		it.getInstancia().getLugar(), 
+																		it.getInstancia().getCentre(), 
+																		it.getInstancia().getTitulacio());
+			InstanciaTascaDTO itOld = tc.getReportFromNomProcesOrgan(nomProcesOrganDTO);
+			try {
+				Document doc2 = Jsoup.parse(itOld.getText());
+				Elements target = null;
+				Elements source = null;
+				if(opcions.length == 2) {
+					target = this.extractTemplateAnchor(doc2, true, opcions[1]);
+					source = this.extractTemplateAnchor(doc, false, opcions[1]);
+				}
+				else {
+					target = this.extractTemplateAnchor(doc2, true, opcions[1], opcions[2]);
+					source = this.extractTemplateAnchor(doc, false, opcions[1], opcions[2]);
+				}
+				
+				if(target == null || source == null) {
+					return doc;
+				}
+				Element e = source.get(0);
+				e.before(this.clear(target.outerHtml()));
+				source.remove();
+			}
+			catch(NullPointerException e){
+				System.out.println("No previous version found for "+it.getIdInstanciaTasca().toString());
+			}
+			
+		}
+		return doc;
+	}
+	
+	public Elements extractTemplateAnchor(Document doc, Boolean target, String... args) {
+		// <a id="pam"></a>		
+		
+		/* Hardcoded to fix anchor positions in existing templates */
+		try{
+			String anchor_ini = args[0];
+			Element eFirst = doc.select("p:has(a#"+anchor_ini+")").first();
+			Elements elems = eFirst.nextElementSiblings();
+			Elements table = elems.select("table:not(.pdfignore)");
+			return table;
+		}
+		catch(Exception e){
+			return null;
+		}
+		
+		/* Use this code in future */
+		/* 
+		if(args.length == 2){
+			String anchor_end = args[1];
+			Element eLast = doc.select("p:has(a#"+anchor_end+")").first();
+			boolean remove = false;
+			for(Iterator<Element> iter = elems.iterator(); iter.hasNext();){
+				Element x = iter.next();
+				if(remove){
+					iter.remove();
+				}
+				if(x.id().equals(anchor_end) || x.html().contains(anchor_end)){
+					remove = true;
+				}
+			}
+		}
+		*/
+		
+	}
+
+	public String clear(String html){
+		return html.replaceAll("data-mce-style=\"[ ,;:.\\d\\w\\(\\)\\#\\%-]{1,}\"", "")
+				   .replaceAll("font-family:[ \\d\\w-,]{1,};", "")
+				   .replaceAll("font-size:[ \\d\\w,\\.]{1,};", "")
+				   .replaceAll("\\<\\!\\-\\-tinycomments\\|2\\.1\\|data\\:application\\/json\\;base64\\,[A-Za-z0-9\\/\\+\\\\]*\\=*\\-\\-\\>", "")
+				   .replaceAll("data\\-mce\\-annotation\\-uid\\=\"mce\\-conversation\\_[a-zA-Z0-9]+\"", "")
+				   .replaceAll("data\\-mce\\-annotation\\=\"tinycomments\"", "")
+				   .replaceAll("class=\"mce-annotation\"", "");
+	}
+		
+	public static String asString(Resource resource) {
+        try (Reader reader = new InputStreamReader(resource.getInputStream(), "UTF-8")) {
+            return FileCopyUtils.copyToString(reader);
+        } catch (IOException e) {
+            throw new UncheckedIOException(e);
+        }
+    }
+	
+	public static String readFileToString(String path) {
+	    ResourceLoader resourceLoader = new DefaultResourceLoader();
+	    Resource resource = resourceLoader.getResource(path);
+	    return asString(resource);
+	}
+		
+}

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

@@ -0,0 +1,104 @@
+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) {
+		return this.usuariRepository.findByUsername(usuari);
+	}
+	
+	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();
+	}
+	
+}

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

@@ -0,0 +1,177 @@
+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.save(u);
+		this.usuarisRolRepository.flush();
+	}
+	
+	public void delete(UsuarisRol ur) {
+		this.usuarisRolRepository.delete(ur);
+	}
+	
+	
+}

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

@@ -0,0 +1,67 @@
+package es.uv.saic.web;
+
+import java.io.IOException;
+
+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.RestController;
+
+import es.uv.saic.shared.dto.ArchiveOrganDTO;
+import es.uv.saic.shared.dto.CategoriaDTO;
+import es.uv.saic.shared.dto.DocumentTmpDTO;
+import es.uv.saic.shared.domain.Document;
+import es.uv.saic.service.DocumentService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+@RestController
+@RequestMapping("/document")
+public class DocumentController {
+    @Autowired
+    private DocumentService ds;
+    /*
+     * endpoint para save
+     */
+    @PostMapping("/save")
+    public Document save(@RequestBody Document doc) {
+        return ds.save(doc); 
+    }
+
+    /*
+     * endpoing para findByCategoriaOrgan
+     */
+    @PostMapping("/categoria/organ")
+    public Document findByCategoriaOrgan(@RequestBody CategoriaDTO categoria) {
+        return ds.findByCategoriaOrgan(categoria.getCategoria(), categoria.getLugar(), categoria.getTlugar());
+    }
+
+    /*
+     * endpoint para findByID
+     */
+    @GetMapping("/{idDocument}")
+    public Document findByID(@PathVariable Integer idDocument) {
+       return ds.findById(idDocument);
+    }
+    
+
+    /*
+     * endpoint para upload
+     */
+    @PostMapping("/upload")
+    public String upload(@RequestBody DocumentTmpDTO documentTmpDTO) throws IOException {
+        return ds.upload(documentTmpDTO.getIdDocument(), documentTmpDTO.getIdCategoria(),
+            documentTmpDTO.getLugar(), documentTmpDTO.getTlugar(), documentTmpDTO.getFile());
+    }
+
+    @PostMapping("/archive")
+    public Document archive(@RequestBody Document doc) {
+        return ds.archive(doc);
+    }
+
+    @PostMapping("/archive/organ")
+    public void archiveByOrgan(@RequestBody ArchiveOrganDTO organ) {
+        ds.archiveByOrgan(organ.getLugar(), organ.getTlugar());
+    }
+}

+ 496 - 0
src/main/java/es/uv/saic/web/DownloadController.java

@@ -0,0 +1,496 @@
+package es.uv.saic.web;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Optional;
+
+import org.apache.commons.io.FilenameUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.FileSystemResource;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.HttpStatus;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.service.DocumentService;
+import es.uv.saic.service.PlantillaService;
+import es.uv.saic.shared.domain.Document;
+import es.uv.saic.shared.dto.InstanciaTascaDTO;
+import es.uv.saic.shared.dto.OrganDTO;
+import es.uv.saic.shared.dto.PdfDTO;
+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.shared.feign.IndicadorClient;
+import es.uv.saic.shared.feign.OrganClient;
+import es.uv.saic.shared.feign.ProceduresClient;
+import es.uv.saic.shared.feign.TascaClient;
+import fr.opensagres.xdocreport.core.XDocReportException;
+import fr.opensagres.xdocreport.core.io.internal.ByteArrayOutputStream;
+import fr.opensagres.xdocreport.document.IXDocReport;
+import fr.opensagres.xdocreport.document.images.FileImageProvider;
+import fr.opensagres.xdocreport.document.images.IImageProvider;
+import fr.opensagres.xdocreport.document.registry.XDocReportRegistry;
+import fr.opensagres.xdocreport.template.IContext;
+import fr.opensagres.xdocreport.template.TemplateEngineKind;
+import fr.opensagres.xdocreport.template.formatter.FieldsMetadata;
+import jakarta.servlet.http.HttpServletResponse;
+
+
+@RestController
+public class DownloadController {
+
+	@Autowired 
+	private PlantillaService pls;
+
+	@Autowired
+	private DocumentService ds;
+	
+	@Value("${saic.data.filePath}")
+	private String filePath;
+	
+	@Value("${saic.data.docsPath}")
+	private String docsPath;
+	
+	@Value("${saic.data.templates.fileNotFound}")
+	private String fileNotFound;
+	
+	@Value("${saic.data.templates.filePath}")
+	private String templatePath;
+	
+	@Value("${saic.data.templates.logoPath}")
+	private String logoPath;
+
+	@Autowired
+	private TascaClient tc;
+
+	@Autowired
+	private OrganClient oc;
+	
+	@Autowired
+	private IndicadorClient ic;
+
+	@Autowired
+	private ProceduresClient pc;
+
+	/*
+	 * Download a file associated with a task instance
+	 * @param model
+	 * @param idInstanciaTasca The ID of the task instance
+	 * @param response HttpServletResponse
+	 * @return A FileSystemResource
+	 *  representing the file to download
+	 */
+	@GetMapping(value="/download/{fileName}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+	public ResponseEntity<byte[]> download(@PathVariable("fileName") BigInteger idInstanciaTasca, HttpServletResponse response) throws FileNotFoundException {		
+		
+		try {
+			InstanciaTascaDTO i = tc.findInstanciaTascaById(idInstanciaTasca);
+			FileSystemResource file = null;
+			if(i.getTasca().getIdTipus() == 22){
+				file = new FileSystemResource(i.getEvidencia());
+				String extension = "."+FilenameUtils.getExtension(i.getEvidencia());
+				String name = "-"+i.getTasca().getNomEvidenciaCas().replace(" ", "_");
+				response.setHeader("Content-Disposition", "attachment; filename="+i.getTasca().getCodiEvidencia()+name+extension);
+			}
+			else{
+				file = new FileSystemResource(this.filePath+i.getEvidencia());
+				response.setHeader("Content-Disposition", "attachment; filename="+i.getEvidencia());
+			}
+			
+			if (!file.exists()) {
+				return ResponseEntity.ok(new FileSystemResource(this.fileNotFound).getInputStream().readAllBytes());
+			}
+
+			return ResponseEntity.ok(file.getInputStream().readAllBytes());
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		
+		return null;
+	}
+
+	// PARA BORRAR
+	/*
+	 * Download a document by its ID
+	 * @param model
+	 * @param idDocument The ID of the document to download
+	 * @param response HttpServletResponse
+	 * @return A FileSystemResource representing the document to download
+	 */
+	@GetMapping(value="/download/document/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+	@ResponseBody
+	public ResponseEntity<byte[]> downloadDocument(@PathVariable("id") Integer idDocument, HttpServletResponse response) {		
+		Document document = ds.findById(idDocument);
+		FileSystemResource file = new FileSystemResource(document.getRuta());
+		if(!file.exists()) {
+			return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
+		}
+		
+		response.setHeader("Content-Disposition", "attachment; filename="+file.getFilename());
+		try {
+			return ResponseEntity.ok(file.getInputStream().readAllBytes());
+		} catch (IOException e) {
+			e.printStackTrace();
+		}
+		return null;
+	}
+
+	/*
+	 * Download the latest report for a given process and degree
+	 * @param model
+	 * @param idTitulacio The ID of the degree
+	 * @param nomProces The name of the process
+	 * @param response HttpServletResponse
+	 * @return A FileSystemResource representing the report to download
+	 */
+	@GetMapping(value="/download/report/{t}/{p}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+	@ResponseBody 
+	public ResponseEntity<byte[]> downloadReport(@PathVariable("t") Integer idTitulacio, @PathVariable("p") String nomProces,
+			HttpServletResponse response) throws IOException, XDocReportException {
+		
+		OrganDTO titulacio = oc.findByID("T", idTitulacio);
+		
+		ProcesDTO procesDTO = new ProcesDTO(nomProces, titulacio.getLugar(), 
+														titulacio.getLugar2(), 
+														titulacio.getTambit());
+		TascaInformeTransferDTO it = tc.getLastByProcName(procesDTO);
+		
+		if(it != null) {
+			if((new File(this.filePath+it.getEvidencia())).exists()) {
+				response.setHeader("Content-Disposition", "attachment; filename="+Integer.toString(idTitulacio)+"_"+nomProces+".pdf");
+				return ResponseEntity.ok(new FileSystemResource(this.filePath+it.getEvidencia()).getInputStream().readAllBytes());
+			}
+		}
+		
+		return ResponseEntity.ok(new FileSystemResource(this.filePath+it.getEvidencia()).getInputStream().readAllBytes());
+	}
+
+
+	/*
+	 * Download a populated template for a given task instance
+	 * @param model
+	 * @param idTascai The ID of the task instance
+	 * @param response HttpServletResponse
+	 * @return A byte array representing the populated template to download
+	 */
+	@GetMapping(value="/download/template/{id}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+	@ResponseBody 
+	public ResponseEntity<byte[]> downloadTemplate(@PathVariable("id") BigInteger idTascai, HttpServletResponse response) throws IOException, XDocReportException {
+		XDocReportRegistry.getRegistry().clear();
+		String reportId = "none";
+		InstanciaTascaDTO it = tc.findInstanciaTascaById(idTascai);
+		TascaDTO tasca = it.getTasca();
+		 
+		/* Check if specific template exists */
+		Integer idTitulacio = it.getInstancia().getTitulacio();
+		String templatePath = this.templatePath+tasca.getCodiEvidencia().replace(".", "_")+".docx";
+		if(it.getInstancia().getTambit().equals("G") | idTitulacio == 1) {
+			File f = new File(this.templatePath+"/T1/"+tasca.getCodiEvidencia().replace(".", "_")+".docx");
+			if(f.exists() && !f.isDirectory()) { 
+				templatePath = this.templatePath+"/T1/"+tasca.getCodiEvidencia().replace(".", "_")+".docx";
+			}
+		}
+		else if(it.getInstancia().getTambit().equals("M") | idTitulacio == 2) {
+			File f = new File(this.templatePath+"/T2/"+tasca.getCodiEvidencia().replace(".", "_")+".docx");
+			if(f.exists() && !f.isDirectory()) { 
+				templatePath = this.templatePath+"/T2/"+tasca.getCodiEvidencia().replace(".", "_")+".docx";
+			}
+		}
+		
+		File f = new File(templatePath);
+		if(!f.exists()) {
+			return ResponseEntity.ok(new FileSystemResource(this.fileNotFound).getInputStream().readAllBytes());
+		}
+				
+		InputStream in = new FileInputStream(f);
+		IXDocReport report = XDocReportRegistry.getRegistry().loadReport(in, reportId, TemplateEngineKind.Velocity);
+		FieldsMetadata metadata = new FieldsMetadata();
+		metadata.addFieldAsImage("logo");
+		report.setFieldsMetadata(metadata);
+		IContext context = report.createContext();
+		IImageProvider img;
+		if(new File(this.logoPath+"C"+Integer.toString(it.getInstancia().getCentre())+".png").exists()) {
+			img = new FileImageProvider(new File(this.logoPath+"C"+Integer.toString(it.getInstancia().getCentre())+".png"));
+		}
+		else {
+			img = new FileImageProvider(new File(this.logoPath+"C0.png"));
+		}
+		context.put("logo", img);
+		context.put("centre", it.getInstancia().getNomval());
+		context.put("titulacio", it.getInstancia().getNomcas());
+		context.put("curs", Integer.toString(it.getInstancia().getCursAvaluat()-1)+" - "+Integer.toString(it.getInstancia().getCursAvaluat()));
+		context.put("curs_anterior", Integer.toString(it.getInstancia().getCursAvaluat()-2)+" - "+Integer.toString(it.getInstancia().getCursAvaluat()-1));
+		
+		Integer idCentre = it.getInstancia().getCentre();
+			
+		if(tasca.getIdTipus() == 14) { // Iterable template task
+			List<OrganDTO> titulacions = new ArrayList<OrganDTO>();
+			Integer ambit = idTitulacio/(int)1000;
+			titulacions = oc.getTitulacionsByTypeCentre(it.getInstancia().getLugar(), ambit);
+			List<HashMap<String, String>> data = new ArrayList<HashMap<String, String>>();
+			for(OrganDTO x : titulacions) {
+				HashMap<String, String> t = ic.getTemplateDataArray(x.getLugar2(), idCentre, it.getInstancia().getCursAvaluat());
+				t.put("titulacio", x.getNomCas());
+				data.add(t);
+			} 
+			context.put("data", data);
+			addToContext(ic.getTemplateData(idTitulacio, idCentre, it.getInstancia().getCursAvaluat()), context);
+		}
+		else { // NO iterable template task
+			addToContext(ic.getTemplateData(idTitulacio, idCentre, it.getInstancia().getCursAvaluat()), context);
+		}	
+		
+		ByteArrayOutputStream out = new ByteArrayOutputStream();  
+		report.process(context, out);
+
+	    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+tasca.getCodiEvidencia()+".docx\"");
+		return ResponseEntity.ok(out.toByteArray());
+		
+	}
+
+	/*
+	 * Test endpoint to generate a populated template for a given task (id of task)
+	 * @param model
+	 * @param idTitulacio The ID of the degree
+	 * @param idCentre The ID of the center
+	 * @param idTascap The ID of the task
+	 * @param idProces The ID of the process
+	 * @param response HttpServletResponse
+	 * @return A byte array representing the populated template
+	 */
+	@GetMapping(value="/download/test/template/{titulacio}/{centre}/{idProces}/{idTascap}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+	@ResponseBody
+	public ResponseEntity<byte[]> testTemplate(@PathVariable("titulacio") Integer idTitulacio, @PathVariable("centre") Integer idCentre,
+			@PathVariable("idTascap") Integer idTascap, @PathVariable("idProces") Integer idProces, HttpServletResponse response) 
+					throws IOException, XDocReportException {
+		XDocReportRegistry.getRegistry().clear();
+		String reportId = "none";
+		
+		TascaDTO tasca = tc.getByProcesTascap(idProces, idTascap);
+		ProcesDTO proces = pc.findById(idProces);
+		OrganDTO titulacio = oc.findByID("T", idTitulacio);
+		OrganDTO centre = oc.findByID("C", idCentre);
+		Integer ambit = idTitulacio/(int)1000;
+		
+		/* Check if specific template exists */
+		String templatePath = this.templatePath+tasca.getCodiEvidencia().replace(".", "_")+".docx";
+		if(titulacio.getTambit().equals("G") | idTitulacio == 1) {
+			File f = new File(this.templatePath+"/T1/"+tasca.getCodiEvidencia().replace(".", "_")+".docx");
+			if(f.exists() && !f.isDirectory()) { 
+				templatePath = this.templatePath+"/T1/"+tasca.getCodiEvidencia().replace(".", "_")+".docx";
+			}
+		}
+		else if(titulacio.getTambit().equals("M") | idTitulacio == 2) {
+			File f = new File(this.templatePath+"/T2/"+tasca.getCodiEvidencia().replace(".", "_")+".docx");
+			if(f.exists() && !f.isDirectory()) { 
+				templatePath = this.templatePath+"/T2/"+tasca.getCodiEvidencia().replace(".", "_")+".docx";
+			}
+		}
+		
+		InputStream in = new FileInputStream(new File(templatePath));
+		IXDocReport report = XDocReportRegistry.getRegistry().loadReport(in, reportId, TemplateEngineKind.Velocity);
+		FieldsMetadata metadata = new FieldsMetadata();
+		metadata.addFieldAsImage("logo");
+		report.setFieldsMetadata(metadata);
+		IContext context = report.createContext();
+		IImageProvider img;
+		if(new File(this.logoPath+"C"+Integer.toString(idCentre)+".png").exists()) {
+			img = new FileImageProvider(new File(this.logoPath+"C"+Integer.toString(idCentre)+".png"));
+		}
+		else {
+			img = new FileImageProvider(new File(this.logoPath+"C0.png"));
+		}
+		context.put("logo", img);
+		context.put("centre", centre.getNomVal());
+		context.put("titulacio", titulacio.getNomVal());
+		context.put("curs", Integer.toString(proces.getCursAvaluat()-1)+" - "+Integer.toString(proces.getCursAvaluat()));
+		context.put("curs_anterior", Integer.toString(proces.getCursAvaluat()-2)+" - "+Integer.toString(proces.getCursAvaluat()-1));
+		
+		if(tasca.getIdTipus() == 14) { // Iterable template task
+			List<OrganDTO> titulacions = new ArrayList<OrganDTO>();
+			titulacions = oc.getTitulacionsByTypeCentre(centre.getLugar2(), ambit);
+			List<HashMap<String, String>> data = new ArrayList<HashMap<String, String>>();
+			for(OrganDTO x : titulacions) {
+				HashMap<String, String> t = ic.getTemplateDataArray(x.getLugar2(), idCentre, proces.getCursAvaluat());
+				t.put("titulacio", x.getNomCas());
+				data.add(t);
+			}
+			context.put("data", data);
+			addToContext(ic.getTemplateData(idTitulacio, idCentre, proces.getCursAvaluat()), context);
+		}
+		else { // NO iterable template task
+			addToContext(ic.getTemplateData(idTitulacio, idCentre, proces.getCursAvaluat()), context);
+		}
+		
+		ByteArrayOutputStream out = new ByteArrayOutputStream();  
+		report.process(context, out);
+	    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+tasca.getCodiEvidencia()+".docx\"");
+
+		return ResponseEntity.ok(out.toByteArray());
+	}
+
+	/*
+	 * Test endpoint to generate a populated template for a given degree and evidence (Type of task)
+	 * @param model
+	 * @param idTitulacio The ID of the degree
+	 * @param idCentre The ID of the center
+	 * @param evidencia The name of the evidence
+	 * @param curs The academic year
+	 * @param tipusTasca The type of task
+	 * @param response HttpServletResponse
+	 * @return A byte array representing the populated template
+	 */
+	@GetMapping(value="/download/test/template2/{titulacio}/{centre}/{evidencia}/{curs}/{tipusTasca}", produces = MediaType.APPLICATION_OCTET_STREAM_VALUE)
+	@ResponseBody
+	public ResponseEntity<byte[]> testTemplate(Model model, @PathVariable("titulacio") Integer idTitulacio, @PathVariable("centre") Integer idCentre,
+			@PathVariable("evidencia") String evidencia, @PathVariable("curs") Integer curs, @PathVariable("tipusTasca") Integer tipusTasca, 
+			HttpServletResponse response) throws IOException, XDocReportException {
+		XDocReportRegistry.getRegistry().clear();
+		String reportId = "none";
+		
+		OrganDTO titulacio = oc.findByID("T", idTitulacio);
+		OrganDTO centre = oc.findByID("C", idCentre);
+		Integer ambit = idTitulacio/(int)1000;
+		
+		/* Check if specific template exists */
+		String templatePath = this.templatePath+evidencia.replace(".", "_").replace(" (G)", "").replace(" (M)", "")+".docx";
+		if(idTitulacio < 2000 | idTitulacio == 1) {
+			File f = new File(this.templatePath+"/T1/"+evidencia.replace(".", "_").replace(" (G)", "").replace(" (M)", "")+".docx");
+			if(f.exists() && !f.isDirectory()) { 
+				templatePath = this.templatePath+"/T1/"+evidencia.replace(".", "_").replace(" (G)", "").replace(" (M)", "")+".docx";
+			}
+			ambit = 1;
+		}
+		else if((idTitulacio >= 2000 & idTitulacio < 3000) | idTitulacio == 2) {
+			File f = new File(this.templatePath+"/T2/"+evidencia.replace(".", "_").replace(" (G)", "").replace(" (M)", "")+".docx");
+			if(f.exists() && !f.isDirectory()) { 
+				templatePath = this.templatePath+"/T2/"+evidencia.replace(".", "_").replace(" (G)", "").replace(" (M)", "")+".docx";
+			}
+			ambit = 2;
+		}
+		
+		InputStream in = new FileInputStream(new File(templatePath));
+		IXDocReport report = XDocReportRegistry.getRegistry().loadReport(in, reportId, TemplateEngineKind.Velocity);
+		FieldsMetadata metadata = new FieldsMetadata();
+		metadata.addFieldAsImage("logo");
+		report.setFieldsMetadata(metadata);
+		IContext context = report.createContext();
+		IImageProvider img;
+		if(new File(this.logoPath+"C"+Integer.toString(idCentre)+".png").exists()) {
+			img = new FileImageProvider(new File(this.logoPath+"C"+Integer.toString(idCentre)+".png"));
+		}
+		else {
+			img = new FileImageProvider(new File(this.logoPath+"C0.png"));
+		}
+		context.put("logo", img);
+		context.put("centre", centre.getNomVal());
+		context.put("titulacio", titulacio.getNomVal());
+		context.put("curs", Integer.toString(curs-1)+" - "+Integer.toString(curs));
+		context.put("curs_anterior", Integer.toString(curs-2)+" - "+Integer.toString(curs-1));
+		
+		if(tipusTasca == 14) { // Iterable template task
+			List<OrganDTO> titulacions = new ArrayList<OrganDTO>();
+			titulacions = oc.getTitulacionsByTypeCentre(centre.getLugar2(), ambit);
+			List<HashMap<String, String>> data = new ArrayList<HashMap<String, String>>();
+			for(OrganDTO x : titulacions) {
+				HashMap<String, String> t = ic.getTemplateDataArray(x.getLugar2(), idCentre, curs);
+				t.put("titulacio", x.getNomCas());
+				data.add(t);
+			}
+			context.put("data", data);
+			addToContext(ic.getTemplateData(idTitulacio, idCentre, curs), context);
+		}
+		else { // NO iterable template task
+			addToContext(ic.getTemplateData(idTitulacio, idCentre, curs), context);
+		}
+		
+		ByteArrayOutputStream out = new ByteArrayOutputStream();  
+		report.process(context, out);
+	    response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+evidencia+".docx\"");
+	    response.setHeader(HttpHeaders.CONTENT_TYPE, "application/vnd.openxmlformats-officedocument.wordprocessingml.document");
+
+		return ResponseEntity.ok(out.toByteArray());
+		
+	}
+	
+	/*
+	 * Generate a PDF from the content of a task instance (unused)
+	 * @param model
+	 * @param idTascai The ID of the task instance
+	 * @param response HttpServletResponse
+	 * @return A byte array representing the generated PDF
+	 */
+	@GetMapping(value="/download/pdf/{idTascai}")
+	@ResponseBody
+	public byte[] downloadTemplatePdf(@PathVariable("idTascai") BigInteger idTascai, HttpServletResponse response) throws IOException, InterruptedException {
+		InstanciaTascaDTO it = tc.findInstanciaTascaById(idTascai);
+		response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\""+it.getIdInstanciaTasca()+".pdf\"");
+		response.setHeader(HttpHeaders.CONTENT_TYPE, "application/pdf");
+		return pls.toPDF(it.getText(), Optional.of(idTascai));
+	}
+
+	/*
+	 * Generate a PDF preview from provided content
+	 * @param model
+	 * @param content The content to convert to PDF
+	 * @param idtascai Optional ID of the task instance for context
+	 * @param response HttpServletResponse
+	 */
+	@PostMapping(value="/download/pdf/preview")
+	@ResponseBody
+	public byte[] downloadTemplatePdf(HttpServletResponse response,
+			@RequestBody PdfDTO pdf) throws IOException, InterruptedException {
+		response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"borrador.pdf\"");
+		response.setHeader(HttpHeaders.CONTENT_TYPE, "application/pdf");	
+		return pls.toPDF(pdf.getContent(), pdf.getIdtascai());
+	}
+
+
+	private void addToContext(HashMap<String,String> templateData, IContext context) {
+		for (HashMap.Entry<String, String> entry : templateData.entrySet()) {
+		    context.put(entry.getKey(), formatValue(entry.getValue()));
+		}
+	}
+
+	/*
+	 * Format a value for template insertion
+	 * @param v The value to format
+	 * @return The formatted value
+	 */
+	private String formatValue(String v) {	
+		if(v == null) return ""; 
+		if(v.equals("NP")) return "NP";
+		if(v.isEmpty() | v.isBlank()) {
+			return "";
+		}
+		
+		try{
+	        return String.format("%.2f", Float.parseFloat(v)).replace(",", ".");
+	    } 
+		catch(NumberFormatException e){ }
+		
+		try{
+	        return Integer.toString(Integer.parseInt(v));
+	    } 
+		catch(NumberFormatException e){ }
+		
+		return v;
+	}
+}

+ 89 - 0
src/main/java/es/uv/saic/web/PlantillaController.java

@@ -0,0 +1,89 @@
+package es.uv.saic.web;
+
+import java.io.IOException;
+import java.util.List;
+
+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.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import es.uv.saic.service.PlantillaService;
+import es.uv.saic.shared.domain.Plantilla;
+import es.uv.saic.shared.dto.PdfDTO;
+import es.uv.saic.shared.dto.TemplateDataDTO;
+
+
+@RestController
+@RequestMapping("/plantilla")
+public class PlantillaController {
+    @Autowired
+    private PlantillaService ps;
+
+    @GetMapping
+    public List<Plantilla> findAll() {
+        return ps.findAll();
+    }
+
+    @GetMapping("/{id}")
+    public Plantilla findByID(@PathVariable Integer id) {
+        return ps.findByID(id);
+    }
+    
+    @GetMapping("/{versio}/{codi}/{ambit}")
+    public Plantilla findByVersioCodiAmbit(@PathVariable Integer versio,
+        @PathVariable String codi,
+        @PathVariable String ambit) {
+        try {
+            Plantilla plantilla = ps.findByVersioCodiAmbit(versio, codi, ambit);
+
+             return plantilla ;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return null;
+        }
+    }
+
+    @PostMapping
+    public String save(@RequestBody Plantilla plantilla) {
+        try {
+            ps.save(plantilla);
+            return "Save";
+        } catch (Exception e) {
+            e.printStackTrace();
+            return "Error guardando la plantilla: " + e.getMessage();
+        }
+    }
+    
+    @DeleteMapping
+    public String delete(@RequestBody Plantilla plantilla) {
+        ps.delete(plantilla);
+        return "Delete";
+    }
+    
+    @GetMapping("/used/{id}")
+    public boolean isUsed(@PathVariable Integer id) {
+       return ps.isUsed(id);
+    }
+
+    @PostMapping("/addTemplateData")
+    public String addTemplateData(@RequestBody TemplateDataDTO td) {
+        return ps.addTemplateData(td.getInstanciaTasca(), td.getTemplate());
+    }
+    
+    @PostMapping("/addTemplateData2")
+    public String addTemplateData2(@RequestBody TemplateDataDTO td) {
+        return ps.addTemplateData(td.getIdTitulacio(), td.getIdCentre(), td.getCurs(), td.getTemplate());        
+}
+
+    @PostMapping("/savePDF")
+    public String savePDF(@RequestBody PdfDTO pdf) throws IOException, InterruptedException {
+        return ps.savePDF(pdf.getContent(), pdf.getIdtasca());        
+
+    }
+    
+}

+ 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

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

@@ -0,0 +1,29 @@
+# Urls
+saic.url.domain = http://127.0.0.1
+saic.url.public = ${saic.url.docs.domain}/public
+
+# 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

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

@@ -0,0 +1,48 @@
+spring.application.name = saic-docs-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
+
+# 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

+ 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() {
+	}
+
+}