diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..3b41682 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +/mvnw text eol=lf +*.cmd text eol=crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..667aaef --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..c0bcafe --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -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 diff --git a/mvnw b/mvnw new file mode 100755 index 0000000..bd8896b --- /dev/null +++ b/mvnw @@ -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-,maven-mvnd--}/ +[ -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 "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..92450f9 --- /dev/null +++ b/mvnw.cmd @@ -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-,maven-mvnd--}/ +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" diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..012e97d --- /dev/null +++ b/pom.xml @@ -0,0 +1,107 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.5.8 + + + com.example + TaskManager + 0.0.1-SNAPSHOT + TaskManager + Demo project for Spring Boot + + + + + + + + + + + + + + + 25 + + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.springframework.boot + spring-boot-starter-web + + + io.github.wimdeblauwe + htmx-spring-boot-thymeleaf + 4.0.1 + + + + org.springframework.boot + spring-boot-devtools + runtime + true + + + com.mysql + mysql-connector-j + runtime + + + org.projectlombok + lombok + true + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + + + + org.projectlombok + lombok + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + org.projectlombok + lombok + + + + + + + + diff --git a/src/main/java/com/example/TaskManager/Controllers/CreateTask.java b/src/main/java/com/example/TaskManager/Controllers/CreateTask.java new file mode 100644 index 0000000..c1be68e --- /dev/null +++ b/src/main/java/com/example/TaskManager/Controllers/CreateTask.java @@ -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"; + } + +} diff --git a/src/main/java/com/example/TaskManager/Controllers/Dashboard.java b/src/main/java/com/example/TaskManager/Controllers/Dashboard.java new file mode 100644 index 0000000..ad9400e --- /dev/null +++ b/src/main/java/com/example/TaskManager/Controllers/Dashboard.java @@ -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 allYears = taskService.allTaskYears(userId); + Map>> finalMap = new HashMap<>(); + + for (Integer year : allYears) { + + List rows = taskService.findTasksByYearWithQuarter(year, userId); + + // Convert raw rows into quarter tasks mapping + Map> 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"; + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/TaskManager/Controllers/LandingPage.java b/src/main/java/com/example/TaskManager/Controllers/LandingPage.java new file mode 100644 index 0000000..a072386 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Controllers/LandingPage.java @@ -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"; + + } + +} diff --git a/src/main/java/com/example/TaskManager/Controllers/Login.java b/src/main/java/com/example/TaskManager/Controllers/Login.java new file mode 100644 index 0000000..75782fe --- /dev/null +++ b/src/main/java/com/example/TaskManager/Controllers/Login.java @@ -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"; + } +} diff --git a/src/main/java/com/example/TaskManager/Controllers/Profile.java b/src/main/java/com/example/TaskManager/Controllers/Profile.java new file mode 100644 index 0000000..a65df04 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Controllers/Profile.java @@ -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"; + + } + +} diff --git a/src/main/java/com/example/TaskManager/Controllers/Register.java b/src/main/java/com/example/TaskManager/Controllers/Register.java new file mode 100644 index 0000000..e5dbfd3 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Controllers/Register.java @@ -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"; + } +} diff --git a/src/main/java/com/example/TaskManager/Controllers/Tasks.java b/src/main/java/com/example/TaskManager/Controllers/Tasks.java new file mode 100644 index 0000000..c00c538 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Controllers/Tasks.java @@ -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()); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/TaskManager/DTO/TasksWithCategory.java b/src/main/java/com/example/TaskManager/DTO/TasksWithCategory.java new file mode 100644 index 0000000..b62995d --- /dev/null +++ b/src/main/java/com/example/TaskManager/DTO/TasksWithCategory.java @@ -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; + +} diff --git a/src/main/java/com/example/TaskManager/Models/Category.java b/src/main/java/com/example/TaskManager/Models/Category.java new file mode 100644 index 0000000..315ba85 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Models/Category.java @@ -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(); + } + +} diff --git a/src/main/java/com/example/TaskManager/Models/Task.java b/src/main/java/com/example/TaskManager/Models/Task.java new file mode 100644 index 0000000..5d98cd2 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Models/Task.java @@ -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(); + } + +} diff --git a/src/main/java/com/example/TaskManager/Models/User.java b/src/main/java/com/example/TaskManager/Models/User.java new file mode 100644 index 0000000..c90258f --- /dev/null +++ b/src/main/java/com/example/TaskManager/Models/User.java @@ -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(); + } + +} diff --git a/src/main/java/com/example/TaskManager/Repo/CategoryRepo.java b/src/main/java/com/example/TaskManager/Repo/CategoryRepo.java new file mode 100644 index 0000000..6697663 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Repo/CategoryRepo.java @@ -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 { + + @Query(value = "SELECT * FROM categories", nativeQuery = true) + List findAllCategories(); + + @Query(value = "SELECT * FROM categories WHERE categoryName =:categoryName", nativeQuery = true) + List allCategoriesByName(@Param("categoryName") String category); + + @Query(value = "SELECT category_name FROM categories WHERE id =:categoryId", nativeQuery = true) + Category allCategoriesByName(@Param("categoryId") Long categoryId); + +} diff --git a/src/main/java/com/example/TaskManager/Repo/TaskRepo.java b/src/main/java/com/example/TaskManager/Repo/TaskRepo.java new file mode 100644 index 0000000..8c28291 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Repo/TaskRepo.java @@ -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 { + + @Query(value = """ + SELECT * FROM tasks + WHERE assigned_to = :userId + ORDER BY due_date DESC + """, nativeQuery = true) + Page 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 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 allTaskByCategory(Pageable pageable, + @Param("categoryId") int categoryId, + @Param("userId") Long userId); + + @Query(value = "SELECT * FROM tasks WHERE priority = ?", nativeQuery = true) + Page 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 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 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 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 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 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 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 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); + +} diff --git a/src/main/java/com/example/TaskManager/Repo/UserRepo.java b/src/main/java/com/example/TaskManager/Repo/UserRepo.java new file mode 100644 index 0000000..820056e --- /dev/null +++ b/src/main/java/com/example/TaskManager/Repo/UserRepo.java @@ -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{ + + + @Query(value = "SELECT * FROM users u", nativeQuery = true) + Page findAllUsers(Pageable pageable); + + Optional findByUsername(String username); + Optional findByEmail(String email); + +} diff --git a/src/main/java/com/example/TaskManager/Security/CustomUserDetailService.java b/src/main/java/com/example/TaskManager/Security/CustomUserDetailService.java new file mode 100644 index 0000000..2a356de --- /dev/null +++ b/src/main/java/com/example/TaskManager/Security/CustomUserDetailService.java @@ -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")); + } + +} \ No newline at end of file diff --git a/src/main/java/com/example/TaskManager/Security/CustomUserDetails.java b/src/main/java/com/example/TaskManager/Security/CustomUserDetails.java new file mode 100644 index 0000000..accca8d --- /dev/null +++ b/src/main/java/com/example/TaskManager/Security/CustomUserDetails.java @@ -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 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; + + } + +} diff --git a/src/main/java/com/example/TaskManager/Security/SecurityConfig.java b/src/main/java/com/example/TaskManager/Security/SecurityConfig.java new file mode 100644 index 0000000..99f60cf --- /dev/null +++ b/src/main/java/com/example/TaskManager/Security/SecurityConfig.java @@ -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(); + } + +} diff --git a/src/main/java/com/example/TaskManager/Services/CategoryService.java b/src/main/java/com/example/TaskManager/Services/CategoryService.java new file mode 100644 index 0000000..b4b0992 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Services/CategoryService.java @@ -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 findAll(){ + return categoryRepo.findAll(); + } + + public List findAllCategories(){ + return categoryRepo.findAllCategories(); + } + + +} diff --git a/src/main/java/com/example/TaskManager/Services/ScheduledService.java b/src/main/java/com/example/TaskManager/Services/ScheduledService.java new file mode 100644 index 0000000..0664eca --- /dev/null +++ b/src/main/java/com/example/TaskManager/Services/ScheduledService.java @@ -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"); + + } + +} diff --git a/src/main/java/com/example/TaskManager/Services/TaskService.java b/src/main/java/com/example/TaskManager/Services/TaskService.java new file mode 100644 index 0000000..9b91838 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Services/TaskService.java @@ -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 findAll() { + return taskRepo.findAll(); + } + + public List allOverDueTasks(Long assignedTo) { + return taskRepo.allTasksByStatus("Overdue", assignedTo); + } + + public List allTaskYears(Long assignedTo) { + return taskRepo.allTaskYears(assignedTo); + } + + public Page findAllTasks(int pageNo, int pageSize, Long assignedTo) { + Pageable pageable = PageRequest.of(pageNo, pageSize); + return taskRepo.findAllTasks(pageable, assignedTo); + } + + public Page findAllTasksWithCategories(int pageNo, int pageSize, Long assignedTo) { + Pageable pageable = PageRequest.of(pageNo, pageSize ); + return taskRepo.findAllTasksWithCategories(pageable, assignedTo); + } + + public Page 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 getDailyTasks(Long assignedTo) { + return taskRepo.getDailyTasks(assignedTo); + } + + public List allTasksByDate(LocalDate date, Long assignedTo) { + return taskRepo.allTasksByDate(date, assignedTo); + } + + public List 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 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); + } + +} diff --git a/src/main/java/com/example/TaskManager/Services/UserService.java b/src/main/java/com/example/TaskManager/Services/UserService.java new file mode 100644 index 0000000..b7c5f55 --- /dev/null +++ b/src/main/java/com/example/TaskManager/Services/UserService.java @@ -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 findAll(){ + return userRepo.findAll(); + } + + public Page 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); + + } + +} diff --git a/src/main/java/com/example/TaskManager/TaskManagerApplication.java b/src/main/java/com/example/TaskManager/TaskManagerApplication.java new file mode 100644 index 0000000..76caa85 --- /dev/null +++ b/src/main/java/com/example/TaskManager/TaskManagerApplication.java @@ -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); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..adc90ff --- /dev/null +++ b/src/main/resources/application.properties @@ -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 diff --git a/src/main/resources/static/images/landingPageTable.png b/src/main/resources/static/images/landingPageTable.png new file mode 100644 index 0000000..33ff899 Binary files /dev/null and b/src/main/resources/static/images/landingPageTable.png differ diff --git a/src/main/resources/templates/component/header.html b/src/main/resources/templates/component/header.html new file mode 100644 index 0000000..ff315b2 --- /dev/null +++ b/src/main/resources/templates/component/header.html @@ -0,0 +1,23 @@ + \ No newline at end of file diff --git a/src/main/resources/templates/component/nav-sidebar.html b/src/main/resources/templates/component/nav-sidebar.html new file mode 100644 index 0000000..4489c87 --- /dev/null +++ b/src/main/resources/templates/component/nav-sidebar.html @@ -0,0 +1,74 @@ +
+
+ +
+
+ Menu +
+ +
+ +
+ + + +
+
+
+ +
+
+
User Account
+ user@example.com +
+ + + +
+ +
+
+
+
+ \ No newline at end of file diff --git a/src/main/resources/templates/createTask.html b/src/main/resources/templates/createTask.html new file mode 100644 index 0000000..103cf65 --- /dev/null +++ b/src/main/resources/templates/createTask.html @@ -0,0 +1,102 @@ + + + + + + + Create Task + + + + + + + + +
+ +
+
+ +

