Compare commits

...

27 Commits

Author SHA1 Message Date
b68933b676 added camp registration stop message 2025-03-10 18:16:19 +01:00
c692f7881b allow kerstin to access camp list 2025-03-04 23:02:10 +01:00
72b91dd8bc corrected label 2025-01-31 10:12:05 +01:00
44498c5ea1 costs for nights, not days 2025-01-31 10:05:40 +01:00
faa8140b0c build.gradle aktualisiert 2025-01-31 10:03:59 +01:00
1733821102 omit npe 2025-01-30 22:48:18 +01:00
30fb52a606 camp admin added 2025-01-30 22:37:22 +01:00
bdc0d84e69 added special prices for special wishes 2025-01-15 21:55:24 +01:00
5d61ae6013 added details view 2024-12-13 19:17:27 +01:00
6feecb06c3 added more user friendly welcome page 2024-12-11 22:56:33 +01:00
eef6295bae also respecting day slots 2024-12-03 21:13:52 +01:00
ebb603b569 basic extensions 2024-12-02 22:58:28 +01:00
a0a32563cc removed obsolete code 2024-12-01 19:40:18 +01:00
3c90142162 preview page depending on roles 2024-12-01 19:38:19 +01:00
9c10da579c java 21 2024-12-01 19:20:57 +01:00
634c0d7b1c basic registration feature 2024-12-01 19:16:05 +01:00
bf88306d85 fixed damaged deleting slots, see #5 2024-11-11 23:24:34 +01:00
a1ad23920a repaired datatables extension 2024-10-03 10:03:26 +02:00
8d5a493bf7 ical extension 2024-09-02 21:29:07 +02:00
d28f6b45fc using Kinderstunde klein for date privileges 2024-07-30 11:22:50 +02:00
e3ebc387bb added next 2024-07-30 10:43:25 +02:00
aadfdfa9b5 corrected lib 2024-01-13 16:18:29 +01:00
e568589181 added download option, see #1 2024-01-13 15:59:57 +01:00
cffe483969 german time representation by keeping the ordering right; see #3 2024-01-13 15:42:59 +01:00
f1f9a5be9b fixed null on token 2023-12-26 21:01:06 +01:00
47bcd311ea securing with nextcloud oidc 2023-12-26 18:28:37 +01:00
6fdd4a57e0 added basic spring boot security 2023-12-17 20:56:56 +01:00
62 changed files with 2002 additions and 233 deletions

View File

@ -19,7 +19,7 @@
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-17/"/>
<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.jst.j2ee.internal.web.container"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer">
<attributes>

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=21
org.eclipse.jdt.core.compiler.compliance=21
org.eclipse.jdt.core.compiler.source=21

View File

@ -3,5 +3,5 @@
<fixed facet="jst.java"/>
<fixed facet="jst.web"/>
<installed facet="jst.web" version="2.4"/>
<installed facet="jst.java" version="17"/>
<installed facet="jst.java" version="21"/>
</faceted-project>

View File

