project scaffold
This commit is contained in:
commit
090f2888d0
52 changed files with 6011 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
.agents
|
||||
.DS_Store
|
||||
100
README.md
Normal file
100
README.md
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
# TimeTrack MVP
|
||||
|
||||
Minimal vertical slice for task management using:
|
||||
|
||||
- React + TypeScript + Tailwind CSS
|
||||
- Java + Quarkus
|
||||
- PostgreSQL
|
||||
- Keycloak prepared for future authentication
|
||||
|
||||
## Structure
|
||||
|
||||
- `frontend/`: Vite React app with task list, task creation, and status updates
|
||||
- `backend/`: Quarkus API with a simple Clean Architecture split
|
||||
- `docker-compose.yml`: local PostgreSQL and Keycloak services
|
||||
|
||||
## Run infrastructure
|
||||
|
||||
Start PostgreSQL and Keycloak:
|
||||
|
||||
```bash
|
||||
docker-compose up
|
||||
```
|
||||
|
||||
Services:
|
||||
|
||||
- PostgreSQL: `localhost:5432`
|
||||
- Keycloak: `http://localhost:8081`
|
||||
- Keycloak admin: `admin` / `admin`
|
||||
|
||||
## Run the backend
|
||||
|
||||
The backend uses the Maven wrapper generated by Quarkus.
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./mvnw quarkus:dev
|
||||
```
|
||||
|
||||
Default database settings already point to the local Docker PostgreSQL instance:
|
||||
|
||||
- database: `timetrack`
|
||||
- user: `timetrack`
|
||||
- password: `timetrack`
|
||||
|
||||
Main API endpoints:
|
||||
|
||||
- `POST /tasks`
|
||||
- `PATCH /tasks/{id}/status`
|
||||
- `GET /tasks`
|
||||
|
||||
Example create request:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/tasks \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"name":"Prepare sprint review","description":"Collect release notes"}'
|
||||
```
|
||||
|
||||
## Run the frontend
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
|
||||
The Vite dev server runs on `http://localhost:5173` and proxies `/api/*` to the backend on `http://localhost:8080`.
|
||||
|
||||
If needed, you can override the API target with `VITE_API_URL`.
|
||||
|
||||
## Architecture notes
|
||||
|
||||
The backend keeps the vertical slice intentionally small:
|
||||
|
||||
- `domain`: task entity and status enum
|
||||
- `application`: use cases and repository port
|
||||
- `adapter/in`: REST API
|
||||
- `adapter/out`: PostgreSQL persistence with Panache
|
||||
|
||||
Important pragmatic decisions:
|
||||
|
||||
- UUIDs are generated in the application layer to keep the slice simple.
|
||||
- Flyway owns schema creation with a single migration.
|
||||
- Keycloak is available locally now but not integrated into the app yet.
|
||||
|
||||
## Verification
|
||||
|
||||
Backend:
|
||||
|
||||
```bash
|
||||
cd backend
|
||||
./mvnw test
|
||||
```
|
||||
|
||||
Frontend:
|
||||
|
||||
```bash
|
||||
cd frontend
|
||||
npm run build
|
||||
```
|
||||
5
backend/.dockerignore
Normal file
5
backend/.dockerignore
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
*
|
||||
!target/*-runner
|
||||
!target/*-runner.jar
|
||||
!target/lib/*
|
||||
!target/quarkus-app/*
|
||||
45
backend/.gitignore
vendored
Normal file
45
backend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
#Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
release.properties
|
||||
.flattened-pom.xml
|
||||
|
||||
# Eclipse
|
||||
.project
|
||||
.classpath
|
||||
.settings/
|
||||
bin/
|
||||
|
||||
# IntelliJ
|
||||
.idea
|
||||
*.ipr
|
||||
*.iml
|
||||
*.iws
|
||||
|
||||
# NetBeans
|
||||
nb-configuration.xml
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode
|
||||
.factorypath
|
||||
|
||||
# OSX
|
||||
.DS_Store
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# patch
|
||||
*.orig
|
||||
*.rej
|
||||
|
||||
# Local environment
|
||||
.env
|
||||
|
||||
# Plugin directory
|
||||
/.quarkus/cli/plugins/
|
||||
# TLS Certificates
|
||||
.certs/
|
||||
4
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
4
backend/.mvn/wrapper/maven-wrapper.properties
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
wrapperVersion=3.3.4
|
||||
distributionType=only-script
|
||||
distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.12/apache-maven-3.9.12-bin.zip
|
||||
distributionSha256Sum=305773a68d6ddfd413df58c82b3f8050e89778e777f3a745c8e5b8cbea4018ef
|
||||
79
backend/README.md
Normal file
79
backend/README.md
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
# backend
|
||||
|
||||
This project uses Quarkus, the Supersonic Subatomic Java Framework.
|
||||
|
||||
If you want to learn more about Quarkus, please visit its website: <https://quarkus.io/>.
|
||||
|
||||
## Running the application in dev mode
|
||||
|
||||
You can run your application in dev mode that enables live coding using:
|
||||
|
||||
```shell script
|
||||
./mvnw quarkus:dev
|
||||
```
|
||||
|
||||
> **_NOTE:_** Quarkus now ships with a Dev UI, which is available in dev mode only at <http://localhost:8080/q/dev/>.
|
||||
|
||||
## Packaging and running the application
|
||||
|
||||
The application can be packaged using:
|
||||
|
||||
```shell script
|
||||
./mvnw package
|
||||
```
|
||||
|
||||
It produces the `quarkus-run.jar` file in the `target/quarkus-app/` directory.
|
||||
Be aware that it’s not an _über-jar_ as the dependencies are copied into the `target/quarkus-app/lib/` directory.
|
||||
|
||||
The application is now runnable using `java -jar target/quarkus-app/quarkus-run.jar`.
|
||||
|
||||
If you want to build an _über-jar_, execute the following command:
|
||||
|
||||
```shell script
|
||||
./mvnw package -Dquarkus.package.jar.type=uber-jar
|
||||
```
|
||||
|
||||
The application, packaged as an _über-jar_, is now runnable using `java -jar target/*-runner.jar`.
|
||||
|
||||
## Creating a native executable
|
||||
|
||||
You can create a native executable using:
|
||||
|
||||
```shell script
|
||||
./mvnw package -Dnative
|
||||
```
|
||||
|
||||
Or, if you don't have GraalVM installed, you can run the native executable build in a container using:
|
||||
|
||||
```shell script
|
||||
./mvnw package -Dnative -Dquarkus.native.container-build=true
|
||||
```
|
||||
|
||||
You can then execute your native executable with: `./target/backend-1.0.0-SNAPSHOT-runner`
|
||||
|
||||
If you want to learn more about building native executables, please consult <https://quarkus.io/guides/maven-tooling>.
|
||||
|
||||
## Related Guides
|
||||
|
||||
- REST ([guide](https://quarkus.io/guides/rest)): A Jakarta REST implementation utilizing build time processing and Vert.x. This extension is not compatible with the quarkus-resteasy extension, or any of the extensions that depend on it.
|
||||
- Flyway ([guide](https://quarkus.io/guides/flyway)): Handle your database schema migrations
|
||||
- Hibernate ORM with Panache ([guide](https://quarkus.io/guides/hibernate-orm-panache)): Simplify your persistence code for Hibernate ORM via the active record or the repository pattern
|
||||
- JDBC Driver - PostgreSQL ([guide](https://quarkus.io/guides/datasource)): Connect to the PostgreSQL database via JDBC
|
||||
|
||||
## Provided Code
|
||||
|
||||
### Hibernate ORM
|
||||
|
||||
Create your first JPA entity
|
||||
|
||||
[Related guide section...](https://quarkus.io/guides/hibernate-orm)
|
||||
|
||||
|
||||
[Related Hibernate with Panache section...](https://quarkus.io/guides/hibernate-orm-panache)
|
||||
|
||||
|
||||
### REST
|
||||
|
||||
Easily start your REST Web Services
|
||||
|
||||
[Related guide section...](https://quarkus.io/guides/getting-started-reactive#reactive-jax-rs-resources)
|
||||
295
backend/mvnw
vendored
Executable file
295
backend/mvnw
vendored
Executable file
|
|
@ -0,0 +1,295 @@
|
|||
#!/bin/sh
|
||||
# ----------------------------------------------------------------------------
|
||||
# Licensed to the Apache Software Foundation (ASF) under one
|
||||
# or more contributor license agreements. See the NOTICE file
|
||||
# distributed with this work for additional information
|
||||
# regarding copyright ownership. The ASF licenses this file
|
||||
# to you under the Apache License, Version 2.0 (the
|
||||
# "License"); you may not use this file except in compliance
|
||||
# with the License. You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing,
|
||||
# software distributed under the License is distributed on an
|
||||
# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
# KIND, either express or implied. See the License for the
|
||||
# specific language governing permissions and limitations
|
||||
# under the License.
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
#
|
||||
# Optional ENV vars
|
||||
# -----------------
|
||||
# JAVA_HOME - location of a JDK home dir, required when download maven via java source
|
||||
# MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
# MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
# MVNW_VERBOSE - true: enable verbose log; debug: trace the mvnw script; others: silence the output
|
||||
# ----------------------------------------------------------------------------
|
||||
|
||||
set -euf
|
||||
[ "${MVNW_VERBOSE-}" != debug ] || set -x
|
||||
|
||||
# OS specific support.
|
||||
native_path() { printf %s\\n "$1"; }
|
||||
case "$(uname)" in
|
||||
CYGWIN* | MINGW*)
|
||||
[ -z "${JAVA_HOME-}" ] || JAVA_HOME="$(cygpath --unix "$JAVA_HOME")"
|
||||
native_path() { cygpath --path --windows "$1"; }
|
||||
;;
|
||||
esac
|
||||
|
||||
# set JAVACMD and JAVACCMD
|
||||
set_java_home() {
|
||||
# For Cygwin and MinGW, ensure paths are in Unix format before anything is touched
|
||||
if [ -n "${JAVA_HOME-}" ]; then
|
||||
if [ -x "$JAVA_HOME/jre/sh/java" ]; then
|
||||
# IBM's JDK on AIX uses strange locations for the executables
|
||||
JAVACMD="$JAVA_HOME/jre/sh/java"
|
||||
JAVACCMD="$JAVA_HOME/jre/sh/javac"
|
||||
else
|
||||
JAVACMD="$JAVA_HOME/bin/java"
|
||||
JAVACCMD="$JAVA_HOME/bin/javac"
|
||||
|
||||
if [ ! -x "$JAVACMD" ] || [ ! -x "$JAVACCMD" ]; then
|
||||
echo "The JAVA_HOME environment variable is not defined correctly, so mvnw cannot run." >&2
|
||||
echo "JAVA_HOME is set to \"$JAVA_HOME\", but \"\$JAVA_HOME/bin/java\" or \"\$JAVA_HOME/bin/javac\" does not exist." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
else
|
||||
JAVACMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v java
|
||||
)" || :
|
||||
JAVACCMD="$(
|
||||
'set' +e
|
||||
'unset' -f command 2>/dev/null
|
||||
'command' -v javac
|
||||
)" || :
|
||||
|
||||
if [ ! -x "${JAVACMD-}" ] || [ ! -x "${JAVACCMD-}" ]; then
|
||||
echo "The java/javac command does not exist in PATH nor is JAVA_HOME set, so mvnw cannot run." >&2
|
||||
return 1
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# hash string like Java String::hashCode
|
||||
hash_string() {
|
||||
str="${1:-}" h=0
|
||||
while [ -n "$str" ]; do
|
||||
char="${str%"${str#?}"}"
|
||||
h=$(((h * 31 + $(LC_CTYPE=C printf %d "'$char")) % 4294967296))
|
||||
str="${str#?}"
|
||||
done
|
||||
printf %x\\n $h
|
||||
}
|
||||
|
||||
verbose() { :; }
|
||||
[ "${MVNW_VERBOSE-}" != true ] || verbose() { printf %s\\n "${1-}"; }
|
||||
|
||||
die() {
|
||||
printf %s\\n "$1" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
trim() {
|
||||
# MWRAPPER-139:
|
||||
# Trims trailing and leading whitespace, carriage returns, tabs, and linefeeds.
|
||||
# Needed for removing poorly interpreted newline sequences when running in more
|
||||
# exotic environments such as mingw bash on Windows.
|
||||
printf "%s" "${1}" | tr -d '[:space:]'
|
||||
}
|
||||
|
||||
scriptDir="$(dirname "$0")"
|
||||
scriptName="$(basename "$0")"
|
||||
|
||||
# parse distributionUrl and optional distributionSha256Sum, requires .mvn/wrapper/maven-wrapper.properties
|
||||
while IFS="=" read -r key value; do
|
||||
case "${key-}" in
|
||||
distributionUrl) distributionUrl=$(trim "${value-}") ;;
|
||||
distributionSha256Sum) distributionSha256Sum=$(trim "${value-}") ;;
|
||||
esac
|
||||
done <"$scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
|
||||
case "${distributionUrl##*/}" in
|
||||
maven-mvnd-*bin.*)
|
||||
MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/
|
||||
case "${PROCESSOR_ARCHITECTURE-}${PROCESSOR_ARCHITEW6432-}:$(uname -a)" in
|
||||
*AMD64:CYGWIN* | *AMD64:MINGW*) distributionPlatform=windows-amd64 ;;
|
||||
:Darwin*x86_64) distributionPlatform=darwin-amd64 ;;
|
||||
:Darwin*arm64) distributionPlatform=darwin-aarch64 ;;
|
||||
:Linux*x86_64*) distributionPlatform=linux-amd64 ;;
|
||||
*)
|
||||
echo "Cannot detect native platform for mvnd on $(uname)-$(uname -m), use pure java version" >&2
|
||||
distributionPlatform=linux-amd64
|
||||
;;
|
||||
esac
|
||||
distributionUrl="${distributionUrl%-bin.*}-$distributionPlatform.zip"
|
||||
;;
|
||||
maven-mvnd-*) MVN_CMD=mvnd.sh _MVNW_REPO_PATTERN=/maven/mvnd/ ;;
|
||||
*) MVN_CMD="mvn${scriptName#mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;;
|
||||
esac
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
distributionUrlNameMain="${distributionUrlName%.*}"
|
||||
distributionUrlNameMain="${distributionUrlNameMain%-bin}"
|
||||
MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}"
|
||||
MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")"
|
||||
|
||||
exec_maven() {
|
||||
unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || :
|
||||
exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD"
|
||||
}
|
||||
|
||||
if [ -d "$MAVEN_HOME" ]; then
|
||||
verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
exec_maven "$@"
|
||||
fi
|
||||
|
||||
case "${distributionUrl-}" in
|
||||
*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;;
|
||||
*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;;
|
||||
esac
|
||||
|
||||
# prepare tmp dir
|
||||
if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then
|
||||
clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; }
|
||||
trap clean HUP INT TERM EXIT
|
||||
else
|
||||
die "cannot create temp dir"
|
||||
fi
|
||||
|
||||
mkdir -p -- "${MAVEN_HOME%/*}"
|
||||
|
||||
# Download and Install Apache Maven
|
||||
verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
verbose "Downloading from: $distributionUrl"
|
||||
verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
# select .zip or .tar.gz
|
||||
if ! command -v unzip >/dev/null; then
|
||||
distributionUrl="${distributionUrl%.zip}.tar.gz"
|
||||
distributionUrlName="${distributionUrl##*/}"
|
||||
fi
|
||||
|
||||
# verbose opt
|
||||
__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR=''
|
||||
[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v
|
||||
|
||||
# normalize http auth
|
||||
case "${MVNW_PASSWORD:+has-password}" in
|
||||
'') MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;;
|
||||
esac
|
||||
|
||||
if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then
|
||||
verbose "Found wget ... using wget"
|
||||
wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl"
|
||||
elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then
|
||||
verbose "Found curl ... using curl"
|
||||
curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl"
|
||||
elif set_java_home; then
|
||||
verbose "Falling back to use Java to download"
|
||||
javaSource="$TMP_DOWNLOAD_DIR/Downloader.java"
|
||||
targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
cat >"$javaSource" <<-END
|
||||
public class Downloader extends java.net.Authenticator
|
||||
{
|
||||
protected java.net.PasswordAuthentication getPasswordAuthentication()
|
||||
{
|
||||
return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() );
|
||||
}
|
||||
public static void main( String[] args ) throws Exception
|
||||
{
|
||||
setDefault( new Downloader() );
|
||||
java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() );
|
||||
}
|
||||
}
|
||||
END
|
||||
# For Cygwin/MinGW, switch paths to Windows format before running javac and java
|
||||
verbose " - Compiling Downloader.java ..."
|
||||
"$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java"
|
||||
verbose " - Running Downloader.java ..."
|
||||
"$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")"
|
||||
fi
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
if [ -n "${distributionSha256Sum-}" ]; then
|
||||
distributionSha256Result=false
|
||||
if [ "$MVN_CMD" = mvnd.sh ]; then
|
||||
echo "Checksum validation is not supported for maven-mvnd." >&2
|
||||
echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
elif command -v sha256sum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c - >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
elif command -v shasum >/dev/null; then
|
||||
if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then
|
||||
distributionSha256Result=true
|
||||
fi
|
||||
else
|
||||
echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2
|
||||
echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ $distributionSha256Result = false ]; then
|
||||
echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2
|
||||
echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# unzip and move
|
||||
if command -v unzip >/dev/null; then
|
||||
unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip"
|
||||
else
|
||||
tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar"
|
||||
fi
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
actualDistributionDir=""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
if [ -d "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" ]; then
|
||||
if [ -f "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$distributionUrlNameMain"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
# enable globbing to iterate over items
|
||||
set +f
|
||||
for dir in "$TMP_DOWNLOAD_DIR"/*; do
|
||||
if [ -d "$dir" ]; then
|
||||
if [ -f "$dir/bin/$MVN_CMD" ]; then
|
||||
actualDistributionDir="$(basename "$dir")"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
set -f
|
||||
fi
|
||||
|
||||
if [ -z "$actualDistributionDir" ]; then
|
||||
verbose "Contents of $TMP_DOWNLOAD_DIR:"
|
||||
verbose "$(ls -la "$TMP_DOWNLOAD_DIR")"
|
||||
die "Could not find Maven distribution directory in extracted archive"
|
||||
fi
|
||||
|
||||
verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$actualDistributionDir/mvnw.url"
|
||||
mv -- "$TMP_DOWNLOAD_DIR/$actualDistributionDir" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME"
|
||||
|
||||
clean || :
|
||||
exec_maven "$@"
|
||||
189
backend/mvnw.cmd
vendored
Executable file
189
backend/mvnw.cmd
vendored
Executable file
|
|
@ -0,0 +1,189 @@
|
|||
<# : batch portion
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Licensed to the Apache Software Foundation (ASF) under one
|
||||
@REM or more contributor license agreements. See the NOTICE file
|
||||
@REM distributed with this work for additional information
|
||||
@REM regarding copyright ownership. The ASF licenses this file
|
||||
@REM to you under the Apache License, Version 2.0 (the
|
||||
@REM "License"); you may not use this file except in compliance
|
||||
@REM with the License. You may obtain a copy of the License at
|
||||
@REM
|
||||
@REM http://www.apache.org/licenses/LICENSE-2.0
|
||||
@REM
|
||||
@REM Unless required by applicable law or agreed to in writing,
|
||||
@REM software distributed under the License is distributed on an
|
||||
@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
@REM KIND, either express or implied. See the License for the
|
||||
@REM specific language governing permissions and limitations
|
||||
@REM under the License.
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@REM ----------------------------------------------------------------------------
|
||||
@REM Apache Maven Wrapper startup batch script, version 3.3.4
|
||||
@REM
|
||||
@REM Optional ENV vars
|
||||
@REM MVNW_REPOURL - repo url base for downloading maven distribution
|
||||
@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven
|
||||
@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output
|
||||
@REM ----------------------------------------------------------------------------
|
||||
|
||||
@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0)
|
||||
@SET __MVNW_CMD__=
|
||||
@SET __MVNW_ERROR__=
|
||||
@SET __MVNW_PSMODULEP_SAVE=%PSModulePath%
|
||||
@SET PSModulePath=
|
||||
@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @(
|
||||
IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B)
|
||||
)
|
||||
@SET PSModulePath=%__MVNW_PSMODULEP_SAVE%
|
||||
@SET __MVNW_PSMODULEP_SAVE=
|
||||
@SET __MVNW_ARG0_NAME__=
|
||||
@SET MVNW_USERNAME=
|
||||
@SET MVNW_PASSWORD=
|
||||
@IF NOT "%__MVNW_CMD__%"=="" ("%__MVNW_CMD__%" %*)
|
||||
@echo Cannot start maven from wrapper >&2 && exit /b 1
|
||||
@GOTO :EOF
|
||||
: end batch / begin powershell #>
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
if ($env:MVNW_VERBOSE -eq "true") {
|
||||
$VerbosePreference = "Continue"
|
||||
}
|
||||
|
||||
# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties
|
||||
$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl
|
||||
if (!$distributionUrl) {
|
||||
Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties"
|
||||
}
|
||||
|
||||
switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) {
|
||||
"maven-mvnd-*" {
|
||||
$USE_MVND = $true
|
||||
$distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip"
|
||||
$MVN_CMD = "mvnd.cmd"
|
||||
break
|
||||
}
|
||||
default {
|
||||
$USE_MVND = $false
|
||||
$MVN_CMD = $script -replace '^mvnw','mvn'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
# apply MVNW_REPOURL and calculate MAVEN_HOME
|
||||
# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-<version>,maven-mvnd-<version>-<platform>}/<hash>
|
||||
if ($env:MVNW_REPOURL) {
|
||||
$MVNW_REPO_PATTERN = if ($USE_MVND -eq $False) { "/org/apache/maven/" } else { "/maven/mvnd/" }
|
||||
$distributionUrl = "$env:MVNW_REPOURL$MVNW_REPO_PATTERN$($distributionUrl -replace "^.*$MVNW_REPO_PATTERN",'')"
|
||||
}
|
||||
$distributionUrlName = $distributionUrl -replace '^.*/',''
|
||||
$distributionUrlNameMain = $distributionUrlName -replace '\.[^.]*$','' -replace '-bin$',''
|
||||
|
||||
$MAVEN_M2_PATH = "$HOME/.m2"
|
||||
if ($env:MAVEN_USER_HOME) {
|
||||
$MAVEN_M2_PATH = "$env:MAVEN_USER_HOME"
|
||||
}
|
||||
|
||||
if (-not (Test-Path -Path $MAVEN_M2_PATH)) {
|
||||
New-Item -Path $MAVEN_M2_PATH -ItemType Directory | Out-Null
|
||||
}
|
||||
|
||||
$MAVEN_WRAPPER_DISTS = $null
|
||||
if ((Get-Item $MAVEN_M2_PATH).Target[0] -eq $null) {
|
||||
$MAVEN_WRAPPER_DISTS = "$MAVEN_M2_PATH/wrapper/dists"
|
||||
} else {
|
||||
$MAVEN_WRAPPER_DISTS = (Get-Item $MAVEN_M2_PATH).Target[0] + "/wrapper/dists"
|
||||
}
|
||||
|
||||
$MAVEN_HOME_PARENT = "$MAVEN_WRAPPER_DISTS/$distributionUrlNameMain"
|
||||
$MAVEN_HOME_NAME = ([System.Security.Cryptography.SHA256]::Create().ComputeHash([byte[]][char[]]$distributionUrl) | ForEach-Object {$_.ToString("x2")}) -join ''
|
||||
$MAVEN_HOME = "$MAVEN_HOME_PARENT/$MAVEN_HOME_NAME"
|
||||
|
||||
if (Test-Path -Path "$MAVEN_HOME" -PathType Container) {
|
||||
Write-Verbose "found existing MAVEN_HOME at $MAVEN_HOME"
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
exit $?
|
||||
}
|
||||
|
||||
if (! $distributionUrlNameMain -or ($distributionUrlName -eq $distributionUrlNameMain)) {
|
||||
Write-Error "distributionUrl is not valid, must end with *-bin.zip, but found $distributionUrl"
|
||||
}
|
||||
|
||||
# prepare tmp dir
|
||||
$TMP_DOWNLOAD_DIR_HOLDER = New-TemporaryFile
|
||||
$TMP_DOWNLOAD_DIR = New-Item -Itemtype Directory -Path "$TMP_DOWNLOAD_DIR_HOLDER.dir"
|
||||
$TMP_DOWNLOAD_DIR_HOLDER.Delete() | Out-Null
|
||||
trap {
|
||||
if ($TMP_DOWNLOAD_DIR.Exists) {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
}
|
||||
|
||||
New-Item -Itemtype Directory -Path "$MAVEN_HOME_PARENT" -Force | Out-Null
|
||||
|
||||
# Download and Install Apache Maven
|
||||
Write-Verbose "Couldn't find MAVEN_HOME, downloading and installing it ..."
|
||||
Write-Verbose "Downloading from: $distributionUrl"
|
||||
Write-Verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName"
|
||||
|
||||
$webclient = New-Object System.Net.WebClient
|
||||
if ($env:MVNW_USERNAME -and $env:MVNW_PASSWORD) {
|
||||
$webclient.Credentials = New-Object System.Net.NetworkCredential($env:MVNW_USERNAME, $env:MVNW_PASSWORD)
|
||||
}
|
||||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||||
$webclient.DownloadFile($distributionUrl, "$TMP_DOWNLOAD_DIR/$distributionUrlName") | Out-Null
|
||||
|
||||
# If specified, validate the SHA-256 sum of the Maven distribution zip file
|
||||
$distributionSha256Sum = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionSha256Sum
|
||||
if ($distributionSha256Sum) {
|
||||
if ($USE_MVND) {
|
||||
Write-Error "Checksum validation is not supported for maven-mvnd. `nPlease disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties."
|
||||
}
|
||||
Import-Module $PSHOME\Modules\Microsoft.PowerShell.Utility -Function Get-FileHash
|
||||
if ((Get-FileHash "$TMP_DOWNLOAD_DIR/$distributionUrlName" -Algorithm SHA256).Hash.ToLower() -ne $distributionSha256Sum) {
|
||||
Write-Error "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised. If you updated your Maven version, you need to update the specified distributionSha256Sum property."
|
||||
}
|
||||
}
|
||||
|
||||
# unzip and move
|
||||
Expand-Archive "$TMP_DOWNLOAD_DIR/$distributionUrlName" -DestinationPath "$TMP_DOWNLOAD_DIR" | Out-Null
|
||||
|
||||
# Find the actual extracted directory name (handles snapshots where filename != directory name)
|
||||
$actualDistributionDir = ""
|
||||
|
||||
# First try the expected directory name (for regular distributions)
|
||||
$expectedPath = Join-Path "$TMP_DOWNLOAD_DIR" "$distributionUrlNameMain"
|
||||
$expectedMvnPath = Join-Path "$expectedPath" "bin/$MVN_CMD"
|
||||
if ((Test-Path -Path $expectedPath -PathType Container) -and (Test-Path -Path $expectedMvnPath -PathType Leaf)) {
|
||||
$actualDistributionDir = $distributionUrlNameMain
|
||||
}
|
||||
|
||||
# If not found, search for any directory with the Maven executable (for snapshots)
|
||||
if (!$actualDistributionDir) {
|
||||
Get-ChildItem -Path "$TMP_DOWNLOAD_DIR" -Directory | ForEach-Object {
|
||||
$testPath = Join-Path $_.FullName "bin/$MVN_CMD"
|
||||
if (Test-Path -Path $testPath -PathType Leaf) {
|
||||
$actualDistributionDir = $_.Name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$actualDistributionDir) {
|
||||
Write-Error "Could not find Maven distribution directory in extracted archive"
|
||||
}
|
||||
|
||||
Write-Verbose "Found extracted Maven distribution directory: $actualDistributionDir"
|
||||
Rename-Item -Path "$TMP_DOWNLOAD_DIR/$actualDistributionDir" -NewName $MAVEN_HOME_NAME | Out-Null
|
||||
try {
|
||||
Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null
|
||||
} catch {
|
||||
if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) {
|
||||
Write-Error "fail to move MAVEN_HOME"
|
||||
}
|
||||
} finally {
|
||||
try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null }
|
||||
catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" }
|
||||
}
|
||||
|
||||
Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD"
|
||||
150
backend/pom.xml
Normal file
150
backend/pom.xml
Normal file
|
|
@ -0,0 +1,150 @@
|
|||
<?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>
|
||||
<groupId>net.beetechgroup.beetask</groupId>
|
||||
<artifactId>backend</artifactId>
|
||||
<version>1.0.0-SNAPSHOT</version>
|
||||
<packaging>quarkus</packaging>
|
||||
|
||||
<properties>
|
||||
<compiler-plugin.version>3.15.0</compiler-plugin.version>
|
||||
<maven.compiler.release>17</maven.compiler.release>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||
<quarkus.platform.version>3.34.5</quarkus.platform.version>
|
||||
<skipITs>true</skipITs>
|
||||
<surefire-plugin.version>3.5.4</surefire-plugin.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>${quarkus.platform.artifact-id}</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-openapi</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-flyway</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-h2</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-validator</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-arc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.rest-assured</groupId>
|
||||
<artifactId>rest-assured</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<version>${quarkus.platform.version}</version>
|
||||
<extensions>true</extensions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>${compiler-plugin.version}</version>
|
||||
<configuration>
|
||||
<parameters>true</parameters>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<configuration>
|
||||
<argLine>@{argLine}</argLine>
|
||||
<systemPropertyVariables>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>${surefire-plugin.version}</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>integration-test</goal>
|
||||
<goal>verify</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
<configuration>
|
||||
<argLine>@{argLine}</argLine>
|
||||
<systemPropertyVariables>
|
||||
<native.image.path>${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
||||
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||
<maven.home>${maven.home}</maven.home>
|
||||
</systemPropertyVariables>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>native</id>
|
||||
<activation>
|
||||
<property>
|
||||
<name>native</name>
|
||||
</property>
|
||||
</activation>
|
||||
<properties>
|
||||
<quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
|
||||
<skipITs>false</skipITs>
|
||||
<quarkus.native.enabled>true</quarkus.native.enabled>
|
||||
</properties>
|
||||
</profile>
|
||||
</profiles>
|
||||
</project>
|
||||
100
backend/src/main/docker/Dockerfile.jvm
Normal file
100
backend/src/main/docker/Dockerfile.jvm
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./mvnw package
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.jvm -t quarkus/backend-jvm .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/backend-jvm
|
||||
#
|
||||
# If you want to include the debug port into your docker image
|
||||
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
|
||||
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
|
||||
# when running the container
|
||||
#
|
||||
# Then run the container using :
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/backend-jvm
|
||||
#
|
||||
# This image uses the `run-java.sh` script to run the application.
|
||||
# This scripts computes the command line to execute your Java application, and
|
||||
# includes memory/GC tuning.
|
||||
# You can configure the behavior using the following environment properties:
|
||||
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
|
||||
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
|
||||
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
|
||||
# in JAVA_OPTS (example: "-Dsome.property=foo")
|
||||
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
|
||||
# used to calculate a default maximal heap memory based on a containers restriction.
|
||||
# If used in a container without any memory constraints for the container then this
|
||||
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
|
||||
# of the container available memory as set here. The default is `50` which means 50%
|
||||
# of the available memory is used as an upper boundary. You can skip this mechanism by
|
||||
# setting this value to `0` in which case no `-Xmx` option is added.
|
||||
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
|
||||
# is used to calculate a default initial heap memory based on the maximum heap memory.
|
||||
# If used in a container without any memory constraints for the container then this
|
||||
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
|
||||
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
|
||||
# is used as the initial heap size. You can skip this mechanism by setting this value
|
||||
# to `0` in which case no `-Xms` option is added (example: "25")
|
||||
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
|
||||
# This is used to calculate the maximum value of the initial heap memory. If used in
|
||||
# a container without any memory constraints for the container then this option has
|
||||
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
|
||||
# here. The default is 4096MB which means the calculated value of `-Xms` never will
|
||||
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
|
||||
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
|
||||
# when things are happening. This option, if set to true, will set
|
||||
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
|
||||
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
|
||||
# true").
|
||||
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
|
||||
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
|
||||
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
|
||||
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
|
||||
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
|
||||
# (example: "20")
|
||||
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
|
||||
# (example: "40")
|
||||
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
|
||||
# (example: "4")
|
||||
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
|
||||
# previous GC times. (example: "90")
|
||||
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
|
||||
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
|
||||
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
|
||||
# contain the necessary JRE command-line options to specify the required GC, which
|
||||
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
|
||||
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
|
||||
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
|
||||
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
|
||||
# accessed directly. (example: "foo.example.com,bar.example.com")
|
||||
#
|
||||
# You can find more information about the UBI base runtime images and their configuration here:
|
||||
# https://rh-openjdk.github.io/redhat-openjdk-containers/
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi9/openjdk-25-runtime:1.24
|
||||
|
||||
ENV LANGUAGE='en_US:en'
|
||||
|
||||
|
||||
# We make four distinct layers so if there are application changes the library layers can be re-used
|
||||
COPY --chown=185 target/quarkus-app/lib/ /deployments/lib/
|
||||
COPY --chown=185 target/quarkus-app/*.jar /deployments/
|
||||
COPY --chown=185 target/quarkus-app/app/ /deployments/app/
|
||||
COPY --chown=185 target/quarkus-app/quarkus/ /deployments/quarkus/
|
||||
|
||||
EXPOSE 8080
|
||||
USER 185
|
||||
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
||||
|
||||
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
|
||||
|
||||
96
backend/src/main/docker/Dockerfile.legacy-jar
Normal file
96
backend/src/main/docker/Dockerfile.legacy-jar
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in JVM mode
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./mvnw package -Dquarkus.package.jar.type=legacy-jar
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.legacy-jar -t quarkus/backend-legacy-jar .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/backend-legacy-jar
|
||||
#
|
||||
# If you want to include the debug port into your docker image
|
||||
# you will have to expose the debug port (default 5005 being the default) like this : EXPOSE 8080 5005.
|
||||
# Additionally you will have to set -e JAVA_DEBUG=true and -e JAVA_DEBUG_PORT=*:5005
|
||||
# when running the container
|
||||
#
|
||||
# Then run the container using :
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/backend-legacy-jar
|
||||
#
|
||||
# This image uses the `run-java.sh` script to run the application.
|
||||
# This scripts computes the command line to execute your Java application, and
|
||||
# includes memory/GC tuning.
|
||||
# You can configure the behavior using the following environment properties:
|
||||
# - JAVA_OPTS: JVM options passed to the `java` command (example: "-verbose:class") - Be aware that this will override
|
||||
# the default JVM options, use `JAVA_OPTS_APPEND` to append options
|
||||
# - JAVA_OPTS_APPEND: User specified Java options to be appended to generated options
|
||||
# in JAVA_OPTS (example: "-Dsome.property=foo")
|
||||
# - JAVA_MAX_MEM_RATIO: Is used when no `-Xmx` option is given in JAVA_OPTS. This is
|
||||
# used to calculate a default maximal heap memory based on a containers restriction.
|
||||
# If used in a container without any memory constraints for the container then this
|
||||
# option has no effect. If there is a memory constraint then `-Xmx` is set to a ratio
|
||||
# of the container available memory as set here. The default is `50` which means 50%
|
||||
# of the available memory is used as an upper boundary. You can skip this mechanism by
|
||||
# setting this value to `0` in which case no `-Xmx` option is added.
|
||||
# - JAVA_INITIAL_MEM_RATIO: Is used when no `-Xms` option is given in JAVA_OPTS. This
|
||||
# is used to calculate a default initial heap memory based on the maximum heap memory.
|
||||
# If used in a container without any memory constraints for the container then this
|
||||
# option has no effect. If there is a memory constraint then `-Xms` is set to a ratio
|
||||
# of the `-Xmx` memory as set here. The default is `25` which means 25% of the `-Xmx`
|
||||
# is used as the initial heap size. You can skip this mechanism by setting this value
|
||||
# to `0` in which case no `-Xms` option is added (example: "25")
|
||||
# - JAVA_MAX_INITIAL_MEM: Is used when no `-Xms` option is given in JAVA_OPTS.
|
||||
# This is used to calculate the maximum value of the initial heap memory. If used in
|
||||
# a container without any memory constraints for the container then this option has
|
||||
# no effect. If there is a memory constraint then `-Xms` is limited to the value set
|
||||
# here. The default is 4096MB which means the calculated value of `-Xms` never will
|
||||
# be greater than 4096MB. The value of this variable is expressed in MB (example: "4096")
|
||||
# - JAVA_DIAGNOSTICS: Set this to get some diagnostics information to standard output
|
||||
# when things are happening. This option, if set to true, will set
|
||||
# `-XX:+UnlockDiagnosticVMOptions`. Disabled by default (example: "true").
|
||||
# - JAVA_DEBUG: If set remote debugging will be switched on. Disabled by default (example:
|
||||
# true").
|
||||
# - JAVA_DEBUG_PORT: Port used for remote debugging. Defaults to 5005 (example: "8787").
|
||||
# - CONTAINER_CORE_LIMIT: A calculated core limit as described in
|
||||
# https://www.kernel.org/doc/Documentation/scheduler/sched-bwc.txt. (example: "2")
|
||||
# - CONTAINER_MAX_MEMORY: Memory limit given to the container (example: "1024").
|
||||
# - GC_MIN_HEAP_FREE_RATIO: Minimum percentage of heap free after GC to avoid expansion.
|
||||
# (example: "20")
|
||||
# - GC_MAX_HEAP_FREE_RATIO: Maximum percentage of heap free after GC to avoid shrinking.
|
||||
# (example: "40")
|
||||
# - GC_TIME_RATIO: Specifies the ratio of the time spent outside the garbage collection.
|
||||
# (example: "4")
|
||||
# - GC_ADAPTIVE_SIZE_POLICY_WEIGHT: The weighting given to the current GC time versus
|
||||
# previous GC times. (example: "90")
|
||||
# - GC_METASPACE_SIZE: The initial metaspace size. (example: "20")
|
||||
# - GC_MAX_METASPACE_SIZE: The maximum metaspace size. (example: "100")
|
||||
# - GC_CONTAINER_OPTIONS: Specify Java GC to use. The value of this variable should
|
||||
# contain the necessary JRE command-line options to specify the required GC, which
|
||||
# will override the default of `-XX:+UseParallelGC` (example: -XX:+UseG1GC).
|
||||
# - HTTPS_PROXY: The location of the https proxy. (example: "myuser@127.0.0.1:8080")
|
||||
# - HTTP_PROXY: The location of the http proxy. (example: "myuser@127.0.0.1:8080")
|
||||
# - NO_PROXY: A comma separated lists of hosts, IP addresses or domains that can be
|
||||
# accessed directly. (example: "foo.example.com,bar.example.com")
|
||||
#
|
||||
# You can find more information about the UBI base runtime images and their configuration here:
|
||||
# https://rh-openjdk.github.io/redhat-openjdk-containers/
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi9/openjdk-25-runtime:1.24
|
||||
|
||||
ENV LANGUAGE='en_US:en'
|
||||
|
||||
|
||||
COPY target/lib/* /deployments/lib/
|
||||
COPY target/*-runner.jar /deployments/quarkus-run.jar
|
||||
|
||||
EXPOSE 8080
|
||||
USER 185
|
||||
ENV JAVA_OPTS_APPEND="-Dquarkus.http.host=0.0.0.0 -Djava.util.logging.manager=org.jboss.logmanager.LogManager"
|
||||
ENV JAVA_APP_JAR="/deployments/quarkus-run.jar"
|
||||
|
||||
ENTRYPOINT [ "/opt/jboss/container/java/run/run-java.sh" ]
|
||||
29
backend/src/main/docker/Dockerfile.native
Normal file
29
backend/src/main/docker/Dockerfile.native
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./mvnw package -Dnative
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.native -t quarkus/backend .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/backend
|
||||
#
|
||||
# The ` registry.access.redhat.com/ubi9/ubi-minimal:9.7` base image is based on UBI 9.
|
||||
# To use UBI 8, switch to `quay.io/ubi8/ubi-minimal:8.10`.
|
||||
###
|
||||
FROM registry.access.redhat.com/ubi9/ubi-minimal:9.7
|
||||
WORKDIR /work/
|
||||
RUN chown 1001 /work \
|
||||
&& chmod "g+rwX" /work \
|
||||
&& chown 1001:root /work
|
||||
COPY --chown=1001:root --chmod=0755 target/*-runner /work/application
|
||||
|
||||
EXPOSE 8080
|
||||
USER 1001
|
||||
|
||||
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
|
||||
32
backend/src/main/docker/Dockerfile.native-micro
Normal file
32
backend/src/main/docker/Dockerfile.native-micro
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
####
|
||||
# This Dockerfile is used in order to build a container that runs the Quarkus application in native (no JVM) mode.
|
||||
# It uses a micro base image, tuned for Quarkus native executables.
|
||||
# It reduces the size of the resulting container image.
|
||||
# Check https://quarkus.io/guides/quarkus-runtime-base-image for further information about this image.
|
||||
#
|
||||
# Before building the container image run:
|
||||
#
|
||||
# ./mvnw package -Dnative
|
||||
#
|
||||
# Then, build the image with:
|
||||
#
|
||||
# docker build -f src/main/docker/Dockerfile.native-micro -t quarkus/backend .
|
||||
#
|
||||
# Then run the container using:
|
||||
#
|
||||
# docker run -i --rm -p 8080:8080 quarkus/backend
|
||||
#
|
||||
# The `quay.io/quarkus/ubi9-quarkus-micro-image:2.0` base image is based on UBI 9.
|
||||
# To use UBI 8, switch to `quay.io/quarkus/quarkus-micro-image:2.0`.
|
||||
###
|
||||
FROM quay.io/quarkus/ubi9-quarkus-micro-image:2.0
|
||||
WORKDIR /work/
|
||||
RUN chown 1001 /work \
|
||||
&& chmod "g+rwX" /work \
|
||||
&& chown 1001:root /work
|
||||
COPY --chown=1001:root --chmod=0755 target/*-runner /work/application
|
||||
|
||||
EXPOSE 8080
|
||||
USER 1001
|
||||
|
||||
ENTRYPOINT ["./application", "-Dquarkus.http.host=0.0.0.0"]
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
package net.beetechgroup.beetask.entities;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class Task {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String description;
|
||||
private String status;
|
||||
private String project;
|
||||
private List<String> activities;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getProject() {
|
||||
return project;
|
||||
}
|
||||
|
||||
public void setProject(String project) {
|
||||
this.project = project;
|
||||
}
|
||||
|
||||
public List<String> getActivities() {
|
||||
return activities;
|
||||
}
|
||||
|
||||
public void setActivities(List<String> activities) {
|
||||
this.activities = activities;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package net.beetechgroup.beetask.frameworks.config;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.inject.Produces;
|
||||
import net.beetechgroup.beetask.usecase.createtask.CreateTaskUseCase;
|
||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||
|
||||
@ApplicationScoped
|
||||
public class TaskUseCaseConfig {
|
||||
|
||||
@Produces
|
||||
public CreateTaskUseCase createTaskUseCase(TaskRepository taskRepository) {
|
||||
return new CreateTaskUseCase(taskRepository);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.entities;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.GeneratedValue;
|
||||
import jakarta.persistence.GenerationType;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
@Entity
|
||||
@Table(name = "tasks")
|
||||
public class TaskEntity {
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
private String title;
|
||||
private String description;
|
||||
private String status;
|
||||
private String project;
|
||||
@jakarta.persistence.ElementCollection
|
||||
@jakarta.persistence.CollectionTable(name = "task_activities", joinColumns = @jakarta.persistence.JoinColumn(name = "task_id"))
|
||||
@jakarta.persistence.Column(name = "activity")
|
||||
private List<String> activities;
|
||||
|
||||
public Long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(Long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public void setTitle(String title) {
|
||||
this.title = title;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public String getProject() {
|
||||
return project;
|
||||
}
|
||||
|
||||
public void setProject(String project) {
|
||||
this.project = project;
|
||||
}
|
||||
|
||||
public List<String> getActivities() {
|
||||
return activities;
|
||||
}
|
||||
|
||||
public void setActivities(List<String> activities) {
|
||||
this.activities = activities;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.mapper;
|
||||
|
||||
import net.beetechgroup.beetask.entities.Task;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity;
|
||||
|
||||
public class TaskMapper {
|
||||
|
||||
public static TaskEntity toEntity(Task task) {
|
||||
TaskEntity entity = new TaskEntity();
|
||||
entity.setId(task.getId());
|
||||
entity.setTitle(task.getTitle());
|
||||
entity.setDescription(task.getDescription());
|
||||
entity.setStatus(task.getStatus());
|
||||
entity.setProject(task.getProject());
|
||||
entity.setActivities(task.getActivities());
|
||||
return entity;
|
||||
}
|
||||
|
||||
public static Task toDomain(TaskEntity entity) {
|
||||
if (entity == null) return null;
|
||||
|
||||
Task task = new Task();
|
||||
task.setId(entity.getId());
|
||||
task.setTitle(entity.getTitle());
|
||||
task.setDescription(entity.getDescription());
|
||||
task.setStatus(entity.getStatus());
|
||||
task.setProject(entity.getProject());
|
||||
task.setActivities(entity.getActivities());
|
||||
|
||||
return task;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
package net.beetechgroup.beetask.frameworks.persistence.repository;
|
||||
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import net.beetechgroup.beetask.entities.Task;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.entities.TaskEntity;
|
||||
import net.beetechgroup.beetask.frameworks.persistence.mapper.TaskMapper;
|
||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||
|
||||
@ApplicationScoped
|
||||
public class TaskRepositoryImpl implements TaskRepository {
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Task save(Task task) {
|
||||
TaskEntity entity = TaskMapper.toEntity(task);
|
||||
em.persist(entity);
|
||||
return TaskMapper.toDomain(entity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
package net.beetechgroup.beetask.interfaceadapters.controllers;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.Consumes;
|
||||
import jakarta.ws.rs.POST;
|
||||
import jakarta.ws.rs.Path;
|
||||
import jakarta.ws.rs.Produces;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.eclipse.microprofile.openapi.annotations.Operation;
|
||||
import org.eclipse.microprofile.openapi.annotations.responses.APIResponse;
|
||||
import org.eclipse.microprofile.openapi.annotations.tags.Tag;
|
||||
import net.beetechgroup.beetask.entities.Task;
|
||||
import net.beetechgroup.beetask.interfaceadapters.dtos.TaskDTOMapper;
|
||||
import net.beetechgroup.beetask.interfaceadapters.dtos.TaskRequestDTO;
|
||||
import net.beetechgroup.beetask.interfaceadapters.dtos.TaskResponseDTO;
|
||||
import net.beetechgroup.beetask.usecase.createtask.CreateTaskUseCase;
|
||||
|
||||
@Path("/tasks")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
@Tag(name = "Tasks", description = "Task management operations")
|
||||
public class TaskController {
|
||||
|
||||
@Inject
|
||||
CreateTaskUseCase createTaskUseCase;
|
||||
|
||||
@POST
|
||||
@Operation(summary = "Create a new task", description = "Creates a new task in the system")
|
||||
@APIResponse(responseCode = "201", description = "Task created successfully")
|
||||
public TaskResponseDTO createTask(TaskRequestDTO request) {
|
||||
Task task = createTaskUseCase.execute(TaskDTOMapper.toInput(request));
|
||||
return TaskDTOMapper.toResponse(task);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package net.beetechgroup.beetask.interfaceadapters.dtos;
|
||||
|
||||
import net.beetechgroup.beetask.entities.Task;
|
||||
import net.beetechgroup.beetask.usecase.createtask.CreateTaskInput;
|
||||
|
||||
public class TaskDTOMapper {
|
||||
|
||||
public static CreateTaskInput toInput(TaskRequestDTO request) {
|
||||
return new CreateTaskInput(
|
||||
request.title(),
|
||||
request.description(),
|
||||
request.status(),
|
||||
request.project(),
|
||||
request.activities()
|
||||
);
|
||||
}
|
||||
|
||||
public static TaskResponseDTO toResponse(Task task) {
|
||||
return new TaskResponseDTO(
|
||||
task.getId(),
|
||||
task.getTitle(),
|
||||
task.getDescription(),
|
||||
task.getStatus(),
|
||||
task.getProject(),
|
||||
task.getActivities()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package net.beetechgroup.beetask.interfaceadapters.dtos;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TaskRequestDTO(
|
||||
String title,
|
||||
String description,
|
||||
String status,
|
||||
String project,
|
||||
List<String> activities) {
|
||||
}
|
||||
|
|
@ -0,0 +1,12 @@
|
|||
package net.beetechgroup.beetask.interfaceadapters.dtos;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record TaskResponseDTO(
|
||||
Long id,
|
||||
String title,
|
||||
String description,
|
||||
String status,
|
||||
String project,
|
||||
List<String> activities) {
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
package net.beetechgroup.beetask.usecase.createtask;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public record CreateTaskInput(
|
||||
String title,
|
||||
String description,
|
||||
String status,
|
||||
String project,
|
||||
List<String> activities) {
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
package net.beetechgroup.beetask.usecase.createtask;
|
||||
|
||||
public class CreateTaskOutput {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package net.beetechgroup.beetask.usecase.createtask;
|
||||
|
||||
import net.beetechgroup.beetask.entities.Task;
|
||||
import net.beetechgroup.beetask.usecase.repository.TaskRepository;
|
||||
|
||||
public class CreateTaskUseCase {
|
||||
private final TaskRepository taskRepository;
|
||||
|
||||
public CreateTaskUseCase(TaskRepository taskRepository) {
|
||||
this.taskRepository = taskRepository;
|
||||
}
|
||||
|
||||
public Task execute(CreateTaskInput input) {
|
||||
Task task = new Task();
|
||||
task.setTitle(input.title());
|
||||
task.setDescription(input.description());
|
||||
task.setStatus(input.status());
|
||||
task.setProject(input.project());
|
||||
task.setActivities(input.activities());
|
||||
return taskRepository.save(task);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package net.beetechgroup.beetask.usecase.repository;
|
||||
|
||||
import net.beetechgroup.beetask.entities.Task;
|
||||
|
||||
public interface TaskRepository {
|
||||
Task save(Task task);
|
||||
}
|
||||
20
backend/src/main/resources/application.properties
Normal file
20
backend/src/main/resources/application.properties
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
quarkus.http.port=8080
|
||||
quarkus.http.cors=true
|
||||
quarkus.http.cors.origins=http://localhost:5173
|
||||
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.username=${DB_USER:timetrack}
|
||||
quarkus.datasource.password=${DB_PASSWORD:timetrack}
|
||||
quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/timetrack}
|
||||
|
||||
%dev.quarkus.datasource.db-kind=h2
|
||||
%dev.quarkus.datasource.jdbc.url=jdbc:h2:mem:beetask;DB_CLOSE_DELAY=-1
|
||||
%dev.quarkus.hibernate-orm.database.generation=update
|
||||
|
||||
quarkus.flyway.migrate-at-start=false
|
||||
quarkus.hibernate-orm.database.generation=validate
|
||||
|
||||
# Swagger & OpenAPI
|
||||
quarkus.smallrye-openapi.path=/openapi
|
||||
quarkus.swagger-ui.path=/swagger
|
||||
quarkus.swagger-ui.always-include=true
|
||||
13
backend/src/main/resources/db/migration/V1__create_tasks.sql
Normal file
13
backend/src/main/resources/db/migration/V1__create_tasks.sql
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
CREATE TABLE tasks (
|
||||
id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
|
||||
title VARCHAR(255) NOT NULL,
|
||||
description VARCHAR(2000),
|
||||
status VARCHAR(32) NOT NULL,
|
||||
project VARCHAR(255)
|
||||
);
|
||||
|
||||
CREATE TABLE task_activities (
|
||||
task_id BIGINT NOT NULL,
|
||||
activity VARCHAR(255),
|
||||
CONSTRAINT fk_task_activities_task FOREIGN KEY (task_id) REFERENCES tasks (id)
|
||||
);
|
||||
6
backend/src/main/resources/import.sql
Normal file
6
backend/src/main/resources/import.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
-- This file allow to write SQL commands that will be emitted in test and dev.
|
||||
-- The commands are commented as their support depends of the database
|
||||
-- insert into myentity (id, field) values(1, 'field-1');
|
||||
-- insert into myentity (id, field) values(2, 'field-2');
|
||||
-- insert into myentity (id, field) values(3, 'field-3');
|
||||
-- alter sequence myentity_seq restart with 4;
|
||||
358
backlog.md
Normal file
358
backlog.md
Normal file
|
|
@ -0,0 +1,358 @@
|
|||
# 📋 Product Backlog — TimeTrack
|
||||
|
||||
---
|
||||
|
||||
## 🟢 EPIC 1 — Autenticação e Organização
|
||||
|
||||
### 🧩 US-001 — Criar organização
|
||||
|
||||
**Como** dono
|
||||
**Quero** criar uma organização
|
||||
**Para** gerenciar meu time
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Deve ser possível criar uma organização com nome
|
||||
* O criador deve automaticamente se tornar DONO
|
||||
* Organização deve ter ID único
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-002 — Entrar em uma organização
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** solicitar entrada em uma organização
|
||||
**Para** participar do time
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Usuário pode solicitar entrada
|
||||
* Solicitação fica pendente
|
||||
* Dono pode visualizar solicitações
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-003 — Aprovar solicitação
|
||||
|
||||
**Como** dono
|
||||
**Quero** aprovar/rejeitar solicitações
|
||||
**Para** controlar acesso
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Dono pode aprovar ou rejeitar
|
||||
* Usuário aprovado entra como colaborador
|
||||
* Status da solicitação é atualizado
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-004 — Gerenciar usuários
|
||||
|
||||
**Como** dono
|
||||
**Quero** adicionar gestores e colaboradores
|
||||
**Para** montar meu time
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Dono pode definir role (MANAGER / EMPLOYEE)
|
||||
* Usuário fica vinculado à organização
|
||||
|
||||
---
|
||||
|
||||
## 🟢 EPIC 2 — Projetos, Categorias e Estrutura
|
||||
|
||||
### 🧩 US-005 — Criar projeto
|
||||
|
||||
**Como** gestor ou dono
|
||||
**Quero** criar um projeto
|
||||
**Para** organizar tarefas
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Projeto deve ter nome
|
||||
* Projeto pertence a uma organização
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-006 — Associar gestores ao projeto
|
||||
|
||||
**Como** dono
|
||||
**Quero** definir gestores responsáveis por um projeto
|
||||
**Para** delegar acompanhamento
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Projeto pode ter múltiplos gestores
|
||||
* Apenas gestores da organização podem ser vinculados
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-007 — Criar categorias
|
||||
|
||||
**Como** gestor
|
||||
**Quero** criar categorias
|
||||
**Para** classificar tarefas
|
||||
|
||||
**Prioridade:** Alta
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Categoria tem nome
|
||||
* Categoria pertence à organização
|
||||
|
||||
---
|
||||
|
||||
## 🟢 EPIC 3 — Gestão de Tarefas
|
||||
|
||||
### 🧩 US-008 — Criar tarefa
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** criar uma tarefa
|
||||
**Para** organizar meu trabalho
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Deve conter nome, categoria, projeto
|
||||
* Status inicial: aberta
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-009 — Atualizar status da tarefa
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** atualizar o status
|
||||
**Para** refletir progresso
|
||||
|
||||
**Prioridade:** Alta
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Status: aberta, em andamento, concluída
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-010 — Listar tarefas
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** visualizar minhas tarefas
|
||||
**Para** acompanhar meu trabalho
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Listar tarefas do usuário
|
||||
* Filtro por status
|
||||
|
||||
---
|
||||
|
||||
## 🟢 EPIC 4 — Controle de Tempo
|
||||
|
||||
### 🧩 US-011 — Iniciar tarefa
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** iniciar uma tarefa
|
||||
**Para** registrar tempo
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Cria um TimeEntry com horário inicial
|
||||
* Apenas uma tarefa ativa por usuário
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-012 — Pausar tarefa
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** pausar uma tarefa
|
||||
**Para** interromper contagem
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Fecha o TimeEntry atual
|
||||
* Tarefa fica pausada
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-013 — Retomar tarefa
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** retomar uma tarefa
|
||||
**Para** continuar registro
|
||||
|
||||
**Prioridade:** Alta
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Novo TimeEntry é criado
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-014 — Finalizar tarefa
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** finalizar uma tarefa
|
||||
**Para** concluir trabalho
|
||||
|
||||
**Prioridade:** Alta
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Status muda para concluída
|
||||
* Não pode mais iniciar novamente
|
||||
|
||||
---
|
||||
|
||||
## 🟢 EPIC 5 — Dashboard do Colaborador
|
||||
|
||||
### 🧩 US-015 — Ver tarefas de ontem
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** ver tarefas de ontem
|
||||
**Para** revisar meu trabalho
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Lista tarefas com atividade no dia anterior
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-016 — Ver tarefas abertas
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** ver tarefas abertas
|
||||
**Para** planejar o dia
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Lista tarefas não concluídas
|
||||
|
||||
---
|
||||
|
||||
## 🟢 EPIC 6 — Relatórios do Colaborador
|
||||
|
||||
### 🧩 US-017 — Relatório mensal
|
||||
|
||||
**Como** colaborador
|
||||
**Quero** ver relatório mensal
|
||||
**Para** acompanhar meu desempenho
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Total de horas por mês
|
||||
* Agrupamento por categoria
|
||||
* Agrupamento por projeto
|
||||
|
||||
---
|
||||
|
||||
## 🟢 EPIC 7 — Monitoramento do Gestor
|
||||
|
||||
### 🧩 US-018 — Ver tempo por colaborador
|
||||
|
||||
**Como** gestor
|
||||
**Quero** ver tempo gasto por colaborador
|
||||
**Para** acompanhar produtividade
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Exibe horas por colaborador
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-019 — Ver tempo por projeto
|
||||
|
||||
**Como** gestor
|
||||
**Quero** ver tempo por projeto
|
||||
**Para** entender esforço
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Soma de horas por projeto
|
||||
|
||||
---
|
||||
|
||||
### 🧩 US-020 — Ver eficiência
|
||||
|
||||
**Como** gestor
|
||||
**Quero** ver eficiência
|
||||
**Para** avaliar performance
|
||||
|
||||
**Prioridade:** MVP
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Fórmula:
|
||||
|
||||
* horas trabalhadas / horas disponíveis
|
||||
* Visualização por dia, semana e mês
|
||||
|
||||
---
|
||||
|
||||
## 🟡 EPIC 8 — Configurações
|
||||
|
||||
### 🧩 US-021 — Definir horas disponíveis
|
||||
|
||||
**Como** dono/gestor
|
||||
**Quero** configurar horas disponíveis
|
||||
**Para** calcular eficiência
|
||||
|
||||
**Prioridade:** Média
|
||||
|
||||
**Critérios de Aceitação:**
|
||||
|
||||
* Pode ser por usuário ou organização
|
||||
|
||||
---
|
||||
|
||||
## 🔵 Sugestão de MVP (Sprint inicial)
|
||||
|
||||
Entregar primeiro:
|
||||
|
||||
* US-001 a US-006 (estrutura)
|
||||
* US-008, 010 (tarefas)
|
||||
* US-011, 012 (tempo)
|
||||
* US-015, 016 (dashboard básico)
|
||||
* US-017 (relatório simples)
|
||||
* US-018, 019, 020 (gestor)
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Observação importante
|
||||
|
||||
Esse backlog já está pronto para:
|
||||
|
||||
* Jira
|
||||
* Notion
|
||||
* GitHub Projects
|
||||
* Ou alimentar uma IA (Cursor/Cline)
|
||||
|
||||
---
|
||||
|
||||
40
docker-compose.yml
Normal file
40
docker-compose.yml
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:18.3
|
||||
container_name: timetrack-postgres
|
||||
environment:
|
||||
POSTGRES_USER: timetrack
|
||||
POSTGRES_PASSWORD: timetrack
|
||||
POSTGRES_DB: timetrack
|
||||
ports:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql
|
||||
- ./docker/postgres/init:/docker-entrypoint-initdb.d
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U timetrack"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
keycloak:
|
||||
image: quay.io/keycloak/keycloak:26.6.1
|
||||
container_name: timetrack-keycloak
|
||||
command: start-dev
|
||||
environment:
|
||||
KEYCLOAK_ADMIN: admin
|
||||
KEYCLOAK_ADMIN_PASSWORD: admin
|
||||
KC_DB: postgres
|
||||
KC_DB_URL_HOST: postgres
|
||||
KC_DB_URL_PORT: 5432
|
||||
KC_DB_URL_DATABASE: keycloak
|
||||
KC_DB_USERNAME: timetrack
|
||||
KC_DB_PASSWORD: timetrack
|
||||
ports:
|
||||
- "8081:8080"
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
1
docker/postgres/init/01-create-keycloak-database.sql
Normal file
1
docker/postgres/init/01-create-keycloak-database.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
CREATE DATABASE keycloak;
|
||||
24
frontend/.gitignore
vendored
Normal file
24
frontend/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
73
frontend/README.md
Normal file
73
frontend/README.md
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
# React + TypeScript + Vite
|
||||
|
||||
This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
|
||||
|
||||
Currently, two official plugins are available:
|
||||
|
||||
- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Oxc](https://oxc.rs)
|
||||
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
||||
|
||||
## React Compiler
|
||||
|
||||
The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation).
|
||||
|
||||
## Expanding the ESLint configuration
|
||||
|
||||
If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules:
|
||||
|
||||
```js
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
|
||||
// Remove tseslint.configs.recommended and replace with this
|
||||
tseslint.configs.recommendedTypeChecked,
|
||||
// Alternatively, use this for stricter rules
|
||||
tseslint.configs.strictTypeChecked,
|
||||
// Optionally, add this for stylistic rules
|
||||
tseslint.configs.stylisticTypeChecked,
|
||||
|
||||
// Other configs...
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
|
||||
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
|
||||
|
||||
```js
|
||||
// eslint.config.js
|
||||
import reactX from 'eslint-plugin-react-x'
|
||||
import reactDom from 'eslint-plugin-react-dom'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
// Other configs...
|
||||
// Enable lint rules for React
|
||||
reactX.configs['recommended-typescript'],
|
||||
// Enable lint rules for React DOM
|
||||
reactDom.configs.recommended,
|
||||
],
|
||||
languageOptions: {
|
||||
parserOptions: {
|
||||
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||
tsconfigRootDir: import.meta.dirname,
|
||||
},
|
||||
// other options...
|
||||
},
|
||||
},
|
||||
])
|
||||
```
|
||||
23
frontend/eslint.config.js
Normal file
23
frontend/eslint.config.js
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
import js from '@eslint/js'
|
||||
import globals from 'globals'
|
||||
import reactHooks from 'eslint-plugin-react-hooks'
|
||||
import reactRefresh from 'eslint-plugin-react-refresh'
|
||||
import tseslint from 'typescript-eslint'
|
||||
import { defineConfig, globalIgnores } from 'eslint/config'
|
||||
|
||||
export default defineConfig([
|
||||
globalIgnores(['dist']),
|
||||
{
|
||||
files: ['**/*.{ts,tsx}'],
|
||||
extends: [
|
||||
js.configs.recommended,
|
||||
tseslint.configs.recommended,
|
||||
reactHooks.configs.flat.recommended,
|
||||
reactRefresh.configs.vite,
|
||||
],
|
||||
languageOptions: {
|
||||
ecmaVersion: 2020,
|
||||
globals: globals.browser,
|
||||
},
|
||||
},
|
||||
])
|
||||
13
frontend/index.html
Normal file
13
frontend/index.html
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>frontend</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
3309
frontend/package-lock.json
generated
Normal file
3309
frontend/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
32
frontend/package.json
Normal file
32
frontend/package.json
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"lint": "eslint .",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.39.4",
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/node": "^24.12.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.1",
|
||||
"eslint": "^9.39.4",
|
||||
"eslint-plugin-react-hooks": "^7.0.1",
|
||||
"eslint-plugin-react-refresh": "^0.5.2",
|
||||
"globals": "^17.4.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "~6.0.2",
|
||||
"typescript-eslint": "^8.58.0",
|
||||
"vite": "^8.0.4"
|
||||
}
|
||||
}
|
||||
1
frontend/public/favicon.svg
Normal file
1
frontend/public/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 9.3 KiB |
24
frontend/public/icons.svg
Normal file
24
frontend/public/icons.svg
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<symbol id="bluesky-icon" viewBox="0 0 16 17">
|
||||
<g clip-path="url(#bluesky-clip)"><path fill="#08060d" d="M7.75 7.735c-.693-1.348-2.58-3.86-4.334-5.097-1.68-1.187-2.32-.981-2.74-.79C.188 2.065.1 2.812.1 3.251s.241 3.602.398 4.13c.52 1.744 2.367 2.333 4.07 2.145-2.495.37-4.71 1.278-1.805 4.512 3.196 3.309 4.38-.71 4.987-2.746.608 2.036 1.307 5.91 4.93 2.746 2.72-2.746.747-4.143-1.747-4.512 1.702.189 3.55-.4 4.07-2.145.156-.528.397-3.691.397-4.13s-.088-1.186-.575-1.406c-.42-.19-1.06-.395-2.741.79-1.755 1.24-3.64 3.752-4.334 5.099"/></g>
|
||||
<defs><clipPath id="bluesky-clip"><path fill="#fff" d="M.1.85h15.3v15.3H.1z"/></clipPath></defs>
|
||||
</symbol>
|
||||
<symbol id="discord-icon" viewBox="0 0 20 19">
|
||||
<path fill="#08060d" d="M16.224 3.768a14.5 14.5 0 0 0-3.67-1.153c-.158.286-.343.67-.47.976a13.5 13.5 0 0 0-4.067 0c-.128-.306-.317-.69-.476-.976A14.4 14.4 0 0 0 3.868 3.77C1.546 7.28.916 10.703 1.231 14.077a14.7 14.7 0 0 0 4.5 2.306q.545-.748.965-1.587a9.5 9.5 0 0 1-1.518-.74q.191-.14.372-.293c2.927 1.369 6.107 1.369 8.999 0q.183.152.372.294-.723.437-1.52.74.418.838.963 1.588a14.6 14.6 0 0 0 4.504-2.308c.37-3.911-.63-7.302-2.644-10.309m-9.13 8.234c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.894 0 1.614.82 1.599 1.82.001 1-.705 1.82-1.6 1.82m5.91 0c-.878 0-1.599-.82-1.599-1.82 0-.998.705-1.82 1.6-1.82.893 0 1.614.82 1.599 1.82 0 1-.706 1.82-1.6 1.82"/>
|
||||
</symbol>
|
||||
<symbol id="documentation-icon" viewBox="0 0 21 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="m15.5 13.333 1.533 1.322c.645.555.967.833.967 1.178s-.322.623-.967 1.179L15.5 18.333m-3.333-5-1.534 1.322c-.644.555-.966.833-.966 1.178s.322.623.966 1.179l1.534 1.321"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M17.167 10.836v-4.32c0-1.41 0-2.117-.224-2.68-.359-.906-1.118-1.621-2.08-1.96-.599-.21-1.349-.21-2.848-.21-2.623 0-3.935 0-4.983.369-1.684.591-3.013 1.842-3.641 3.428C3 6.449 3 7.684 3 10.154v2.122c0 2.558 0 3.838.706 4.726q.306.383.713.671c.76.536 1.79.64 3.581.66"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M3 10a2.78 2.78 0 0 1 2.778-2.778c.555 0 1.209.097 1.748-.047.48-.129.854-.503.982-.982.145-.54.048-1.194.048-1.749a2.78 2.78 0 0 1 2.777-2.777"/>
|
||||
</symbol>
|
||||
<symbol id="github-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M9.356 1.85C5.05 1.85 1.57 5.356 1.57 9.694a7.84 7.84 0 0 0 5.324 7.44c.387.079.528-.168.528-.376 0-.182-.013-.805-.013-1.454-2.165.467-2.616-.935-2.616-.935-.349-.91-.864-1.143-.864-1.143-.71-.48.051-.48.051-.48.787.051 1.2.805 1.2.805.695 1.194 1.817.857 2.268.649.064-.507.27-.857.49-1.052-1.728-.182-3.545-.857-3.545-3.87 0-.857.31-1.558.8-2.104-.078-.195-.349-1 .077-2.078 0 0 .657-.208 2.14.805a7.5 7.5 0 0 1 1.946-.26c.657 0 1.328.092 1.946.26 1.483-1.013 2.14-.805 2.14-.805.426 1.078.155 1.883.078 2.078.502.546.799 1.247.799 2.104 0 3.013-1.818 3.675-3.558 3.87.284.247.528.714.528 1.454 0 1.052-.012 1.896-.012 2.156 0 .208.142.455.528.377a7.84 7.84 0 0 0 5.324-7.441c.013-4.338-3.48-7.844-7.773-7.844" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
<symbol id="social-icon" viewBox="0 0 20 20">
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M12.5 6.667a4.167 4.167 0 1 0-8.334 0 4.167 4.167 0 0 0 8.334 0"/>
|
||||
<path fill="none" stroke="#aa3bff" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.35" d="M2.5 16.667a5.833 5.833 0 0 1 8.75-5.053m3.837.474.513 1.035c.07.144.257.282.414.309l.93.155c.596.1.736.536.307.965l-.723.73a.64.64 0 0 0-.152.531l.207.903c.164.715-.213.991-.84.618l-.872-.52a.63.63 0 0 0-.577 0l-.872.52c-.624.373-1.003.094-.84-.618l.207-.903a.64.64 0 0 0-.152-.532l-.723-.729c-.426-.43-.289-.864.306-.964l.93-.156a.64.64 0 0 0 .412-.31l.513-1.034c.28-.562.735-.562 1.012 0"/>
|
||||
</symbol>
|
||||
<symbol id="x-icon" viewBox="0 0 19 19">
|
||||
<path fill="#08060d" fill-rule="evenodd" d="M1.893 1.98c.052.072 1.245 1.769 2.653 3.77l2.892 4.114c.183.261.333.48.333.486s-.068.089-.152.183l-.522.593-.765.867-3.597 4.087c-.375.426-.734.834-.798.905a1 1 0 0 0-.118.148c0 .01.236.017.664.017h.663l.729-.83c.4-.457.796-.906.879-.999a692 692 0 0 0 1.794-2.038c.034-.037.301-.34.594-.675l.551-.624.345-.392a7 7 0 0 1 .34-.374c.006 0 .93 1.306 2.052 2.903l2.084 2.965.045.063h2.275c1.87 0 2.273-.003 2.266-.021-.008-.02-1.098-1.572-3.894-5.547-2.013-2.862-2.28-3.246-2.273-3.266.008-.019.282-.332 2.085-2.38l2-2.274 1.567-1.782c.022-.028-.016-.03-.65-.03h-.674l-.3.342a871 871 0 0 1-1.782 2.025c-.067.075-.405.458-.75.852a100 100 0 0 1-.803.91c-.148.172-.299.344-.99 1.127-.304.343-.32.358-.345.327-.015-.019-.904-1.282-1.976-2.808L6.365 1.85H1.8zm1.782.91 8.078 11.294c.772 1.08 1.413 1.973 1.425 1.984.016.017.241.02 1.05.017l1.03-.004-2.694-3.766L7.796 5.75 5.722 2.852l-1.039-.004-1.039-.004z" clip-rule="evenodd"/>
|
||||
</symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.9 KiB |
121
frontend/src/App.tsx
Normal file
121
frontend/src/App.tsx
Normal file
|
|
@ -0,0 +1,121 @@
|
|||
import { useEffect, useState } from 'react'
|
||||
import { createTask, listTasks, updateTaskStatus } from './api'
|
||||
import { CreateTaskForm } from './components/CreateTaskForm'
|
||||
import { TaskItem } from './components/TaskItem'
|
||||
import type { Task, TaskStatus } from './types'
|
||||
|
||||
function App() {
|
||||
const [tasks, setTasks] = useState<Task[]>([])
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [isSubmitting, setIsSubmitting] = useState(false)
|
||||
const [updatingTaskIds, setUpdatingTaskIds] = useState<string[]>([])
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
async function loadTasks() {
|
||||
try {
|
||||
setIsLoading(true)
|
||||
const data = await listTasks()
|
||||
setTasks(data)
|
||||
} catch (loadError) {
|
||||
setError(loadError instanceof Error ? loadError.message : 'Could not load tasks.')
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
void loadTasks()
|
||||
}, [])
|
||||
|
||||
async function handleCreateTask(input: { name: string; description: string }) {
|
||||
try {
|
||||
setIsSubmitting(true)
|
||||
setError(null)
|
||||
const createdTask = await createTask(input)
|
||||
setTasks((current) => [createdTask, ...current])
|
||||
} catch (submitError) {
|
||||
setError(submitError instanceof Error ? submitError.message : 'Could not create task.')
|
||||
throw submitError
|
||||
} finally {
|
||||
setIsSubmitting(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleStatusChange(taskId: string, status: TaskStatus) {
|
||||
try {
|
||||
setError(null)
|
||||
setUpdatingTaskIds((current) => [...current, taskId])
|
||||
const updatedTask = await updateTaskStatus(taskId, status)
|
||||
|
||||
setTasks((current) =>
|
||||
current.map((task) => (task.id === updatedTask.id ? updatedTask : task)),
|
||||
)
|
||||
} catch (updateError) {
|
||||
setError(updateError instanceof Error ? updateError.message : 'Could not update the task.')
|
||||
} finally {
|
||||
setUpdatingTaskIds((current) => current.filter((id) => id !== taskId))
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<main className="min-h-screen bg-[radial-gradient(circle_at_top,_rgba(125,211,252,0.35),_transparent_35%),linear-gradient(180deg,_#f8fafc_0%,_#e2e8f0_100%)] px-4 py-10 text-slate-800">
|
||||
<div className="mx-auto grid max-w-6xl gap-6 lg:grid-cols-[420px_minmax(0,1fr)]">
|
||||
<section className="space-y-6">
|
||||
<div className="rounded-[2rem] border border-white/70 bg-white/70 p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur">
|
||||
<p className="text-sm uppercase tracking-[0.3em] text-slate-500">TimeTrack MVP</p>
|
||||
<h1 className="mt-4 max-w-sm text-5xl font-semibold tracking-[-0.04em] text-slate-950">
|
||||
Task management in one simple slice.
|
||||
</h1>
|
||||
<p className="mt-4 text-base leading-7 text-slate-600">
|
||||
Create tasks, move them through progress, and keep the flow visible while the wider
|
||||
platform is still being built.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<CreateTaskForm isSubmitting={isSubmitting} onSubmit={handleCreateTask} />
|
||||
</section>
|
||||
|
||||
<section className="rounded-[2.5rem] border border-white/70 bg-white/65 p-6 shadow-[0_24px_80px_rgba(15,23,42,0.08)] backdrop-blur">
|
||||
<div className="flex flex-col gap-3 border-b border-slate-200 pb-5 sm:flex-row sm:items-end sm:justify-between">
|
||||
<div>
|
||||
<p className="text-sm uppercase tracking-[0.3em] text-slate-500">Task list</p>
|
||||
<h2 className="mt-2 text-3xl font-semibold tracking-[-0.04em] text-slate-950">
|
||||
Current work
|
||||
</h2>
|
||||
</div>
|
||||
<p className="text-sm text-slate-500">{tasks.length} tasks loaded</p>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<div className="mt-5 rounded-2xl border border-rose-200 bg-rose-50 px-4 py-3 text-sm text-rose-700">
|
||||
{error}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{isLoading ? (
|
||||
<div className="mt-6 rounded-[2rem] border border-dashed border-slate-300 px-6 py-12 text-center text-slate-500">
|
||||
Loading tasks...
|
||||
</div>
|
||||
) : tasks.length === 0 ? (
|
||||
<div className="mt-6 rounded-[2rem] border border-dashed border-slate-300 px-6 py-12 text-center text-slate-500">
|
||||
No tasks yet. Create the first one from the panel on the left.
|
||||
</div>
|
||||
) : (
|
||||
<div className="mt-6 grid gap-4">
|
||||
{tasks.map((task) => (
|
||||
<TaskItem
|
||||
key={task.id}
|
||||
task={task}
|
||||
isUpdating={updatingTaskIds.includes(task.id)}
|
||||
onStatusChange={handleStatusChange}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
|
||||
export default App
|
||||
47
frontend/src/api.ts
Normal file
47
frontend/src/api.ts
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
import type { Task, TaskStatus } from './types'
|
||||
|
||||
const API_BASE_URL = import.meta.env.VITE_API_URL ?? '/api'
|
||||
|
||||
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...(init?.headers ?? {}),
|
||||
},
|
||||
...init,
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const fallbackMessage = 'Something went wrong while talking to the API.'
|
||||
let message = fallbackMessage
|
||||
|
||||
try {
|
||||
const error = (await response.json()) as { message?: string }
|
||||
message = error.message ?? fallbackMessage
|
||||
} catch {
|
||||
message = fallbackMessage
|
||||
}
|
||||
|
||||
throw new Error(message)
|
||||
}
|
||||
|
||||
return (await response.json()) as T
|
||||
}
|
||||
|
||||
export function listTasks(): Promise<Task[]> {
|
||||
return request<Task[]>('/tasks')
|
||||
}
|
||||
|
||||
export function createTask(input: { name: string; description?: string }): Promise<Task> {
|
||||
return request<Task>('/tasks', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(input),
|
||||
})
|
||||
}
|
||||
|
||||
export function updateTaskStatus(taskId: string, status: TaskStatus): Promise<Task> {
|
||||
return request<Task>(`/tasks/${taskId}/status`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status }),
|
||||
})
|
||||
}
|
||||
63
frontend/src/components/CreateTaskForm.tsx
Normal file
63
frontend/src/components/CreateTaskForm.tsx
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
import { type FormEvent, useState } from 'react'
|
||||
|
||||
interface CreateTaskFormProps {
|
||||
isSubmitting: boolean
|
||||
onSubmit: (input: { name: string; description: string }) => Promise<void>
|
||||
}
|
||||
|
||||
export function CreateTaskForm({ isSubmitting, onSubmit }: CreateTaskFormProps) {
|
||||
const [name, setName] = useState('')
|
||||
const [description, setDescription] = useState('')
|
||||
|
||||
async function handleSubmit(event: FormEvent<HTMLFormElement>) {
|
||||
event.preventDefault()
|
||||
await onSubmit({ name, description })
|
||||
setName('')
|
||||
setDescription('')
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className="rounded-[2rem] border border-slate-900/10 bg-slate-950 p-6 text-white shadow-[0_24px_80px_rgba(15,23,42,0.24)]"
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
<div className="mb-6">
|
||||
<p className="text-sm uppercase tracking-[0.25em] text-sky-200">Create task</p>
|
||||
<h2 className="mt-3 text-3xl font-semibold">Capture work while it is fresh.</h2>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label className="block text-sm text-slate-200">
|
||||
<span className="mb-2 block font-medium">Name</span>
|
||||
<input
|
||||
className="w-full rounded-2xl border border-white/10 bg-white/10 px-4 py-3 text-white outline-none transition placeholder:text-slate-400 focus:border-sky-300"
|
||||
placeholder="Prepare sprint review"
|
||||
value={name}
|
||||
onChange={(event) => setName(event.target.value)}
|
||||
disabled={isSubmitting}
|
||||
required
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="block text-sm text-slate-200">
|
||||
<span className="mb-2 block font-medium">Description</span>
|
||||
<textarea
|
||||
className="min-h-28 w-full rounded-2xl border border-white/10 bg-white/10 px-4 py-3 text-white outline-none transition placeholder:text-slate-400 focus:border-sky-300"
|
||||
placeholder="Optional context for this task"
|
||||
value={description}
|
||||
onChange={(event) => setDescription(event.target.value)}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className="mt-6 inline-flex items-center rounded-full bg-sky-300 px-5 py-3 text-sm font-semibold text-slate-950 transition hover:bg-sky-200 disabled:cursor-not-allowed disabled:opacity-70"
|
||||
>
|
||||
{isSubmitting ? 'Saving...' : 'Create task'}
|
||||
</button>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
57
frontend/src/components/TaskItem.tsx
Normal file
57
frontend/src/components/TaskItem.tsx
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
import type { Task, TaskStatus } from '../types'
|
||||
|
||||
const STATUS_LABELS: Record<TaskStatus, string> = {
|
||||
OPEN: 'Open',
|
||||
IN_PROGRESS: 'In progress',
|
||||
DONE: 'Done',
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<TaskStatus, string> = {
|
||||
OPEN: 'bg-white/70 text-slate-700 ring-slate-200',
|
||||
IN_PROGRESS: 'bg-amber-100 text-amber-900 ring-amber-200',
|
||||
DONE: 'bg-emerald-100 text-emerald-900 ring-emerald-200',
|
||||
}
|
||||
|
||||
interface TaskItemProps {
|
||||
task: Task
|
||||
isUpdating: boolean
|
||||
onStatusChange: (taskId: string, status: TaskStatus) => void
|
||||
}
|
||||
|
||||
export function TaskItem({ task, isUpdating, onStatusChange }: TaskItemProps) {
|
||||
return (
|
||||
<article className="rounded-[2rem] border border-white/60 bg-white/75 p-5 shadow-[0_20px_60px_rgba(15,23,42,0.08)] backdrop-blur">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="space-y-2">
|
||||
<span
|
||||
className={`inline-flex rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-[0.2em] ring-1 ${STATUS_STYLES[task.status]}`}
|
||||
>
|
||||
{STATUS_LABELS[task.status]}
|
||||
</span>
|
||||
<h3 className="text-xl font-semibold text-slate-900">{task.name}</h3>
|
||||
</div>
|
||||
<label className="min-w-32 text-sm font-medium text-slate-600">
|
||||
<span className="mb-2 block">Status</span>
|
||||
<select
|
||||
className="w-full rounded-2xl border border-slate-200 bg-white px-3 py-2 text-slate-900 outline-none transition focus:border-slate-400"
|
||||
value={task.status}
|
||||
onChange={(event) => onStatusChange(task.id, event.target.value as TaskStatus)}
|
||||
disabled={isUpdating}
|
||||
>
|
||||
<option value="OPEN">Open</option>
|
||||
<option value="IN_PROGRESS">In progress</option>
|
||||
<option value="DONE">Done</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<p className="mt-4 min-h-12 text-sm leading-6 text-slate-600">
|
||||
{task.description || 'No description provided.'}
|
||||
</p>
|
||||
|
||||
<p className="mt-4 text-xs uppercase tracking-[0.2em] text-slate-400">
|
||||
Created {new Date(task.createdAt).toLocaleString()}
|
||||
</p>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
26
frontend/src/index.css
Normal file
26
frontend/src/index.css
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
font-family: "Sora", "Avenir Next", "Segoe UI", sans-serif;
|
||||
color: #0f172a;
|
||||
background: #e2e8f0;
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body,
|
||||
#root {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
10
frontend/src/main.tsx
Normal file
10
frontend/src/main.tsx
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './index.css'
|
||||
import App from './App.tsx'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
9
frontend/src/types.ts
Normal file
9
frontend/src/types.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
export type TaskStatus = 'OPEN' | 'IN_PROGRESS' | 'DONE'
|
||||
|
||||
export interface Task {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
status: TaskStatus
|
||||
createdAt: string
|
||||
}
|
||||
25
frontend/tsconfig.app.json
Normal file
25
frontend/tsconfig.app.json
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023", "DOM", "DOM.Iterable"],
|
||||
"module": "esnext",
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
7
frontend/tsconfig.json
Normal file
7
frontend/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
{
|
||||
"files": [],
|
||||
"references": [
|
||||
{ "path": "./tsconfig.app.json" },
|
||||
{ "path": "./tsconfig.node.json" }
|
||||
]
|
||||
}
|
||||
24
frontend/tsconfig.node.json
Normal file
24
frontend/tsconfig.node.json
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||
"target": "es2023",
|
||||
"lib": ["ES2023"],
|
||||
"module": "esnext",
|
||||
"types": ["node"],
|
||||
"skipLibCheck": true,
|
||||
|
||||
/* Bundler mode */
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
|
||||
/* Linting */
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"erasableSyntaxOnly": true,
|
||||
"noFallthroughCasesInSwitch": true
|
||||
},
|
||||
"include": ["vite.config.ts"]
|
||||
}
|
||||
17
frontend/vite.config.ts
Normal file
17
frontend/vite.config.ts
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:8080',
|
||||
changeOrigin: true,
|
||||
rewrite: (path) => path.replace(/^\/api/, ''),
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
205
specification/product-description.md
Normal file
205
specification/product-description.md
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
# 📄 PRD — Sistema de Gestão de Tempo e Tarefas
|
||||
|
||||
## 🧭 Nome do Projeto
|
||||
|
||||
TimeTrack (nome provisório)
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Objetivo
|
||||
|
||||
Permitir que colaboradores registrem e acompanhem o tempo gasto em tarefas, enquanto gestores e donos da organização monitoram produtividade, alocação de tempo e eficiência operacional.
|
||||
|
||||
---
|
||||
|
||||
## 👥 Atores do Sistema
|
||||
|
||||
### 🔹 Dono (Owner)
|
||||
|
||||
* Nível mais alto da organização
|
||||
* Responsável por gerenciar usuários e estrutura
|
||||
|
||||
### 🔹 Gestor (Manager)
|
||||
|
||||
* Acompanha desempenho do time
|
||||
* Analisa métricas e relatórios
|
||||
|
||||
### 🔹 Colaborador (Employee)
|
||||
|
||||
* Registra tarefas
|
||||
* Controla tempo de trabalho
|
||||
|
||||
---
|
||||
|
||||
## 🏢 Estrutura Organizacional
|
||||
|
||||
* O sistema deve suportar múltiplas organizações
|
||||
* Cada organização contém:
|
||||
|
||||
* Dono (1)
|
||||
* Gestores (N)
|
||||
* Colaboradores (N)
|
||||
|
||||
### Regras:
|
||||
|
||||
* Apenas o Dono pode:
|
||||
|
||||
* Adicionar Gestores e Colaboradores
|
||||
* Aprovar pedidos de entrada na organização
|
||||
* Colaboradores podem solicitar participação em uma organização
|
||||
|
||||
---
|
||||
|
||||
## 🧩 Funcionalidades Principais
|
||||
|
||||
### 👨💻 Colaborador
|
||||
|
||||
#### Registro de tarefas
|
||||
|
||||
* Criar tarefa com:
|
||||
|
||||
* Nome
|
||||
* Descrição
|
||||
* Categoria
|
||||
* Projeto
|
||||
* Status (ex: aberta, em andamento, concluída)
|
||||
|
||||
#### Controle de tempo
|
||||
|
||||
* Iniciar tarefa
|
||||
* Pausar tarefa
|
||||
* Retomar tarefa
|
||||
* Finalizar tarefa
|
||||
|
||||
#### Relatórios
|
||||
|
||||
* Relatório mensal:
|
||||
|
||||
* Horas trabalhadas por mês
|
||||
* Agrupamento por categoria
|
||||
* Agrupamento por projeto
|
||||
|
||||
#### Dashboard diário
|
||||
|
||||
* Tarefas trabalhadas no dia anterior
|
||||
* Tarefas abertas (planejamento do dia atual)
|
||||
|
||||
---
|
||||
|
||||
### 📊 Gestor
|
||||
|
||||
#### Monitoramento de equipe
|
||||
|
||||
* Tempo gasto por colaborador em tarefas
|
||||
* Tempo gasto por projeto
|
||||
|
||||
#### Métricas de produtividade
|
||||
|
||||
* Relação:
|
||||
|
||||
* Horas trabalhadas / horas disponíveis
|
||||
* Visualização por:
|
||||
|
||||
* Dia
|
||||
* Semana
|
||||
* Mês
|
||||
|
||||
---
|
||||
|
||||
### 👑 Dono
|
||||
|
||||
#### Gestão de usuários
|
||||
|
||||
* Criar colaboradores e gestores
|
||||
* Aprovar solicitações de entrada
|
||||
|
||||
#### Gestão organizacional
|
||||
|
||||
* Criar e gerenciar organização
|
||||
* Visão global de métricas (igual ou superior ao gestor)
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Regras de Negócio
|
||||
|
||||
* Uma tarefa deve pertencer a:
|
||||
|
||||
* Um colaborador
|
||||
* Um projeto
|
||||
* Uma categoria
|
||||
* Uma tarefa pode ter múltiplos períodos de tempo (start/pause)
|
||||
* O tempo total de uma tarefa = soma dos períodos ativos
|
||||
* Apenas uma tarefa pode estar ativa por colaborador ao mesmo tempo (regra recomendada)
|
||||
* Horas disponíveis podem ser:
|
||||
|
||||
* Configuráveis por organização ou colaborador
|
||||
|
||||
### 📁 Projetos
|
||||
|
||||
* Um projeto pode ter:
|
||||
|
||||
* Um ou mais gestores responsáveis
|
||||
* Gestores associados a um projeto podem:
|
||||
|
||||
* Visualizar métricas e tarefas relacionadas ao projeto
|
||||
* Acompanhar desempenho dos colaboradores naquele projeto
|
||||
|
||||
---
|
||||
|
||||
## 📊 Métricas e Indicadores
|
||||
|
||||
* Total de horas trabalhadas
|
||||
* Horas por projeto
|
||||
* Horas por categoria
|
||||
* Eficiência:
|
||||
|
||||
* horas trabalhadas / horas disponíveis
|
||||
* Ranking de produtividade (opcional)
|
||||
|
||||
---
|
||||
|
||||
## 🗂️ Entidades Principais
|
||||
|
||||
* Organization
|
||||
* User
|
||||
|
||||
* role: OWNER | MANAGER | EMPLOYEE
|
||||
* Project
|
||||
|
||||
* relação N:N com gestores
|
||||
* Task
|
||||
* TimeEntry (períodos de tempo)
|
||||
* Category
|
||||
* MembershipRequest
|
||||
|
||||
---
|
||||
|
||||
## 🚫 Fora de Escopo (Inicial)
|
||||
|
||||
* Integração com folha de pagamento
|
||||
* Controle financeiro
|
||||
* Chat interno
|
||||
* Notificações em tempo real
|
||||
* App mobile (primeira versão web)
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Stack Sugerida (alinhada com seu perfil)
|
||||
|
||||
* Frontend: React + TypeScript
|
||||
* Backend: Java + Quarkus
|
||||
* Autenticação: Keycloak
|
||||
* Banco: PostgreSQL
|
||||
|
||||
---
|
||||
|
||||
## 🔮 Possíveis Evoluções
|
||||
|
||||
* Integração com calendário (Google/Outlook)
|
||||
* Alertas de produtividade
|
||||
* IA para sugestão de tarefas
|
||||
* Previsão de tempo de execução
|
||||
* Relatórios avançados com BI
|
||||
|
||||
---
|
||||
|
||||
Loading…
Reference in a new issue