Create Task

+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+
+
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/dashboard.html b/src/main/resources/templates/dashboard.html new file mode 100644 index 0000000..63d9f17 --- /dev/null +++ b/src/main/resources/templates/dashboard.html @@ -0,0 +1,504 @@ + + + + + + + + + + Dashboard + + + + + + + + + + + + +
+ +
+
+ +
+ +
+ +
+ + +
+
+
+

Daily Tasks

+
+
    +
  • + +
    + +
    + + Task Title
    + 00:00 +
    + Cat +
    +
  • +
  • + All caught up. +
  • +
+
+
+
+ +
+
+

Weekly Tasks

+
+
    +
  • +
    +
    + + Title
    + Date +
    + Cat +
    +
  • +
  • + Clear week ahead. +
  • +
+
+
+
+
+ +
+
+

Quarterly Overview

+ + + +
+
+ +
+
+
+
Q1
+
+
    + +
  • + Title +
    + Category + Date +
    +
  • + +
  • + No tasks +
  • +
+
+
+
+
+
+
+
+
+ +
+ +
+
+

Volume

+
+ +
+
+
+ +
+
+

Completion

+
+ +
+
+
+ +
+
+

Status Breakdown

+ +
+
+
+ Overdue + 0% +
+
+
+
+
+
+ +
+
+ Pending + 0% +
+
+
+
+
+ +
+
+ In Progress + 0% +
+
+
+
+
+ +
+
+ On Hold + 0% +
+
+
+
+
+ +
+
+ Completed + 0% +
+
+
+
+
+
+
+
+
+ +
+ + + +
+
+ +
+ + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/landing-page.html b/src/main/resources/templates/landing-page.html new file mode 100644 index 0000000..40f38ba --- /dev/null +++ b/src/main/resources/templates/landing-page.html @@ -0,0 +1,257 @@ + + + + + + + + TaskManager - Get Things Done, Retro Style. + + + + + + + + + + + +
+
+
+
+