@ -1,19 +1,25 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.1.3'
id "io.spring.dependency-management" version "1.1.2"
id 'org.springframework.boot' version '3.3.4'
id "io.spring.dependency-management" version "1.1.6"
id 'war'
id 'eclipse'
id 'application'
}
group = 'de.jottyfan.bico'
version = '0.0.4'
version = '0.2.6'
description = """BibleClassOrganizer"""
sourceCompatibility = 17
targetCompatibility = 17
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
sourceCompatibility = 21
targetCompatibility = 21
mainClassName = "de.jottyfan.bico.Main"
@ -35,24 +41,32 @@ war {
"Implementation-Timestamp": new Date())
}
}
baseName = project.name
version = version
archiveName = 'BiCO.war'
archiveBaseName = project.name
archiveVersion = version
archiveFileName = 'BiCO.war'
}
dependencies {
implementation 'de.jottyfan:bicolib:4'
implementation 'de.jottyfan:bicolib:8'
implementation 'org.mnode.ical4j:ical4j:4.0.4'
implementation 'org.springframework.boot:spring-boot-starter-jooq'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:latest.release'
implementation 'org.springframework.security:spring-security-oauth2-client'
implementation 'org.webjars:bootstrap:5.3.1'
implementation 'org.webjars.npm:bootstrap-icons:1.10.5'
implementation 'org.webjars:bootstrap:5.3.3'
implementation 'org.webjars.npm:bootstrap-icons:1.11.3'
implementation 'org.webjars:jquery:3.7.1'
implementation 'org.webjars:datatables:1.13.5'
implementation 'org.webjars.npm:datatables.net:2.1.7'
implementation 'org.webjars.npm:datatables.net-buttons:3.1.1'
implementation 'org.webjars.npm:datatables.net-responsive:3.0.1'
implementation 'org.webjars.npm:datatables.net-bs5:2.1.7'
implementation 'org.springframework.boot:spring-boot-devtools'
@ -69,3 +83,8 @@ test {
springBoot {
buildInfo()
}
tasks.withType(JavaCompile).configureEach {
options.compilerArgs.add("-parameters")
}

Binary file not shown.

View File

@ -1,5 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

309
gradlew vendored
View File

@ -1,78 +1,127 @@
#!/usr/bin/env sh
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed 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.
#
##############################################################################
##
## Gradle start up script for UN*X
##
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
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
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS=""
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
MAX_FD=maximum
warn () {
echo "$*"
}
} >&2
die () {
echo
echo "$*"
echo
exit 1
}
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
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"
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@ -81,92 +130,120 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin, switch paths to Windows format before running java
if $cygwin ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=$((i+1))
done
case $i in
(0) set -- ;;
(1) set -- "$args0" ;;
(2) set -- "$args0" "$args1" ;;
(3) set -- "$args0" "$args1" "$args2" ;;
(4) set -- "$args0" "$args1" "$args2" "$args3" ;;
(5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
(6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
(7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
(8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
(9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=$(save "$@")
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS --illegal-access=permit $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
cd "$(dirname "$0")"
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

View File

@ -1,5 +1,7 @@
package de.jottyfan.bico;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
@ -12,6 +14,9 @@ import org.springframework.boot.web.servlet.support.SpringBootServletInitializer
*/
@SpringBootApplication
public class Main extends SpringBootServletInitializer {
public static final Logger LOGGER = LogManager.getLogger(Main.class);
@Override
protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
return application.sources(Main.class);

View File

@ -17,7 +17,7 @@ import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
*
*/
@Configuration
public class DatabaseConfiguration {
public class DatabaseConfig {
@Autowired
private DataSource dataSource;

View File

@ -0,0 +1,26 @@
package de.jottyfan.bico.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
/**
*
* @author jotty
*
*/
@Configuration
public class SecurityConfig {
@Bean
SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.authorizeHttpRequests(
// @formatter:off
r -> r.requestMatchers("/", "/error", "/css/**", "/js/**", "/webjars/**", "/template").permitAll()
.requestMatchers("/**").authenticated())
.oauth2Login(l -> l.authorizationEndpoint(e -> e.baseUri("/oauth2/authorize-client")));
// @formatter:on
return http.build();
}
}

View File

@ -1,8 +1,16 @@
package de.jottyfan.bico.modules;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.ui.Model;
import java.security.Principal;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.web.bind.annotation.ModelAttribute;
import de.jottyfan.bico.Main;
import de.jottyfan.bico.modules.profile.ProfileService;
/**
@ -11,19 +19,74 @@ import de.jottyfan.bico.modules.profile.ProfileService;
*
*/
public abstract class CommonController {
private static final List<String> admins = List.of("andre.sieber", "tobias.kuehne", "jotty", "kerstin.meisel");
@Autowired
private ProfileService profileService;
@Value("${spring.security.oauth2.client.provider.nextcloud.issuer-uri}")
private String nextcloudUrl;
@ModelAttribute("isCampAdmin")
public Boolean isCampAdmin(Principal principal) {
return principal == null ? false : admins.contains(principal.getName());
}
@ModelAttribute("hasBUrole")
public Boolean hasBURole(Principal principal) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) principal;
if (token != null) {
OAuth2User user = token.getPrincipal();
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) user.getAttributes().get("roles");
return roles.contains("Bibelunterricht");
} else {
Main.LOGGER.warn("token is null, no roles can be detected");
return false;
}
}
@ModelAttribute("hasDateRole")
public Boolean hasDateRole(Principal principal) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) principal;
if (token != null) {
OAuth2User user = token.getPrincipal();
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) user.getAttributes().get("roles");
return roles.contains("Kinderstunde klein");
} else {
Main.LOGGER.warn("token is null, no roles can be detected");
return false;
}
}
@ModelAttribute("hasAnyRole")
public Boolean hasAnyRole(Principal principal) {
OAuth2AuthenticationToken token = (OAuth2AuthenticationToken) principal;
if (token != null) {
OAuth2User user = token.getPrincipal();
@SuppressWarnings("unchecked")
List<String> roles = (List<String>) user.getAttributes().get("roles");
return roles.size() > 0;
} else {
Main.LOGGER.warn("token is null, no roles can be detected");
return false;
}
}
/**
* get the theme for the current session
*
* @return the theme; light or dark at the moment
*/
public Model useThemedModel(Model model) {
// TODO: add profile's user name
String username = "jotty";
model.addAttribute("theme", profileService.getTheme(username));
return model;
@ModelAttribute("theme")
public String getTheme() {
String username = SecurityContextHolder.getContext().getAuthentication().getName();
return profileService.getTheme(username);
}
@ModelAttribute("nextcloudUrl")
public String getNextcloudUrl() {
return nextcloudUrl;
}
}

View File

@ -0,0 +1,51 @@
package de.jottyfan.bico.modules.camp;
import java.io.IOException;
import java.security.Principal;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import de.jottyfan.bico.modules.CommonController;
import jakarta.servlet.http.HttpServletResponse;
/**
*
* @author jotty
*
*/
@Configuration
@Controller
public class AdminRegistrationController extends CommonController {
@Autowired
private AdminRegistrationService service;
@GetMapping("/camp/registration/admin")
public String getList(Model model, Principal principal) {
if (isCampAdmin(principal)) {
model.addAttribute("list", service.getAllRegistrations());
model.addAttribute("ages", service.getAges());
}
return "/camp/list";
}
@GetMapping("/camp/registration/admin/download")
@ResponseBody
public String download(HttpServletResponse response, Principal principal) throws IOException {
if (isCampAdmin(principal)) {
response.setHeader("Content-Disposition", String.format("attachment; filename=Gemeindefreizeit-Anmeldungen-%s.csv",
LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME)));
response.setContentType("text/csv; charset=utf-8");
return service.getDownload();
} else {
return "forbidden";
}
}
}

View File

@ -0,0 +1,33 @@
package de.jottyfan.bico.modules.camp;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.db.camp.tables.records.TAgeRecord;
import de.jottyfan.bico.modules.camp.model.RegistrationBean;
/**
*
* @author jotty
*
*/
@Service
public class AdminRegistrationService {
@Autowired
private RegistrationRepository repository;
public List<RegistrationBean> getAllRegistrations() {
return repository.getAllRegistrations();
}
public List<TAgeRecord> getAges() {
return repository.getAges();
}
public String getDownload() {
return repository.getDownload();
}
}

View File

@ -0,0 +1,77 @@
package de.jottyfan.bico.modules.camp;
import java.security.Principal;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.bico.db.camp.enums.EnumSex;
import de.jottyfan.bico.modules.CommonController;
import de.jottyfan.bico.modules.camp.model.RegistrationBean;
import jakarta.validation.Valid;
/**
*
* @author jotty
*
*/
@Controller
public class RegistrationController extends CommonController {
@Autowired
private RegistrationService service;
@GetMapping("/camp/registration")
public String loadForm(Model model, Principal principal) {
model.addAttribute("registrations", service.getRegistrations(principal));
model.addAttribute("bean", RegistrationBean.withAllDays());
model.addAttribute("sexes", EnumSex.values());
model.addAttribute("ages", service.getAges());
return "/camp/registration";
}
@PostMapping("/camp/registration/submit")
public String submitAddForm(@Valid @ModelAttribute("bean") RegistrationBean bean, BindingResult bindingResult,
Model model, Principal principal) {
if (bindingResult.hasErrors()) {
model.addAttribute("registrations", new ArrayList<RegistrationBean>()); // hack to make "Neue Anmeldung" appear
model.addAttribute("sexes", EnumSex.values());
model.addAttribute("ages", service.getAges());
return "/camp/registration";
}
service.save(bean, principal);
return "redirect:/camp/registration";
}
@GetMapping("/camp/registration/edit/{id}")
public String loadEditForm(@PathVariable("id") Integer id, Model model, Principal principal) {
model.addAttribute("bean", service.getBeanOfPrincipal(id, principal));
model.addAttribute("sexes", EnumSex.values());
model.addAttribute("ages", service.getAges());
return "/camp/edit";
}
@GetMapping("/camp/registration/delete/{id}")
public String delete(@PathVariable("id") Integer id, Model model, Principal principal) {
service.delete(id, principal);
return "redirect:/camp/registration";
}
@PostMapping("/camp/registration/correct")
public String submitEditForm(@Valid @ModelAttribute("bean") RegistrationBean bean, BindingResult bindingResult,
Model model, Principal principal) {
if (bindingResult.hasErrors()) {
model.addAttribute("sexes", EnumSex.values());
return "/camp/registration";
}
service.save(bean, principal);
return "redirect:/camp/registration";
}
}

View File

@ -0,0 +1,222 @@
package de.jottyfan.bico.modules.camp;
import static de.jottyfan.bico.db.camp.Tables.T_AGE;
import static de.jottyfan.bico.db.camp.Tables.T_REGISTRATION;
import java.time.LocalDateTime;
import java.util.List;
import org.jooq.DSLContext;
import org.jooq.DeleteConditionStep;
import org.jooq.InsertValuesStep20;
import org.jooq.Record19;
import org.jooq.SelectConditionStep;
import org.jooq.SelectOnConditionStep;
import org.jooq.SelectSeekStep1;
import org.jooq.UpdateConditionStep;
import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.Main;
import de.jottyfan.bico.db.camp.enums.EnumCamp;
import de.jottyfan.bico.db.camp.enums.EnumSex;
import de.jottyfan.bico.db.camp.tables.records.TAgeRecord;
import de.jottyfan.bico.db.camp.tables.records.TRegistrationRecord;
import de.jottyfan.bico.modules.camp.model.RegistrationBean;
import jakarta.validation.Valid;
/**
*
* @author jotty
*
*/
@Repository
public class RegistrationRepository {
@Autowired
private DSLContext jooq;
/**
* save the validated bean
*
* @param bean the bean
* @param registrator the principal name
*/
public void save(@Valid RegistrationBean bean, String registrator) {
jooq.transaction(t -> {
if (bean.getPkRegistration() == null) {
InsertValuesStep20<TRegistrationRecord, EnumCamp, String, String, String, EnumSex, Boolean, String, Integer, Boolean, String, Boolean, Integer, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean> sql = DSL.using(t)
// @formatter:off
.insertInto(T_REGISTRATION,
T_REGISTRATION.CAMP,
T_REGISTRATION.REGISTRATOR,
T_REGISTRATION.FORENAME,
T_REGISTRATION.SURNAME,
T_REGISTRATION.SEX,
T_REGISTRATION.BARRIER_FREE,
T_REGISTRATION.NUTRITION,
T_REGISTRATION.DRIVER_PROVIDE_PLACES,
T_REGISTRATION.WANT_PLACE_IN_CAR,
T_REGISTRATION.DISEASES,
T_REGISTRATION.REQUIRE_PAYMENT,
T_REGISTRATION.FK_AGE,
T_REGISTRATION.DAY0,
T_REGISTRATION.DAY1,
T_REGISTRATION.DAY2,
T_REGISTRATION.DAY3,
T_REGISTRATION.DAY4,
T_REGISTRATION.TOWELS,
T_REGISTRATION.BED_LINEN,
T_REGISTRATION.COT)
.values(EnumCamp.Gemeindefreizeit_2025, registrator, bean.getForename(), bean.getSurname(), EnumSex.lookupLiteral(bean.getSex()),
bean.getBarrierFree(), bean.getNutrition(), bean.getDriverProvidePlaces(), bean.getWantPlaceInCar(), bean.getDiseases(), bean.getRequirePayment(),
bean.getFkAge(), bean.getDay0(), bean.getDay1(), bean.getDay2(), bean.getDay3(), bean.getDay4(), bean.getTowels(), bean.getBedLinen(), bean.getCot());
// @formatter:on
Main.LOGGER.trace(sql);
sql.execute();
} else {
UpdateConditionStep<TRegistrationRecord> sql = DSL.using(t)
// @formatter:off
.update(T_REGISTRATION)
.set(T_REGISTRATION.FORENAME, bean.getForename())
.set(T_REGISTRATION.SURNAME, bean.getSurname())
.set(T_REGISTRATION.SEX, EnumSex.lookupLiteral(bean.getSex()))
.set(T_REGISTRATION.REGISTRATOR, registrator)
.set(T_REGISTRATION.BARRIER_FREE, bean.getBarrierFree())
.set(T_REGISTRATION.NUTRITION, bean.getNutrition())
.set(T_REGISTRATION.DRIVER_PROVIDE_PLACES, bean.getDriverProvidePlaces())
.set(T_REGISTRATION.WANT_PLACE_IN_CAR, bean.getWantPlaceInCar())
.set(T_REGISTRATION.DISEASES, bean.getDiseases())
.set(T_REGISTRATION.REQUIRE_PAYMENT, bean.getRequirePayment())
.set(T_REGISTRATION.FK_AGE, bean.getFkAge())
.set(T_REGISTRATION.DAY0, bean.getDay0())
.set(T_REGISTRATION.DAY1, bean.getDay1())
.set(T_REGISTRATION.DAY2, bean.getDay2())
.set(T_REGISTRATION.DAY3, bean.getDay3())
.set(T_REGISTRATION.DAY4, bean.getDay4())
.set(T_REGISTRATION.TOWELS, bean.getTowels())
.set(T_REGISTRATION.BED_LINEN, bean.getBedLinen())
.set(T_REGISTRATION.COT, bean.getCot())
.where(T_REGISTRATION.PK_REGISTRATION.eq(bean.getPkRegistration()));
// @formatter:on
Main.LOGGER.trace(sql);
sql.execute();
}
});
}
/**
* get all registrations (for admins only)
*
* @return the registrations
*/
public List<RegistrationBean> getAllRegistrations() {
SelectSeekStep1<TRegistrationRecord, LocalDateTime> sql = jooq
// @formatter:off
.selectFrom(T_REGISTRATION)
.orderBy(T_REGISTRATION.CREATED);
// @formatter:on
Main.LOGGER.trace(sql);
return sql.fetchInto(RegistrationBean.class);
}
/**
* get all registrations of name
*
* @param name the name
* @return the registrations
*/
public List<RegistrationBean> getRegistrations(String name) {
SelectConditionStep<TRegistrationRecord> sql = jooq
// @formatter:off
.selectFrom(T_REGISTRATION)
.where(T_REGISTRATION.REGISTRATOR.eq(name));
// @formatter:on
Main.LOGGER.trace(sql);
return sql.fetchInto(RegistrationBean.class);
}
/**
* get the registration of id if the creator is the name; null else
*
* @param id the id
* @param name the name
* @return the bean or null
*/
public RegistrationBean getBean(Integer id, String name) {
SelectConditionStep<TRegistrationRecord> sql = jooq
// @formatter:off
.selectFrom(T_REGISTRATION)
.where(T_REGISTRATION.PK_REGISTRATION.eq(id))
.and(T_REGISTRATION.REGISTRATOR.eq(name));
// @formatter:on
Main.LOGGER.trace(sql);
return sql.fetchOneInto(RegistrationBean.class);
}
/**
* delete the registration if the name is the creator
*
* @param id the id
* @param name the name
*/
public void delete(Integer id, String name) {
DeleteConditionStep<TRegistrationRecord> sql = jooq
// @formatter:off
.deleteFrom(T_REGISTRATION)
.where(T_REGISTRATION.PK_REGISTRATION.eq(id))
.and(T_REGISTRATION.REGISTRATOR.eq(name));
// @formatter:on
Main.LOGGER.trace(sql);
sql.execute();
}
/**
* get all ages
*
* @return the ages
*/
public List<TAgeRecord> getAges() {
SelectSeekStep1<TAgeRecord, Integer> sql = jooq
// @formatter:off
.selectFrom(T_AGE)
.orderBy(T_AGE.PK_AGE);
// @formatter:on
Main.LOGGER.trace(sql);
return sql.fetchInto(TAgeRecord.class);
}
/**
* get CSV version of the list, only for admins
*
* @return the csv
*/
public String getDownload() {
SelectOnConditionStep<Record19<LocalDateTime, String, String, EnumSex, String, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, Boolean, String, String, Integer, Boolean>> sql = jooq
// @formatter:off
.select(T_REGISTRATION.CREATED,
T_REGISTRATION.FORENAME,
T_REGISTRATION.SURNAME,
T_REGISTRATION.SEX,
T_AGE.NAME,
T_REGISTRATION.DAY0,
T_REGISTRATION.DAY1,
T_REGISTRATION.DAY2,
T_REGISTRATION.DAY3,
T_REGISTRATION.DAY4,
T_REGISTRATION.BARRIER_FREE,
T_REGISTRATION.TOWELS,
T_REGISTRATION.BED_LINEN,
T_REGISTRATION.COT,
T_REGISTRATION.REQUIRE_PAYMENT,
T_REGISTRATION.DISEASES,
T_REGISTRATION.NUTRITION,
T_REGISTRATION.DRIVER_PROVIDE_PLACES,
T_REGISTRATION.WANT_PLACE_IN_CAR)
.from(T_REGISTRATION)
.leftJoin(T_AGE).on(T_AGE.PK_AGE.eq(T_REGISTRATION.FK_AGE));
// @formatter:on
Main.LOGGER.trace(sql);
return sql.fetch().formatCSV(true);
}
}

View File

@ -0,0 +1,72 @@
package de.jottyfan.bico.modules.camp;
import java.security.Principal;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.db.camp.tables.records.TAgeRecord;
import de.jottyfan.bico.modules.camp.model.RegistrationBean;
import jakarta.validation.Valid;
/**
*
* @author jotty
*
*/
@Service
public class RegistrationService {
@Autowired
private RegistrationRepository repository;
/**
* save the bean content
*
* @param bean the bean
*/
public void save(@Valid RegistrationBean bean, Principal principal) {
repository.save(bean, principal.getName());
}
/**
* get the registrations of principal
*
* @param principal the principal
* @return the registrations
*/
public List<RegistrationBean> getRegistrations(Principal principal) {
return repository.getRegistrations(principal.getName());
}
/**
* get the registration if the principal was the creator
*
* @param id the ID of the registration
* @param principal the principal
* @return the bean or null if not found or allowed
*/
public RegistrationBean getBeanOfPrincipal(Integer id, Principal principal) {
return repository.getBean(id, principal.getName());
}
/**
* delete the registration if the principal was the creator
*
* @param id the ID of the registration
* @param principal the principal
*/
public void delete(Integer id, Principal principal) {
repository.delete(id, principal.getName());
}
/**
* get the ages
*
* @return the ages
*/
public List<TAgeRecord> getAges() {
return repository.getAges();
}
}

View File

@ -0,0 +1,320 @@
package de.jottyfan.bico.modules.camp.model;
import java.io.Serializable;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
/**
*
* @author jotty
*
*/
public class RegistrationBean implements Serializable {
private static final long serialVersionUID = 1L;
private Integer pkRegistration;
@NotBlank
private String forename;
@NotBlank
private String surname;
@NotBlank
private String sex;
@NotNull
private Boolean barrierFree;
private Boolean day0;
private Boolean day1;
private Boolean day2;
private Boolean day3;
private Boolean day4;
private Boolean towels;
private Boolean bedLinen;
private Boolean cot;
private String nutrition;
private Integer driverProvidePlaces;
private Boolean wantPlaceInCar;
private String diseases;
private Boolean requirePayment;
@NotNull
private Integer fkAge;
public static final RegistrationBean withAllDays() {
RegistrationBean bean = new RegistrationBean();
bean.setDay0(true);
bean.setDay1(true);
bean.setDay2(true);
bean.setDay3(true);
bean.setDay4(true);
return bean;
}
/**
* @return the forename
*/
public String getForename() {
return forename;
}
/**
* @param forename the forename to set
*/
public void setForename(String forename) {
this.forename = forename;
}
/**
* @return the surname
*/
public String getSurname() {
return surname;
}
/**
* @param surname the surname to set
*/
public void setSurname(String surname) {
this.surname = surname;
}
/**
* @return the sex
*/
public String getSex() {
return sex;
}
/**
* @param sex the sex to set
*/
public void setSex(String sex) {
this.sex = sex;
}
/**
* @return the pkRegistration
*/
public Integer getPkRegistration() {
return pkRegistration;
}
/**
* @param pkRegistration the pkRegistration to set
*/
public void setPkRegistration(Integer pkRegistration) {
this.pkRegistration = pkRegistration;
}
/**
* @return the barrierFree
*/
public Boolean getBarrierFree() {
return barrierFree;
}
/**
* @param barrierFree the barrierFree to set
*/
public void setBarrierFree(Boolean barrierFree) {
this.barrierFree = barrierFree;
}
/**
* @return the nutrition
*/
public String getNutrition() {
return nutrition;
}
/**
* @param nutrition the nutrition to set
*/
public void setNutrition(String nutrition) {
this.nutrition = nutrition;
}
/**
* @return the diseases
*/
public String getDiseases() {
return diseases;
}
/**
* @param diseases the diseases to set
*/
public void setDiseases(String diseases) {
this.diseases = diseases;
}
/**
* @return the driverProvidePlaces
*/
public Integer getDriverProvidePlaces() {
return driverProvidePlaces;
}
/**
* @param driverProvidePlaces the driverProvidePlaces to set
*/
public void setDriverProvidePlaces(Integer driverProvidePlaces) {
this.driverProvidePlaces = driverProvidePlaces;
}
/**
* @return the wantPlaceInCar
*/
public Boolean getWantPlaceInCar() {
return wantPlaceInCar;
}
/**
* @param wantPlaceInCar the wantPlaceInCar to set
*/
public void setWantPlaceInCar(Boolean wantPlaceInCar) {
this.wantPlaceInCar = wantPlaceInCar;
}
/**
* @return the requirePayment
*/
public Boolean getRequirePayment() {
return requirePayment;
}
/**
* @param requirePayment the requirePayment to set
*/
public void setRequirePayment(Boolean requirePayment) {
this.requirePayment = requirePayment;
}
/**
* @return the fkAge
*/
public Integer getFkAge() {
return fkAge;
}
/**
* @param fkAge the fkAge to set
*/
public void setFkAge(Integer fkAge) {
this.fkAge = fkAge;
}
/**
* @return the day0
*/
public Boolean getDay0() {
return day0;
}
/**
* @param day0 the day0 to set
*/
public void setDay0(Boolean day0) {
this.day0 = day0;
}
/**
* @return the day1
*/
public Boolean getDay1() {
return day1;
}
/**
* @param day1 the day1 to set
*/
public void setDay1(Boolean day1) {
this.day1 = day1;
}
/**
* @return the day2
*/
public Boolean getDay2() {
return day2;
}
/**
* @param day2 the day2 to set
*/
public void setDay2(Boolean day2) {
this.day2 = day2;
}
/**
* @return the day3
*/
public Boolean getDay3() {
return day3;
}
/**
* @param day3 the day3 to set
*/
public void setDay3(Boolean day3) {
this.day3 = day3;
}
/**
* @return the day4
*/
public Boolean getDay4() {
return day4;
}
/**
* @param day4 the day4 to set
*/
public void setDay4(Boolean day4) {
this.day4 = day4;
}
/**
* @return the towels
*/
public Boolean getTowels() {
return towels;
}
/**
* @param towels the towels to set
*/
public void setTowels(Boolean towels) {
this.towels = towels;
}
/**
* @return the bedLinen
*/
public Boolean getBedLinen() {
return bedLinen;
}
/**
* @param bedLinen the bedLinen to set
*/
public void setBedLinen(Boolean bedLinen) {
this.bedLinen = bedLinen;
}
/**
* @return the cot
*/
public Boolean getCot() {
return cot;
}
/**
* @param cot the cot to set
*/
public void setCot(Boolean cot) {
this.cot = cot;
}
}

View File

@ -0,0 +1,29 @@
package de.jottyfan.bico.modules.download;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
import de.jottyfan.bico.modules.CommonController;
/**
*
* @author jotty
*
*/
@Controller
public class DownloadController extends CommonController {
@Autowired
private DownloadService service;
@GetMapping(value = "/download", produces = "text/csv")
@ResponseBody
@ResponseStatus(HttpStatus.OK)
public String download() {
return service.getCsv();
}
}

View File

@ -0,0 +1,31 @@
package de.jottyfan.bico.modules.download;
import static de.jottyfan.bico.db.public_.Tables.V_CALENDAR;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.SelectWhereStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.public_.tables.records.VCalendarRecord;
/**
*
* @author jotty
*
*/
@Repository
public class DownloadRepository {
private static final Logger LOGGER = LogManager.getLogger(DownloadRepository.class);
@Autowired
private DSLContext jooq;
public String getCsv() {
SelectWhereStep<VCalendarRecord> sql = jooq.selectFrom(V_CALENDAR);
LOGGER.trace(sql);
return sql.fetch().formatCSV();
}
}

View File

@ -0,0 +1,20 @@
package de.jottyfan.bico.modules.download;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
*
* @author jotty
*
*/
@Service
public class DownloadService {
@Autowired
private DownloadRepository repository;
public String getCsv() {
return repository.getCsv();
}
}

View File

@ -0,0 +1,30 @@
package de.jottyfan.bico.modules.ical;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import de.jottyfan.bico.modules.CommonController;
import jakarta.servlet.http.HttpServletResponse;
/**
*
* @author jotty
*
*/
@Controller
public class IcalController extends CommonController{
@Autowired
private IcalService service;
@GetMapping("/ical")
public void getIcalExport(HttpServletResponse response) throws Exception {
response.setHeader("Content-Disposition", "attachment; filename=bico.ics");
response.setContentType("application/octet-stream");
response.setHeader("Content-Transfer-Encoding", "binary");
response.setHeader("Accept-Ranges", "bytes");
service.createCalendar(response.getOutputStream());
response.flushBuffer();
}
}

View File

@ -0,0 +1,31 @@
package de.jottyfan.bico.modules.ical;
import static de.jottyfan.bico.db.public_.Tables.V_CALENDAR;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.SelectWhereStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.public_.tables.records.VCalendarRecord;
/**
*
* @author jotty
*
*/
@Repository
public class IcalRepository {
private static final Logger LOGGER = LogManager.getLogger(IcalRepository.class);
@Autowired
private DSLContext jooq;
public VCalendarRecord[] getAllDates() {
SelectWhereStep<VCalendarRecord> sql = jooq.selectFrom(V_CALENDAR);
LOGGER.trace(sql);
return sql.fetchArray();
}
}

View File

@ -0,0 +1,40 @@
package de.jottyfan.bico.modules.ical;
import java.io.IOException;
import java.io.OutputStream;
import java.time.LocalDateTime;
import java.time.LocalTime;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.db.public_.tables.records.VCalendarRecord;
import net.fortuna.ical4j.data.CalendarOutputter;
import net.fortuna.ical4j.model.Calendar;
import net.fortuna.ical4j.model.component.VEvent;
import net.fortuna.ical4j.validate.ValidationException;
/**
*
* @author jotty
*
*/
@Service
public class IcalService {
@Autowired
private IcalRepository repository;
public void createCalendar(OutputStream stream) throws ValidationException, IOException {
Calendar calendar = new Calendar();
CalendarOutputter out = new CalendarOutputter();
for (VCalendarRecord record : repository.getAllDates()) {
String summary = record.getFullname();
LocalDateTime startEvent = LocalDateTime.of(record.getSlotDay(), LocalTime.of(10, 30));
LocalDateTime endEvent = LocalDateTime.of(record.getSlotDay(), LocalTime.of(12, 0));
VEvent event = new VEvent(startEvent, endEvent, summary);
calendar.add(event);
}
out.output(calendar, stream);;
}
}

View File

@ -1,7 +1,6 @@
package de.jottyfan.bico.modules.index;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import de.jottyfan.bico.modules.CommonController;
@ -14,8 +13,7 @@ import de.jottyfan.bico.modules.CommonController;
@Controller
public class IndexController extends CommonController {
@GetMapping("/")
public String getIndex(Model model) {
useThemedModel(model);
return "redirect:/sheet";
public String getIndex() {
return "redirect:/next";
}
}

View File

@ -9,7 +9,7 @@ import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import de.jottyfan.bico.db.tables.records.TLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonRecord;
import de.jottyfan.bico.modules.CommonController;
/**
*

View File

@ -1,8 +1,8 @@
package de.jottyfan.bico.modules.lesson;
import static de.jottyfan.bico.db.Tables.T_LESSON;
import static de.jottyfan.bico.db.Tables.T_PERSON;
import static de.jottyfan.bico.db.Tables.T_SLOT;
import static de.jottyfan.bico.db.public_.Tables.T_LESSON;
import static de.jottyfan.bico.db.public_.Tables.T_PERSON;
import static de.jottyfan.bico.db.public_.Tables.T_SLOT;
import java.time.LocalDate;
import java.util.List;
@ -19,8 +19,8 @@ import org.jooq.UpdateConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.tables.records.TLessonRecord;
import de.jottyfan.bico.db.tables.records.TPersonRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TPersonRecord;
/**
*

View File

@ -6,8 +6,8 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.db.tables.records.TLessonRecord;
import de.jottyfan.bico.db.tables.records.TPersonRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TPersonRecord;
/**
*

View File

@ -0,0 +1,38 @@
package de.jottyfan.bico.modules.next;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import de.jottyfan.bico.modules.CommonController;
/**
*
* @author jotty
*
*/
@Controller
public class NextController extends CommonController {
@Autowired
private NextService service;
@GetMapping("/next")
public String getNext(Model model) {
model.addAttribute("list", service.getNext(LocalDate.now()));
return "/next";
}
@GetMapping("/next/{date}")
public String getNextForDate(@PathVariable("date") String dateStr, Model model) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse(dateStr, formatter);
model.addAttribute("list", service.getNext(date));
return "/next";
}
}

View File

@ -0,0 +1,56 @@
package de.jottyfan.bico.modules.next;
import static de.jottyfan.bico.db.public_.Tables.V_CALENDAR;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jooq.DSLContext;
import org.jooq.Record2;
import org.jooq.SelectSeekStep1;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.modules.next.model.NextBean;
/**
*
* @author jotty
*
*/
@Repository
public class NextRepository {
private static final Logger LOGGER = LogManager.getLogger(NextRepository.class);
@Autowired
private DSLContext jooq;
/**
* get the next dates from date on
*
* @param date the date
* @return the next dates as beans in a list; an empty list at least
*/
public List<NextBean> getNext(LocalDate date) {
SelectSeekStep1<Record2<String, LocalDate>, LocalDate> sql = jooq
// @formatter:off
.selectDistinct(V_CALENDAR.FULLNAME, V_CALENDAR.SLOT_DAY)
.from(V_CALENDAR)
.where(V_CALENDAR.SLOT_DAY.ge(date))
.orderBy(V_CALENDAR.SLOT_DAY.asc());
// @formatter:on
LOGGER.trace(sql);
Iterator<Record2<String, LocalDate>> i = sql.fetch().iterator();
List<NextBean> list = new ArrayList<>();
while (i.hasNext()) {
Record2<String, LocalDate> r = i.next();
list.add(NextBean.of(r.get(V_CALENDAR.FULLNAME), r.get(V_CALENDAR.SLOT_DAY)));
}
return list;
}
}

View File

@ -0,0 +1,32 @@
package de.jottyfan.bico.modules.next;
import java.time.LocalDate;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.modules.next.model.NextBean;
/**
*
* @author jotty
*
*/
@Service
public class NextService {
@Autowired
private NextRepository repository;
/**
* get the next dates
*
* @param date the date
* @return the list of next dates
*/
public List<NextBean> getNext(LocalDate date) {
return repository.getNext(date);
}
}

View File

@ -0,0 +1,54 @@
package de.jottyfan.bico.modules.next.model;
import java.io.Serializable;
import java.time.LocalDate;
/**
*
* @author jotty
*
*/
public class NextBean implements Serializable {
private static final long serialVersionUID = 1L;
private String fullname;
private LocalDate day;
private NextBean() {
}
public final static NextBean of(String fullname, LocalDate day) {
NextBean bean = new NextBean();
bean.setDay(day);
bean.setFullname(fullname);
return bean;
}
/**
* @return the fullname
*/
public String getFullname() {
return fullname;
}
/**
* @return the day
*/
public LocalDate getDay() {
return day;
}
/**
* @param fullname the fullname to set
*/
private void setFullname(String fullname) {
this.fullname = fullname;
}
/**
* @param day the day to set
*/
private void setDay(LocalDate day) {
this.day = day;
}
}

View File

@ -25,7 +25,7 @@ public class ProfileController extends CommonController {
* @param theme the theme
*/
@PostMapping("/updateTheme/{theme}")
public ResponseEntity<?> updateTheme(@PathVariable String theme) {
public ResponseEntity<?> updateTheme(@PathVariable("theme") String theme) {
// TODO: add profile's user name
String username = "jotty";
service.updateTheme(username, theme);

View File

@ -1,6 +1,6 @@
package de.jottyfan.bico.modules.profile;
import static de.jottyfan.bico.db.Tables.T_PROFILE;
import static de.jottyfan.bico.db.public_.Tables.T_PROFILE;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
@ -10,7 +10,7 @@ import org.jooq.SelectConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.tables.records.TProfileRecord;
import de.jottyfan.bico.db.public_.tables.records.TProfileRecord;
import de.jottyfan.bico.modules.profile.model.ProfileBean;
/**

View File

@ -20,7 +20,7 @@ public class SheetController extends CommonController {
@GetMapping("/sheet")
public String getSheet(Model model) {
useThemedModel(model).addAttribute("list", service.getList());
model.addAttribute("list", service.getList());
return "/sheet";
}
}

View File

@ -1,6 +1,6 @@
package de.jottyfan.bico.modules.sheet;
import static de.jottyfan.bico.db.Tables.V_CALENDAR;
import static de.jottyfan.bico.db.public_.Tables.V_CALENDAR;
import java.util.List;
@ -11,7 +11,7 @@ import org.jooq.SelectWhereStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.tables.records.VCalendarRecord;
import de.jottyfan.bico.db.public_.tables.records.VCalendarRecord;
/**
*

View File

@ -5,7 +5,7 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.db.tables.records.VCalendarRecord;
import de.jottyfan.bico.db.public_.tables.records.VCalendarRecord;
/**
*

View File

@ -30,20 +30,20 @@ public class SlotController extends CommonController {
}
@GetMapping("/slot/{id}")
public String load(@PathVariable Integer id, Model model) {
public String load(@PathVariable("id") Integer id, Model model) {
model.addAttribute("bean", id == null ? new SlotBean() : service.loadSlot(id));
model.addAttribute("hasLesson", service.slotHasLesson(id));
return "/slot/item";
}
@GetMapping("/slot/{id}/delete")
public String loadEnsurance(@PathVariable Integer id, Model model) {
public String loadEnsurance(@PathVariable("id") Integer id, Model model) {
model.addAttribute("bean", service.loadDeletableSlot(id));
return "/slot/delete";
}
@GetMapping("/slot/{id}/destroy")
public String destroy(@PathVariable Integer id, Model model) {
public String destroy(@PathVariable("id") Integer id, Model model) {
service.removeSlot(id);
return "redirect:/";
}

View File

@ -1,7 +1,7 @@
package de.jottyfan.bico.modules.slot;
import static de.jottyfan.bico.db.Tables.T_LESSON;
import static de.jottyfan.bico.db.Tables.T_SLOT;
import static de.jottyfan.bico.db.public_.Tables.T_LESSON;
import static de.jottyfan.bico.db.public_.Tables.T_SLOT;
import java.util.Iterator;
@ -15,8 +15,8 @@ import org.jooq.UpdateConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.tables.records.TLessonRecord;
import de.jottyfan.bico.db.tables.records.TSlotRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TSlotRecord;
import de.jottyfan.bico.modules.slot.model.SlotBean;
/**
@ -139,7 +139,7 @@ public class SlotRepository {
.selectFrom(T_LESSON)
.where(T_LESSON.FK_SLOT.eq(slotId));
// @formatter:on
LOGGER.trace(sql);
LOGGER.info(sql);
return sql.fetch().size() > 0;
}
}

View File

@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import de.jottyfan.bico.db.tables.records.TSubjectRecord;
import de.jottyfan.bico.db.public_.tables.records.TSubjectRecord;
import de.jottyfan.bico.modules.CommonController;
/**

View File

@ -1,9 +1,9 @@
package de.jottyfan.bico.modules.subject;
import static de.jottyfan.bico.db.Tables.T_LESSON_SUBJECT;
import static de.jottyfan.bico.db.Tables.T_SOURCE;
import static de.jottyfan.bico.db.Tables.T_SUBJECT;
import static de.jottyfan.bico.db.Tables.V_LESSON;
import static de.jottyfan.bico.db.public_.Tables.T_LESSON_SUBJECT;
import static de.jottyfan.bico.db.public_.Tables.T_SOURCE;
import static de.jottyfan.bico.db.public_.Tables.T_SUBJECT;
import static de.jottyfan.bico.db.public_.Tables.V_LESSON;
import java.util.List;
@ -19,9 +19,9 @@ import org.jooq.UpdateConditionStep;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.tables.records.TSourceRecord;
import de.jottyfan.bico.db.tables.records.TSubjectRecord;
import de.jottyfan.bico.db.tables.records.VLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TSourceRecord;
import de.jottyfan.bico.db.public_.tables.records.TSubjectRecord;
import de.jottyfan.bico.db.public_.tables.records.VLessonRecord;
/**
*

View File

@ -5,9 +5,9 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.db.tables.records.TSourceRecord;
import de.jottyfan.bico.db.tables.records.TSubjectRecord;
import de.jottyfan.bico.db.tables.records.VLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TSourceRecord;
import de.jottyfan.bico.db.public_.tables.records.TSubjectRecord;
import de.jottyfan.bico.db.public_.tables.records.VLessonRecord;
/**
*

View File

@ -2,7 +2,7 @@ package de.jottyfan.bico.modules.subject.model;
import java.io.Serializable;
import de.jottyfan.bico.db.tables.records.TSubjectRecord;
import de.jottyfan.bico.db.public_.tables.records.TSubjectRecord;
/**
*

View File

@ -8,7 +8,7 @@ import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import de.jottyfan.bico.db.tables.records.TLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonRecord;
import de.jottyfan.bico.modules.CommonController;
import jakarta.servlet.http.HttpServletRequest;

View File

@ -1,10 +1,10 @@
package de.jottyfan.bico.modules.theme;
import static de.jottyfan.bico.db.Tables.T_LESSON;
import static de.jottyfan.bico.db.Tables.T_LESSON_SUBJECT;
import static de.jottyfan.bico.db.Tables.T_SLOT;
import static de.jottyfan.bico.db.Tables.T_SOURCE;
import static de.jottyfan.bico.db.Tables.T_SUBJECT;
import static de.jottyfan.bico.db.public_.Tables.T_LESSON;
import static de.jottyfan.bico.db.public_.Tables.T_LESSON_SUBJECT;
import static de.jottyfan.bico.db.public_.Tables.T_SLOT;
import static de.jottyfan.bico.db.public_.Tables.T_SOURCE;
import static de.jottyfan.bico.db.public_.Tables.T_SUBJECT;
import java.time.LocalDate;
import java.util.ArrayList;
@ -25,8 +25,8 @@ import org.jooq.impl.DSL;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import de.jottyfan.bico.db.tables.records.TLessonRecord;
import de.jottyfan.bico.db.tables.records.TLessonSubjectRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonSubjectRecord;
import de.jottyfan.bico.modules.theme.model.KeyValueBean;
import de.jottyfan.bico.modules.theme.model.ThemeBean;

View File

@ -6,7 +6,7 @@ import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import de.jottyfan.bico.db.tables.records.TLessonRecord;
import de.jottyfan.bico.db.public_.tables.records.TLessonRecord;
import de.jottyfan.bico.modules.theme.model.KeyValueBean;
import de.jottyfan.bico.modules.theme.model.ThemeBean;

View File

@ -0,0 +1,9 @@
{
"properties": [
{
"name": "nextcloud.url",
"type": "java.lang.String",
"description": "the URL to nextcloud for logout operations"
}
]
}

View File

@ -9,17 +9,10 @@ spring.datasource.password = ${db.password}
server.servlet.context-path = ${my.context-path:/BiCO}
# security
spring.security.oauth2.client.registration.keycloak.client-id = ${keycloak.client-id}
spring.security.oauth2.client.registration.keycloak.scope = openid
spring.security.oauth2.client.registration.keycloak.authorization-grant-type = authorization_code
spring.security.oauth2.client.registration.keycloak.redirect-uri = ${keycloak.redirect-uri}
spring.security.oauth2.client.provider.keycloak.issuer-uri = ${keycloak.issuer-uri}
spring.security.oauth2.client.provider.keycloak.authorization-uri = ${keycloak.openid-url}/auth
spring.security.oauth2.client.provider.keycloak.token-uri = ${keycloak.openid-url}/token
spring.security.oauth2.client.provider.keycloak.user-info-uri = ${keycloak.openid-url}/userinfo
spring.security.oauth2.client.provider.keycloak.jwk-set-uri = ${keycloak.openid-url}/certs
spring.security.oauth2.client.provider.keycloak.user-name-attribute = preferred_username
# for development only
server.port = 8081
# nextcloud open ID connection
spring.security.oauth2.client.provider.nextcloud.issuer-uri = ${nextcloud.issuer-uri}
spring.security.oauth2.client.registration.nextcloud.client-id = ${nextcloud.client-id}
spring.security.oauth2.client.registration.nextcloud.client-secret = ${nextcloud.client-secret}
spring.security.oauth2.client.registration.nextcloud.authorization-grant-type = authorization_code
spring.security.oauth2.client.registration.nextcloud.redirect-uri = ${nextcloud.redirect-uri}
spring.security.oauth2.client.registration.nextcloud.client-authentication-method = client_secret_post

View File

@ -28,7 +28,7 @@ body {
}
.rightaligned {
right: 5px;
right: 20px;
position: absolute;
}
@ -51,3 +51,18 @@ body {
width: 400px;
margin: auto;
}
.campbadge {
border: 1px solid #222;
color: #222;
}
[data-bs-theme=dark] .campbadge {
border-color: silver;
color: silver;
}
.full-size {
width: 100% !important;
max-width: inherit !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@ -1,7 +1,7 @@
toggleDarkMode = function() {
var oldValue = $("html").attr("data-bs-theme");
var newValue = oldValue == "dark" ? "light" : "dark";
var updateUrl = /*[[@{/updateTheme}]]*/ 'updateTheme';
var updateUrl = /*[[@{/updateTheme}]]*/ '/BiCO/updateTheme';
updateUrl = updateUrl + "/" + newValue;
$("html").attr("data-bs-theme", newValue);
$.ajax({

View File

@ -0,0 +1,122 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="container">
<div class="row g-2">
<div class="col-sm-12">
<h2>Anmeldung zur Gemeindefreizeit 2025 bearbeiten</h2>
</div>
</div>
<div class="alert alert-danger" th:unless="${bean}">Sie haben keine Berechtigung, die Anmeldung mit dieser URL zu ändern.</div>
<form th:action="@{/camp/registration/correct}" method="post" th:object="${bean}" th:if="${bean}">
<input type="hidden" th:field="*{pkRegistration}" />
<div class="row g-3">
<div class="col-sm-3">Vorname</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('forename')}" th:errors="*{forename}" class="text-danger"></span> <input type="text" th:field="*{forename}" class="form-control" />
</div>
<div class="col-sm-3">Nachname</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('surname')}" th:errors="*{surname}" class="text-danger"></span> <input type="text" th:field="*{surname}" class="form-control" />
</div>
<div class="col-sm-3">Geschlecht</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('sex')}" th:errors="*{sex}" class="text-danger"></span> <select th:field="*{sex}" class="form-select">
<option value="">--- bitte wählen ---</option>
<option th:each="s : ${sexes}" th:value="${s}" th:text="${s}"></option>
</select>
</div>
<div class="col-sm-3">Alter</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('fkAge')}" th:errors="*{fkAge}" class="text-danger"></span>
<select th:field="*{fkAge}" class="form-select">
<option th:each="a : ${ages}" th:value="${a.pkAge}" th:label="${a.name} + ', ' + ${a.price} + ' € / Übernachtung'"></option>
</select>
</div>
<div class="col-sm-3">Welche Tage</div>
<div class="col-sm-9">
<input id="day0" type="checkbox" name="active" th:field="*{day0}" class="form-check-input" />
<label class="form-check-label" for="day0">Donnerstag</label>
<input id="day1" type="checkbox" name="active" th:field="*{day1}" class="form-check-input" />
<label class="form-check-label" for="day1">Freitag</label>
<input id="day2" type="checkbox" name="active" th:field="*{day2}" class="form-check-input" />
<label class="form-check-label" for="day2">Sonnabend</label>
<input id="day3" type="checkbox" name="active" th:field="*{day3}" class="form-check-input" />
<label class="form-check-label" for="day3">Sonntag</label>
<input id="day4" type="checkbox" name="active" th:field="*{day4}" class="form-check-input" />
<label class="form-check-label" for="day4">Montag</label>
</div>
<div class="col-sm-3">Sonderleistungen</div>
<div class="col-sm-9">
<input id="towels" type="checkbox" name="active" th:field="*{towels}" class="form-check-input" />
<label class="form-check-label" for="towels">Handtücher: 2 € / Person</label><br />
<input id="bed_linen" type="checkbox" name="active" th:field="*{bedLinen}" class="form-check-input" />
<label class="form-check-label" for="bed_linen">Bettwäsche: 7 € / Person</label><br />
<input id="cot" type="checkbox" name="active" th:field="*{cot}" class="form-check-input" />
<label class="form-check-label" for="cot">Kinderbett: 10 € einmalig</label>
</div>
<div class="col-sm-3">Barrierefrei</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('barrierFree')}" th:errors="*{barrierFree}" class="text-danger"></span>
<select th:field="*{barrierFree}" class="form-select">
<option value="false">Nein</option>
<option value="true">Ja</option>
</select>
</div>
<div class="col-sm-3">Spezielle Ernährung (Allergien, Unverträglichkeiten)</div>
<div class="col-sm-9">
<textarea th:field="*{nutrition}" class="form-control"></textarea>
</div>
<div class="col-sm-3">Biete Plätze zum Mitfahren</div>
<div class="col-sm-9">
<input th:field="*{driverProvidePlaces}" type="number" min="0" max="8" class="form-control" />
</div>
<div class="col-sm-3">Benötige eine Mitfahrgelegenheit</div>
<div class="col-sm-9">
<input id="wantPlaceInCar" type="checkbox" name="active" th:field="*{wantPlaceInCar}" class="form-check-input" />
<label class="form-check-label" for="wantPlaceInCar">Ja, brauche ich</label>
</div>
<div class="col-sm-3">Besonderheiten / Krankheiten</div>
<div class="col-sm-9">
<textarea th:field="*{diseases}" class="form-control"></textarea>
</div>
<div class="col-sm-3">Finanzierungsunterstützung</div>
<div class="col-sm-9">
<input id="requirePayment" type="checkbox" name="active" th:field="*{requirePayment}" class="form-check-input" />
<label class="form-check-label" for="requirePayment">Ja, brauche ich</label>
</div>
<div class="col-sm-3"></div>
<div class="col-sm-9">
<button type="submit" class="btn btn-outline-success">Korrigeren</button>
&nbsp; <a th:href="@{/camp/registration}" class="btn btn-outline-secondary">Abbrechen</a> &nbsp;
<button type="button" class="btn btn-outline-danger" data-bs-toggle="modal" data-bs-target="#deleteModal">Stornieren</button>
<div class="modal fade" id="deleteModal" tabindex="-1" aria-labelledby="deleteModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="deleteModalLabel">Löschen einer Anmeldung</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
Soll die Anmeldung von <span th:text="${bean.forename}"></span>&nbsp;<span th:text="${bean.surname}"></span>&nbsp;wirklich gelöscht werden?
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Abbrechen</button>
<a th:href="@{/camp/registration/delete/{id}(id=${bean.pkRegistration})}" class="btn btn-outline-danger">Endgültig löschen</a>
</div>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</th:block>
</body>
</html>

View File

@ -0,0 +1,81 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="container full-size">
<div class="row g-2">
<div class="col-sm-12">
<h2>Anmeldungen zur Gemeindefreizeit 2025</h2>
</div>
</div>
<div class="alert alert-danger" th:unless="${list}">Sie haben keine Berechtigung, die Anmeldungen einzusehen.</div>
<div th:if="${list}">
<table id="table" class="table table-striped">
<thead>
<tr>
<th>Vorname</th>
<th>Nachname</th>
<th>Geschlecht</th>
<th>Alter</th>
<th>Tage dabei</th>
<th>Extras</th>
<th>Unverträglichkeit</th>
<th>Mitfahrplätze</th>
<th>Besonderheiten</th>
</tr>
</thead>
<tbody>
<tr th:each="l : ${list}">
<td th:text="${l.forename}"></td>
<td th:text="${l.surname}"></td>
<td th:text="${l.sex}"></td>
<td>
<th:block th:each="a : ${ages}">
<span th:text="${a.name}" th:if="${a.pkAge == l.fkAge}"></span>
</th:block>
</td>
<td>
<span th:if="${l.day0}" class="badge campbadge">Donnerstag</span>
<span th:if="${l.day1}" class="badge campbadge">Freitag</span>
<span th:if="${l.day2}" class="badge campbadge">Sonnabend</span>
<span th:if="${l.day3}" class="badge campbadge">Sonntag</span>
<span th:if="${l.day4}" class="badge campbadge">Montag</span>
</td>
<td>
<span th:if="${l.barrierFree}" class="badge campbadge">barrierefrei</span>
<span th:if="${l.towels}" class="badge campbadge">Handtücher</span>
<span th:if="${l.bedLinen}" class="badge campbadge">Bettwäsche</span>
<span th:if="${l.cot}" class="badge campbadge">Kinderbett</span>
<span th:if="${l.requirePayment}" class="badge campbadge">finanzielle Unterstützung erbeten</span>
</td>
<td th:text="${l.nutrition}"></td>
<td>
<span th:if="${l.driverProvidePlaces}">biete <span th:text="${l.driverProvidePlaces}"></span></span>
<span th:if="${l.wantPlaceInCar}">benötige 1</span>
</td>
<td th:text="${l.diseases}"></td>
</tr>
</tbody>
</table>
<script th:inline="javascript">
/*<![CDATA[*/
$(document).ready(function() {
$("#table").DataTable({
"language" : locale_de,
"responsive" : true
});
});
/*]]>*/
</script>
</div>
<div class="row g-2" th:if="${list}">
<div class="col-sm-12">
<a th:href="@{/camp/registration/admin/download}" class="btn btn-outline-primary">Download</a>
</div>
</div>
</div>
</div>
</th:block>
</body>
</html>

View File

@ -0,0 +1,170 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="container">
<div class="row g-2">
<div class="col-sm-12">
<h2>Anmeldung zur Gemeindefreizeit 2025</h2>
</div>
</div>
<script th:inline="javascript">
function toggleTo(opener, name) {
$('.nav-opener').removeClass('active');
$('.tabpanel').hide();
$(opener).addClass('active');
$(name).show();
}
$(document).ready(function() {
var regsize = parseInt(/*[[${registrations.size()}]]*/"0");
$("#details").hide();
if (regsize < 1) {
$("#open_new").addClass("active");
$("#new").show();
$("#open_found").removeClass("active");
$("#found").hide();
} else {
$("#open_new").removeClass("active");
$("#new").hide();
$("#open_found").addClass("active");
$("#found").show();
}
});
</script>
<ul class="nav nav-tabs">
<li class="nav-item"><a id="open_new" class="nav-link active nav-opener" aria-current="page" href="#" onclick="toggleTo(this, '#new')">Neue Anmeldung</a></li>
<li class="nav-item"><a id="open_found" class="nav-link nav-opener" href="#" onclick="toggleTo(this, '#found')">Bisherige Anmeldungen</a></li>
<li class="nav-item"><a id="open_details" class="nav-link nav-opener" href="#" onclick="toggleTo(this, '#details')">Überweisung</a></li>
</ul>
<div class="tab-content">
<div id="new" class="tabpanel">
<div class="alert alert-info">Die Zeit für die Anmeldung zur Gemeindefreizeit 2025 ist vorbei.</div>
<form th:action="@{/camp/registration/submit}" method="post" th:object="${bean}" th:if="${oldie}">
<div class="row g-3">
<div class="col-sm-3">Vorname</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('forename')}" th:errors="*{forename}" class="text-danger"></span> <input type="text" th:field="*{forename}" class="form-control" />
</div>
<div class="col-sm-3">Nachname</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('surname')}" th:errors="*{surname}" class="text-danger"></span> <input type="text" th:field="*{surname}" class="form-control" />
</div>
<div class="col-sm-3">Geschlecht</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('sex')}" th:errors="*{sex}" class="text-danger"></span> <select th:field="*{sex}" class="form-select">
<option value="">--- bitte wählen ---</option>
<option th:each="s : ${sexes}" th:value="${s}" th:text="${s}"></option>
</select>
</div>
<div class="col-sm-3">Alter</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('fkAge')}" th:errors="*{fkAge}" class="text-danger"></span>
<select th:field="*{fkAge}" class="form-select">
<option th:each="a : ${ages}" th:value="${a.pkAge}" th:label="${a.name} + ', ' + ${a.price} + ' € / Übernachtung'"></option>
</select>
</div>
<div class="col-sm-3">Welche Tage</div>
<div class="col-sm-9">
<input id="day0" type="checkbox" name="active" th:field="*{day0}" class="form-check-input" />
<label class="form-check-label" for="day0">Donnerstag</label>
<input id="day1" type="checkbox" name="active" th:field="*{day1}" class="form-check-input" />
<label class="form-check-label" for="day1">Freitag</label>
<input id="day2" type="checkbox" name="active" th:field="*{day2}" class="form-check-input" />
<label class="form-check-label" for="day2">Sonnabend</label>
<input id="day3" type="checkbox" name="active" th:field="*{day3}" class="form-check-input" />
<label class="form-check-label" for="day3">Sonntag</label>
<input id="day4" type="checkbox" name="active" th:field="*{day4}" class="form-check-input" />
<label class="form-check-label" for="day4">Montag</label>
</div>
<div class="col-sm-3">Sonderleistungen</div>
<div class="col-sm-9">
<input id="towels" type="checkbox" name="active" th:field="*{towels}" class="form-check-input" />
<label class="form-check-label" for="towels">Handtücher: 2 € / Person</label><br />
<input id="bed_linen" type="checkbox" name="active" th:field="*{bedLinen}" class="form-check-input" />
<label class="form-check-label" for="bed_linen">Bettwäsche: 7 € / Person</label><br />
<input id="cot" type="checkbox" name="active" th:field="*{cot}" class="form-check-input" />
<label class="form-check-label" for="cot">Kinderbett: 10 € einmalig</label>
</div>
<div class="col-sm-3">Barrierefrei</div>
<div class="col-sm-9">
<span th:if="${#fields.hasErrors('barrierFree')}" th:errors="*{barrierFree}" class="text-danger"></span>
<select th:field="*{barrierFree}" class="form-select">
<option value="false">Nein</option>
<option value="true">Ja</option>
</select>
</div>
<div class="col-sm-3">Spezielle Ernährung (Allergien, Unverträglichkeiten)</div>
<div class="col-sm-9">
<textarea th:field="*{nutrition}" class="form-control"></textarea>
</div>
<div class="col-sm-3">Biete Plätze zum Mitfahren</div>
<div class="col-sm-9">
<input th:field="*{driverProvidePlaces}" type="number" min="0" max="8" class="form-control" />
</div>
<div class="col-sm-3">Benötige eine Mitfahrgelegenheit</div>
<div class="col-sm-9">
<input id="wantPlaceInCar" type="checkbox" name="active" th:field="*{wantPlaceInCar}" class="form-check-input" />
<label class="form-check-label" for="wantPlaceInCar">Ja, brauche ich</label>
</div>
<div class="col-sm-3">Besonderheiten / Krankheiten</div>
<div class="col-sm-9">
<textarea th:field="*{diseases}" class="form-control"></textarea>
</div>
<div class="col-sm-3">Finanzierungsunterstützung</div>
<div class="col-sm-9">
<input id="requirePayment" type="checkbox" name="active" th:field="*{requirePayment}" class="form-check-input" />
<label class="form-check-label" for="requirePayment">Ja, brauche ich</label>
</div>
<div class="col-sm-3"></div>
<div class="col-sm-9">
<button type="submit" class="btn btn-outline-success">Speichern</button>
&nbsp; <a th:href="@{/camp/registration}" class="btn btn-outline-secondary">Abbrechen</a>&nbsp;
</div>
</div>
</form>
</div>
<div id="found" class="tabpanel">
<table class="table table-striped">
<thead>
<tr>
<th>Vorname</th>
<th>Nachname</th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<tr th:each="r : ${registrations}">
<td th:text="${r.forename}"></td>
<td th:text="${r.surname}"></td>
<td><a th:href="@{/camp/registration/edit/{id}(id=${r.pkRegistration})}" class="btn btn-outline-secondary"><i class="bi bi-pencil"></i></a></td>
</tr>
</tbody>
</table>
</div>
<div id="details" class="tabpanel">
<div class="card">
<div class="card-header">Informationen zu den Kosten</div>
<div class="card-body">
<i>Bitte überweise den Betrag, der sich aus Deiner Anmeldung ergibt, auf das Gemeindekonto:</i><br />
Kontoverbindung<br />
EFG Dresden Süd-Ost<br />
IBAN: DE40 8601 0090 0102 0449 00<br />
BIC: PBNKDEFF Konto 102 044 900<br />
Postbank Leipzig BLZ 860 100 90<br />
</div>
<div class="card-footer">
<img th:src="@{/images/Bildschirmfoto 2024-12-02 um 13.36.25.png}" style="max-width: 600px; width: 100%" /><br />
<img th:src="@{/images/PastedGraphic-2.png}" style="max-width: 600px; width: 100%" />
</div>
</div>
</div>
</div>
</div>
</div>
</th:block>
</body>
</html>

