TaskManager Application 1.0

This commit is contained in:
Kiyan 2025-12-13 19:16:28 +02:00
parent 9e59297c57
commit c1e675fc7f
40 changed files with 3483 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
/mvnw text eol=lf
*.cmd text eol=crlf

33
.gitignore vendored Normal file
View File

@ -0,0 +1,33 @@
HELP.md
target/
.mvn/wrapper/maven-wrapper.jar
!**/src/main/**/target/
!**/src/test/**/target/
### STS ###
.apt_generated
.classpath
.factorypath
.project
.settings
.springBeans
.sts4-cache
### IntelliJ IDEA ###
.idea
*.iws
*.iml
*.ipr
### NetBeans ###
/nbproject/private/
/nbbuild/
/dist/
/nbdist/
/.nb-gradle/
build/
!**/src/main/**/build/
!**/src/test/**/build/
### VS Code ###
.vscode/

3
.mvn/wrapper/maven-wrapper.properties vendored Normal file
View File

@ -0,0 +1,3 @@
wrapperVersion=3.3.4
distributionType=only-script
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.11/apache-maven-3.9.11-bin.zip

295
mvnw vendored Executable file
View File

@ -0,0 +1,295 @@
#!/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
#
# http://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.
# ----------------------------------------------------------------------------
# ----------------------------------------------------------------------------
# Apache Maven Wrapper startup batch script, version 3.3.4
#
# Optional ENV vars
# -----------------
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
# MVNW_REPOURL - repo url base for downloading maven distribution
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
# ----------------------------------------------------------------------------
set -euf
[ "${MVNW_VERBOSE-}" != debug ] || set -x
# OS specific support.
native_path() { printf %s\\n "$1"; }
case "$(uname)" in
CYGWIN* | MINGW*)
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
native_path() { cygpath --path --windows "$1"; }
;;
esac
# set JAVACMD and JAVACCMD
set_java_home() {
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
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"
JAVACCMD="$JAVA_HOME/jre/sh/javac"
else
JAVACMD="$JAVA_HOME/bin/java"
JAVACCMD="$JAVA_HOME/bin/javac"
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
return 1
fi
fi
else
JAVACMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v java
)" || :
JAVACCMD="$(
'set' +e
'unset' -f command 2>/dev/null
'command' -v javac
)" || :
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
return 1
fi
fi
}
# hash string like Java String::hashCode
hash_string() {
str="${1:-}" h=0
while [ -n "$str" ]; do
char="${str%"${str#?}"}"
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
str="${str#?}"
done
printf %x\\n $h
}
verbose() { :; }
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
die() {
printf %s\\n "$1" >&2
exit 1
}
trim() {
# MWRAPPER-139:
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
# Needed for removing poorly interpreted newline sequences when running in more
# exotic environments such as mingw bash on Windows.
printf "%s" "${1}" | tr -d '[:space:]'
}
scriptDir="$(dirname "$0")"
scriptName="$(basename "$0")"
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
while IFS="=" read -r key value; do
case "${key-}" in
distributionUrl) distributionUrl=$(trim "${value-}") ;;
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
esac
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
case "${distributionUrl##*/}" in
maven-mvnd-*bin.*)
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
*)
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
distributionPlatform=linux-amd64
;;
esac
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
;;
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
esac
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
distributionUrlName="${distributionUrl##*/}"
distributionUrlNameMain="${distributionUrlName%.*}"
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
exec_maven() {
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
}
if [ -d "$MAVEN_HOME" ]; then
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
exec_maven "$@"
fi
case "${distributionUrl-}" in
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
esac
# prepare tmp dir
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
trap clean HUP INT TERM EXIT
else
die "cannot create temp dir"
fi
mkdir -p -- "${MAVEN_HOME%/*}"
# Download and Install Apache Maven
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
verbose "Downloading from: $distributionUrl"
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
# select .zip or .tar.gz
if ! command -v unzip >/dev/null; then
distributionUrl="${distributionUrl%.zip}.tar.gz"
distributionUrlName="${distributionUrl##*/}"
fi
# verbose opt
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
# normalize http auth
case "${MVNW_PASSWORD:+has-password}" in
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
esac
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
verbose "Found wget ... using wget"
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
verbose "Found curl ... using curl"
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
elif set_java_home; then
verbose "Falling back to use Java to download"
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
cat >"$javaSource" <<-END
public class Downloader extends java.net.Authenticator
{
protected java.net.PasswordAuthentication getPasswordAuthentication()
{
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
}
public static void main( String[] args ) throws Exception
{
setDefault( new Downloader() );
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
}
}
END
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
verbose " - Compiling Downloader.java ..."
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
verbose " - Running Downloader.java ..."
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
fi
# If specified, validate the SHA-256 sum of the Maven distribution zip file
if [ -n "${distributionSha256Sum-}" ]; then
distributionSha256Result=false
if [ "$MVN_CMD" = mvnd.sh ]; then
echo "Checksum validation is not supported for maven-mvnd." >&2
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
elif command -v sha256sum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
distributionSha256Result=true
fi
elif command -v shasum >/dev/null; then
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
distributionSha256Result=true
fi
else
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
exit 1
fi
if [ $distributionSha256Result = false ]; then
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
exit 1
fi
fi
# unzip and move
if command -v unzip >/dev/null; then
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
else
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
fi
# Find the actual extracted directory name (handles snapshots where filename != directory name)
actualDistributionDir=""
# First try the expected directory name (for regular distributions)
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
actualDistributionDir="$distributionUrlNameMain"
fi
fi
# If not found, search for any directory with the Maven executable (for snapshots)
if [ -z "$actualDistributionDir" ]; then
# enable globbing to iterate over items
set +f
for dir in "$TMP_DOWNLOAD_DIR"/*; do
if [ -d "$dir" ]; then
if [ -f "$dir/bin/$MVN_CMD" ]; then
actualDistributionDir="$(basename "$dir")"
break
fi
fi
done
set -f
fi
if [ -z "$actualDistributionDir" ]; then
verbose "Contents of $TMP_DOWNLOAD_DIR:"
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
die "Could not find Maven distribution directory in extracted archive"
fi
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
clean || :
exec_maven "$@"

189
mvnw.cmd vendored Normal file
View File

@ -0,0 +1,189 @@
<# : batch portion
@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 http://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 Apache Maven Wrapper startup batch script, version 3.3.4
@REM
@REM Optional ENV vars
@REM MVNW_REPOURL - repo url base for downloading maven distribution
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
@REM ----------------------------------------------------------------------------
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
@SET __MVNW_CMD__=
@SET __MVNW_ERROR__=
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
@SET PSModulePath=
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
)
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
@SET __MVNW_PSMODULEP_SAVE=
@SET __MVNW_ARG0_NAME__=
@SET MVNW_USERNAME=
@SET MVNW_PASSWORD=
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
@echo Cannot start maven from wrapper >&2 && exit /b 1
@GOTO :EOF
: end batch / begin powershell #>
$ErrorActionPreference = "Stop"
if ($env:MVNW_VERBOSE -eq "true") {
$VerbosePreference = "Continue"
}
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
if (!$distributionUrl) {
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
}
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
"maven-mvnd-*" {
$USE_MVND = $true
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
$MVN_CMD = "mvnd.cmd"
break
}
default {
$USE_MVND = $false
$MVN_CMD = $script -replace '^mvnw','mvn'
break
}
}
# apply MVNW_REPOURL and calculate MAVEN_HOME
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
if ($env:MVNW_REPOURL) {
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
}
$distributionUrlName = $distributionUrl -replace '^.*/',''
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
$MAVEN_M2_PATH = "$HOME/.m2"
if ($env:MAVEN_USER_HOME) {
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
}
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
}
$MAVEN_WRAPPER_DISTS = $null
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
} else {
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
}
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
exit $?
}
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
}
# prepare tmp dir
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
trap {
if ($TMP_DOWNLOAD_DIR.Exists) {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
}
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
# Download and Install Apache Maven
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
Write-Verbose "Downloading from: $distributionUrl"
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
$webclient = New-Object System.Net.WebClient
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
}
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
# If specified, validate the SHA-256 sum of the Maven distribution zip file
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
if ($distributionSha256Sum) {
if ($USE_MVND) {
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
}
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
}
}
# unzip and move
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
# Find the actual extracted directory name (handles snapshots where filename != directory name)
$actualDistributionDir = ""
# First try the expected directory name (for regular distributions)
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
$actualDistributionDir = $distributionUrlNameMain
}
# If not found, search for any directory with the Maven executable (for snapshots)
if (!$actualDistributionDir) {
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
if (Test-Path -Path $testPath -PathType Leaf) {
$actualDistributionDir = $_.Name
}
}
}
if (!$actualDistributionDir) {
Write-Error "Could not find Maven distribution directory in extracted archive"
}
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
try {
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
} catch {
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
Write-Error "fail to move MAVEN_HOME"
}
} finally {
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
}
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"

107
pom.xml Normal file
View File

@ -0,0 +1,107 @@
<?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 https://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>3.5.8</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>TaskManager</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>TaskManager</name>
<description>Demo project for Spring Boot</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>25</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.github.wimdeblauwe</groupId>
<artifactId>htmx-spring-boot-thymeleaf</artifactId>
<version>4.0.1</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>

View File

@ -0,0 +1,69 @@
package com.example.TaskManager.Controllers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.TaskManager.Models.Task;
import com.example.TaskManager.Security.CustomUserDetails;
import com.example.TaskManager.Services.CategoryService;
import com.example.TaskManager.Services.TaskService;
@Controller
public class CreateTask {
@Autowired
private CategoryService categoryService;
@Autowired
private TaskService taskService;
private Long taskId;
@GetMapping("/createTask")
public String createTask(Model model, Authentication authentication,
@RequestParam(required = false) Long id) {
// user details
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
model.addAttribute("userName", userDetails.getUsername());
model.addAttribute("userEmail", userDetails.getEmail());
if (id == null) {
model.addAttribute("task", new Task());
model.addAttribute("comCategories", categoryService.findAllCategories());
model.addAttribute("resetBtnText", "Clear");
model.addAttribute("saveBtnText", "Create Task");
} else {
taskId = id;
model.addAttribute("task", taskService.findTaskById(id));
model.addAttribute("comCategories", categoryService.findAllCategories());
model.addAttribute("resetBtnText", "Reset");
model.addAttribute("saveBtnText", "Update Task");
}
return "createTask";
}
@PostMapping("/createTask/save")
public String saveTask(@ModelAttribute Task task, Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
if (taskId == null) {
task.setCreatedBy(userDetails.getUsername());
task.setAssignedTo(userDetails.getId());
taskService.saveTask(task);
} else {
task.setUpdatedBy(userDetails.getUsername());
taskService.updateTask(taskId, task);
}
return "redirect:/dashboard";
}
}