+ STOP OVERTHINKING.
START DOING. +

+

+ A minimalist task manager with a retro soul. + Cut through the noise with a tool designed for focus, + not distraction. +

+ +
+ +
+
+
+
+
+
+
+
+
+ Image of Task Table +
+
+
+
+
+
+
+
+ + +
+
+
+
+

Retro Simple. Modern Power.

+

We stripped away the bloat. What's left is pure productivity engine.

+
+
+
+
+
+ +
+

HTMX Powered Speed

+

Experience single-page-application speed without the complex JavaScript frameworks. It just feels snappy.

+
+
+
+ +
+

Dynamic Filtering

+

Instantly slice and dice your tasks by status, priority, or search terms. Find what matters, fast.

+
+
+
+ +
+

Secure & Simple

+

Standard Spring Security keeps your data safe. No complex settings, just secure by default.

+
+
+
+
+ +
+
+

Ready to get organized the old-school way?

+ + Start Now - It's Free + +
+
+ + +
+
+

+ © 2025 TaskManager.
+ Built with Spring Boot & HTMX. +

+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..49c02d1 --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,77 @@ + + + + + + + Login + + + + + + + +
+
+

Task Manager

+
+
+ +
+
+

Login

+ + + + + + + + + + +
+ + + + + + + + +
+ + +
+ + + + + +
+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/profile.html b/src/main/resources/templates/profile.html new file mode 100644 index 0000000..9776462 --- /dev/null +++ b/src/main/resources/templates/profile.html @@ -0,0 +1,89 @@ + + + + + + + Profile + + + + + + + + + + +
+ +
+
+ +