View File

@ -0,0 +1,10 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="alert alert-danger">Es ist ein Fehler aufgetreten. Wenden Sie sich bitte an Ihren Entwickler.</div>
</div>
</th:block>
</body>
</html>

View File

@ -3,7 +3,7 @@
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="container">
<div class="container" sec:authorize="hasRole('Bibelunterricht')">
<div class="row g-2">
<div class="col-sm-12">
<h2>Dozent-Reservierung</h2>
@ -30,7 +30,8 @@
<div class="col-sm-9">
<button type="submit" class="btn btn-outline-success">Speichern</button>&nbsp;
<a th:href="@{/}" class="btn btn-outline-secondary">Abbrechen</a>&nbsp;
<a th:href="@{/lesson/{id}/remove(id=${bean.pkLesson})}" class="btn btn-outline-danger">Löschen</a>
<a th:href="@{/lesson/{id}/remove(id=${bean.pkLesson})}" class="btn btn-outline-danger">Zuordnung löschen</a>
<a th:href="@{/slot/{id}(id=${bean.fkSlot})}" class="btn btn-outline-warning">Termin bearbeiten</a>
</div>
</div>
</form>

View File

@ -0,0 +1,24 @@
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="container" th:if="${hasDateRole || hasBUrole}">
<pre>Kommende Einteilung für den Bibelunterricht</pre>
<div class="row">
<div class="col-sm-6 col-md-4 col-lg-2 card p-2 m-1" th:each="s : ${list}">
<div th:text="${#temporals.format(s.day, 'dd.MM.yyyy')}"></div>
<div th:text="${s.fullname}"></div>
</div>
<div class="alert alert-info" th:if="${list.size() < 1}">Es gibt noch keine neuen Termine oder Zusagen für Termine.</div>
</div>
</div>
<div class="container" th:unless="${hasDateRole || hasBUrole}">
<div class="alert alert-info">
Willkommen im Anmeldeportal für die Gemeindefreizeit. Durch das Anklicken des Buttons <a class="btn btn-outline-secondary" th:href="@{/camp/registration}">Anmeldung Gemeindefreizeit</a> kannst du das Anmeldeformular öffnen.
</div>
</div>
</div>
</th:block>
</body>
</html>