View File

@ -0,0 +1,122 @@
package com.example.TaskManager.Controllers;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import com.example.TaskManager.Models.Task;
import com.example.TaskManager.Security.CustomUserDetails;
import com.example.TaskManager.Services.TaskService;
@Controller
public class Dashboard {
@Autowired
private TaskService taskService;
@GetMapping("/dashboard")
String dashboard(Model model, Authentication authentication) {
// user details
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
model.addAttribute("userName", userDetails.getUsername());
model.addAttribute("userEmail", userDetails.getEmail());
Long userId = userDetails.getId();
// each status percentage
model.addAttribute("overduePercentage", taskService.getPercentOfTasksByStatus("Overdue", userId));
model.addAttribute("pendingPercentage", taskService.getPercentOfTasksByStatus("Pending", userId));
model.addAttribute("inProgressPercentage", taskService.getPercentOfTasksByStatus("In Progress", userId));
model.addAttribute("onHoldPercentage", taskService.getPercentOfTasksByStatus("On Hold", userId));
model.addAttribute("completedPercentage", taskService.getPercentOfTasksByStatus("Completed", userId));
// Daily + Weekly Tasks List
model.addAttribute("weeklyTasks", taskService.getWeeklyTasks(userDetails.getId()));
model.addAttribute("todaysTasks", taskService.getDailyTasks(userDetails.getId()));
// charts
model.addAttribute("totalCompletedTasks", taskService.totalCompletedTasks(userId));
model.addAttribute("totalNumTasks", taskService.totalNumTasks(userId));
model.addAttribute("categoryNames", taskService.getCategoryNames());
model.addAttribute("categoryCounts", taskService.getCategoryCounts(userId));
List<Integer> allYears = taskService.allTaskYears(userId);
Map<Integer, Map<Integer, List<Object[]>>> finalMap = new HashMap<>();
for (Integer year : allYears) {
List<Object[]> rows = taskService.findTasksByYearWithQuarter(year, userId);
// Convert raw rows into quarter tasks mapping
Map<Integer, List<Object[]>> quarters = new HashMap<>();
quarters.put(1, new ArrayList<>());
quarters.put(2, new ArrayList<>());
quarters.put(3, new ArrayList<>());
quarters.put(4, new ArrayList<>());
for (Object[] row : rows) {
int quarter = ((Number) row[row.length - 1]).intValue();
quarters.get(quarter).add(row);
}
finalMap.put(year, quarters);
}
model.addAttribute("yearQuarterTasks", finalMap);
model.addAttribute("overdueTasks", taskService.allOverDueTasks(userId));
model.addAttribute("years", allYears);
return "dashboard";
}
@PatchMapping("/completeDailyTask/{id}")
String completeDailyTask(Model model, @PathVariable("id") Long id, Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Task task = taskService.findTaskById(id);
if (task.getStatus().contains("Completed")) {
task.setStatus("Pending");
} else {
task.setStatus("Completed");
}
task.setUpdatedBy(userDetails.getUsername());
taskService.updateTask(id, task);
model.addAttribute("todaysTasks", taskService.getDailyTasks(userDetails.getId()));
return "dashboard :: daily-tasks";
}
@PatchMapping("/completeWeeklyTask/{id}")
String completeWeeklyTask(Model model,
@PathVariable("id") Long id,
Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Task task = taskService.findTaskById(id);
if (task.getStatus().contains("Completed")) {
task.setStatus("Pending");
} else {
task.setStatus("Completed");
}
task.setUpdatedBy(userDetails.getUsername());
taskService.updateTask(id, task);
model.addAttribute("weeklyTasks", taskService.getWeeklyTasks(userDetails.getId()));
return "dashboard :: weekly-tasks";
}
}

View File

@ -0,0 +1,16 @@
package com.example.TaskManager.Controllers;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
@Controller
public class LandingPage {
@GetMapping("/")
private String landingPage(){
return "landing-page";
}
}

View File

@ -0,0 +1,32 @@
package com.example.TaskManager.Controllers;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
@Controller
public class Login {
@GetMapping("/login")
public String login(
@RequestParam(value = "error", required = false) String error,
@RequestParam(value = "register", required = false) String register,
@RequestParam(value = "logout", required = false) String logout,
Model model) {
if (error != null) {
model.addAttribute("errorMessage", "Invalid username or password.");
}
if (register != null) {
model.addAttribute("successMessage", "Account created successfully. Please log in.");
}
if (logout != null) {
model.addAttribute("logoutMessage", "You have been logged out successfully.");
}
return "login";
}
}

View File

@ -0,0 +1,49 @@
package com.example.TaskManager.Controllers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import com.example.TaskManager.Models.User;
import com.example.TaskManager.Security.CustomUserDetails;
import com.example.TaskManager.Services.UserService;
@Controller
public class Profile {
@Autowired
private UserService userService;
@GetMapping("/profile")
private String profile(Model model, Authentication authentication) {
// user details
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
model.addAttribute("userName", userDetails.getUsername());
model.addAttribute("userEmail", userDetails.getEmail());
model.addAttribute("user", userDetails.convertToUser());
return "profile";
}
@PostMapping("/updateProfile/update/{id}")
private String updateProfile(@PathVariable("id") Long id,
@ModelAttribute User userDetails, Authentication authentication) {
CustomUserDetails authDetails = (CustomUserDetails) authentication.getPrincipal();
userDetails.setUpdatedBy(authDetails.getUsername());
userService.updateUser(id, userDetails);
return "redirect:/dashboard";
}
}

View File

@ -0,0 +1,33 @@
package com.example.TaskManager.Controllers;
import com.example.TaskManager.Models.User;
import com.example.TaskManager.Services.UserService;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
@RequiredArgsConstructor
public class Register {
private final UserService userService;
private final PasswordEncoder passwordEncoder;
@GetMapping("/register")
public String register(org.springframework.ui.Model model){
model.addAttribute("user", new User());
return "register";
}
@PostMapping("/register")
public String createAccount(@ModelAttribute("user") User user){
user.setPassword(passwordEncoder.encode(user.getPassword()));
user.setCreatedBy("Admin");
userService.save(user);
return "redirect:/login?register=true";
}
}

View File

@ -0,0 +1,128 @@
package com.example.TaskManager.Controllers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import com.example.TaskManager.Models.Task;
import com.example.TaskManager.Security.CustomUserDetails;
import com.example.TaskManager.Services.TaskService;
@Controller
public class Tasks {
@Autowired
private TaskService taskService;
@GetMapping("/tasks")
public String tasks(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(defaultValue = "Active") String tab, // NEW: Default to "Active"
Model model,
Authentication authentication) {
loadTaskData(model, "", page, size, null, null, tab, "due_date", authentication);
return "viewTasks";
}
@GetMapping("/tasks/data")
public String loadTable(
@RequestParam(defaultValue = "") String search,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(required = false) String status,
@RequestParam(required = false) String priority,
@RequestParam(defaultValue = "Active") String tab, // NEW
Model model,
Authentication authentication) {
loadTaskData(model, search, page, size, status, priority, tab, "due_date", authentication);
return "viewTasks :: results-block";
}
@DeleteMapping("/deleteTask/{id}")
public String deleteTask(@PathVariable Long id,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(defaultValue = "") String search,
@RequestParam(required = false) String status,
@RequestParam(required = false) String priority,
@RequestParam(defaultValue = "Active") String tab, // NEW
Model model,
Authentication authentication) {
taskService.deleteTask(id);
loadTaskData(model, search, page, size, status, priority, tab, "due_date", authentication);
return "viewTasks :: results-block";
}
@PatchMapping("/complateTask/{id}")
public String completeTask(@PathVariable Long id,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "25") int size,
@RequestParam(defaultValue = "") String search,
@RequestParam(required = false) String status,
@RequestParam(required = false) String priority,
@RequestParam(defaultValue = "Active") String tab, // NEW
Model model,
Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Task task = taskService.findTaskById(id);
if (task.getStatus().contains("Completed")){
task.setStatus("Pending");
}else{
task.setStatus("Completed");
}
task.setUpdatedBy(userDetails.getUsername());
taskService.updateTask(id, task);
loadTaskData(model, search, page, size, status, priority, tab, "due_date", authentication);
return "viewTasks :: results-block";
}
private void loadTaskData(
Model model,
String search,
int page,
int size,
String status,
String priority,
String tab,
String sortByColumn,
Authentication authentication) {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
Long userId = (userDetails.getId() == null) ? (long) 0 : userDetails.getId();
Page<?> taskPage = taskService.searchTasksDynamic(
search,
status,
priority,
tab,
page,
size,
sortByColumn,
userId);
model.addAttribute("taskPage", taskPage);
model.addAttribute("currentTab", tab); // NEW: Add the current tab to the model
// user details in sidebar
model.addAttribute("userName", userDetails.getUsername());
model.addAttribute("userEmail", userDetails.getEmail());
}
}

View File

@ -0,0 +1,29 @@
package com.example.TaskManager.DTO;
import org.springframework.format.annotation.DateTimeFormat;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class TasksWithCategory {
private Long Id;
private String title;
private String description;
private String status;
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
private java.sql.Timestamp dueDate;
private String category;
private String priority;
}

View File

@ -0,0 +1,58 @@
package com.example.TaskManager.Models;
import java.time.LocalDateTime;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.*;
@Entity
@Table(name = "categories")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Category {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column
private String categoryName;
@Column
private String description;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private String createdBy;
@Column
private LocalDateTime updatedAt;
@Column
private String updatedBy;
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,76 @@
package com.example.TaskManager.Models;
import java.time.LocalDateTime;
import org.springframework.format.annotation.DateTimeFormat;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import jakarta.persistence.Table;
import lombok.*;
@Entity
@Table(name = "tasks")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Task {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long Id;
@Column
private String title;
@Column
private String description;
@Column
private String status;
@Column
@DateTimeFormat(pattern = "yyyy-MM-dd'T'HH:mm")
LocalDateTime dueDate;
@Column
private Long categoryId;
@Column
private String priority;
@Column
private Long assignedTo;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private String createdBy;
@Column
private LocalDateTime updatedAt;
@Column
private String updatedBy;
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,62 @@
package com.example.TaskManager.Models;
import java.time.LocalDateTime;
import jakarta.persistence.*;
import lombok.*;
@Entity
@Table(name = "users")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, unique = true)
private String username;
@Column(nullable = false)
private String password;
@Column(nullable = false, unique = true)
private String email;
@Column
private String firstName;
@Column
private String lastName;
@Column(unique = true)
private String phoneNumber;
@Column(nullable = false)
private LocalDateTime createdAt;
@Column(nullable = false)
private String createdBy;
@Column
private LocalDateTime updatedAt;
@Column
private String updatedBy;
@PrePersist
protected void onCreate() {
LocalDateTime now = LocalDateTime.now();
this.createdAt = now;
this.updatedAt = now;
}
@PreUpdate
protected void onUpdate() {
this.updatedAt = LocalDateTime.now();
}
}

