TaskManager Application 1.0
This commit is contained in:
parent
9e59297c57
commit
c1e675fc7f
|
|
@ -0,0 +1,2 @@
|
|||
/mvnw text eol=lf
|
||||
*.cmd text eol=crlf
|
||||
|
|
@ -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/
|
||||
|
|
@ -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
|
||||
|
|
@ -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 "$@"
|
||||
|
|
@ -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"
|
||||
|
|
@ -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>
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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";
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -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 |
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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">
|
||||
© 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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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='{"page": ' + (${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='{"page": ' + (${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='{"tab": "Active", "page": 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='{"tab": "Completed", "page": 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='{"tab": "Overdue", "page": 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='{"tab": "All", "page": 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>
|
||||
|
|
@ -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() {
|
||||
}
|
||||
|
||||
}
|
||||
Loading…
Reference in New Issue