View File

@ -3,7 +3,7 @@
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<table id="table" class="table table-striped">
<table id="table" class="table table-striped" sec:authorize="hasRole('Bibelunterricht')">
<thead>
<tr>
<th>Tag</th>
@ -14,7 +14,7 @@
</thead>
<tbody>
<tr th:each="s : ${list}">
<td><span th:text="${#temporals.format(s.slotDay, 'yyyy-MM-dd')}"></span></td>
<td th:data-sort="${#temporals.format(s.slotDay, 'yyyy-MM-dd')}"><a th:href="@{/slot/{id}(id=${s.pkSlot})}" th:text="${#temporals.format(s.slotDay, 'dd.MM.yyyy')}"></a></td>
<td><a th:href="@{/lesson?slotId={id}(id=${s.pkSlot})}" class="btn btn-outline-secondary"> <span th:text="${s.abbreviation}" th:if="${s.abbreviation}"></span> <i
class="bi bi-pencil" th:if="${s.abbreviation == null || s.abbreviation.isBlank()}"></i>
</a></td>
@ -32,8 +32,7 @@
</div>
</div>
</td>
<td><a th:href="@{/slot/{id}(id=${s.pkSlot})}" class="btn btn-outline-secondary rightaligned"><i class="bi bi-pencil"></i></a>
<span th:text="${s.slotNotes}" class="rightpadding64"></span></td>
<td><span th:text="${s.slotNotes}" class="rightpadding64"></span></td>
</tr>
</tbody>
<tfoot>
@ -48,10 +47,6 @@
/*<![CDATA[*/
$(document).ready(function() {
$("#table").DataTable({
"columnDefs" : [ {
"targets" : 0,
"type" : "date-eu"
} ],
"order" : [ [ 0, 'desc' ] ],
"language" : locale_de
});

View File

@ -2,7 +2,7 @@
<html xmlns:th="http://www.thymeleaf.org" layout:decorate="~{template}" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" xmlns:sec="http://www.thymeleaf.org/extras/spring-security">
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="borderdist" sec:authorize="hasRole('Bibelunterricht')">
<div class="alert alert-danger" th:if="${bean}">
Wollen Sie den Slot <span th:text="${#temporals.format(bean.slotDay, 'dd.MM.yyyy')}"></span> wirklich löschen?<br />
<a th:href="@{/slot/{id}/destroy(id=${bean.pkSlot})}" class="btn btn-outline-danger" th:if="${bean.pkSlot}">Ja, definitiv</a>

View File

@ -3,7 +3,7 @@
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="container">
<div class="container" sec:authorize="hasRole('Bibelunterricht')">
<div class="row g-2">
<div class="col-sm-12">
<h2>Terminfestlegung</h2>

View File

@ -4,14 +4,17 @@
<title>Bible Class Organizer</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link th:rel="stylesheet" type="text/css" media="all" th:href="@{/webjars/bootstrap/5.3.1/css/bootstrap.min.css}" />
<link th:rel="stylesheet" type="text/css" media="all" th:href="@{/webjars/bootstrap-icons/1.10.5/font/bootstrap-icons.css}" />
<link th:rel="stylesheet" type="text/css" media="all" th:href="@{/webjars/datatables/1.13.5/css/jquery.dataTables.min.css}" />
<link th:rel="stylesheet" type="text/css" media="all" th:href="@{/css/style.css}" />
<script th:src="@{/webjars/bootstrap/5.3.1/js/bootstrap.bundle.min.js}"></script>
<script th:src="@{/webjars/jquery/3.7.1/jquery.min.js}"></script>
<script th:src="@{/webjars/datatables/1.13.5/js/jquery.dataTables.min.js}"></script>
<script th:src="@{/js/dataTables.de.js}"></script>
<link rel="stylesheet" type="text/css" media="all" th:href="@{/webjars/bootstrap/5.3.3/css/bootstrap.min.css}" />
<link rel="stylesheet" type="text/css" media="all" th:href="@{/webjars/bootstrap-icons/1.11.3/font/bootstrap-icons.css}" />
<link rel="stylesheet" type="text/css" media="all" th:href="@{/webjars/datatables.net-bs5/2.1.7/css/dataTables.bootstrap5.min.css}"/>
<link rel="stylesheet" type="text/css" media="all" th:href="@{/css/style.css}" />
<script type="application/javascript" th:src="@{/webjars/bootstrap/5.3.3/js/bootstrap.bundle.min.js}"></script>
<script type="application/javascript" th:src="@{/webjars/jquery/3.7.1/jquery.min.js}"></script>
<script type="application/javascript" th:src="@{/webjars/datatables.net/2.1.7/js/dataTables.min.js}"></script>
<script type="application/javascript" th:src="@{/js/dataTables.de.js}"></script>
<script type="application/javascript" th:src="@{/webjars/datatables.net-bs5/2.1.7/js/dataTables.bootstrap5.min.js}"></script>
<script type="application/javascript" th:src="@{/webjars/datatables.net-responsive/3.0.1/js/dataTables.responsive.min.js}"></script>
<script type="application/javascript" th:src="@{/webjars/datatables.net-buttons/3.1.1/js/dataTables.buttons.min.js}"></script>
<script th:src="@{/js/stylehelp.js}"></script>
</head>
<body>
@ -21,21 +24,37 @@
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent" style="margin-right: 20px">
<ul class="navbar-nav mb-2 mb-lg-0">
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/}" style="margin-left: 12px">Einteilung</a></li>
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/subject/list}" style="margin-left: 12px">Themen</a></li>
<ul class="navbar-nav mb-2 mb-lg-0" th:if="${hasAnyRole}">
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/camp/registration}" style="margin-left: 12px">Anmeldung Gemeindefreizeit</a></li>
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/camp/registration/admin}" style="margin-left: 12px" th:if="${isCampAdmin}">Gemeindefreizeitliste</a></li>
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/next}" style="margin-left: 12px" th:if="${hasDateRole || hasBUrole}">Dienstplan</a></li>
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/sheet}" style="margin-left: 12px" th:if="${hasBUrole}">Einteilung</a></li>
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/subject/list}" style="margin-left: 12px" th:if="${hasBUrole}">Themen</a></li>
<li class="nav-item"><a class="btn btn-outline-secondary" th:href="@{/download}" style="margin-left: 12px" th:if="${hasBUrole}">Download</a></li>
</ul>
<ul layout:fragment="header"></ul>
<ul class="nav navbar-nav ms-auto">
<li class="nav-item" th:if="${hasDateRole}">
<a th:href="@{/ical}" class="btn btn-outline-secondary"><i class="bi bi-calendar-week"></i></a>
</li>
<li class="nav-item">
<a href="https://gitlab.com/jottyfan/bico/-/issues" class="btn btn-outline-secondary" target="_blank" th:text="${'v' + @manifestBean.getVersion()}"></a>
<a href="https://git.jottyfan.de/church/BiCO" class="btn btn-outline-secondary" target="_blank" th:text="${'v' + @manifestBean.getVersion()}"></a>
</li>
<li class="nav-item">
<a href="#" class="btn btn-outline-secondary" onclick="toggleDarkMode()"><i class="bi bi-moon"></i></a>
</li>
<li class="nav-item">
<a th:href="@{/logout}" class="btn btn-outline-secondary">⏼ Abmelden</a>
<a th:href="@{${nextcloudUrl}}" class="btn btn-outline-secondary">&#8594; nextcloud</a>
</li>
</ul>
</div>
</nav>
<div layout:fragment="content">content</div>
<div layout:fragment="content" th:if="${hasAnyRole}">content</div>
<div th:unless="${hasAnyRole}">
<div class="borderdist">
<div class="alert alert-danger">Leider fehlen Ihnen die Berechtigungen, um diese Anwendung nutzen zu können.</div>
</div>
</div>
</body>
</html>

View File

@ -3,7 +3,7 @@
<body>
<th:block layout:fragment="content">
<div class="borderdist">
<div class="container">
<div class="container" sec:authorize="hasRole('Bibelunterricht')">
<div class="row g-2">
<h1>
Themen für den <span th:text="${#temporals.format(day, 'dd.MM.yyyy')}"></span>