View File

@ -0,0 +1,23 @@
package com.example.TaskManager.Repo;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.example.TaskManager.Models.Category;
@Repository
public interface CategoryRepo extends JpaRepository<Category, Long> {
@Query(value = "SELECT * FROM categories", nativeQuery = true)
List<Category> findAllCategories();
@Query(value = "SELECT * FROM categories WHERE categoryName =:categoryName", nativeQuery = true)
List<Category> allCategoriesByName(@Param("categoryName") String category);
@Query(value = "SELECT category_name FROM categories WHERE id =:categoryId", nativeQuery = true)
Category allCategoriesByName(@Param("categoryId") Long categoryId);
}

View File

@ -0,0 +1,252 @@
package com.example.TaskManager.Repo;
import java.util.List;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import com.example.TaskManager.DTO.TasksWithCategory;
import com.example.TaskManager.Models.Task;
@Repository
public interface TaskRepo extends JpaRepository<Task, Long> {
@Query(value = """
SELECT * FROM tasks
WHERE assigned_to = :userId
ORDER BY due_date DESC
""", nativeQuery = true)
Page<Task> findAllTasks(Pageable pageable,
@Param("userId") Long userId);
@Query(value = """
SELECT t.id, t.title, t.description, t.status, t.due_date,
c.category_name, t.priority
FROM tasks t
JOIN categories c
ON t.category_id = c.id
WHERE t.status <> 'Completed'
AND assigned_to = :userId
ORDER BY t.due_date DESC""", nativeQuery = true)
Page<TasksWithCategory> findAllTasksWithCategories(Pageable pageable,
@Param("userId") Long userId);
@Query(value = """
SELECT
t.id, t.title, t.description, t.status, t.due_date,
c.category_name, t.priority
FROM tasks t
JOIN categories c ON t.category_id = c.id
WHERE t.category_id = :categoryId
AND assigned_to = :userId
""", nativeQuery = true)
Page<TasksWithCategory> allTaskByCategory(Pageable pageable,
@Param("categoryId") int categoryId,
@Param("userId") Long userId);
@Query(value = "SELECT * FROM tasks WHERE priority = ?", nativeQuery = true)
Page<Task> allTasksByPriority(Pageable pageable, String priority);
@Query(value = """
SELECT t.id, t.title, t.description, t.status, t.due_date,
c.category_name, t.priority
FROM tasks t
JOIN categories c ON t.category_id = c.id
WHERE t.status = :status
AND assigned_to = :userId
ORDER BY DATE(t.due_date) DESC, TIME(t.due_date)
""", nativeQuery = true)
List<TasksWithCategory> allTasksByStatus(@Param("status") String Status,
@Param("userId") Long userId);
@Query(value = """
SELECT *
FROM tasks
WHERE due_date >= :date
AND due_date < DATE_ADD(:date, INTERVAL 1 DAY)
AND assigned_to = :userId
""", nativeQuery = true)
List<Task> allTasksByDate(@Param("date") java.time.LocalDate date,
@Param("userId") Long userId);
@Query(value = """
SELECT
t.id, t.title, t.description, t.status, t.due_date,
c.category_name, t.priority
FROM tasks t
JOIN categories c ON t.category_id = c.id
WHERE due_date >= CURRENT_DATE()
AND due_date < DATE_ADD(CURRENT_DATE(), INTERVAL 1 DAY)
AND assigned_to = :userId
ORDER BY FIELD(t.status, 'Completed'), due_date
""", nativeQuery = true)
List<TasksWithCategory> getDailyTasks(@Param("userId") Long userid);
@Query(value = """
SELECT DISTINCT YEAR(due_date)
FROM tasks
WHERE assigned_to = :userId
ORDER BY YEAR(due_date)
""", nativeQuery = true)
List<Integer> allTaskYears(@Param("userId") Long userId);
@Query(value = """
SELECT t.id, t.title, t.description, t.status, t.due_date,
c.category_name, t.priority, QUARTER(t.due_date) AS quarter
FROM tasks t
JOIN categories c ON t.category_id = c.id
WHERE YEAR(t.due_date) = :year
AND assigned_to = :userId
ORDER BY QUARTER(t.due_date), t.due_date
""", nativeQuery = true)
List<Object[]> findTasksByYearWithQuarter(@Param("year") int year,
@Param("userId") Long userId);
@Modifying
@Query(value = """
UPDATE tasks
SET status = 'Overdue'
WHERE due_date <= NOW()
AND (status = 'Pending' OR status = 'On Hold' OR Status = 'In Progress')
""", nativeQuery = true)
int updateStatusForOverdueTasks();
@Query(value = """
SELECT
t.id,
t.title,
t.description,
t.status,
t.due_date,
c.category_name,
t.priority
FROM tasks t
JOIN categories c ON t.category_id = c.id
WHERE assigned_to = :userId
AND (
:tabStatus IS NULL OR :tabStatus = '' OR
(:tabStatus = 'All' AND 1=1) OR -- 'All' tab shows all records
(:tabStatus = 'Completed' AND t.status = 'Completed') OR
(:tabStatus = 'Overdue' AND t.status = 'Overdue') OR
(:tabStatus = 'Active' AND t.status NOT IN ('Completed', 'Overdue'))
)
AND (
:tabStatus = 'All' OR :tabStatus = 'Completed' OR t.status <> 'Completed'
)
-- SEARCH filter (ignored when NULL)
AND (
:search IS NULL OR :search = '' OR
CONCAT_WS(' ',
t.id,
t.title,
t.description,
t.status,
DATE_FORMAT(t.due_date, '%Y-%m-%d'),
DATE_FORMAT(t.due_date, '%Y/%m/%d'),
DATE_FORMAT(t.due_date, '%m/%d/%Y'),
DATE_FORMAT(t.due_date, '%d/%m/%Y'),
c.category_name,
t.priority
) LIKE CONCAT('%', :search, '%')
)
-- STATUS filter (ignored when NULL)
AND (
:status IS NULL OR :status = '' OR t.status = :status
)
-- PRIORITY filter (ignored when NULL)
AND (
:priority IS NULL OR :priority = '' OR t.priority = :priority
)
""", nativeQuery = true)
Page<TasksWithCategory> searchTasksDynamic(
Pageable pageable,
@Param("search") String search,
@Param("status") String status,
@Param("priority") String priority,
@Param("tabStatus") String tabStatus,
@Param("userId") Long userId);
@Query(value = """
SELECT IFNULL(FLOOR(COUNT(status) / (SELECT COUNT(status) FROM tasks where assigned_to = :userId) * 100), 0) AS 'average'
FROM tasks
WHERE status = :status
AND assigned_to = :userId
""", nativeQuery = true)
int getPercentOfTasksByStatus(@Param("status") String Status,
@Param("userId") Long userID);
@Query(value = """
SELECT
t.id,
t.title,
t.description,
t.status,
t.due_date,
c.category_name,
t.priority
FROM tasks t
JOIN categories c ON t.category_id = c.id
WHERE due_date > DATE_ADD(CURDATE(), INTERVAL 1 DAY)
AND due_date <= DATE_ADD(CURDATE(), INTERVAL 8 DAY)
AND assigned_to = :userId
ORDER BY FIELD(t.status, 'Completed'), due_date
""", nativeQuery = true)
List<TasksWithCategory> getWeeklyTasks(@Param("userId") Long userId);
@Query(value = """
SELECT FLOOR(COUNT(*) * 100.0 / (SELECT COUNT(*) FROM tasks WHERE assigned_to = :userId))
AS completedPercentaged
FROM tasks
WHERE status = 'Completed'
AND assigned_to = :userId
""", nativeQuery = true)
double getCompletedPercentage(@Param("userId") Long userId);
@Query(value = """
select COUNT(*)
FROM tasks
where status = 'Completed'
AND assigned_to = :userId
""", nativeQuery = true)
int totalCompletedTasks(@Param("userId") Long userId);
@Query(value = """
SELECT COUNT(*) as total_records
FROM tasks
WHERE assigned_to = :userId
""", nativeQuery = true)
int totalNumTasks(@Param("userId") Long userId);
@Query(value = """
SELECT category_name
FROM categories
ORDER BY id
""", nativeQuery = true)
String[] getCategoryNames();
@Query(value = """
SELECT
COUNT(t.id) AS category_count
FROM categories c
LEFT JOIN tasks t
ON t.category_id = c.id
AND t.assigned_to = :userId
GROUP BY c.id, c.category_name
ORDER BY c.id
""", nativeQuery = true)
int[] getCategoryCounts(@Param("userId") Long userId);
}

View File

@ -0,0 +1,22 @@
package com.example.TaskManager.Repo;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.stereotype.Repository;
import com.example.TaskManager.Models.User;
@Repository
public interface UserRepo extends JpaRepository<User, Long>{
@Query(value = "SELECT * FROM users u", nativeQuery = true)
Page<User> findAllUsers(Pageable pageable);
Optional<User> findByUsername(String username);
Optional<User> findByEmail(String email);
}

View File

@ -0,0 +1,25 @@
package com.example.TaskManager.Security;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import com.example.TaskManager.Repo.UserRepo;
import lombok.RequiredArgsConstructor;
@Service
@RequiredArgsConstructor
public class CustomUserDetailService implements UserDetailsService{
private final UserRepo userRepo;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
return userRepo.findByUsername(username)
.map(CustomUserDetails::new)
.orElseThrow(() -> new UsernameNotFoundException("User not found"));
}
}

View File