Update Profile

+ +
+ + + + + + + + + + + + + + + + + + + + + +
+ + +
+ + +
+
+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/register.html b/src/main/resources/templates/register.html new file mode 100644 index 0000000..3898e6e --- /dev/null +++ b/src/main/resources/templates/register.html @@ -0,0 +1,60 @@ + + + + + + + CreateAccount + + + + + + + + + +
+
+

Task Manager

+
+
+ +
+
+

Create Account

+ +
+ + + + + + + + + + + + +
+ Login +
+ +
+
+
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/viewTasks.html b/src/main/resources/templates/viewTasks.html new file mode 100644 index 0000000..ede024f --- /dev/null +++ b/src/main/resources/templates/viewTasks.html @@ -0,0 +1,246 @@ + + + + + + + + + ViewTasks + + + + + + + + + +
+ +
+ +
+ +
+
+ + +
+
+ +
+
+ Loading... +
+
+ +
+
+ + + +
+
+
+
+ +
+
+ + + + + Page 1 + of 1 + + + +
+
+ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + +
CompletedTitleDescriptionStatusDue DateCategoryPriorityActions
Task TitleDesc + StatusDateCatHigh +
+ Edit + + +
+
+ + No tasks found. +
+
+
+
+ +
+ + + + + + + \ No newline at end of file diff --git a/src/test/java/com/example/TaskManager/TaskManagerApplicationTests.java b/src/test/java/com/example/TaskManager/TaskManagerApplicationTests.java new file mode 100644 index 0000000..b61ef4d --- /dev/null +++ b/src/test/java/com/example/TaskManager/TaskManagerApplicationTests.java @@ -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() { + } + +}