@ -0,0 +1,72 @@
package com.example.TaskManager.Security;
import java.util.Collection;
import java.util.List;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.example.TaskManager.Models.User;
public class CustomUserDetails implements UserDetails {
private final User user;
public CustomUserDetails(User user) {
this.user = user;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return user.getPassword();
}
@Override
public String getUsername() {
return user.getUsername();
}
public String getEmail() {
return user.getEmail();
}
public String getFirstName() {
return user.getFirstName();
}
public String getLastName() {
return user.getLastName();
}
public String getPhoneNumber() {
return user.getPhoneNumber();
}
public Long getId() {
return user.getId();
}
public User convertToUser(){
User user = new User();
user.setId(getId());
user.setPassword(getPassword());
user.setUsername(getUsername());
user.setFirstName(getFirstName());
user.setLastName(getLastName());
user.setEmail(getEmail());
user.setPhoneNumber(getPhoneNumber());
return user;
}
}

View File

@ -0,0 +1,59 @@
package com.example.TaskManager.Security;
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.factory.PasswordEncoderFactories;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.beans.factory.annotation.Value;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
private final CustomUserDetailService customUserDetailsService;
private final String rememberMeKey;
public SecurityConfig(CustomUserDetailService customUserDetailsService,
@Value("${security.rememberme.key}") String rememberMeKey) {
this.customUserDetailsService = customUserDetailsService;
this.rememberMeKey = rememberMeKey;
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/register").permitAll()
.anyRequest().authenticated())
.formLogin(form -> form
.loginPage("/login")
.loginProcessingUrl("/login")
.defaultSuccessUrl("/dashboard", true)
.failureUrl("/login?error=true")
.permitAll())
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessUrl("/login?logout=true")
.deleteCookies("JSESSIONID")
.invalidateHttpSession(true)
.permitAll())
.rememberMe(r -> r
.rememberMeParameter("remember-me")
.tokenValiditySeconds(7 * 24 * 60 * 60)
.key(rememberMeKey)
.userDetailsService(customUserDetailsService));
return http.build();
}
@Bean
public PasswordEncoder passwordEncoder() {
return PasswordEncoderFactories.createDelegatingPasswordEncoder();
}
}

View File

@ -0,0 +1,26 @@
package com.example.TaskManager.Services;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.TaskManager.Models.Category;
import com.example.TaskManager.Repo.CategoryRepo;
@Service
public class CategoryService {
@Autowired
private CategoryRepo categoryRepo;
public List<Category> findAll(){
return categoryRepo.findAll();
}
public List<Category> findAllCategories(){
return categoryRepo.findAllCategories();
}
}

View File

@ -0,0 +1,32 @@
package com.example.TaskManager.Services;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.example.TaskManager.Repo.TaskRepo;
@Service
public class ScheduledService {
@Autowired
private TaskRepo taskRepo;
/*
update all tasks that have a status of Pending and have passed there
due date to have a status of Overdue.
There check happends every 8 hours
8 hours = 8 * 60 minutes * 60 seconds * 1000 milliseconds = 28800000
*/
@Scheduled(fixedRate = 900000, initialDelay = 5000)
@Transactional
void updateStatusForOverdueTasks(){
int updatedCount = taskRepo.updateStatusForOverdueTasks();
System.out.println(updatedCount + " Records updated");
}
}

View File

@ -0,0 +1,139 @@
package com.example.TaskManager.Services;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;
import com.example.TaskManager.DTO.TasksWithCategory;
import com.example.TaskManager.Models.Task;
import com.example.TaskManager.Repo.TaskRepo;
@Service
public class TaskService {
@Autowired
private TaskRepo taskRepo;
public void saveTask(Task task) {
taskRepo.save(task);
}
public List<Task> findAll() {
return taskRepo.findAll();
}
public List<TasksWithCategory> allOverDueTasks(Long assignedTo) {
return taskRepo.allTasksByStatus("Overdue", assignedTo);
}
public List<Integer> allTaskYears(Long assignedTo) {
return taskRepo.allTaskYears(assignedTo);
}
public Page<Task> findAllTasks(int pageNo, int pageSize, Long assignedTo) {
Pageable pageable = PageRequest.of(pageNo, pageSize);
return taskRepo.findAllTasks(pageable, assignedTo);
}
public Page<TasksWithCategory> findAllTasksWithCategories(int pageNo, int pageSize, Long assignedTo) {
Pageable pageable = PageRequest.of(pageNo, pageSize );
return taskRepo.findAllTasksWithCategories(pageable, assignedTo);
}
public Page<TasksWithCategory> searchTasksDynamic(
String search,
String status,
String priority,
String tab,
int page,
int size,
String sortByColumn,
Long assignedTo) {
Pageable pageable = PageRequest.of(page, size, Sort.by(sortByColumn));
// convert blank to null. better for sql handling
search = (search == null || search.isBlank()) ? null : search.trim();
status = (status == null || status.isBlank()) ? null : status.trim();
priority = (priority == null || priority.isBlank()) ? null : priority.trim();
return taskRepo.searchTasksDynamic(pageable, search, status, priority, tab, assignedTo);
}
public List<TasksWithCategory> getDailyTasks(Long assignedTo) {
return taskRepo.getDailyTasks(assignedTo);
}
public List<Task> allTasksByDate(LocalDate date, Long assignedTo) {
return taskRepo.allTasksByDate(date, assignedTo);
}
public List<Object[]> findTasksByYearWithQuarter(Integer year,Long assignedTo) {
return taskRepo.findTasksByYearWithQuarter(year, assignedTo);
}
public Task findTaskById(Long id) {
return taskRepo.findById(id).get();
}
public void updateTask(Long taskId, Task newTask) {
taskRepo.findById(taskId)
.orElseThrow(() -> new RuntimeException("No Task with id:" + taskId + "found"));
Task task = taskRepo.findById(taskId).get();
task.setTitle(newTask.getTitle());
task.setDescription(newTask.getDescription());
task.setPriority(newTask.getPriority());
task.setStatus(newTask.getStatus());
task.setCategoryId(newTask.getCategoryId());
task.setDueDate(newTask.getDueDate());
task.setUpdatedAt(LocalDateTime.now());
task.setUpdatedBy(newTask.getUpdatedBy());
taskRepo.save(task);
}
public void deleteTask(Long taskId) {
taskRepo.findById(taskId)
.orElseThrow(() -> new RuntimeException("No Task with id:" + taskId + "found"));
taskRepo.deleteById(taskId);
}
public int getPercentOfTasksByStatus(String status, Long assignedTo) {
return taskRepo.getPercentOfTasksByStatus(status,assignedTo);
}
public List<TasksWithCategory> getWeeklyTasks(Long assignedTo) {
return taskRepo.getWeeklyTasks(assignedTo);
}
public double getCompletedPercentage(Long assignedTo) {
return taskRepo.getCompletedPercentage(assignedTo);
}
public int totalNumTasks(Long assignedTo) {
return taskRepo.totalNumTasks(assignedTo);
}
public int totalCompletedTasks(Long assignedTo){
return taskRepo.totalCompletedTasks(assignedTo);
}
public String[] getCategoryNames() {
return taskRepo.getCategoryNames();
}
public int[] getCategoryCounts(Long assignedTo) {
return taskRepo.getCategoryCounts(assignedTo);
}
}

View File

@ -0,0 +1,51 @@
package com.example.TaskManager.Services;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import com.example.TaskManager.Models.User;
import com.example.TaskManager.Repo.UserRepo;
@Service
public class UserService {
@Autowired
private UserRepo userRepo;
public List<User> findAll(){
return userRepo.findAll();
}
public Page<User> findAllUsers(int pageNo, int pageSize){
Pageable pageable = PageRequest.of(pageNo, pageSize);
return userRepo.findAllUsers(pageable);
}
public void save(User user){
userRepo.save(user);
}
public void updateUser(Long id, User newUserDetails){
User user = userRepo.findById(id).get();
user.setFirstName(newUserDetails.getFirstName());
user.setLastName(newUserDetails.getLastName());
user.setUsername(newUserDetails.getUsername());
user.setEmail(newUserDetails.getEmail());
user.setPhoneNumber(newUserDetails.getPhoneNumber());
userRepo.save(user);
}
}

View File

@ -0,0 +1,15 @@
package com.example.TaskManager;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
@EnableScheduling
public class TaskManagerApplication {
public static void main(String[] args) {
SpringApplication.run(TaskManagerApplication.class, args);
}
}

View File

@ -0,0 +1,19 @@
spring.application.name=TaskManager
spring.application.name=task_manager
spring.datasource.url=jdbc:mysql://192.168.0.150:3306/task_manager?useSSL=false&serverTimezone=UTC
spring.datasource.username=TasksUser
spring.datasource.password=MckopServerTasks
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
spring.jpa.show-sql=true
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
security.rememberme.key=DevSecretKey
server.address=0.0.0.0
server.port=8085

Binary file not shown.

After

Width:  |  Height:  |  Size: 212 KiB

View File

@ -0,0 +1,23 @@
<header th:fragment="header" class="navbar navbar-light bg-white border-bottom border-dark sticky-top p-3" style="box-shadow: 0 4px 0 -2px rgba(0,0,0,0.1);">
<div class="container-fluid">
<div class="d-flex align-items-center">
<button class="btn btn-light border border-dark me-3 shadow-sm"
type="button"
data-bs-toggle="offcanvas"
data-bs-target="#offcanvas-menu"
aria-controls="offcanvas-menu"
aria-label="Toggle navigation"
style="box-shadow: 2px 2px 0 black !important;">
<i class="bi bi-list fs-5"></i>
</button>
<a th:href="@{/dashboard}" class="d-flex align-items-center text-decoration-none text-dark">
<i class="bi bi-kanban-fill fs-3 me-2" th:if="${logo == null}"></i>
<img th:if="${logo != null}" th:src="${logo}" class="img-fluid me-3" alt="Logo" style="height: 30px;">
<h1 class="h4 mb-0 fw-bold" th:text="${Header} ?: 'Task Manager'">Task Manager</h1>
</a>
</div>
</div>
</header>

View File

@ -0,0 +1,74 @@
<div th:fragment="nav-sidebar">
<div class="offcanvas offcanvas-start border-end border-dark" tabindex="-1" id="offcanvas-menu"
aria-labelledby="offcanvasLabel" style="box-shadow: 4px 0 0 rgba(0,0,0,0.1);">
<div class="offcanvas-header bg-light border-bottom border-dark">
<h5 class="offcanvas-title fw-bold" id="offcanvasLabel">
<i class="bi bi-menu-button-wide me-2"></i>Menu
</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="Close"></button>
</div>
<div class="offcanvas-body d-flex flex-column justify-content-between p-0">
<nav class="p-3">
<ul class="nav nav-pills flex-column gap-2">
<li class="nav-item">
<a class="nav-link link-dark border border-transparent" th:href="@{/dashboard}"
style="transition: all 0.2s;">
<i class="bi bi-bar-chart"></i> Dashboard
</a>
</li>
<li class="nav-item">
<a class="nav-link link-dark border border-transparent" th:href="@{/tasks}">
<i class="bi bi-list-task me-2"></i> View Tasks
</a>
</li>
<li class="nav-item">
<a class="nav-link link-dark border border-transparent" th:href="@{/createTask}">
<i class="bi bi-plus-square me-2"></i> Create Task
</a>
</li>
</ul>
</nav>
<div class="border-top border-dark bg-light p-3">
<div class="d-flex align-items-center">
<div class="flex-shrink-0">
<i class="bi bi-person-circle fs-3"></i>
</div>
<div class="flex-grow-1 ms-3">
<h6 class="mb-0 fw-bold" th:text="${userName}">User Account</h6>
<small class="text-muted" th:text="${userEmail}">user@example.com</small>
</div>
<button class="btn btn-sm btn-outline-dark border-0">
<!-- <i class="bi bi-gear-fill"></i> -->
<!-- Replace the previous button + dropup block with the following -->
<div class="dropup">
<!-- Toggle button (keeps the gear icon) -->
<button class="btn btn-sm btn-outline-dark" type="button" id="userMenuButton"
data-bs-toggle="dropdown" aria-expanded="false">
<i class="bi bi-gear-fill"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userMenuButton">
<li>
<form th:action="@{/logout}" method="post" class="m-0">
<input type="hidden" th:name="${_csrf.parameterName}"
th:value="${_csrf.token}" />
<button type="submit" class="dropdown-item">Sign Out</button>
</form>
</li>
<li><a class="dropdown-item" th:href="@{/profile}">Profile</a></li>
</ul>
</div>
</button>
</div>
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,102 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Create Task</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<style>
.task-box {
box-shadow: 6px 6px black;
}
button:hover {
opacity: 0.9;
}
body {
font-family: 'Courier New', Courier, monospace;
}
</style>
</head>
<body class="w-100 h-100">
<div th:replace="~{component/header :: header}"></div>
<main class="container d-flex justify-content-center align-items-start py-4" style="height: 90%;">
<div style="border: 2px solid black;" class="task-box px-4 py-5 col-12 col-md-8 col-lg-6">
<h2 class="mb-4">Create Task</h2>
<form th:action="@{/createTask/save}" th:object="${task}" method="post">
<!-- Title -->
<label for="title" class="form-label mb-0">Title</label>
<input id="title" th:field="*{title}" class="form-control rounded-0 mb-3" maxlength="50" style="border-color:black;"
type="text" required autofocus>
<!-- Category -->
<label for="category" class="form-label mb-0">Category</label>
<select id="category" th:field="*{categoryId}" class="form-select rounded-0 mb-3"
style="border-color:black;" required>
<option value="" disabled selected>Select Category</option>
<option th:each="cat : ${comCategories}" th:value="${cat.id}" th:text="${cat.categoryName}">
</option>
</select>
<!-- Status -->
<label for="status" class="form-label mb-0">Status</label>
<select id="status" th:field="*{status}" class="form-select rounded-0 mb-3" style="border-color:black;"
required>
<option value="" disabled selected>Select Status</option>
<option value="In Progress">In Progress</option>
<option value="Pending">Pending</option>
<option value="Completed">Completed</option>
<option value="On Hold">On Hold</option>
<option value="Overdue">Overdue</option>
<option value="Closed">Closed</option>
</select>
<!-- Priority -->
<label for="priority" class="form-label mb-0">Priority</label>
<select id="priority" th:field="*{priority}" class="form-select rounded-0 mb-3"
style="border-color:black;" required>
<option value="" disabled selected>Select Priority</option>
<option value="Critical">Critical</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
<!-- Due Date + Time -->
<label for="dueDate" class="form-label mb-0">Due Date & Time</label>
<input id="dueDate" th:field="*{dueDate}" class="form-control rounded-0 mb-3" type="datetime-local"
style="border-color:black;" required>
<!-- Description -->
<label for="description" class="form-label mb-0">Description</label>
<textarea id="description" th:field="*{description}" maxlength="100" class="form-control rounded-0 mb-3"
style="border-color:black;height: 90px;"></textarea>
<div class="d-inline-flex w-100">
<button th:text="${resetBtnText}" class="btn btn-outline-secondary rounded-0 w-100 me-1" type="reset">
Clear
</button>
<button th:text="${saveBtnText}" class="btn btn-outline-primary rounded-0 w-100 ms-1" type="submit">
Create Task
</button>
</div>
</form>
</div>
</main>
<div th:replace="~{component/nav-sidebar :: nav-sidebar}"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,504 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="light" class="h-100">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="Task Dashboard Overview">
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
<title>Dashboard</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
<style>
/* --- Retro/Brutalist Theme Variables --- */
:root {
--retro-border-color: #000;
--retro-border-width: 1px;
--retro-shadow-offset: 3px;
}
/* --- Global Layout Fixes --- */
body {
display: flex;
flex-direction: column;
overflow-x: hidden;
font-family: 'Courier New', Courier, monospace;
}
/* --- Component Styles --- */
.retro-container {
border: var(--retro-border-width) solid var(--retro-border-color);
background: white;
height: 100%;
position: relative;
}
/* Shadow Directions */
.shadow-sidebar {
box-shadow: var(--retro-shadow-offset) 0 0 var(--retro-border-color);
}
.shadow-card-right {
box-shadow: var(--retro-shadow-offset) var(--retro-shadow-offset) 0 var(--retro-border-color);
}
.shadow-card-left {
box-shadow: calc(var(--retro-shadow-offset) * -1) var(--retro-shadow-offset) 0 var(--retro-border-color);
}
.shadow-double {
box-shadow:
var(--retro-shadow-offset) var(--retro-shadow-offset) 0 var(--retro-border-color),
calc(var(--retro-shadow-offset) * -1) var(--retro-shadow-offset) 0 var(--retro-border-color);
}
/* Desktop Sidebar Styling */
@media (min-width: 992px) {
.retro-sidebar-desktop {
border-left: var(--retro-border-width) solid var(--retro-border-color);
box-shadow: calc(var(--retro-shadow-offset) * -1) 0 0 var(--retro-border-color);
/* Sticky positioning for the sidebar on desktop */
position: sticky;
top: 0;
height: 100vh;
overflow-y: hidden;
/* Inner container handles scroll */
}
}
/* Scrollbar aesthetics */
.custom-scroll {
overflow-y: auto;
scrollbar-width: thin;
scrollbar-color: #000 #f1f1f1;
}
/* Tab Overrides */
.nav-tabs .nav-link {
border: 1px solid transparent;
color: black;
border-radius: 0;
/* Remove rounded corners */
}
.nav-tabs .nav-link:hover {
border-color: #ccc;
}
.nav-tabs .nav-link.active {
border-color: black black white black;
font-weight: bold;
background-color: white;
}
/* Progress Bar Retro Style */
.progress {
border-radius: 0;
border: 1px solid black;
background-color: #f8f9fa;
height: 1.5rem;
box-shadow: 2px 2px 0px rgba(0, 0, 0, 0.1);
}
.progress-bar {
border-radius: 0;
border-right: 1px solid black;
/* Separation line */
color: black;
font-weight: bold;
}
</style>
</head>
<body class="h-100 bg-light">
<header th:replace="~{component/header :: header}"></header>
<div class="container-fluid flex-grow-1 d-flex flex-column">
<div class="row flex-grow-1 h-100 position-relative">
<main class="col-12 col-lg-9 col-xl-10 py-3 d-flex flex-column gap-4">
<div class="d-lg-none mb-2">
<button class="btn btn-warning border border-dark w-100 shadow-sm fw-bold rounded-0" type="button"
data-bs-toggle="offcanvas" data-bs-target="#overdueSidebar" aria-controls="overdueSidebar">
<i class="bi bi-exclamation-triangle-fill me-2"></i> View Overdue Tasks
</button>
</div>
<!-- Daily Tasks-->
<section class="row g-4 mb-1">
<div class="col-12 col-md-6" id="daily-tasks" th:fragment="daily-tasks">
<div class="retro-container p-3 shadow-card-right d-flex flex-column">
<h3 class="h4 border-bottom border-dark pb-2 mb-3 fw-bold">Daily Tasks</h3>
<div class="custom-scroll flex-grow-1" style="max-height: 40vh;">
<ul class="list-group list-group-flush">
<li th:each="task : ${todaysTasks}"
class="list-group-item p-2 border-bottom border-secondary-subtle"
th:classappend="${task.status == 'Completed'} ? 'bg-secondary-subtle' :''">
<div class="d-flex justify-content-between align-items-center">
<div
th:classappend="${task.status == 'Completed'} ? 'text-decoration-line-through text-muted' : ''">
<input
th:checked="${task.status == 'Completed'}"
type="checkbox"
name="dailyCheckBox"
id="dailyCheckBox"
th:attr="hx-patch=@{/completeDailyTask/{id}(id=${task.id})}"
hx-trigger="click delay:200ms"
hx-target="#daily-tasks"
hx-swap="outerHTML">
<span class="fw-bold text-truncate" th:text="${task.title}">Task Title</span><br>
<small class="text-muted"><i class="bi bi-clock me-1"></i><span
th:text="${#dates.format(task.dueDate, 'HH:mm')}">00:00</span></small>
</div>
<span class="badge bg-white text-dark border border-dark rounded-0"
th:text="${task.category}">Cat</span>
</div>
</li>
<li th:if="${#lists.isEmpty(todaysTasks)}"
class="list-group-item text-center text-muted border-0 py-4">
All caught up.
</li>
</ul>
</div>
</div>
</div>
<div class="col-12 col-md-6" id="weekly-tasks" th:fragment="weekly-tasks">
<div class="retro-container p-3 shadow-card-left d-flex flex-column">
<h3 class="h4 border-bottom border-dark pb-2 mb-3 fw-bold">Weekly Tasks</h3>
<div class="custom-scroll flex-grow-1" style="max-height: 40vh;">
<ul class="list-group list-group-flush">
<li th:each="task : ${weeklyTasks}"
class="list-group-item p-2 border-bottom border-secondary-subtle"
th:classappend="${task.status == 'Completed'} ? 'bg-secondary-subtle' : ''">
<div class="d-flex justify-content-between align-items-center">
<div
th:classappend="${task.status == 'Completed'} ? 'text-decoration-line-through text-muted' : ''">
<input
th:checked="${task.status == 'Completed'}"
type="checkbox"
th:attr="hx-patch=@{/completeWeeklyTask/{id}(id=${task.id})}"
hx-target="#weekly-tasks"
hx-trigger="click delay:100ms"
hx-swap="outerHTML"
name="weeklyTasks"
id="weeklyTasks">
<span class="fw-bold text-truncate" th:text="${task.title}">Title</span><br>
<small class="text-muted"><i class="bi bi-calendar-event me-1"></i><span
th:text="${#dates.format(task.dueDate, 'dd/MM')}">Date</span></small>
</div>
<span class="badge bg-white text-dark border border-dark rounded-0"
th:text="${task.category}">Cat</span>
</div>
</li>
<li th:if="${#lists.isEmpty(weeklyTasks)}"
class="list-group-item text-center text-muted border-0 py-4">
Clear week ahead.
</li>
</ul>
</div>
</div>
</div>
</section>
<section class="flex-grow-1">
<div class="retro-container p-3 shadow-double h-100">
<h3 class="h4 border-bottom border-dark pb-2 mb-3 fw-bold">Quarterly Overview</h3>
<ul class="nav nav-tabs border-bottom-0" role="tablist">
<li th:each="year, stat : ${years}" class="nav-item" role="presentation">
<button class="nav-link" th:classappend="${stat.last} ? ' active' : ''"
data-bs-toggle="tab" th:data-bs-target="'#year-' + ${year}" type="button" role="tab"
th:aria-controls="'year-' + ${year}" th:aria-selected="${stat.last}"
th:text="${year}">
2025
</button>
</li>
</ul>
<div class="tab-content border border-dark p-3 bg-white" style="min-height: 300px;">
<div th:each="year, stat : ${years}" class="tab-pane fade"
th:classappend="${stat.last} ? ' show active' : ''" role="tabpanel"
th:id="'year-' + ${year}">
<div class="row g-3">
<div class="col-12 col-md-6 col-xl-3" th:each="q : ${#numbers.sequence(1,4)}">
<div class="border border-secondary bg-light p-2 h-100">
<h5 class="text-center border-bottom border-secondary pb-2 fw-bold">Q<span
th:text="${q}">1</span></h5>
<div class="custom-scroll" style="max-height: 200px;">
<ul class="list-group list-group-flush small"
th:if="${yearQuarterTasks != null and yearQuarterTasks.get(year) != null}">
<li th:each="t : ${yearQuarterTasks.get(year).get(q)}"
class="list-group-item bg-light border-bottom px-1">
<strong th:text="${t[1]}"
class="d-block text-truncate">Title</strong>
<div class="d-flex justify-content-between text-muted"
style="font-size: 0.85em;">
<span th:text="${t[5]}">Category</span>
<span
th:text="${#dates.format(t[4], 'yyyy/MM/dd HH:mm')}">Date</span>
</div>
</li>
<li th:if="${yearQuarterTasks.get(year).get(q) == null or #lists.isEmpty(yearQuarterTasks.get(year).get(q))}"
class="text-center text-muted fst-italic py-2">
No tasks
</li>
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</section>
<section class="row g-4 pb-4">
<div class="col-12 col-md-4">
<div class="retro-container p-3 shadow-card-right d-flex flex-column h-100">
<h3 class="h4 border-bottom border-dark pb-2 mb-3 fw-bold">Volume</h3>
<div class="flex-grow-1 d-flex align-items-center justify-content-center"
style="position: relative; height: 250px;">
<canvas id="categoryChart"></canvas>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="retro-container p-3 shadow-double d-flex flex-column h-100">
<h3 class="h4 border-bottom border-dark pb-2 mb-3 fw-bold">Completion</h3>
<div class="flex-grow-1 d-flex align-items-center justify-content-center"
style="position: relative; height: 250px;">
<canvas id="completionChart"></canvas>
</div>
</div>
</div>
<div class="col-12 col-md-4">
<div class="retro-container p-3 shadow-card-left d-flex flex-column h-100">
<h3 class="h4 border-bottom border-dark pb-2 mb-3 fw-bold">Status Breakdown</h3>
<div class="d-flex flex-column justify-content-evenly h-100">
<div>
<div class="d-flex justify-content-between small fw-bold mb-1">
<span>Overdue</span>
<span th:text="${overduePercentage} + '%'">0%</span>
</div>
<div class="progress mb-3">
<div class="progress-bar bg-danger"
th:style="'width: ' + ${overduePercentage} + '%;'">
</div>
</div>
</div>
<div>
<div class="d-flex justify-content-between small fw-bold mb-1">
<span>Pending</span>
<span th:text="${pendingPercentage} + '%'">0%</span>
</div>
<div class="progress mb-3">
<div class="progress-bar bg-warning text-dark"
th:style="'width: ' + ${pendingPercentage} + '%;'"></div>
</div>
</div>
<div>
<div class="d-flex justify-content-between small fw-bold mb-1">
<span>In Progress</span>
<span th:text="${inProgressPercentage} + '%'">0%</span>
</div>
<div class="progress mb-3">
<div class="progress-bar bg-primary"
th:style="'width: ' + ${inProgressPercentage} + '%;'"></div>
</div>
</div>
<div>
<div class="d-flex justify-content-between small fw-bold mb-1">
<span>On Hold</span>
<span th:text="${onHoldPercentage} + '%'">0%</span>
</div>
<div class="progress mb-3">
<div class="progress-bar bg-primary"
th:style="'width: ' + ${onHoldPercentage} + '%;'"></div>
</div>
</div>
<div>
<div class="d-flex justify-content-between small fw-bold mb-1">
<span>Completed</span>
<span th:text="${completedPercentage.intValue()} + '%'">0%</span>
</div>
<div class="progress">
<div class="progress-bar bg-success"
th:style="'width: ' + ${completedPercentage} + '%;'"></div>
</div>
</div>
</div>
</div>
</div>
</section>
</main>
<aside class="col-12 col-lg-3 col-xl-2 offcanvas-lg offcanvas-end bg-white retro-sidebar-desktop p-0"
tabindex="-1" id="overdueSidebar" aria-labelledby="overdueSidebarLabel">
<div class="offcanvas-header border-bottom border-dark">
<h5 class="offcanvas-title fw-bold" id="overdueSidebarLabel">Overdue Tasks</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" data-bs-target="#overdueSidebar"
aria-label="Close"></button>
</div>
<div class="d-flex flex-column h-100 p-3">
<h3
class="h4 text-center mb-3 text-danger fw-bold d-none d-lg-block border-bottom border-dark pb-2">
<i class="bi bi-exclamation-triangle me-2"></i>Overdue
</h3>
<div class="custom-scroll flex-grow-1">
<ul class="list-group list-group-flush" th:if="${!#lists.isEmpty(overdueTasks)}">
<li th:each="task : ${overdueTasks}"
class="list-group-item border-bottom border-dark px-1 bg-transparent">
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 h6 fw-bold text-truncate" th:text="${task.title}">Task Title</h5>
</div>
<small class="text-danger fw-bold d-block mb-1">
<!-- <i class="bi bi-calendar-x me-1"></i> -->
<span th:text="${#dates.format(task.dueDate, 'yyyy/MM/dd HH:mm')}">2025-11-22</span>
</small>
<span class="badge bg-secondary rounded-0 border border-dark text-white"
th:text="${task.category}">Category</span>
</li>
</ul>
<div th:if="${#lists.isEmpty(overdueTasks)}" class="text-center text-muted mt-5">
<i class="bi bi-check-circle fs-1 text-success"></i>
<p class="mt-2 fw-bold">No overdue tasks!</p>
</div>
</div>
</div>
</aside>
</div>
</div>
<div th:replace="~{component/nav-sidebar :: nav-sidebar}"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
<script th:inline="javascript">
document.body.addEventListener(
'htmx:configRequest', function (event) {
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
event.detail.headers[csrfHeader] = csrfToken;
});
document.addEventListener("DOMContentLoaded", function () {
const commonOptions = {
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: {
labels: {
font: { family: "'Courier New', Courier, monospace", size: 12 },
color: '#000'
}
}
},
elements: {
arc: { borderWidth: 2, borderColor: '#000' },
bar: { borderWidth: 2, borderColor: '#000' }
}
};
// ---- Inject chart data from java ----
const categoryLabels = /*[[${categoryNames}]]*/[];
const categoryValues = /*[[${categoryCounts}]]*/[];
// ---- Colours for categories ----
const categoryColors = [
'#ff6384', // pink/red
'#36a2eb', // blue
'#ffcd56', // yellow
'#4bc0c0', // teal
'#9966ff', // purple
'#ff9f40', // orange
'#6f42c1', // violet
'#20c997', // emerald
'#d63384', // magenta
'#0dcaf0' // cyan
];
const categoryBackgrounds = categoryLabels.map((_, i) => categoryColors[i % categoryColors.length]);
new Chart(document.getElementById('categoryChart'), {
type: 'bar',
data: {
labels: categoryLabels,
datasets: [{
label: 'Tasks',
data: categoryValues,
backgroundColor: categoryBackgrounds,
barPercentage: 0.6
}]
},
options: {
...commonOptions,
plugins: {
...commonOptions.plugins,
legend: {
display: false
}
}
}
});
// ---- Inject data from java ----
const totalCompletedTasks = /*[[${totalCompletedTasks}]]*/ 0;
const totalNumTasks = /*[[${totalNumTasks}]]*/ 0;
const remainingPct = totalNumTasks - totalCompletedTasks;
new Chart(document.getElementById('completionChart'), {
type: 'doughnut',
data: {
labels: ['Completed', 'Remaining'],
datasets: [{
data: [totalCompletedTasks, remainingPct],
backgroundColor: ['#198754', '#e9ecef']
}]
},
options: {
...commonOptions,
cutout: '60%'
}
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,257 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="description" content="Manage your tasks with retro minimalist style.">
<title>TaskManager - Get Things Done, Retro Style.</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<style>
/* Core Theme Aesthetics */
:root {
--retro-shadow-color: black;
--retro-border-color: black;
}
body {
font-family: 'Courier New', Courier, monospace; /* The signature font */
background-color: #f8f9fa !important; /* Ensure bg-light hex */
overflow-x: hidden;
}
/* Retro Container Style (used for screenshots or feature boxes) */
.retro-box {
background: white;
border: 2px solid var(--retro-border-color);
box-shadow: 6px 6px 0px var(--retro-shadow-color);
}
/* Retro Header/Navbar styling */
.navbar {
border-bottom: 2px solid var(--retro-border-color);
background: white !important;
padding-top: 1rem;
padding-bottom: 1rem;
}
.navbar-brand {
font-weight: 900;
letter-spacing: -1px;
font-size: 1.5rem;
}
.nav-link {
color: var(--retro-border-color) !important;
font-weight: 600;
}
.nav-link:hover {
text-decoration: underline;
}
/* Retro Buttons for Landing Page (Bolder than app buttons) */
.retro-btn-lg {
border: 2px solid var(--retro-border-color);
font-weight: 700;
padding: 0.75rem 1.5rem;
position: relative;
transition: all 0.15s ease-in-out;
text-transform: uppercase;
letter-spacing: 1px;
}
/* Primary CTA - Black fill */
.retro-btn-primary {
background-color: var(--retro-border-color);
color: white;
box-shadow: 4px 4px 0px rgba(0,0,0,0.2); /* Subtle shadow when inactive */
}
.retro-btn-primary:hover {
background-color: white;
color: black;
box-shadow: 5px 5px 0px var(--retro-shadow-color);
transform: translate(-2px, -2px);
}
.retro-btn-primary:active {
box-shadow: 1px 1px 0px var(--retro-shadow-color);
transform: translate(2px, 2px);
}
/* Secondary CTA - White fill */
.retro-btn-outline {
background-color: white;
color: black;
box-shadow: 4px 4px 0px var(--retro-shadow-color);
}
.retro-btn-outline:hover {
box-shadow: 6px 6px 0px var(--retro-shadow-color);
transform: translate(-2px, -2px);
}
.retro-btn-outline:active {
box-shadow: 1px 1px 0px var(--retro-shadow-color);
transform: translate(2px, 2px);
}
/* Feature Icons */
.feature-icon {
font-size: 2.5rem;
margin-bottom: 1rem;
border: 2px solid black;
width: 80px;
height: 80px;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 3px 3px 0px black;
background: white;
}
/* Hero Section tweaks */
.hero-section {
padding: 5rem 0;
}
.hero-headline {
font-weight: 900;
font-size: 3rem;
line-height: 1.1;
}
/* Mockup styling */
.browser-mockup-header {
border-bottom: 2px solid black;
padding: 0.5rem;
display: flex;
gap: 0.5rem;
background: #eee;
}
.browser-dot {width: 12px; height: 12px; border: 1px solid black; border-radius: 50%; background: white;}
</style>
</head>
<body>
<nav class="navbar navbar-expand-lg fixed-top">
<div class="container">
<a class="navbar-brand" href="#">
<i class="bi bi-check2-square me-2"></i>TaskManager
</a>
<button class="navbar-toggler retro-btn-outline me-2" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNav">
<ul class="navbar-nav ms-auto align-items-center">
<li class="nav-item"><a class="nav-link mx-2" href="#features">Features</a></li>
<li class="nav-item ms-lg-3">
<a th:href="@{/dashboard}" class="btn retro-btn-outline btn-sm fw-bold px-3">Sign In</a>
</li>
</ul>
</div>
</div>
</nav>
<header class="hero-section mt-5">
<div class="container">
<div class="row align-items-center gy-5">
<div class="col-lg-6 order-2 order-lg-1">
<h1 class="hero-headline mb-4">
STOP OVERTHINKING.<br>START DOING.
</h1>
<p class="lead mb-5 pe-lg-5">
A minimalist task manager with a retro soul.
Cut through the noise with a tool designed for focus,
not distraction.
</p>
<div class="d-flex flex-column flex-sm-row gap-3">
<a th:href="@{/dashboard}" class="btn retro-btn-lg retro-btn-primary">
Start Doing
</a>
</div>
</div>
<div class="col-lg-6 order-1 order-lg-2">
<div class="retro-box">
<div class="browser-mockup-header">
<div class="browser-dot"></div>
<div class="browser-dot"></div>
<div class="browser-dot"></div>
</div>
<div class="bg-light d-flex justify-content-center align-items-center text-center p-4" style="aspect-ratio: 4/3; border-bottom: 2px solid black;">
<div>
<img th:src="@{images/landingPageTable.png}" width="100%" alt="Image of Task Table">
</div>
</div>
</div>
<div style="margin-top: -20px; margin-left: 20px; height: 20px; background: transparent; border-right: 2px solid black; border-bottom: 2px solid black;"></div>
</div>
</div>
</div>
</header>
<section id="features" class="py-5 bg-white" style="border-top: 2px solid black; border-bottom: 2px solid black;">
<div class="container py-5">
<div class="row text-center mb-5">
<div class="col-lg-8 mx-auto">
<h2 class="fw-w900 text-uppercase mb-3" style="font-weight: 900; letter-spacing: -1px;">Retro Simple. Modern Power.</h2>
<p class="lead text-muted">We stripped away the bloat. What's left is pure productivity engine.</p>
</div>
</div>
<div class="row g-5 py-4">
<div class="col-md-4 d-flex flex-column align-items-center text-center">
<div class="feature-icon">
<i class="bi bi-lightning-charge"></i>
</div>
<h4 class="fw-bold mt-3">HTMX Powered Speed</h4>
<p>Experience single-page-application speed without the complex JavaScript frameworks. It just feels snappy.</p>
</div>
<div class="col-md-4 d-flex flex-column align-items-center text-center">
<div class="feature-icon">
<i class="bi bi-filter-square"></i>
</div>
<h4 class="fw-bold mt-3">Dynamic Filtering</h4>
<p>Instantly slice and dice your tasks by status, priority, or search terms. Find what matters, fast.</p>
</div>
<div class="col-md-4 d-flex flex-column align-items-center text-center">
<div class="feature-icon">
<i class="bi bi-shield-lock"></i>
</div>
<h4 class="fw-bold mt-3">Secure & Simple</h4>
<p>Standard Spring Security keeps your data safe. No complex settings, just secure by default.</p>
</div>
</div>
</div>
</section>
<section class="py-5 bg-light">
<div class="container text-center py-5">
<h3 class="fw-bold mb-4">Ready to get organized the old-school way?</h3>
<a th:href="@{/register}" class="btn retro-btn-lg retro-btn-primary px-5">
Start Now - It's Free
</a>
</div>
</section>
<footer class="py-4 bg-white" style="border-top: 2px solid black;">
<div class="container text-center">
<p class="mb-0 fw-bold">
&copy; 2025 TaskManager. <br>
Built with Spring Boot & HTMX.
</p>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,77 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" style="width: 100%; height: 100%;">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Login</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
<style>
body {
font-family: 'Courier New', Courier, monospace;
}
</style>
</head>
<body style="width: 100%; height: 100%;">
<div class="container-fluid d-inline-flex py-3" style="height: 25%;">
<div class="d-flex justify-content-center align-items-center" style="width: 100%;">
<h1>Task Manager</h1>
</div>
</div>
<div class="container-fluid d-flex justify-content-center align-items-start" style="height: 75%;">
<div class="border px-5 py-5" style="box-shadow: 6px 6px black; max-width: 400px; width: 100%;">
<h2 class="mb-4">Login</h2>
<!-- Registration success message -->
<div th:if="${successMessage}" class="alert alert-success" role="alert">
<span th:text="${successMessage}"></span>
</div>
<!-- Logout success message -->
<div th:if="${logoutMessage}" class="alert alert-info" role="alert">
<span th:text="${logoutMessage}"></span>
</div>
<!-- Login error message -->
<div th:if="${errorMessage}" class="alert alert-danger" role="alert">
<span th:text="${errorMessage}"></span>
</div>
<form th:action="@{/login}" method="post" style="width: 100%;">
<label class="form-label mb-0">Username</label>
<input class="rounded-0 form-control mb-3" type="text" name="username" required
style="border-color: black;">
<label class="form-label mb-0">Password</label>
<input class="rounded-0 form-control mb-4" type="password" name="password" required
style="border-color: black;">
<!-- Remember Me Checkbox -->
<div class="form-check mb-3">
<input class="form-check-input" type="checkbox" name="remember-me" id="rememberMeCheck">
<label class="form-check-label" for="rememberMeCheck">
Remember Me?
</label>
</div>
<button class="btn btn-outline-primary rounded-0 mb-3" type="submit" style="width: 100%;">
Login
</button>
<div class="d-flex justify-content-center">
<a th:href="@{/register}">Create an Account</a>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,89 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Profile</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css">
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
<style>
.task-box {
box-shadow: 6px 6px black;
}
button:hover {
opacity: 0.9;
}
body {
font-family: 'Courier New', Courier, monospace;
}
</style>
</head>
<body class="w-100 h-100">
<div th:replace="~{component/header :: header}"></div>
<main class="container d-flex justify-content-center align-items-start py-4" style="height: 90%;">
<div style="border: 2px solid black;" class="task-box px-4 py-5 col-12 col-md-8 col-lg-6">
<h2 class="mb-4">Update Profile</h2>
<form th:action="@{/updateProfile/update/{id}(id=${user.id})}" th:object="${user}" method="post">
<!-- First Name -->
<label for="firstName" class="form-label mb-0">First Name</label>
<input id="firstName" th:field="*{firstName}" class="form-control rounded-0 mb-3"
style="border-color:black;" type="text" autofocus>
<!-- Last Name -->
<label for="lastName" class="form-label mb-0">Last Name</label>
<input id="lastName" th:field="*{lastName}" class="form-control rounded-0 mb-3"
style="border-color:black;" type="text">
<!-- Username -->
<label for="username" class="form-label mb-0">Username</label>
<input id="username" th:field="*{username}" class="form-control rounded-0 mb-3"
style="border-color:black;" type="text">
<!-- Email -->
<label for="email" class="form-label mb-0">Email</label>
<input id="email" th:field="*{email}" class="form-control rounded-0 mb-3" style="border-color:black;"
type="email">
<!-- Phone Number -->
<label for="phoneNumber" class="form-label mb-0">Phone Number</label>
<input id="phoneNumber" maxlength="10" th:field="*{phoneNumber}" class="form-control rounded-0 mb-3"
style="border-color:black;" type="tel">
<div class="d-inline-flex w-100">
<button class="btn btn-outline-secondary rounded-0 w-100 me-1" type="reset">
Reset
</button>
<button class="btn btn-outline-primary rounded-0 w-100 ms-1" type="submit">
Update Profile
</button>
</div>
</form>
</div>
</main>
<div th:replace="~{component/nav-sidebar :: nav-sidebar}"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.body.addEventListener(
'htmx:configRequest', function (event) {
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
event.detail.headers[csrfHeader] = csrfToken;
});
</script>
</body>
</html>

View File

@ -0,0 +1,60 @@
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>CreateAccount</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/css/bootstrap.min.css">
<style>
body {
font-family: 'Courier New', Courier, monospace;
}
</style>
</head>
<body style="width: 100%; height: 100%;">
<div class="container-fluid d-inline-flex m-0 ps-0 pe-0 py-3" style="height: 25%;">
<div class="d-flex justify-content-center align-items-center pt-0" style="width: 100%;">
<h1>Task Manager</h1>
</div>
</div>
<div class="container-fluid d-flex justify-content-center align-items-start mt-0" style="height: 75%;">
<div class="border px-5 py-5" style="box-shadow: 6px 6px black;">
<h2 class="mb-4">Create Account</h2>
<form th:action="@{/register}" th:object="${user}" method="post" style="width: 100%;">
<label class="form-label mb-0">Username</label>
<input class="rounded-0 form-control mb-3" th:field="*{username}" type="text"
style="border-color: black;" required autofocus>
<label class="form-label mb-0">Email</label>
<input class="rounded-0 form-control mb-3" th:field="*{email}" type="email" style="border-color: black;"
required>
<label class="form-label mb-0 pb-0">Password</label>
<input class="rounded-0 form-control mb-4" th:field="*{password}" type="password"
style="border-color: black;" required>
<button class="btn btn-outline-primary rounded-0 mb-3" type="submit" style="width: 100%;">
Create Account
</button>
<div class="d-flex justify-content-center">
<a th:href="@{/login}">Login</a>
</div>
</form>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.6/dist/js/bootstrap.bundle.min.js"></script>
</body>
</html>

View File

@ -0,0 +1,246 @@
<!DOCTYPE html>
<html data-bs-theme="light" lang="en" class="h-100" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<meta name="_csrf" th:content="${_csrf.token}">
<meta name="_csrf_header" th:content="${_csrf.headerName}">
<title>ViewTasks</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css">
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
.retro-input {
border: 1px solid black !important;
box-shadow: -2px 2px 0px black;
transition: box-shadow 0.2s;
}
.retro-input:focus {
box-shadow: -1px 1px 0px black;
outline: none;
}
.retro-btn {
border: 1px solid black !important;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
}
.retro-table-container {
border: 1px solid #dee2e6;
box-shadow: 2px 2px 0px black, -2px 2px 0px black;
background: white;
border-radius: 4px;
overflow: auto;
}
.pagination-container {
font-size: 1.1rem;
font-weight: bold;
}
body {
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
font-family: 'Courier New', Courier, monospace;
}
main {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
@media (max-width: 576px) {
.nav-tabs {
flex-wrap: nowrap;
overflow-x: auto;
scrollbar-width: thin;
font-size: smaller;
padding: 0;
}
}
</style>
</head>
<body class="bg-light">
<header th:replace="~{component/header :: header}"></header>
<main id="task-list-container" class="container-fluid py-3">
<section class="row g-3 align-items-center mb-4 flex-shrink-0">
<div class="col-12 col-md-4">
<div class="input-group">
<span class="input-group-text retro-input border-start-0 bg-white border-end-0"><i
class="bi bi-search"></i></span>
<input id="taskSearch" class="form-control border-bottom-0 ps-2 retro-input" type="search"
placeholder="Search tasks..." name="search" th:value="${param.search}" th:hx-get="@{/tasks/data}"
hx-trigger="keyup changed delay:500ms, search" hx-include="#status, #priority, #hiddenTab"
hx-target="#results-block" hx-swap="outerHTML">
</div>
</div>
<div class="col-12 col-md-4 d-flex justify-content-center align-items-center">
<div class="htmx-indicator spinner-border text-primary" role="status">
<span class="visually-hidden">Loading...</span>
</div>
</div>
<div class="col-12 col-md-4 d-flex justify-content-md-end">
<div class="input-group w-100">
<select class="form-select rounded-0 mb-3 retro-input w-50" name="status" id="status"
th:hx-get="@{/tasks/data}" hx-target="#results-block" hx-swap="outerHTML"
hx-include="#taskSearch, #priority, #hiddenTab">
<option value="" selected>Filter By Status</option>
<option value="In Progress">In Progress</option>
<option value="Pending">Pending</option>
<option value="Overdue">Overdue</option>
<option value="On Hold">On Hold</option>
</select>
<select class="form-select rounded-0 mb-3 retro-input w-50" name="priority" id="priority"
th:hx-get="@{/tasks/data}" hx-target="#results-block" hx-swap="outerHTML"
hx-include="#taskSearch, #status, #hiddenTab">
<option value="" selected>Filter By Priority</option>
<option value="Critical">Critical</option>
<option value="High">High</option>
<option value="Medium">Medium</option>
<option value="Low">Low</option>
</select>
</div>
</div>
</section>
<div id="results-block" th:fragment="results-block" class="d-flex flex-column flex-grow-1 overflow-hidden">
<div class="row mb-3 flex-shrink-0">
<div class="col-12 d-flex justify-content-center align-items-center pagination-container">
<button class="btn btn-light retro-btn me-3" th:hx-get="@{/tasks/data}"
hx-include="#taskSearch, #status, #priority, #hiddenTab"
th:attr="hx-vals='{&quot;page&quot;: ' + (${taskPage.number} - 1) + '}'"
hx-target="#results-block" hx-swap="outerHTML" th:disabled="${taskPage.first}">
<i class="bi bi-chevron-left"></i>
</button>
<span class="mx-2">
Page <span th:text="${taskPage.number + 1}">1</span>
of <span th:text="${taskPage.totalPages > 0 ? taskPage.totalPages : 1}">1</span>
</span>
<button class="btn btn-light retro-btn ms-3" th:hx-get="@{/tasks/data}"
hx-include="#taskSearch, #status, #priority, #hiddenTab"
th:attr="hx-vals='{&quot;page&quot;: ' + (${taskPage.number} + 1) + '}'"
hx-target="#results-block" hx-swap="outerHTML" th:disabled="${taskPage.last}">
<i class="bi bi-chevron-right"></i>
</button>
</div>
</div>
<ul class="nav nav-tabs border-bottom-0" id="taskTabs" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link retro-btn" th:classappend="${currentTab == 'Active' ? 'active' : ''}"
th:hx-get="@{/tasks/data}" hx-target="#results-block" hx-swap="outerHTML"
th:attr="hx-vals='{&quot;tab&quot;: &quot;Active&quot;, &quot;page&quot;: 0}'"
aria-current="page" href="#">Active</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link retro-btn" th:classappend="${currentTab == 'Completed' ? 'active' : ''}"
th:hx-get="@{/tasks/data}" hx-target="#results-block" hx-swap="outerHTML"
th:attr="hx-vals='{&quot;tab&quot;: &quot;Completed&quot;, &quot;page&quot;: 0}'"
href="#">Completed</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link retro-btn" th:classappend="${currentTab == 'Overdue' ? 'active' : ''}"
th:hx-get="@{/tasks/data}" hx-target="#results-block" hx-swap="outerHTML"
th:attr="hx-vals='{&quot;tab&quot;: &quot;Overdue&quot;, &quot;page&quot;: 0}'"
href="#">Overdue</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link retro-btn" th:classappend="${currentTab == 'All' ? 'active' : ''}"
th:hx-get="@{/tasks/data}" hx-target="#results-block" hx-swap="outerHTML"
th:attr="hx-vals='{&quot;tab&quot;: &quot;All&quot;, &quot;page&quot;: 0}'" href="#">All
</a>
</li>
</ul>
<input type="hidden" id="hiddenTab" name="tab" th:value="${currentTab}">
<div class="retro-table-container h-100 flex-grow-1">
<table class="table table-hover table-striped mb-0 ">
<thead class="table-light sticky-top" style="z-index: 1;">
<tr>
<th hx-get="/sort" scope="col">Completed</th>
<th scope="col">Title</th>
<th scope="col">Description</th>
<th scope="col">Status</th>
<th scope="col">Due Date</th>
<th scope="col">Category</th>
<th scope="col">Priority</th>
<th scope="col" class="text-center">Actions</th>
</tr>
</thead>
<tbody class="table-group-divider">
<tr th:each="task : ${taskPage.content}">
<td> <input type="checkbox" name="Completed"
th:attr="hx-patch=@{/complateTask/{id}(id=${task.id})}" hx-swap="outerHTML"
hx-include="#taskSearch, #status, #priority, #hiddenTab" hx-target="#results-block"
hx-trigger="click delay:200ms" th:checked="${task.status == 'Completed'}"> </td>
<td class="fw-medium" th:text="${task.title}">Task Title</td>
<td class="text-truncate" style="max-width: 200px;" th:text="${task.description}">Desc
</td>
<td><span class="badge text-bg-light border border-dark text-dark"
th:text="${task.status}">Status</span></td>
<td th:text="${#dates.format(task.dueDate, 'dd/MM/yy HH:mm')}">Date</td>
<td th:text="${task.category}">Cat</td>
<td th:text="${task.priority}">High</td>
<td>
<div class="d-flex justify-content-center gap-2">
<a th:href="@{'/createTask?id=' + ${task.id}}"
class="btn btn-sm btn-outline-primary">Edit</a>
<button class="btn btn-sm btn-outline-danger"
th:attr="hx-delete=@{/deleteTask/{id}(id=${task.id})}"
hx-include="#taskSearch, #status, #priority, #hiddenTab"
hx-confirm="Delete Task?" hx-target="#results-block" hx-swap="outerHTML">
Remove
</button>
</div>
</td>
</tr>
<tr th:if="${taskPage.empty}">
<td colspan="8" class="text-center py-5 text-muted">
<i class="bi bi-inbox fs-1 d-block mb-2"></i>
No tasks found.
</td>
</tr>
</tbody>
</table>
</div>
</div>
</main>
<div th:replace="~{component/nav-sidebar :: nav-sidebar}"></div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.body.addEventListener(
'htmx:configRequest', function (event) {
const csrfToken = document.querySelector('meta[name="_csrf"]').getAttribute('content');
const csrfHeader = document.querySelector('meta[name="_csrf_header"]').getAttribute('content');
event.detail.headers[csrfHeader] = csrfToken;
});
</script>
</body>
</html>

View File

@ -0,0 +1,13 @@
package com.example.TaskManager;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class TaskManagerApplicationTests {
@Test
void contextLoads() {
}
}