commit f12cc8b2ce1053015b749f1c3205190bad03e2d0 Author: Andreas Wilms Date: Mon Sep 8 18:30:35 2025 +0200 init diff --git a/README.md b/README.md new file mode 100644 index 0000000..3d2f868 --- /dev/null +++ b/README.md @@ -0,0 +1,137 @@ +# 🖨️ PrintAuftrag - Automated Personalization & Production System + +A system that automates repetitive business processes, digitizes workflows, reduces mistakes and costs, and provides an easy-to-use, universally accessible web UI. + +--- + +## 💡 What It Does + +- **Automates** repetitive and time-consuming business processes +- **Digitizes** order-to-production workflows +- **Reduces mistakes and costs** by eliminating manual steps and standardizing outputs +- **Accessible everywhere** via a simple and intuitive web interface + +--- + +## 👥 Who Uses It + +This system is in production at **Wolga-Kreativ**, a company specializing in personalized products such as: + +- Lunch boxes +- T-shirts +- Candles +- Other custom items (primarily for children) + +These are sold across multiple platforms: **Amazon**, **Etsy**, and **Shopify**. + +> 🧑‍💻 I’m the developer and operator of this system, affiliated with Wolga-Kreativ as the owner’s son. + +--- + +## 🔁 High-Level Pipeline + +1. 📦 Orders are captured from sales channels and aggregated via **Billbee** +2. 🔄 The system pulls order data from Billbee using its API +3. 🖼️ Personalized product images are generated based on order data +4. 🖨️ Production-ready print files are created for each specific machine +5. 📤 Files are sent to production for manufacturing and packaging +6. 📬 At day's end, production sends a report → system updates statuses → customers are notified via all sales channels + +--- + +## ✅ Key Benefits + +- 🚀 Faster processing and turnaround + _Saves up to two full-time employees during peak seasons_ +- ❌ Fewer manual errors and misprints +- 🎯 Consistent, machine-ready print files +- 📍 Centralized tracking and cross-channel customer notifications + +--- + +## 🧰 Tech Stack + +- **Backend:** Spring Boot, PostgreSQL +- **Frontend:** Next.js, TypeScript, Tailwind CSS +- **Image Generation:** Python +- **Containerization:** Docker +- **Hosting:** Self-hosted on internal server + +--- + +## 🧩 System Components + +### 🖼️ Image Generator + +#### Problem + +Designs were originally made manually in tools like **CorelDraw** or **Photoshop**—unsuitable for automated large-scale personalization. +Each personalized product had to be manually edited and exported—a slow, error-prone process, especially with over **150+ orders/day** during peak season. + +#### Solution + +- Designs are transformed into **SVG templates** with embedded bitmaps +- Stored in the database +- A standalone **Python** program takes prefiltered order data from Billbee and generates the images automatically + +**Technology:** Python + +--- + +### 🖨️ Print File Generator + +#### Problem + +Different machines, product types, and practices require specific print file formats. +Manual import and layout of each image is time-consuming and inconsistent. + +#### Solution + +- Web UI allows users to configure **print templates** +- Templates are saved to the database +- Backend (Spring Boot) generates the correct print files automatically when processing orders + +**Technologies:** Java, Spring Boot, HTML5, TypeScript, Tailwind, Next.js + +--- + +### 🔄 Sales Channel Update + +#### Problem + +After production, each order had to be manually marked as shipped and customers notified—labor-intensive and slow. + +#### Solution + +- End-of-day production sheet is automatically read +- Order statuses are updated via API +- Notifications are sent to customers through **Billbee** + +**Technologies:** TypeScript, Next.js + +--- + +## 🔮 Future Work + +- 🔗 Integrate the Python image generator into the core server workflow +- ⚙️ Improve system automation with deeper **Billbee API** usage +- 📈 Extend with more processes + +--- + +## 🖼️ Screenshots + +### 🎨 Web View – Print Templates + +![Template View 1](/doc/img/template_view_1.jpg) +_Template configuration interface – View 1_ + +![Template View 2](/doc/img/template_view_2.jpg) +_Template configuration interface – View 2_ + +--- + +### 🔁 Abstract Visualization – System Flow + +![System Flow Diagram](/doc/img/system_flow.png) +_High-level overview of the automated order-to-production pipeline_ diff --git a/api/Dockerfile b/api/Dockerfile new file mode 100644 index 0000000..250ebd1 --- /dev/null +++ b/api/Dockerfile @@ -0,0 +1,14 @@ +# Use an official OpenJDK runtime as a parent image +FROM openjdk:21-jdk-slim + +# Set the working directory inside the container +WORKDIR /app + +# Copy the application's JAR file into the container +COPY target/*.jar app.jar + +# Expose the port your Spring Boot application runs on +EXPOSE 8080 + +# Run the JAR file +ENTRYPOINT ["java", "-jar", "app.jar"] \ No newline at end of file diff --git a/api/mvnw b/api/mvnw new file mode 100755 index 0000000..19529dd --- /dev/null +++ b/api/mvnw @@ -0,0 +1,259 @@ +#!/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.2 +# +# 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:]' +} + +# 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 <"${0%/*}/.mvn/wrapper/maven-wrapper.properties" +[ -n "${distributionUrl-}" ] || die "cannot read distributionUrl property in ${0%/*}/.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${0##*/mvnw}" _MVNW_REPO_PATTERN=/org/apache/maven/ ;; +esac + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +[ -z "${MVNW_REPOURL-}" ] || distributionUrl="$MVNW_REPOURL$_MVNW_REPO_PATTERN${distributionUrl#*"$_MVNW_REPO_PATTERN"}" +distributionUrlName="${distributionUrl##*/}" +distributionUrlNameMain="${distributionUrlName%.*}" +distributionUrlNameMain="${distributionUrlNameMain%-bin}" +MAVEN_USER_HOME="${MAVEN_USER_HOME:-${HOME}/.m2}" +MAVEN_HOME="${MAVEN_USER_HOME}/wrapper/dists/${distributionUrlNameMain-}/$(hash_string "$distributionUrl")" + +exec_maven() { + unset MVNW_VERBOSE MVNW_USERNAME MVNW_PASSWORD MVNW_REPOURL || : + exec "$MAVEN_HOME/bin/$MVN_CMD" "$@" || die "cannot exec $MAVEN_HOME/bin/$MVN_CMD" +} + +if [ -d "$MAVEN_HOME" ]; then + verbose "found existing MAVEN_HOME at $MAVEN_HOME" + exec_maven "$@" +fi + +case "${distributionUrl-}" in +*?-bin.zip | *?maven-mvnd-?*-?*.zip) ;; +*) die "distributionUrl is not valid, must match *-bin.zip or maven-mvnd-*.zip, but found '${distributionUrl-}'" ;; +esac + +# prepare tmp dir +if TMP_DOWNLOAD_DIR="$(mktemp -d)" && [ -d "$TMP_DOWNLOAD_DIR" ]; then + clean() { rm -rf -- "$TMP_DOWNLOAD_DIR"; } + trap clean HUP INT TERM EXIT +else + die "cannot create temp dir" +fi + +mkdir -p -- "${MAVEN_HOME%/*}" + +# Download and Install Apache Maven +verbose "Couldn't find MAVEN_HOME, downloading and installing it ..." +verbose "Downloading from: $distributionUrl" +verbose "Downloading to: $TMP_DOWNLOAD_DIR/$distributionUrlName" + +# select .zip or .tar.gz +if ! command -v unzip >/dev/null; then + distributionUrl="${distributionUrl%.zip}.tar.gz" + distributionUrlName="${distributionUrl##*/}" +fi + +# verbose opt +__MVNW_QUIET_WGET=--quiet __MVNW_QUIET_CURL=--silent __MVNW_QUIET_UNZIP=-q __MVNW_QUIET_TAR='' +[ "${MVNW_VERBOSE-}" != true ] || __MVNW_QUIET_WGET='' __MVNW_QUIET_CURL='' __MVNW_QUIET_UNZIP='' __MVNW_QUIET_TAR=v + +# normalize http auth +case "${MVNW_PASSWORD:+has-password}" in +'') MVNW_USERNAME='' MVNW_PASSWORD='' ;; +has-password) [ -n "${MVNW_USERNAME-}" ] || MVNW_USERNAME='' MVNW_PASSWORD='' ;; +esac + +if [ -z "${MVNW_USERNAME-}" ] && command -v wget >/dev/null; then + verbose "Found wget ... using wget" + wget ${__MVNW_QUIET_WGET:+"$__MVNW_QUIET_WGET"} "$distributionUrl" -O "$TMP_DOWNLOAD_DIR/$distributionUrlName" || die "wget: Failed to fetch $distributionUrl" +elif [ -z "${MVNW_USERNAME-}" ] && command -v curl >/dev/null; then + verbose "Found curl ... using curl" + curl ${__MVNW_QUIET_CURL:+"$__MVNW_QUIET_CURL"} -f -L -o "$TMP_DOWNLOAD_DIR/$distributionUrlName" "$distributionUrl" || die "curl: Failed to fetch $distributionUrl" +elif set_java_home; then + verbose "Falling back to use Java to download" + javaSource="$TMP_DOWNLOAD_DIR/Downloader.java" + targetZip="$TMP_DOWNLOAD_DIR/$distributionUrlName" + cat >"$javaSource" <<-END + public class Downloader extends java.net.Authenticator + { + protected java.net.PasswordAuthentication getPasswordAuthentication() + { + return new java.net.PasswordAuthentication( System.getenv( "MVNW_USERNAME" ), System.getenv( "MVNW_PASSWORD" ).toCharArray() ); + } + public static void main( String[] args ) throws Exception + { + setDefault( new Downloader() ); + java.nio.file.Files.copy( java.net.URI.create( args[0] ).toURL().openStream(), java.nio.file.Paths.get( args[1] ).toAbsolutePath().normalize() ); + } + } + END + # For Cygwin/MinGW, switch paths to Windows format before running javac and java + verbose " - Compiling Downloader.java ..." + "$(native_path "$JAVACCMD")" "$(native_path "$javaSource")" || die "Failed to compile Downloader.java" + verbose " - Running Downloader.java ..." + "$(native_path "$JAVACMD")" -cp "$(native_path "$TMP_DOWNLOAD_DIR")" Downloader "$distributionUrl" "$(native_path "$targetZip")" +fi + +# If specified, validate the SHA-256 sum of the Maven distribution zip file +if [ -n "${distributionSha256Sum-}" ]; then + distributionSha256Result=false + if [ "$MVN_CMD" = mvnd.sh ]; then + echo "Checksum validation is not supported for maven-mvnd." >&2 + echo "Please disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + elif command -v sha256sum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | sha256sum -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + elif command -v shasum >/dev/null; then + if echo "$distributionSha256Sum $TMP_DOWNLOAD_DIR/$distributionUrlName" | shasum -a 256 -c >/dev/null 2>&1; then + distributionSha256Result=true + fi + else + echo "Checksum validation was requested but neither 'sha256sum' or 'shasum' are available." >&2 + echo "Please install either command, or disable validation by removing 'distributionSha256Sum' from your maven-wrapper.properties." >&2 + exit 1 + fi + if [ $distributionSha256Result = false ]; then + echo "Error: Failed to validate Maven distribution SHA-256, your Maven distribution might be compromised." >&2 + echo "If you updated your Maven version, you need to update the specified distributionSha256Sum property." >&2 + exit 1 + fi +fi + +# unzip and move +if command -v unzip >/dev/null; then + unzip ${__MVNW_QUIET_UNZIP:+"$__MVNW_QUIET_UNZIP"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -d "$TMP_DOWNLOAD_DIR" || die "failed to unzip" +else + tar xzf${__MVNW_QUIET_TAR:+"$__MVNW_QUIET_TAR"} "$TMP_DOWNLOAD_DIR/$distributionUrlName" -C "$TMP_DOWNLOAD_DIR" || die "failed to untar" +fi +printf %s\\n "$distributionUrl" >"$TMP_DOWNLOAD_DIR/$distributionUrlNameMain/mvnw.url" +mv -- "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" "$MAVEN_HOME" || [ -d "$MAVEN_HOME" ] || die "fail to move MAVEN_HOME" + +clean || : +exec_maven "$@" diff --git a/api/mvnw.cmd b/api/mvnw.cmd new file mode 100644 index 0000000..249bdf3 --- /dev/null +++ b/api/mvnw.cmd @@ -0,0 +1,149 @@ +<# : 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.2 +@REM +@REM Optional ENV vars +@REM MVNW_REPOURL - repo url base for downloading maven distribution +@REM MVNW_USERNAME/MVNW_PASSWORD - user and password for downloading maven +@REM MVNW_VERBOSE - true: enable verbose log; others: silence the output +@REM ---------------------------------------------------------------------------- + +@IF "%__MVNW_ARG0_NAME__%"=="" (SET __MVNW_ARG0_NAME__=%~nx0) +@SET __MVNW_CMD__= +@SET __MVNW_ERROR__= +@SET __MVNW_PSMODULEP_SAVE=%PSModulePath% +@SET PSModulePath= +@FOR /F "usebackq tokens=1* delims==" %%A IN (`powershell -noprofile "& {$scriptDir='%~dp0'; $script='%__MVNW_ARG0_NAME__%'; icm -ScriptBlock ([Scriptblock]::Create((Get-Content -Raw '%~f0'))) -NoNewScope}"`) DO @( + IF "%%A"=="MVN_CMD" (set __MVNW_CMD__=%%B) ELSE IF "%%B"=="" (echo %%A) ELSE (echo %%A=%%B) +) +@SET PSModulePath=%__MVNW_PSMODULEP_SAVE% +@SET __MVNW_PSMODULEP_SAVE= +@SET __MVNW_ARG0_NAME__= +@SET MVNW_USERNAME= +@SET MVNW_PASSWORD= +@IF NOT "%__MVNW_CMD__%"=="" (%__MVNW_CMD__% %*) +@echo Cannot start maven from wrapper >&2 && exit /b 1 +@GOTO :EOF +: end batch / begin powershell #> + +$ErrorActionPreference = "Stop" +if ($env:MVNW_VERBOSE -eq "true") { + $VerbosePreference = "Continue" +} + +# calculate distributionUrl, requires .mvn/wrapper/maven-wrapper.properties +$distributionUrl = (Get-Content -Raw "$scriptDir/.mvn/wrapper/maven-wrapper.properties" | ConvertFrom-StringData).distributionUrl +if (!$distributionUrl) { + Write-Error "cannot read distributionUrl property in $scriptDir/.mvn/wrapper/maven-wrapper.properties" +} + +switch -wildcard -casesensitive ( $($distributionUrl -replace '^.*/','') ) { + "maven-mvnd-*" { + $USE_MVND = $true + $distributionUrl = $distributionUrl -replace '-bin\.[^.]*$',"-windows-amd64.zip" + $MVN_CMD = "mvnd.cmd" + break + } + default { + $USE_MVND = $false + $MVN_CMD = $script -replace '^mvnw','mvn' + break + } +} + +# apply MVNW_REPOURL and calculate MAVEN_HOME +# maven home pattern: ~/.m2/wrapper/dists/{apache-maven-,maven-mvnd--}/ +if ($env:MVNW_REPOURL) { + $MVNW_REPO_PATTERN = if ($USE_MVND) { "/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_HOME_PARENT = "$HOME/.m2/wrapper/dists/$distributionUrlNameMain" +if ($env:MAVEN_USER_HOME) { + $MAVEN_HOME_PARENT = "$env:MAVEN_USER_HOME/wrapper/dists/$distributionUrlNameMain" +} +$MAVEN_HOME_NAME = ([System.Security.Cryptography.MD5]::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 +Rename-Item -Path "$TMP_DOWNLOAD_DIR/$distributionUrlNameMain" -NewName $MAVEN_HOME_NAME | Out-Null +try { + Move-Item -Path "$TMP_DOWNLOAD_DIR/$MAVEN_HOME_NAME" -Destination $MAVEN_HOME_PARENT | Out-Null +} catch { + if (! (Test-Path -Path "$MAVEN_HOME" -PathType Container)) { + Write-Error "fail to move MAVEN_HOME" + } +} finally { + try { Remove-Item $TMP_DOWNLOAD_DIR -Recurse -Force | Out-Null } + catch { Write-Warning "Cannot remove $TMP_DOWNLOAD_DIR" } +} + +Write-Output "MVN_CMD=$MAVEN_HOME/bin/$MVN_CMD" diff --git a/api/pom.xml b/api/pom.xml new file mode 100644 index 0000000..4c4ab6b --- /dev/null +++ b/api/pom.xml @@ -0,0 +1,96 @@ + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 3.4.3 + + + com.server + api + 0.0.1-SNAPSHOT + api + API Server + + + + + + + + + + + + + + + 21 + 9.1.0 + + + + org.springframework.boot + spring-boot-starter + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-devtools + true + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.hibernate.orm + hibernate-core + 6.5.2.Final + + + com.h2database + h2 + 2.2.224 + + + junit + junit + 3.8.1 + test + + + com.itextpdf + itext-core + ${itext.version} + pom + + + com.itextpdf + bouncy-castle-adapter + ${itext.version} + + + org.postgresql + postgresql + 42.7.2 + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + \ No newline at end of file diff --git a/api/src/main/java/com/server/api/ApiApplication.java b/api/src/main/java/com/server/api/ApiApplication.java new file mode 100644 index 0000000..61d9431 --- /dev/null +++ b/api/src/main/java/com/server/api/ApiApplication.java @@ -0,0 +1,12 @@ +package com.server.api; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class ApiApplication { + + public static void main(String[] args) { + SpringApplication.run(ApiApplication.class, args); + } +} diff --git a/api/src/main/java/com/server/api/config/WebConfig.java b/api/src/main/java/com/server/api/config/WebConfig.java new file mode 100644 index 0000000..94f04fa --- /dev/null +++ b/api/src/main/java/com/server/api/config/WebConfig.java @@ -0,0 +1,29 @@ +package com.server.api.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.CorsRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebConfig { + + @Bean + public WebMvcConfigurer corsConfigurer() { + return new WebMvcConfigurer() { + @Override + public void addCorsMappings(CorsRegistry registry) { + registry + .addMapping("/**") // Erlaubt alle Endpunkte + .allowedOrigins( + "http://localhost:3050", + "http://nextjs-app:3050", + "https://erp.wolga-kreativ.de") // Erlaubt Anfragen von Next.js + .allowedMethods( + "GET", "POST", "PUT", "DELETE", "OPTIONS") // Erlaubt spezifische HTTP-Methoden + .allowedHeaders("*") // Erlaubt alle Header + .allowCredentials(true); // Erlaubt Cookies/Authentifizierung + } + }; + } +} diff --git a/api/src/main/java/com/server/api/controller/AuthController.java b/api/src/main/java/com/server/api/controller/AuthController.java new file mode 100644 index 0000000..6cf19d4 --- /dev/null +++ b/api/src/main/java/com/server/api/controller/AuthController.java @@ -0,0 +1,35 @@ +package com.server.api.controller; + +import com.server.api.models.User; +import com.server.api.service.AuthService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/auth") +public class AuthController { + + @Autowired private AuthService authService; + + @PostMapping("/signup") + public ResponseEntity signup(@RequestBody User user){ + if (authService.signUp(user)){ + return ResponseEntity.ok("SignUp successful!"); + }else{ + return ResponseEntity.status(400).body("SignUp failed. Please try again."); + } + } + + @PostMapping("/signin") + public ResponseEntity signin(@RequestBody User user){ + if (authService.signIn(user)){ + return ResponseEntity.ok("SignIn successful!"); + }else{ + return ResponseEntity.status(400).body("SignIn failed. Please try again."); + } + } +} diff --git a/api/src/main/java/com/server/api/controller/VorlageFlaechendruckController.java b/api/src/main/java/com/server/api/controller/VorlageFlaechendruckController.java new file mode 100644 index 0000000..0efa8f8 --- /dev/null +++ b/api/src/main/java/com/server/api/controller/VorlageFlaechendruckController.java @@ -0,0 +1,144 @@ +// filepath: +// /Users/andre/Desktop/api/src/main/java/com/server/api/controller/VorlageFlaechendruckController.java +package com.server.api.controller; + +import com.server.api.controller.dto.RollendruckGenReqDto; +import com.server.api.models.VorlageFlaechendruck; +import com.server.api.service.flaechendruck.VorlageFlaechendruckService; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +import com.server.api.service.flaechendruck.VorlagenFlaechendruckPDFGeneratorService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/vorlageFlaechendruck") +public class VorlageFlaechendruckController { + + @Autowired private VorlageFlaechendruckService vorlageFlaechendruckService; + @Autowired private VorlagenFlaechendruckPDFGeneratorService vorlagenPDFGeneratorService; + + + @PostMapping("/createWithCoordinates") + public ResponseEntity createVorlageWithCoordinates( + @RequestBody VorlageFlaechendruck vorlage) { + vorlageFlaechendruckService.saveVorlageWithCoordinates(vorlage); + return ResponseEntity.ok("Vorlage successfully created with coordinates."); + } + + @PostMapping("/alterVorlage") + public ResponseEntity alterVorlage(@RequestBody VorlageFlaechendruck vorlage){ + vorlageFlaechendruckService.alterVorlage(vorlage); + return ResponseEntity.ok("Vorlage successfully created with coordinates."); + } + + @GetMapping("/getAllFlaechendruckVorlagen") + public List getAllFlaechendruckVorlagen() { + + return vorlageFlaechendruckService.getAllVorlagen(); + } + + @DeleteMapping("/delete/{id}") + public void deleteVorlageFlaechendruck(@PathVariable Long id) { + vorlageFlaechendruckService.deleteVorlageFlaechendruck(id); + } + + @PostMapping(value = "/generate", consumes = "application/json") + public ResponseEntity uploadFiles(@RequestBody RollendruckGenReqDto rollendruckGenReqDto) { + List vorlageIds = rollendruckGenReqDto.getVorlageIds(); + String rootDir = rollendruckGenReqDto.getRootDirectory(); + + String OUTPUT_DIR = String.valueOf((long) (Math.random() * 9_000_000_000L) + 1_000_000_000L)+"/"; + + Path output = Paths.get(OUTPUT_DIR); + + // Ensure the directory path exists before checking it + if (Files.exists(output)) { + try { + deleteDirectoryRecursively(output); + }catch (Exception e) { + + + } + } + + String sharedFolder = null; + String next_url = null; + + try { + sharedFolder = System.getenv("SHARED_FOLDER"); + next_url = System.getenv("NEXT_SERVER_URL"); + if (sharedFolder == null || sharedFolder.isEmpty()) { + sharedFolder = "../frontend/public"; + } + if (next_url == null || next_url.isEmpty()) { + next_url = "http://localhost:3050"; + } + String uploadPath = sharedFolder + "/" + rootDir + "/"; + File dir = new File(uploadPath); + if (!dir.exists() && !dir.isDirectory()) { + throw new Exception("Ordner "+rootDir+" mit Bildern fehlt!"); + } + for (String vorlageId : vorlageIds) { + System.out.println("Wird generiert: " + vorlageId); + vorlagenPDFGeneratorService.generateFlaechendruckPDFs( + Long.parseLong(vorlageId), OUTPUT_DIR, uploadPath); + } + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } + + try { + // move final output folder to shared folder + Path outputDir = output; + Path sharedOutputDir = Paths.get(sharedFolder + "/" + rootDir + "/output"); + + // delete shared output folder if it exists + if (Files.exists(sharedOutputDir)) { + deleteDirectoryRecursively(sharedOutputDir); + } + + Files.walk(outputDir) + .forEach( + source -> { + try { + Path destination = sharedOutputDir.resolve(outputDir.relativize(source)); + Files.createDirectories(destination.getParent()); + Files.copy(source, destination); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + // Output-Ordner löschen + deleteDirectoryRecursively(output); + + return ResponseEntity.status(HttpStatus.OK).build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } + } + + private void deleteDirectoryRecursively(Path path) throws IOException { + if (Files.exists(path)) { + Files.walk(path) + .sorted((a, b) -> b.compareTo(a)) // Dateien zuerst, dann Verzeichnisse + .forEach( + p -> { + try { + Files.delete(p); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } +} diff --git a/api/src/main/java/com/server/api/controller/VorlageRollendruckController.java b/api/src/main/java/com/server/api/controller/VorlageRollendruckController.java new file mode 100644 index 0000000..3031038 --- /dev/null +++ b/api/src/main/java/com/server/api/controller/VorlageRollendruckController.java @@ -0,0 +1,161 @@ +package com.server.api.controller; + + +import com.server.api.controller.dto.RollendruckGenReqDto; +import com.server.api.models.RollenDruckDupliArtikel; +import com.server.api.models.VorlageRollendruck; +import com.server.api.service.rollendruck.VorlageRollendruckService; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import com.server.api.service.rollendruck.VorlageRollendruckPDFGeneratorService; + +import java.io.File; +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +@RestController +@RequestMapping("/VorlageRollenDruck") +public class VorlageRollendruckController { + + @Autowired private VorlageRollendruckService vorlageRollendruckService; + @Autowired private VorlageRollendruckPDFGeneratorService vorlagenPDFGeneratorService; + + + @PostMapping("/createRollenDruckVorlage") + public ResponseEntity createVorlageWithCoordinates( + @RequestBody VorlageRollendruck vorlage) { + vorlageRollendruckService.createRollendruckVorlage(vorlage); + return ResponseEntity.ok("Vorlage Rollendruck successfully created."); + } + + @GetMapping("/getAllRollendruckVorlagen") + public List getAllRollendruckVorlagen() { + // Get all rollendruck templates + return vorlageRollendruckService.getAllVorlagen(); + } + + @PostMapping("/alterVorlage") + public ResponseEntity alterVorlage(@RequestBody VorlageRollendruck vorlage) { + vorlageRollendruckService.alterVorlage(vorlage); + return ResponseEntity.ok("Vorlage successfully created with coordinates."); + } + + @DeleteMapping("/delete/{id}") + public void deleteVorlageRollendruck(@PathVariable Long id) { + vorlageRollendruckService.deleteVorlageRollendruck(id); + } + + @PostMapping(value = "/generate", consumes = "application/json") + public ResponseEntity uploadFiles(@RequestBody RollendruckGenReqDto rollendruckGenReqDto) { + List vorlageIds = rollendruckGenReqDto.getVorlageIds(); + String rootDir = rollendruckGenReqDto.getRootDirectory(); + + String OUTPUT_DIR = String.valueOf((long) (Math.random() * 9_000_000_000L) + 1_000_000_000L)+"/"; + + Path output = Paths.get(OUTPUT_DIR); + + // Ensure the directory path exists before checking it + if (Files.exists(output)) { + try { + deleteDirectoryRecursively(output); + }catch (Exception e) { + + + } + } + + String sharedFolder = null; + String next_url = null; + + try { + sharedFolder = System.getenv("SHARED_FOLDER"); + next_url = System.getenv("NEXT_SERVER_URL"); + if (sharedFolder == null || sharedFolder.isEmpty()) { + sharedFolder = "../frontend/public"; + } + if (next_url == null || next_url.isEmpty()) { + next_url = "http://localhost:3050"; + } + String uploadPath = sharedFolder + "/" + rootDir + "/"; + File dir = new File(uploadPath); + if (!dir.exists() && !dir.isDirectory()) { + throw new Exception("Ordner "+rootDir+" mit Bildern fehlt!"); + } + for (String vorlageId : vorlageIds) { + System.out.println("Wird generiert: " + vorlageId); + vorlagenPDFGeneratorService.generatePdf( + Long.parseLong(vorlageId), OUTPUT_DIR, uploadPath); + } + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } + + try { + // move final output folder to shared folder + Path outputDir = output; + Path sharedOutputDir = Paths.get(sharedFolder + "/" + rootDir + "/output"); + + // delete shared output folder if it exists + if (Files.exists(sharedOutputDir)) { + deleteDirectoryRecursively(sharedOutputDir); + } + + Files.walk(outputDir) + .forEach( + source -> { + try { + Path destination = sharedOutputDir.resolve(outputDir.relativize(source)); + Files.createDirectories(destination.getParent()); + Files.copy(source, destination); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + // Output-Ordner löschen + deleteDirectoryRecursively(output); + + return ResponseEntity.status(HttpStatus.OK).build(); + } catch (Exception e) { + return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage()); + } + } + + @PostMapping("/addDupliArtikel") + public ResponseEntity addDupliArtikel(@RequestBody RollenDruckDupliArtikel artikel) { + vorlageRollendruckService.addDupliArtikel(artikel); + return ResponseEntity.ok("Vorlage successfully created with coordinates."); + } + + @GetMapping("/getAllDupliArtikel") + public List getAllDupliArtikel() { + return vorlageRollendruckService.getAllDupliArtikel(); + } + + @DeleteMapping("deleteDupli/{id}") + public void deleteDupli(@PathVariable Long id){ + vorlageRollendruckService.deleteDupliArtikel(id); + } + + + private void deleteDirectoryRecursively(Path path) throws IOException { + if (Files.exists(path)) { + Files.walk(path) + .sorted((a, b) -> b.compareTo(a)) // Dateien zuerst, dann Verzeichnisse + .forEach( + p -> { + try { + Files.delete(p); + } catch (IOException e) { + throw new UncheckedIOException(e); + } + }); + } + } +} diff --git a/api/src/main/java/com/server/api/controller/dto/RollendruckGenReqDto.java b/api/src/main/java/com/server/api/controller/dto/RollendruckGenReqDto.java new file mode 100644 index 0000000..6cde7dc --- /dev/null +++ b/api/src/main/java/com/server/api/controller/dto/RollendruckGenReqDto.java @@ -0,0 +1,26 @@ +package com.server.api.controller.dto; + +import java.util.List; + +public class RollendruckGenReqDto { + + private List vorlageIds; + private String rootDirectory; + + // Getter und Setter + public List getVorlageIds() { + return vorlageIds; + } + + public void setVorlageIds(List vorlageIds) { + this.vorlageIds = vorlageIds; + } + + public String getRootDirectory() { + return rootDirectory; + } + + public void setRootDirectory(String rootDirectory) { + this.rootDirectory = rootDirectory; + } +} diff --git a/api/src/main/java/com/server/api/models/Coordinates.java b/api/src/main/java/com/server/api/models/Coordinates.java new file mode 100644 index 0000000..aad9115 --- /dev/null +++ b/api/src/main/java/com/server/api/models/Coordinates.java @@ -0,0 +1,68 @@ +package com.server.api.models; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; + +@Entity +@Table(name = "coordinates") +public class Coordinates { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long coordinate_id; + + @ManyToOne + @JoinColumn(name = "flaechendruck_id", nullable = false) + @JsonIgnore + private VorlageFlaechendruck vorlage; + + private Double x; + private Double y; + private Double rotation; + + // Getter und Setter + public Long getId() { + return coordinate_id; + } + + public void setId(Long id) { + this.coordinate_id = id; + } + + public VorlageFlaechendruck getVorlage() { + return vorlage; + } + + public void setVorlage(VorlageFlaechendruck vorlage) { + this.vorlage = vorlage; + } + + public Double getX() { + return x; + } + + public void setX(Double x) { + this.x = x; + } + + public Double getY() { + return y; + } + + public void setY(Double y) { + this.y = y; + } + + public Double getRotation() { + return rotation; + } + + public void setRotation(Double rotation) { + this.rotation = rotation; + } +} diff --git a/api/src/main/java/com/server/api/models/RollenDruckDupliArtikel.java b/api/src/main/java/com/server/api/models/RollenDruckDupliArtikel.java new file mode 100644 index 0000000..ac55945 --- /dev/null +++ b/api/src/main/java/com/server/api/models/RollenDruckDupliArtikel.java @@ -0,0 +1,24 @@ +package com.server.api.models; + +import jakarta.persistence.*; + +@Entity +@Table(name = "RollenDruckDupliArtikel") +public class RollenDruckDupliArtikel { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String product_type; + + public void setProduct_type(String product_type) { + this.product_type = product_type; + } + public String getProduct_type() { + return product_type; + } + + public Long getId() { + return id; + } +} diff --git a/api/src/main/java/com/server/api/models/User.java b/api/src/main/java/com/server/api/models/User.java new file mode 100644 index 0000000..88c11b4 --- /dev/null +++ b/api/src/main/java/com/server/api/models/User.java @@ -0,0 +1,31 @@ +package com.server.api.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +@Entity +@Table(name = "users") +public class User { + + @Id + private String name; + + private String password; + + public void setName(String name){ + this.name = name; + } + + public String getName() { + return name; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getPassword() { + return password; + } +} diff --git a/api/src/main/java/com/server/api/models/VorlageFlaechendruck.java b/api/src/main/java/com/server/api/models/VorlageFlaechendruck.java new file mode 100644 index 0000000..ad7b773 --- /dev/null +++ b/api/src/main/java/com/server/api/models/VorlageFlaechendruck.java @@ -0,0 +1,75 @@ +package com.server.api.models; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import java.util.List; + +@Entity +@Table(name = "vorlageFlaechendruck") +public class VorlageFlaechendruck { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long flaechendruck_id; + + private String printer; + private String product_type; + private Float height; + private Float width; + + @OneToMany(mappedBy = "coordinate_id") + private List coordinates; + + // Getter und Setter + public String getPrinter() { + return printer; + } + + public void setPrinter(String printer) { + this.printer = printer; + } + + public Long getId() { + return flaechendruck_id; + } + + public void setId(Long id) { + this.flaechendruck_id = id; + } + + public String getProduct_type() { + return product_type; + } + + public void setProduct_type(String product_type) { + this.product_type = product_type; + } + + public Float getHeight() { + return height; + } + + public void setHeight(Float height) { + this.height = height; + } + + public Float getWidth() { + return width; + } + + public void setWidth(Float width) { + this.width = width; + } + + public List getCoordinates() { + return coordinates; + } + + public void setCoordinates(List coordinates) { + this.coordinates = coordinates; + } +} diff --git a/api/src/main/java/com/server/api/models/VorlageRollendruck.java b/api/src/main/java/com/server/api/models/VorlageRollendruck.java new file mode 100644 index 0000000..5b9b9e9 --- /dev/null +++ b/api/src/main/java/com/server/api/models/VorlageRollendruck.java @@ -0,0 +1,60 @@ +package com.server.api.models; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.persistence.*; + +@Entity +@Table(name = "vorlageRollendruck") +public class VorlageRollendruck { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + private String printer; + private Float height; + private Float width; + private String articleTypes; + + // Getter and Setter + public String getArticleTypes() { + return articleTypes; + } + + public void setArticleTypes(String articleTypes) { + this.articleTypes = articleTypes; + } + + // Getter und Setter + public String getPrinter() { + return printer; + } + + public void setPrinter(String printer) { + this.printer = printer; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Float getHeight() { + return height; + } + + public void setHeight(Float height) { + this.height = height; + } + + public Float getWidth() { + return width; + } + + public void setWidth(Float width) { + this.width = width; + } +} diff --git a/api/src/main/java/com/server/api/repository/AuthRepository.java b/api/src/main/java/com/server/api/repository/AuthRepository.java new file mode 100644 index 0000000..da81adc --- /dev/null +++ b/api/src/main/java/com/server/api/repository/AuthRepository.java @@ -0,0 +1,19 @@ +package com.server.api.repository; + +import com.server.api.models.User; +import com.server.api.models.VorlageRollendruck; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface AuthRepository extends JpaRepository { + + @Query("SELECT u FROM User u WHERE u.name = :name") + User findByName(@Param("name") String name); + + +} diff --git a/api/src/main/java/com/server/api/repository/CoordinatesRepository.java b/api/src/main/java/com/server/api/repository/CoordinatesRepository.java new file mode 100644 index 0000000..e83ffe3 --- /dev/null +++ b/api/src/main/java/com/server/api/repository/CoordinatesRepository.java @@ -0,0 +1,15 @@ +// filepath: +// /Users/andre/Desktop/PrintAuftrag/api/src/main/java/com/server/api/repository/CoordinatesRepository.java +package com.server.api.repository; + +import com.server.api.models.Coordinates; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; +import java.util.List; + +@Repository +public interface CoordinatesRepository extends JpaRepository { + void deleteByVorlageId(Long vorlageId); // Diese Methode hinzufügen + List findAllByVorlageId(Long vorlageId); + +} diff --git a/api/src/main/java/com/server/api/repository/RollenDruckDupliRepository.java b/api/src/main/java/com/server/api/repository/RollenDruckDupliRepository.java new file mode 100644 index 0000000..d6329d0 --- /dev/null +++ b/api/src/main/java/com/server/api/repository/RollenDruckDupliRepository.java @@ -0,0 +1,10 @@ +package com.server.api.repository; + + +import com.server.api.models.RollenDruckDupliArtikel; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface RollenDruckDupliRepository extends JpaRepository { +} diff --git a/api/src/main/java/com/server/api/repository/VorlageFlaechendruckRepository.java b/api/src/main/java/com/server/api/repository/VorlageFlaechendruckRepository.java new file mode 100644 index 0000000..b8aa76a --- /dev/null +++ b/api/src/main/java/com/server/api/repository/VorlageFlaechendruckRepository.java @@ -0,0 +1,10 @@ +// filepath: +// /Users/andre/Desktop/api/src/main/java/com/server/api/repository/VorlagenFlaechendruckRepository.java +package com.server.api.repository; + +import com.server.api.models.VorlageFlaechendruck; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.stereotype.Repository; + +@Repository +public interface VorlageFlaechendruckRepository extends JpaRepository {} diff --git a/api/src/main/java/com/server/api/repository/VorlageRollendruckRepository.java b/api/src/main/java/com/server/api/repository/VorlageRollendruckRepository.java new file mode 100644 index 0000000..91cdec7 --- /dev/null +++ b/api/src/main/java/com/server/api/repository/VorlageRollendruckRepository.java @@ -0,0 +1,15 @@ +package com.server.api.repository; + +import com.server.api.models.VorlageRollendruck; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +public interface VorlageRollendruckRepository extends JpaRepository { + + @Query("SELECT v FROM VorlageRollendruck v") + List getAllVorlagenRollendruck(); +} diff --git a/api/src/main/java/com/server/api/service/AuthService.java b/api/src/main/java/com/server/api/service/AuthService.java new file mode 100644 index 0000000..2ea4889 --- /dev/null +++ b/api/src/main/java/com/server/api/service/AuthService.java @@ -0,0 +1,61 @@ +package com.server.api.service; + +import com.server.api.models.User; +import com.server.api.repository.AuthRepository; +import com.server.api.utils.HashUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.Optional; + + +@Service +public class AuthService { + + private static final Logger logger = LoggerFactory.getLogger(AuthService.class); + + @Autowired + private AuthRepository authRepository; + + public boolean signUp(User user) { + + try { + // Validate user object (you can implement your own validation logic) + if (user == null || user.getName() == null || user.getPassword() == null) { + throw new IllegalArgumentException("User or required fields cannot be null"); + } + + // Hash password + user.setPassword(HashUtil.hash(user.getPassword())); + + // Save the user to the repository + authRepository.save(user); + return true; + } catch (Exception e) { + logger.error("Error during sign up: ", e); // Log the exception + return false; + } + } + + public boolean signIn(User user) { + if (user == null || user.getName() == null || user.getPassword() == null) { + return false; + } + + try { + Optional maybe = Optional.ofNullable(authRepository.findByName(user.getName())); + if (maybe.isEmpty()) return false; + + User stored = maybe.get(); + // Hash das eingegebene Passwort und vergleiche mit gespeichertem Hash + String hashedInput = HashUtil.hash(user.getPassword()); + return hashedInput.equals(stored.getPassword()); + } catch (Exception e) { + logger.error("Error during sign in: ", e); + return false; + } + } + +} diff --git a/api/src/main/java/com/server/api/service/flaechendruck/VorlageFlaechendruckService.java b/api/src/main/java/com/server/api/service/flaechendruck/VorlageFlaechendruckService.java new file mode 100644 index 0000000..689fa91 --- /dev/null +++ b/api/src/main/java/com/server/api/service/flaechendruck/VorlageFlaechendruckService.java @@ -0,0 +1,102 @@ +package com.server.api.service.flaechendruck; + +import com.server.api.models.Coordinates; +import com.server.api.models.VorlageFlaechendruck; +import com.server.api.repository.CoordinatesRepository; +import com.server.api.repository.VorlageFlaechendruckRepository; +import java.util.ArrayList; +import java.util.List; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class VorlageFlaechendruckService { + + @Autowired private VorlageFlaechendruckRepository vorlageFlaechendruckRepository; + + @Autowired private CoordinatesRepository coordinatesRepository; + + @Transactional + public void deleteVorlageFlaechendruck(Long id) { + coordinatesRepository.deleteByVorlageId(id); + vorlageFlaechendruckRepository.deleteById(id); + } + + public void saveVorlageWithCoordinates(VorlageFlaechendruck vorlageDTO) { + // Erstellen der VorlageFlaechendruck-Entität + VorlageFlaechendruck vorlage = new VorlageFlaechendruck(); + vorlage.setPrinter(vorlageDTO.getPrinter()); + vorlage.setProduct_type(vorlageDTO.getProduct_type()); + vorlage.setHeight(vorlageDTO.getHeight()); + vorlage.setWidth(vorlageDTO.getWidth()); + + // Speichern der VorlageFlaechendruck-Entität + VorlageFlaechendruck savedVorlage = vorlageFlaechendruckRepository.save(vorlage); + + // Speichern der zugehörigen Coordinates + for (Coordinates coordinate : vorlageDTO.getCoordinates()) { + coordinate.setVorlage(savedVorlage); // Fremdschlüssel setzen + coordinatesRepository.save(coordinate); + } + + } + + public void alterVorlage(VorlageFlaechendruck vorlage){ + VorlageFlaechendruck vorlageInDB = vorlageFlaechendruckRepository.findById(vorlage.getId()).get(); + vorlageInDB.setPrinter(vorlage.getPrinter()); + vorlageInDB.setProduct_type(vorlage.getProduct_type()); + vorlageInDB.setHeight(vorlage.getHeight()); + vorlageInDB.setWidth(vorlage.getWidth()); + + List coordinates = coordinatesRepository.findAll(); + List coordinatesToDelete = new ArrayList<>(); + for (Coordinates coordinate : coordinates) { + if (coordinate.getVorlage().getId() == vorlageInDB.getId()) { + coordinatesToDelete.add(coordinate); + } + } + coordinatesRepository.deleteAll(coordinatesToDelete); + + for (Coordinates coordinate : vorlage.getCoordinates()) { + coordinate.setVorlage(vorlageInDB); // Fremdschlüssel setzen + coordinatesRepository.save(coordinate); + } + + vorlageFlaechendruckRepository.save(vorlageInDB); + } + + public List getAllVorlagen() { + List vorlagen = vorlageFlaechendruckRepository.findAll(); + List coordinates = coordinatesRepository.findAll(); + + List vorlagenWithCoordinates = new ArrayList<>(); + for (VorlageFlaechendruck vorlage : vorlagen) { + List vorlageCoordinates = new ArrayList<>(); + for (Coordinates coordinate : coordinates) { + if (coordinate.getVorlage().getId() == vorlage.getId()) { + vorlageCoordinates.add(coordinate); + } + } + vorlage.setCoordinates(vorlageCoordinates); + vorlagenWithCoordinates.add(vorlage); + } + + return vorlagenWithCoordinates; + } + + public VorlageFlaechendruck getVorlageById(long id) { + VorlageFlaechendruck vorlage = vorlageFlaechendruckRepository.findById(id).get(); + List coordinates = coordinatesRepository.findAll(); + + List vorlageCoordinates = new ArrayList<>(); + for (Coordinates coordinate : coordinates) { + if (coordinate.getVorlage().getId() == vorlage.getId()) { + vorlageCoordinates.add(coordinate); + } + } + vorlage.setCoordinates(vorlageCoordinates); + + return vorlage; + } +} diff --git a/api/src/main/java/com/server/api/service/flaechendruck/VorlagenFlaechendruckPDFGeneratorService.java b/api/src/main/java/com/server/api/service/flaechendruck/VorlagenFlaechendruckPDFGeneratorService.java new file mode 100644 index 0000000..4e4c0a6 --- /dev/null +++ b/api/src/main/java/com/server/api/service/flaechendruck/VorlagenFlaechendruckPDFGeneratorService.java @@ -0,0 +1,330 @@ +package com.server.api.service.flaechendruck; + +import com.itextpdf.io.image.ImageData; +import com.itextpdf.io.image.ImageDataFactory; +import com.itextpdf.kernel.geom.PageSize; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.layout.Document; +import com.itextpdf.layout.element.Image; +import com.server.api.models.Coordinates; +import com.server.api.models.VorlageFlaechendruck; +import java.awt.image.BufferedImage; +import java.io.File; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Stream; +import javax.imageio.ImageIO; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class VorlagenFlaechendruckPDFGeneratorService { + @Autowired + private VorlageFlaechendruckService vorlageFlaechendruckService; + + public void generateFlaechendruckPDFs(long vorlageId, String outputDir, String uploadDir) + throws Exception { + VorlageFlaechendruck vorlage = vorlageFlaechendruckService.getVorlageById(vorlageId); + + String imagePath = uploadDir; + + Path outputFolder = Paths.get(outputDir); + if (!Files.exists(outputFolder)) { + Files.createDirectories(outputFolder); + } + PdfGenerator pdfGenerator = new PdfGenerator(imagePath, outputDir); + pdfGenerator.generatePdf(vorlage); + } + + public class PdfGenerator { + private String imagePath; + private String pdfPath; + + public class Article { + private String imagePath; + private String color; + private String productType; + + public Article(String imagePath, String color, String productType) { + this.imagePath = imagePath; + this.color = color; + this.productType = productType; + } + + public String getImagePath() { + return imagePath; + } + + public String getColor() { + return color; + } + + public String getProductType() { + return productType; + } + } + + public class ColorCount { + private String color; + private int count; + + public ColorCount(String color, int count) { + this.color = color; + this.count = count; + } + + public String getColor() { + return color; + } + + public int getCount() { + return count; + } + + public void incrementCount() { + this.count++; + } + + public void decrementCount() { + this.count--; + } + } + + public PdfGenerator(String imagePath, String pdfPath) { + this.imagePath = imagePath; + this.pdfPath = pdfPath; + } + + public void generatePdf(VorlageFlaechendruck vorlage) throws Exception { + + // Bild hinzufügen + File folder = new File(imagePath); + File[] files = folder.listFiles(); + files = getAllFilePaths(imagePath); + + // delete hidden files beginning with . + files = Arrays.stream(files).filter(file -> !file.getName().startsWith(".")).toArray(File[]::new); + + // only keep files with the correct product type + files = Arrays.stream(files) + .filter(file -> file.getName().startsWith(vorlage.getProduct_type().toLowerCase())) + .toArray(File[]::new); + + // Sort files by color + Article[] articles = createArticlesOrder(files, vorlage.getCoordinates().size()); + + Coordinates[] coordinates = vorlage.getCoordinates().toArray(new Coordinates[0]); + float height = vorlage.getHeight(); + float width = vorlage.getWidth(); + int count = 0; + for (int i = 0; i < articles.length; i += coordinates.length) { + count++; + + ArrayList colorCounts = new ArrayList<>(); + float pointsPerMm = 2.83465f; + try (PdfWriter writer = new PdfWriter(pdfPath + count + ".pdf"); + PdfDocument pdf = new PdfDocument(writer); + Document document = new Document( + pdf, new PageSize(width * pointsPerMm, height * pointsPerMm))) { + for (int j = 0; j < coordinates.length; j++) { + if (i + j >= articles.length) { + break; + } + boolean found = false; + for (ColorCount colorCount : colorCounts) { + if (colorCount.getColor().equals(articles[i + j].getColor())) { + colorCount.incrementCount(); + found = true; + break; + } + } + if (!found) { + colorCounts.add(new ColorCount(articles[i + j].getColor(), 1)); + } + + String imagePathTemp = articles[i + j].getImagePath(); + ImageData imageData = ImageDataFactory.create(imagePathTemp); + Image image = new Image(imageData); + + double x = coordinates[j].getX() * pointsPerMm; + double y = coordinates[j].getY() * pointsPerMm; + double rotation = coordinates[j].getRotation(); + + float[] imageSize = getImageSize(imagePathTemp); + + float imageWidth = pixelsToMm(imageSize[0]); + float imageHeight = pixelsToMm(imageSize[1]); + + if (rotation != 0) { + image.setRotationAngle(Math.toRadians(rotation)); + // Swap width and height if the image is rotated + } + + image.scaleToFit(imageWidth * pointsPerMm, imageHeight * pointsPerMm); + + // Adjust x and y to center the image at the given coordinates + if (rotation != 0) { + float temp = imageWidth; + imageWidth = imageHeight; + imageHeight = temp; + } + + double adjustedX = x - (imageWidth * pointsPerMm) / 2; + double adjustedY = y - (imageHeight * pointsPerMm) / 2; + + image.setFixedPosition((float) adjustedX, (float) adjustedY); + document.add(image); + } + } + String saveName = compressString(colorCounts, articles[0].getProductType()); + + // Datei umbenennen + String tempFileName = pdfPath + count + ".pdf"; + File tempFile = new File(tempFileName); + File renamedFile = new File(pdfPath + count + ". " + saveName + ".pdf"); + if (!tempFile.renameTo(renamedFile)) { + throw new Exception("Internal Server Error"); + } + } + } + + private Article[] createArticlesOrder(File[] files, int cors_len) throws Exception { + ArrayList
articles = new ArrayList<>(); + ArrayList colorCounts = new ArrayList<>(); + + for (int i = 0; i < files.length; i++) { + articles.add(getArticleProperties(files[i].getAbsolutePath())); + boolean found = false; + for (ColorCount colorCount : colorCounts) { + if (colorCount.getColor().equals(articles.get(i).getColor())) { + colorCount.incrementCount(); + found = true; + break; + } + } + if (!found) { + colorCounts.add(new ColorCount(articles.get(i).getColor(), 1)); + } + } + ArrayList
sortedArticles = new ArrayList<>(); + + int index = 0; + while (index < colorCounts.size()) { + if (colorCounts.get(index).getCount() >= cors_len) { + int num_added = 0; + ArrayList
removeArticles = new ArrayList<>(); + for (Article artc : articles) { + if (artc.getColor().equals(colorCounts.get(index).getColor())) { + sortedArticles.add(artc); + removeArticles.add(artc); + colorCounts.get(index).decrementCount(); + num_added++; + } + if (num_added >= cors_len) { + break; + } + } + articles.removeAll(removeArticles); + } else { + index++; + } + } + // sort articles by color + articles.sort((a, b) -> a.getColor().compareTo(b.getColor())); + + for (Article artc : articles) { + + sortedArticles.add(artc); + + } + + // transform ArrayList to Array + Article[] sortedArticlesArray = new Article[sortedArticles.size()]; + for (int i = 0; i < sortedArticles.size(); i++) { + sortedArticlesArray[i] = sortedArticles.get(i); + } + return sortedArticlesArray; + } + + private File[] getAllFilePaths(String directoryPath) throws Exception { + try (Stream paths = Files.walk(Paths.get(directoryPath))) { + return paths + .filter(Files::isRegularFile) // Behalte nur reguläre Dateien (keine Verzeichnisse) + .map(Path::toFile) // Konvertiere Path zu File + .toArray(File[]::new); // Sammle die Ergebnisse in ein Array + } + } + + private String compressString(ArrayList colorCounts, String productType) { + StringBuilder result = new StringBuilder(); + for (ColorCount colorCount : colorCounts) { + result.append(colorCount.getCount()); // Häufigkeit + result.append(productType); // Produkttyp + result.append(" "); + result.append(colorCount.getColor()); // Farbe + result.append(" "); + } + // Entferne das letzte Leerzeichen + if (result.length() > 0) { + result.setLength(result.length() - 1); + } + + return result.toString(); + } + + private Article getArticleProperties(String imagePath) throws Exception { + String filename = new File(imagePath).getName(); + String farben = "abcdefghijklmnopqrstuvwxyz"; + + String productType = ""; + int i = 0; + + while (true) { + if (Character.isDigit(filename.charAt(i))) { + break; + } + productType += filename.charAt(i); + i++; + } + while (true) { + if (!Character.isDigit(filename.charAt(i))) { + break; + } + i++; + } + + String color = String.valueOf(filename.charAt(i)); + + // for colors with two letters + if (farben.indexOf(filename.charAt(i + 1)) != -1) { + color += String.valueOf(filename.charAt(i + 1)); + } + + // Prüfe, ob der Buchstabe in der Liste der Farben enthalten ist + if (farben.indexOf(color) == -1 && color.length() != 2) { + throw new Exception("Nicht alle Bilder enthalten Farbbuchstaben!"); + } + + return new Article(imagePath, color, productType); + } + + private float[] getImageSize(String imagePath) { + try { + BufferedImage image = ImageIO.read(new File(imagePath)); + return new float[] { image.getWidth(), image.getHeight() }; + } catch (Exception e) { + + return new float[] { 0, 0 }; + } + } + + private float pixelsToMm(float pixels) { + float dpi = 300; + return pixels / dpi * 25.4f; + } + } +} diff --git a/api/src/main/java/com/server/api/service/rollendruck/VorlageRollendruckPDFGeneratorService.java b/api/src/main/java/com/server/api/service/rollendruck/VorlageRollendruckPDFGeneratorService.java new file mode 100644 index 0000000..f8b36fb --- /dev/null +++ b/api/src/main/java/com/server/api/service/rollendruck/VorlageRollendruckPDFGeneratorService.java @@ -0,0 +1,398 @@ +package com.server.api.service.rollendruck; + +import java.io.File; +import java.net.MalformedURLException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.stream.Stream; + +import javax.imageio.ImageIO; + +import com.itextpdf.kernel.pdf.canvas.PdfCanvas; +import com.itextpdf.layout.element.Paragraph; +import com.server.api.models.VorlageRollendruck; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import com.itextpdf.io.image.ImageData; +import com.itextpdf.io.image.ImageDataFactory; +import com.itextpdf.kernel.geom.PageSize; +import com.itextpdf.kernel.pdf.PdfDocument; +import com.itextpdf.kernel.pdf.PdfWriter; +import com.itextpdf.layout.Document; +import com.itextpdf.layout.element.Image; + +import java.util.List; +import java.awt.image.BufferedImage; + +@Service +public class VorlageRollendruckPDFGeneratorService { + final float pointsPerMm = 2.83464567f; + final float numberPointsForMm = 28.3465f; + + @Autowired VorlageRollendruckService vorlageRollendruckService; + + + private static class ImageObject { + float x1; + float y1; + float x2; + float y2; + String imageGroup; + boolean isRotated; + String imagePath; + + + public ImageObject(float x1, float y1, float x2, float y2, String imageGroup, boolean isRotated, + String imagePath) { + this.x1 = x1; + this.y1 = y1; + this.x2 = x2; + this.y2 = y2; + this.imageGroup = imageGroup; + this.isRotated = isRotated; + + this.imagePath = imagePath; + } + + public float getX1() { + return x1; + } + + public float getY1() { + return y1; + } + + public float getX2() { + return x2; + } + + public float getY2() { + return y2; + } + + + public boolean isRotated() { + return isRotated; + } + + public String getImagePath() { + return imagePath; + } + + } + + private static class ImageGroup { + String name; + List images; + float size; + + public ImageGroup(String name) { + this.name = name; + this.images = new ArrayList<>(); + } + + public String getName() { + return name; + } + + public List getImages() { + return images; + } + + public float getSize() { + return size; + } + + public void setSize(float size) { + this.size = size; + } + } + + private class Surface { + float height; + float width; + List placedImages; + public Surface(float height, float width) { + this.height = height; + this.width = width; + this.placedImages = new ArrayList<>(); + } + private List placeImageGroup(List imagePaths, float width, float height, String imageGroupName) throws MalformedURLException { + List notPlacedImages = new ArrayList<>(); + float cursor_x = 0; + float cursor_y = height * pointsPerMm; + for (String singleImagePath : imagePaths) { + ImageData imageData = ImageDataFactory.create(singleImagePath); + Image image = new Image(imageData); + float[] imageSize = getImageSize(singleImagePath); + float imageWidth = pixelsToMm(imageSize[0]); + float imageHeight = pixelsToMm(imageSize[1]); + image.scaleToFit(imageWidth * pointsPerMm, imageHeight * pointsPerMm); + boolean foundFreePos = false; + boolean isRotated = false; + float abstand = numberPointsForMm * pointsPerMm; + float updated_x = 0; + float updated_y = 0; + boolean isEndOfPage = false; + + while (!foundFreePos) { + // cursor runs through the whole page + if (cursor_x + 1 > width * pointsPerMm) { + cursor_x = 0; + if (cursor_y - 1 < 0) { + isEndOfPage = true; + break; + } else { + cursor_y -= 1; + } + } else { + cursor_x += 1; + } + // check if cursor is not on an image + if (isOverlap(cursor_x, imageWidth, cursor_y - imageHeight * pointsPerMm, imageHeight)) { + continue; + } + + updated_x = cursor_x + abstand; + updated_y = cursor_y - imageHeight * pointsPerMm - abstand; + + + boolean isLeftNeighbourOverlap = false; + for (ImageObject placedImage : placedImages) { + if(placedImage.y1 < cursor_y && placedImage.y2 > updated_y && placedImage.x2 >= cursor_x - 30 && placedImage.x1 < cursor_x - 30){ + isLeftNeighbourOverlap = true; + break; + } + + } + if (isLeftNeighbourOverlap) { + continue; + } + + + if (isOverlap(updated_x, imageWidth, updated_y, imageHeight)) { + continue; + } + + if (updated_x + imageWidth * pointsPerMm < width * pointsPerMm + && updated_y> 0) { + + foundFreePos = true; + } + } + + if (isEndOfPage) { + notPlacedImages.add(singleImagePath); + continue; + } + + if (isRotated) { + placedImages.add(new ImageObject(updated_x, updated_y, + updated_x + imageHeight * pointsPerMm, + updated_y + imageWidth * pointsPerMm, imageGroupName, true, singleImagePath)); + image.setFixedPosition(updated_x, updated_y); + image.setRotationAngle(Math.toRadians(90)); + } else { + placedImages.add(new ImageObject(updated_x, updated_y, + updated_x + imageWidth * pointsPerMm, + updated_y + imageHeight * pointsPerMm, imageGroupName, false, singleImagePath)); + image.setFixedPosition(updated_x, updated_y); + } + + + } + return notPlacedImages; + } + + private boolean isOverlap(float cursor_x, float imageWidth, float cursor_y, float imageHeight) { + for (ImageObject imageObject : placedImages) { + if (imageObject.getX1() < cursor_x + imageWidth * pointsPerMm + && imageObject.getX2() > cursor_x + && imageObject.getY1() < cursor_y + imageHeight * pointsPerMm + && imageObject.getY2() > cursor_y) { + return true; + } + } + return false; + } + + } + + public void generatePdf(long vorlageId, String outputDir, String uploadPath) throws Exception { + // get width and height from vorlage + VorlageRollendruck vorlage = vorlageRollendruckService.getVorlageById(vorlageId); + float width = vorlage.getWidth(); + float height = vorlage.getHeight(); + + List articleTypes = splitArticleTypes(vorlage.getArticleTypes()); + List dupliStrings = vorlageRollendruckService.getAllDupliStrings(); + + + Path outputFolder = Paths.get(outputDir); + if (!Files.exists(outputFolder)) { + Files.createDirectories(outputFolder); + } + String imagePath = uploadPath; + File folder = new File(imagePath); + File[] files = folder.listFiles(); + files = getAllFilePaths(imagePath); + + // delete hidden files beginning with . + files = Arrays.stream(getAllFilePaths(imagePath)).filter(file -> !file.getName().startsWith(".")).toArray(File[]::new); + + + // create subgroups + List imageGroups = new ArrayList<>(); + for (File file : files) { + String name = file.getName(); + int i = 0; + while (i < name.length() && !Character.isDigit(name.charAt(i)) && name.charAt(i) != ' ') { + i++; + } + boolean isGroup = false; + for (ImageGroup imageGroup : imageGroups) { + if (imageGroup.getName().equals(name.substring(0, i).trim())) { + imageGroup.images.add(file.getAbsolutePath()); + isGroup = true; + break; + } + } + if (!isGroup) { + String groupName = name.substring(0, i).trim(); + ImageGroup imageGroup = new ImageGroup(groupName); + imageGroup.images.add(file.getAbsolutePath()); + float[] imageSizes = getImageSize(file.getAbsolutePath()); + float imageSize = pixelsToMm(imageSizes[0]) * pixelsToMm(imageSizes[1]); + imageGroup.setSize(imageSize); + imageGroups.add(imageGroup); + } + + } + + // delete imageGroups that are not in articleTypes + imageGroups.removeIf(imageGroup -> !articleTypes.contains(imageGroup.getName())); + + // sort groups by size + imageGroups.sort((g1, g2) -> Float.compare(g2.getSize(), g1.getSize())); + + // if imageGroup contains dupliStrings double the entries + for (ImageGroup imageGroup : imageGroups) { + if (dupliStrings.contains(imageGroup.getName())) { + List images = new ArrayList<>(imageGroup.getImages()); + for (String image : images) { + imageGroup.getImages().add(image); + } + } + } + + List surfaces = new ArrayList<>(); + surfaces.add(new Surface(height, width)); + int surfaceIndex = 0; + for (ImageGroup imageGroup : imageGroups) { + List notPlacedImages = surfaces.get(surfaceIndex).placeImageGroup(imageGroup.getImages(), width, height, imageGroup.getName()); + while (!notPlacedImages.isEmpty()) { + surfaces.add(new Surface(height, width)); + surfaceIndex++; + notPlacedImages = surfaces.get(surfaceIndex).placeImageGroup(notPlacedImages, width, height, imageGroup.getName()); + } + + } + + + int count = 1; + for (Surface surface: surfaces){ + try (PdfWriter writer = new PdfWriter(outputDir + count + ". " + vorlage.getPrinter() + " Rollendruck.pdf"); + PdfDocument pdf = new PdfDocument(writer); + Document document = new Document( + pdf, new PageSize(width * pointsPerMm, height * pointsPerMm))) { + for (ImageObject imageObject : surface.placedImages) { + ImageData imageData = ImageDataFactory.create(imageObject.getImagePath()); + Image image = getImage(imageObject, imageData); + + // Add image at fixed position + image.setFixedPosition(imageObject.getX1(), imageObject.getY1()); + document.add(image); + + // Add text under image + String caption = imageObject.getImagePath().substring(imageObject.getImagePath().lastIndexOf("/") + 1); + + + float textX = ((imageObject.getX2() - imageObject.getX1())/2) + imageObject.getX1() + caption.length()*2; // Centered + float textY = imageObject.getY1() - numberPointsForMm - 10; // Adjust to place below image + + PdfDocument pdfDocument = document.getPdfDocument(); + PdfCanvas pdfCanvas = new PdfCanvas(pdfDocument.getLastPage()); + pdfCanvas.saveState(); + + // Apply a horizontal mirroring transformation + pdfCanvas.concatMatrix(-1, 0, 0, 1, textX * 2, 0); + + Paragraph paragraph = new Paragraph(caption) + .setFontSize(11) + .setFixedPosition(textX, textY, 200); + + // Add the mirrored paragraph to the document + document.add(paragraph); + + pdfCanvas.restoreState(); + } + + } + count++; + } + + + } + + private static List splitArticleTypes(String articleTypes) { + if (articleTypes == null || articleTypes.isEmpty()) { + return new ArrayList<>(); + } + // Split the string by commas and trim whitespace from each part + articleTypes = articleTypes.replaceAll(" ", ""); // Remove all whitespace + return Arrays.asList(articleTypes.split(",")); + } + + private static Image getImage(ImageObject imageObject, ImageData imageData) { + Image image = new Image(imageData); + if (imageObject.isRotated()) { + image.setRotationAngle(Math.toRadians(90)); + image.setFixedPosition(imageObject.getX1(), imageObject.getY1()); + image.scaleToFit(imageObject.getY2() - imageObject.getY1(), + imageObject.getX2() - imageObject.getX1()); + } else { + image.setFixedPosition(imageObject.getX1(), imageObject.getY1()); + image.scaleToFit(imageObject.getX2() - imageObject.getX1(), + imageObject.getY2() - imageObject.getY1()); + } + return image; + } + + private File[] getAllFilePaths(String directoryPath) throws Exception { + try (Stream paths = Files.walk(Paths.get(directoryPath))) { + return paths + .filter(Files::isRegularFile) // Behalte nur reguläre Dateien (keine Verzeichnisse) + .map(Path::toFile) // Konvertiere Path zu File + .toArray(File[]::new); // Sammle die Ergebnisse in ein Array + } + } + + private float[] getImageSize(String imagePath) { + try { + BufferedImage image = ImageIO.read(new File(imagePath)); + return new float[] { image.getWidth(), image.getHeight() }; + } catch (Exception e) { + System.out.println("Error reading image: " + e.getMessage()); + } + return new float[] { 0, 0 }; + } + + private float pixelsToMm(float pixels) { + float dpi = 300; + return pixels / dpi * 25.4f; + } +} diff --git a/api/src/main/java/com/server/api/service/rollendruck/VorlageRollendruckService.java b/api/src/main/java/com/server/api/service/rollendruck/VorlageRollendruckService.java new file mode 100644 index 0000000..0cf081f --- /dev/null +++ b/api/src/main/java/com/server/api/service/rollendruck/VorlageRollendruckService.java @@ -0,0 +1,79 @@ +package com.server.api.service.rollendruck; + +import com.server.api.models.RollenDruckDupliArtikel; +import com.server.api.models.VorlageRollendruck; +import com.server.api.repository.RollenDruckDupliRepository; +import com.server.api.repository.VorlageRollendruckRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class VorlageRollendruckService { + + @Autowired + private VorlageRollendruckRepository vorlageRollendruckRepository; + + @Autowired + private RollenDruckDupliRepository rollenDruckDupliRepository; + + public void createRollendruckVorlage(VorlageRollendruck vorlage) { + System.out.println("Das sie die Artiekl "+ vorlage.getArticleTypes()); + vorlageRollendruckRepository.save(vorlage); + } + + public List getAllVorlagen() { + return vorlageRollendruckRepository.getAllVorlagenRollendruck(); + } + + public void alterVorlage(VorlageRollendruck vorlage) { + VorlageRollendruck vorlageInDB = vorlageRollendruckRepository.findById(vorlage.getId()).orElseThrow(() -> new RuntimeException("Vorlage not found")); + vorlageInDB.setPrinter(vorlage.getPrinter()); + vorlageInDB.setHeight(vorlage.getHeight()); + vorlageInDB.setWidth(vorlage.getWidth()); + vorlageInDB.setArticleTypes(vorlage.getArticleTypes()); + vorlageRollendruckRepository.save(vorlageInDB); + + } + + public void deleteVorlageRollendruck(Long id) { + VorlageRollendruck vorlage = vorlageRollendruckRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Vorlage not found")); + vorlageRollendruckRepository.delete(vorlage); + } + + public void addDupliArtikel(RollenDruckDupliArtikel artikel) { + // save the article + rollenDruckDupliRepository.save(artikel); + } + + public List getAllDupliArtikel() { + // print all articles + for (RollenDruckDupliArtikel artikel : rollenDruckDupliRepository.findAll()) { + System.out.println(artikel); + } + + return rollenDruckDupliRepository.findAll(); + } + + public void deleteDupliArtikel(Long id) { + RollenDruckDupliArtikel artikel = rollenDruckDupliRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Artikel not found")); + rollenDruckDupliRepository.delete(artikel); + } + + public VorlageRollendruck getVorlageById(Long id) { + return vorlageRollendruckRepository.findById(id) + .orElseThrow(() -> new RuntimeException("Vorlage not found")); + + } + + public List getAllDupliStrings(){ + List dupliArtikelList = rollenDruckDupliRepository.findAll(); + return dupliArtikelList.stream() + .map(RollenDruckDupliArtikel::getProduct_type) + .toList(); + } + +} diff --git a/api/src/main/java/com/server/api/utils/HashUtil.java b/api/src/main/java/com/server/api/utils/HashUtil.java new file mode 100644 index 0000000..308f530 --- /dev/null +++ b/api/src/main/java/com/server/api/utils/HashUtil.java @@ -0,0 +1,25 @@ +package com.server.api.utils; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +public class HashUtil { + + public static String hash(String input) { + try { + MessageDigest digest = MessageDigest.getInstance("SHA-256"); + byte[] hashBytes = digest.digest(input.getBytes()); + StringBuilder hexString = new StringBuilder(); + for (byte b : hashBytes) { + String hex = Integer.toHexString(0xff & b); + if (hex.length() == 1) { + hexString.append('0'); + } + hexString.append(hex); + } + return hexString.toString(); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException("Hashing algorithm not found", e); + } + } +} diff --git a/api/src/main/resources/application.properties b/api/src/main/resources/application.properties new file mode 100644 index 0000000..c87e609 --- /dev/null +++ b/api/src/main/resources/application.properties @@ -0,0 +1,16 @@ +spring.application.name=api + +spring.datasource.url=${DB_URL:jdbc:postgresql://localhost:5432/postgres} +spring.datasource.username=${DB_USERNAME:postgres} +spring.datasource.password=${DB_PASSWORD:mysecretpassword} +spring.datasource.driver-class-name=org.postgresql.Driver +spring.jpa.hibernate.ddl-auto=update +spring.jpa.show-sql=true +spring.datasource.hikari.minimum-idle=5 +spring.datasource.hikari.maximum-pool-size=10 +spring.datasource.hikari.idle-timeout=30000 +spring.datasource.hikari.pool-name=HikariPool-1 +spring.datasource.hikari.max-lifetime=1800000 +spring.datasource.hikari.connection-timeout=30000 +spring.servlet.multipart.enabled=false +spring.profiles.active=development \ No newline at end of file diff --git a/api/src/test/java/com/server/api/ApiApplicationTests.java b/api/src/test/java/com/server/api/ApiApplicationTests.java new file mode 100644 index 0000000..e8ef53d --- /dev/null +++ b/api/src/test/java/com/server/api/ApiApplicationTests.java @@ -0,0 +1,13 @@ +package com.server.api; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ApiApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/api/src/test/java/com/server/api/FlaechendruckTests.java b/api/src/test/java/com/server/api/FlaechendruckTests.java new file mode 100644 index 0000000..ebdfd7c --- /dev/null +++ b/api/src/test/java/com/server/api/FlaechendruckTests.java @@ -0,0 +1,12 @@ +package com.server.api; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class FlaechendruckTests { + @Test + void testRightColorOrdering() { + + } +} diff --git a/api/target/api-0.0.1-SNAPSHOT.jar b/api/target/api-0.0.1-SNAPSHOT.jar new file mode 100644 index 0000000..be0981b Binary files /dev/null and b/api/target/api-0.0.1-SNAPSHOT.jar differ diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..02ae1ce --- /dev/null +++ b/deploy.sh @@ -0,0 +1,18 @@ +-#!/bin/bash + +# Setze das aktuelle Verzeichnis auf den Speicherort des Skripts +cd "$(dirname "$0")" + +#echo "Pull git" +#git pull + +echo "Stopping existing Docker containers..." +docker-compose down --remove-orphans + +echo "Rebuilding and starting Docker containers..." +docker-compose up --build -d + +echo "Clean Docker Var Lib" +docker system prune -a + +echo "Done!" diff --git a/doc/img/system_flow.png b/doc/img/system_flow.png new file mode 100644 index 0000000..3ba307f Binary files /dev/null and b/doc/img/system_flow.png differ diff --git a/doc/img/template_view_1.jpg b/doc/img/template_view_1.jpg new file mode 100644 index 0000000..dfdb9db Binary files /dev/null and b/doc/img/template_view_1.jpg differ diff --git a/doc/img/template_view_2.jpg b/doc/img/template_view_2.jpg new file mode 100644 index 0000000..d945c5f Binary files /dev/null and b/doc/img/template_view_2.jpg differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..ad32bbf --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,64 @@ +version: "3.8" + +services: + springboot-app: + build: + context: ./api + dockerfile: Dockerfile + ports: + - "127.0.0.1:8080:8080" + depends_on: + - postgres-db + environment: + - DB_URL=${DB_URL} + - DB_USERNAME=${DB_USERNAME} + - DB_PASSWORD=${DB_PASSWORD} + - SHARED_FOLDER=/shared + - NEXT_SERVER_URL=${NEXT_SERVER_URL} + volumes: + - /root/storage:/shared + restart: always + networks: + - app-network + + postgres-db: + image: postgres:15 + container_name: postgres-db + environment: + POSTGRES_DB: ${POSTGRES_DB} + POSTGRES_USER: ${POSTGRES_USER} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + ports: + - "127.0.0.1:5432:5432" + volumes: + - .postgres-data:/var/lib/postgresql/data + restart: always + networks: + - app-network + + nextjs-app: + build: + context: ./frontend + dockerfile: Dockerfile + ports: + - "127.0.0.1:3050:3050" + working_dir: /app + command: "npm run start" + depends_on: + - springboot-app + environment: + - NEXT_SERVER_API_URL=${NEXT_SERVER_API_URL} + - BILLBEE_PASSWORD=${BILLBEE_PASSWORD} + - BILLBEE_USERNAME=${BILLBEE_USERNAME} + - BILLBEE_API_KEY=${BILLBEE_API_KEY} + - SESSION_SECRET=${SESSION_SECRET} + restart: always + networks: + - app-network + +volumes: + postgres-data: + +networks: + app-network: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..f0bcd4c --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,22 @@ +FROM node:22-alpine + +# Set working directory +WORKDIR /app + +# Copy package.json und package-lock.json zuerst, um den Cache zu nutzen +COPY package.json package-lock.json ./ + +# Installiere alle Abhängigkeiten +RUN npm install + +# Kopiere den Rest der Anwendung +COPY . . + +# Baue die Next.js-Anwendung +RUN npm run build + +# Expose the port your Next.js app runs on +EXPOSE 3050 + +# Start the Next.js server +CMD ["npm", "run", "start"] \ No newline at end of file diff --git a/frontend/app/dashboard/generateFlaechendruck/action.ts b/frontend/app/dashboard/generateFlaechendruck/action.ts new file mode 100644 index 0000000..c338319 --- /dev/null +++ b/frontend/app/dashboard/generateFlaechendruck/action.ts @@ -0,0 +1,44 @@ +"use server"; +//import { promises as fs } from "fs"; +const apiUrl = process.env.NEXT_SERVER_API_URL; +//const sharedFolder = process.env.SHARED_FOLDER; + +export async function getVorlagen() { + try { + const response = await fetch( + `${apiUrl}/vorlageFlaechendruck/getAllFlaechendruckVorlagen` + ); + if (!response.ok) { + console.error("Response status:", response.status); + throw new Error("Failed to fetch vorlagen"); + } + return await response.json(); + } catch (error) { + console.error("Error fetching vorlagen:", error); + throw new Error("An error occurred while fetching vorlagen."); + } +} + +export async function generateFlaechendruck(formData: FormData) { + const vorlageIds = formData.getAll("vorlageIds") as string[]; + const requestBody = { + rootDirectory: "Orders", + vorlageIds: vorlageIds, + }; + + const response = await fetch(`${apiUrl}/vorlageFlaechendruck/generate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + cache: "no-store", + }); + if (!response.ok) { + const body = await response.text().catch(() => null); + const msg = body ? body : `HTTP ${response.status}`; + return { message: msg, status: false }; + } else { + return { message: "Erfolgreiche Flächendruckgenerierung!", status: true }; + } +} diff --git a/frontend/app/dashboard/generateFlaechendruck/page.tsx b/frontend/app/dashboard/generateFlaechendruck/page.tsx new file mode 100644 index 0000000..e21b389 --- /dev/null +++ b/frontend/app/dashboard/generateFlaechendruck/page.tsx @@ -0,0 +1,173 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { getVorlagen, generateFlaechendruck } from "./action"; +import { toast, Bounce } from "react-toastify"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; + +export default function Page() { + const [vorlagen, setVorlagen] = useState< + { id: number; name: string; printer: string }[] + >([]); + const [selectedVorlagen, setSelectedVorlagen] = useState([]); + const [isUploading, setIsUploading] = useState(false); // Zustand für den Upload + + // API-Aufruf, um Vorlagen zu laden + useEffect(() => { + async function fetchData() { + try { + const data = await getVorlagen(); // Server-Action aufrufen + setVorlagen( + data.map( + (item: { id: number; product_type: string; printer: string }) => ({ + id: item.id, + name: item.product_type, + printer: item.printer, + }) + ) + ); + } catch (err) { + console.error(err); + } + } + + fetchData(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (selectedVorlagen.length === 0) { + return; + } + + const formData = new FormData(); + selectedVorlagen.forEach((id) => { + formData.append("vorlageIds", id.toString()); + }); + setIsUploading(true); + const res = await generateFlaechendruck(formData); + if (res.status) { + toast.success(res.message, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } else { + toast.error(res.message, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } + + setIsUploading(false); + }; + + return ( +
+ <> +
+ + + Generierung Flächendruck + + Wählen sie Vorlagen zur Generierung aus. + + + + + {!isUploading ? ( +
+
+ +
+ {vorlagen.map((vorlage) => ( +
+ { + const id = parseInt(e.target.value, 10); + if (e.target.checked) { + setSelectedVorlagen((prev) => [...prev, id]); + } else { + setSelectedVorlagen((prev) => + prev.filter((v) => v !== id) + ); + } + }} + className="mt-1" + /> + +
+ ))} +
+
+
+ ) : ( +
+
+ + Loading... +
+
+ )} +
+ + {!isUploading ? : null} + +
+
+ +
+ ); +} diff --git a/frontend/app/dashboard/generateRollendruck/action.ts b/frontend/app/dashboard/generateRollendruck/action.ts new file mode 100644 index 0000000..77cdfff --- /dev/null +++ b/frontend/app/dashboard/generateRollendruck/action.ts @@ -0,0 +1,45 @@ +"use server"; +//import { promises as fs } from "fs"; +const apiUrl = process.env.NEXT_SERVER_API_URL; +//const sharedFolder = process.env.SHARED_FOLDER; + +export async function getVorlagen() { + try { + const response = await fetch( + `${apiUrl}/VorlageRollenDruck/getAllRollendruckVorlagen` + ); + if (!response.ok) { + console.error("Response status:", response.status); + throw new Error("Failed to fetch vorlagen"); + } + return await response.json(); + } catch (error) { + console.error("Error fetching vorlagen:", error); + throw new Error("An error occurred while fetching vorlagen."); + } +} + +export async function uploadFiles(formData: FormData) { + const vorlageIds = formData.getAll("vorlageIds") as string[]; + + // JSON-Daten erstellen + const requestBody = { + rootDirectory: "Rollendruck", + vorlageIds: vorlageIds, + }; + const response = await fetch(`${apiUrl}/VorlageRollenDruck/generate`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(requestBody), + cache: "no-store", + }); + if (!response.ok) { + const body = await response.text().catch(() => null); + const msg = body ? body : `HTTP ${response.status}`; + return { message: msg, status: false }; + } else { + return { message: "Erfolgreiche Rollendruckgenerierung!", status: true }; + } +} diff --git a/frontend/app/dashboard/generateRollendruck/page.tsx b/frontend/app/dashboard/generateRollendruck/page.tsx new file mode 100644 index 0000000..184ea16 --- /dev/null +++ b/frontend/app/dashboard/generateRollendruck/page.tsx @@ -0,0 +1,180 @@ +"use client"; + +import { useState, useEffect } from "react"; +import { getVorlagen, uploadFiles } from "./action"; +import { toast, Bounce } from "react-toastify"; + +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Label } from "@/components/ui/label"; + +export default function Page() { + const [vorlagen, setVorlagen] = useState< + { id: number; printer: string; height: number; width: number }[] + >([]); + const [selectedVorlagen, setSelectedVorlagen] = useState([]); + const [isUploading, setIsUploading] = useState(false); // Zustand für den Upload + + // API-Aufruf, um Vorlagen zu laden + useEffect(() => { + async function fetchData() { + try { + const data = await getVorlagen(); // Server-Action aufrufen + setVorlagen( + data.map( + (item: { + id: number; + printer: string; + height: number; + width: number; + }) => ({ + id: item.id, + printer: item.printer, + height: item.height, + width: item.width, + }) + ) + ); + } catch (err) { + console.error("Error fetching vorlagen:", err); + } + } + + fetchData(); + }, []); + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault(); + + if (selectedVorlagen.length === 0) { + alert("Please select at least one Vorlage before submitting."); + return; + } + + const formData = new FormData(); + selectedVorlagen.forEach((id) => { + formData.append("vorlageIds", id.toString()); // Fügt die ausgewählten Vorlagen-IDs hinzu + }); + setIsUploading(true); + const res = await uploadFiles(formData); + if (res.status) { + toast.success(res.message, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } else { + toast.error(res.message, { + position: "bottom-right", + autoClose: 5000, + hideProgressBar: false, + closeOnClick: false, + pauseOnHover: true, + draggable: true, + progress: undefined, + theme: "light", + transition: Bounce, + }); + } + setIsUploading(false); + }; + + return ( +
+ <> +
+ + + Generierung Rollendruck + + Wählen sie Vorlagen zur Generierung aus. + + + + + {!isUploading ? ( +
+
+ +
+ {vorlagen.map((vorlage) => ( +
+ { + const id = parseInt(e.target.value, 10); + if (e.target.checked) { + setSelectedVorlagen((prev) => [...prev, id]); + } else { + setSelectedVorlagen((prev) => + prev.filter((v) => v !== id) + ); + } + }} + className="mt-1" + /> + +
+ ))} +
+
+
+ ) : ( +
+
+ + Loading... +
+
+ )} +
+ + {!isUploading ? : null} + +
+
+ +
+ ); +} diff --git a/frontend/app/dashboard/layout.tsx b/frontend/app/dashboard/layout.tsx new file mode 100644 index 0000000..03956d2 --- /dev/null +++ b/frontend/app/dashboard/layout.tsx @@ -0,0 +1,13 @@ +import AdminPanelLayout from "@/components/admin-panel/admin-panel-layout"; + +export default function DashboardLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + +
{children}
+
+ ); +} diff --git a/frontend/app/dashboard/page.tsx b/frontend/app/dashboard/page.tsx new file mode 100644 index 0000000..9b6a366 --- /dev/null +++ b/frontend/app/dashboard/page.tsx @@ -0,0 +1,14 @@ +export default function Page() { + return ( +
+
+

+ Willkommen im Dashboard +

+

+ Navigieren Sie über das Menü auf der linken Seite zu den Anwendungen. +

+
+
+ ); +} diff --git a/frontend/app/dashboard/updateOrders/page.tsx b/frontend/app/dashboard/updateOrders/page.tsx new file mode 100644 index 0000000..34ccc64 --- /dev/null +++ b/frontend/app/dashboard/updateOrders/page.tsx @@ -0,0 +1,9 @@ +import UploadForm from "@/components/upload-form/page"; + +export default async function Home() { + return ( +
+ +
+ ); +} diff --git a/frontend/app/dashboard/vorlagenFlaechendruck/action.ts b/frontend/app/dashboard/vorlagenFlaechendruck/action.ts new file mode 100644 index 0000000..0eb260a --- /dev/null +++ b/frontend/app/dashboard/vorlagenFlaechendruck/action.ts @@ -0,0 +1,42 @@ +"use server"; + +const apiUrl = process.env.NEXT_SERVER_API_URL; + +export async function getTableData() { + try { + const res = await fetch( + `${apiUrl}/vorlageFlaechendruck/getAllFlaechendruckVorlagen` + ); + if (!res.ok) { + console.error("Response status:", res.status); + } + + const data: { + id: number; + product_type: string; + height: number; + width: number; + printer: string; + coordinates: { + x: number; + y: number; + rotation: number; + }[]; + }[] = await res.json(); + return data; + } catch (error) { + console.error("Error fetching vorlagen:", error); + throw new Error("An error occurred while fetching vorlagen."); + } +} +export async function deleteVorlageFlaechendruck(id: number) { + const res = await fetch(`${apiUrl}/vorlageFlaechendruck/delete/${id}`, { + method: "DELETE", + }); + + if (!res.ok) { + throw new Error("Failed to delete item"); + } + + return; +} diff --git a/frontend/app/dashboard/vorlagenFlaechendruck/components/action.ts b/frontend/app/dashboard/vorlagenFlaechendruck/components/action.ts new file mode 100644 index 0000000..73269bd --- /dev/null +++ b/frontend/app/dashboard/vorlagenFlaechendruck/components/action.ts @@ -0,0 +1,83 @@ +"use server"; + +const apiUrl = process.env.NEXT_SERVER_API_URL; + +export async function createVorlage(formData: FormData) { + const printer = formData.get("printer") as string; + const product_type = formData.get("product_type") as string; + const height = Number(formData.get("height")); + const width = Number(formData.get("width")); + const coordinatesJson = formData.get("coordinates") as string; + const coordinates = JSON.parse(coordinatesJson); + + const payload = { + printer, + product_type, + height, + width, + coordinates, + }; + + try { + const response = await fetch( + `${apiUrl}/vorlageFlaechendruck/createWithCoordinates`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + cache: "no-store", + } + ); + + if (!response.ok) { + console.error("Response status:", response.status); + throw new Error("Failed to submit form"); + } + } catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } +} + +export async function alterVorlage(formData: FormData) { + const id = Number(formData.get("id")); + const printer = formData.get("printer") as string; + const product_type = formData.get("product_type") as string; + const height = Number(formData.get("height")); + const width = Number(formData.get("width")); + const coordinatesJson = formData.get("coordinates") as string; + const coordinates = JSON.parse(coordinatesJson); + + const payload = { + id, + printer, + product_type, + height, + width, + coordinates, + }; + + try { + const response = await fetch( + `${apiUrl}/vorlageFlaechendruck/alterVorlage`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + cache: "no-store", + } + ); + + if (!response.ok) { + console.error("Response status:", response.status); + throw new Error("Failed to submit form"); + } + } catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } +} diff --git a/frontend/app/dashboard/vorlagenFlaechendruck/components/addDialog.tsx b/frontend/app/dashboard/vorlagenFlaechendruck/components/addDialog.tsx new file mode 100644 index 0000000..e1011e5 --- /dev/null +++ b/frontend/app/dashboard/vorlagenFlaechendruck/components/addDialog.tsx @@ -0,0 +1,276 @@ +"use Client"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator"; + +import { Plus } from "lucide-react"; + + +import { useFieldArray } from "react-hook-form"; + +import { createVorlage } from "./action"; + +import { useState } from "react"; + +export default function AddDialog({ onNewEntry }: { onNewEntry: () => void }) { + const [open, setOpen] = useState(false) + const formSchema = z.object({ + Drucker: z.string().min(1, { + message: "Druckername muss mindestens 2 Zeichen lang sein.", + }), + Artikeltyp: z.string().min(1, { + message: "Artikeltyp muss mindestens 2 Zeichen lang sein.", + }), + Höhe: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Höhe muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Höhe muss mindestens 1 sein.", + }) + ), + Breite: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Breite muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Breite muss mindestens 1 sein.", + }) + ), + Koordinaten: z.array( + z.object({ + x: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number().min(0, { + message: "X-Koordinate muss mindestens 0 sein.", + }) + ), + y: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number().min(0, { + message: "Y-Koordinate muss mindestens 0 sein.", + }) + ), + rotation: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number().min(0, { + message: "Rotation muss mindestens 0 sein.", + }) + ), + }) + ).min(1, { + message: "Es muss mindestens eine Koordinate angegeben werden.", + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + Drucker: "", + Artikeltyp: "", + Höhe: 0, + Breite: 0, + Koordinaten: [{ x: 0, y: 0, rotation: 0 }], + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "Koordinaten", + }); + + async function onSubmit(values: z.infer) { + const formData = new FormData(); + + formData.append("printer", values.Drucker); + formData.append("product_type", values.Artikeltyp); + formData.append("height", values.Höhe.toString()); + formData.append("width", values.Breite.toString()); + formData.append("coordinates", JSON.stringify(values.Koordinaten)); + try { + await createVorlage(formData); + setOpen(false); + form.reset(); + onNewEntry(); + } + catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } + + } + return ( +
+ + + + + + + Vorlage hinzufügen +
+ + ( + + Drucker Name + + + + + Der Name des Druckers, auf dem die Vorlage gedruckt wird. + + + + )} + /> + ( + + Artikel Typ + + + + + Der Name des Artikels, der auf der Vorlage gedruckt wird. + + + + )} + /> + ( + + Höhe + + + + + Die Höhe der Vorlage in Millimeter. + + + + )} + /> + ( + + Breite + + + + + + Die Breite der Vorlage in Millimeter. + + + + )} + /> +
+ Koordinaten: + + {fields.map((field, index) => ( +
+ ( + + X-Koordinate + + + + + + )} + /> + ( + + Y-Koordinate + + + + + + )} + /> + ( + + Rotation + + + + + + )} + /> + + +
+ ))} + + +
+ + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/vorlagenFlaechendruck/components/alterDialog.tsx b/frontend/app/dashboard/vorlagenFlaechendruck/components/alterDialog.tsx new file mode 100644 index 0000000..c5802a5 --- /dev/null +++ b/frontend/app/dashboard/vorlagenFlaechendruck/components/alterDialog.tsx @@ -0,0 +1,294 @@ +"use Client"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Separator } from "@/components/ui/separator"; + +import { Plus } from "lucide-react"; + + +import { useFieldArray } from "react-hook-form"; + +import { alterVorlage } from "./action"; + +import { useState } from "react"; + +type TableData = { + id: number; + product_type: string; + height: number; + width: number; + printer: string; + coordinates: { x: number; y: number; rotation: number }[]; +}; + +type AlterDialogProps = { + onNewEntry: () => void; + data: TableData; +}; + +export default function AlterDialog({ onNewEntry, data }: AlterDialogProps) { + const [open, setOpen] = useState(false) + const formSchema = z.object({ + Drucker: z.string().min(1, { + message: "Druckername muss mindestens 2 Zeichen lang sein.", + }), + Artikeltyp: z.string().min(1, { + message: "Artikeltyp muss mindestens 2 Zeichen lang sein.", + }), + Höhe: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Höhe muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Höhe muss mindestens 1 sein.", + }) + ), + Breite: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Breite muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Breite muss mindestens 1 sein.", + }) + ), + Koordinaten: z.array( + z.object({ + x: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number().min(0, { + message: "X-Koordinate muss mindestens 0 sein.", + }) + ), + y: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number().min(0, { + message: "Y-Koordinate muss mindestens 0 sein.", + }) + ), + rotation: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number().min(0, { + message: "Rotation muss mindestens 0 sein.", + }) + ), + }) + ).min(1, { + message: "Es muss mindestens eine Koordinate angegeben werden.", + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + Drucker: data.printer, + Artikeltyp: data.product_type, + Höhe: data.height, + Breite: data.width, + Koordinaten: data.coordinates.map((coord) => ({ + x: coord.x, + y: coord.y, + rotation: coord.rotation, + })), + }, + }); + + const { fields, append, remove } = useFieldArray({ + control: form.control, + name: "Koordinaten", + }); + + async function onSubmit(values: z.infer) { + const formData = new FormData(); + formData.append("id", data.id.toString()); + formData.append("printer", values.Drucker); + formData.append("product_type", values.Artikeltyp); + formData.append("height", values.Höhe.toString()); + formData.append("width", values.Breite.toString()); + formData.append("coordinates", JSON.stringify(values.Koordinaten)); + try { + await alterVorlage(formData); + setOpen(false); + form.reset(); + onNewEntry(); + } + catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } + + } + return ( +
+ + + + + + + Vorlage bearbeiten +
+ + ( + + Drucker Name + + + + + Der Name des Druckers, auf dem die Vorlage gedruckt wird. + + + + )} + /> + ( + + Artikel Typ + + + + + Der Name des Artikels, der auf der Vorlage gedruckt wird. + + + + )} + /> + ( + + Höhe + + + + + Die Höhe der Vorlage in Millimeter. + + + + )} + /> + ( + + Breite + + + + + + Die Breite der Vorlage in Millimeter. + + + + )} + /> +
+ Koordinaten: + + {fields.map((field, index) => ( +
+ ( + + X-Koordinate + + + + + + )} + /> + ( + + Y-Koordinate + + + + + + )} + /> + ( + + Rotation + + + + + + )} + /> + + +
+ ))} + + +
+ + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/vorlagenFlaechendruck/page.tsx b/frontend/app/dashboard/vorlagenFlaechendruck/page.tsx new file mode 100644 index 0000000..fbcfc12 --- /dev/null +++ b/frontend/app/dashboard/vorlagenFlaechendruck/page.tsx @@ -0,0 +1,199 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Ellipsis } from "lucide-react" +import { getTableData, deleteVorlageFlaechendruck } from "./action"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Button } from "@/components/ui/button" +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" + +import AddDialog from "./components/addDialog"; +import AlterDialog from "./components/alterDialog"; + +type TableData = { + id: number; + product_type: string; + height: number; + width: number; + printer: string; + coordinates: { x: number; y: number; rotation: number }[]; +}; +export default function Page() { + const [data, setData] = useState([]); + useEffect(() => { + async function fetchData() { + try { + const result = await getTableData(); + setData(result); + } catch (error) { + console.error("Fehler beim Laden der Daten:", error); + } + } + + fetchData(); + }, []); + + const handleNewEntry = async () => { + try { + const result = await getTableData(); // Daten erneut abrufen + setData(result); // Tabelle aktualisieren + } catch (error) { + console.error("Fehler beim Aktualisieren der Daten:", error); + } + }; + + const deleteEntry = async (id: number) => { + try { + await deleteVorlageFlaechendruck(id); + await handleNewEntry(); + } + catch (error) { + console.error("Fehler beim Löschen der Daten:", error); + } + }; + + + return ( +
+
+ {/* Table Section */} + + Vorlagen Flächendruck + + + Drucker + Artikel Typ + Höhe + Breite + Koordinaten + + + + + + + + {data.map((item) => ( + + {item.printer} + {item.product_type} + {item.height} + {item.width} + + + + + + + + Koordinaten + + Hier sind die Koordinaten für die ausgewählte Vorlage. X und Y Werte in Millimetern. + + +
+ Koordinaten Tabelle + + + X + Y + Rotation + + + + {item.coordinates.map((coordi) => ( + + {coordi.x} + {coordi.y} + {coordi.rotation}° + + ))} + +
+ + + + + + + + + + + + + + + { + event.stopPropagation(); // Verhindert das Schließen des Dropdown-Menüs + }} + > + Eintrag Löschen + + + + Sind Sie sich sicher? + + Diese Aktion ist permanent und kann nicht rückgängig gemacht werden. + + + + Abbrechen + { + deleteEntry(item.id); // Eintrag löschen + }} + > + Löschen + + + + + + + + + + ))} + + +
+
+ ); +} diff --git a/frontend/app/dashboard/vorlagenRollendruck/action.ts b/frontend/app/dashboard/vorlagenRollendruck/action.ts new file mode 100644 index 0000000..fc48f6a --- /dev/null +++ b/frontend/app/dashboard/vorlagenRollendruck/action.ts @@ -0,0 +1,69 @@ +"use server"; + +const apiUrl = process.env.NEXT_SERVER_API_URL; + +export async function getTableData() { + try { + const res = await fetch( + `${apiUrl}/VorlageRollenDruck/getAllRollendruckVorlagen` + ); + if (!res.ok) { + console.error("Response status:", res.status); + } + + const data: { + id: number; + height: number; + width: number; + printer: string; + articleTypes: string; + }[] = await res.json(); + return data; + } catch (error) { + console.error("Error fetching vorlagen:", error); + throw new Error("An error occurred while fetching vorlagen."); + } +} +export async function deleteVorlageRollendruck(id: number) { + const res = await fetch(`${apiUrl}/VorlageRollenDruck/delete/${id}`, { + method: "DELETE", + }); + + if (!res.ok) { + throw new Error("Failed to delete item"); + } + + return; +} + +export async function getTableDataDupli() { + try { + const res = await fetch( + `${apiUrl}/VorlageRollenDruck/getAllDupliArtikel` + ); + if (!res.ok) { + console.error("Response status:", res.status); + } + + const data: { + id: number; + product_type: string; + }[] = await res.json(); + return data; + } catch (error) { + console.error("Error fetching vorlagen:", error); + throw new Error("An error occurred while fetching vorlagen."); + } +} + +export async function deleteDupliEntry(id: number) { + const res = await fetch(`${apiUrl}/VorlageRollenDruck/deleteDupli/${id}`, { + method: "DELETE", + }); + + if (!res.ok) { + throw new Error("Failed to delete item"); + } + + return; +} \ No newline at end of file diff --git a/frontend/app/dashboard/vorlagenRollendruck/components/action.ts b/frontend/app/dashboard/vorlagenRollendruck/components/action.ts new file mode 100644 index 0000000..8994aa1 --- /dev/null +++ b/frontend/app/dashboard/vorlagenRollendruck/components/action.ts @@ -0,0 +1,107 @@ +"use server"; + +const apiUrl = process.env.NEXT_SERVER_API_URL; + +export async function createVorlage(formData: FormData) { + const printer = formData.get("printer") as string; + const height = Number(formData.get("height")); + const width = Number(formData.get("width")); + const articleTypes = formData.get("articleTypes") as string; + + const payload = { + printer, + height, + width, + articleTypes + }; + + try { + const response = await fetch( + `${apiUrl}/VorlageRollenDruck/createRollenDruckVorlage`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + cache: "no-store", + } + ); + + if (!response.ok) { + console.error("Response status:", response.status); + throw new Error("Failed to submit form"); + } + } catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } +} + +export async function alterVorlage(formData: FormData) { + const id = Number(formData.get("id")); + const printer = formData.get("printer") as string; + const height = Number(formData.get("height")); + const width = Number(formData.get("width")); + const articleTypes = formData.get("articleTypes") as string; + + const payload = { + id, + printer, + height, + width, + articleTypes + }; + + try { + const response = await fetch( + `${apiUrl}/VorlageRollenDruck/alterVorlage`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + cache: "no-store", + } + ); + + if (!response.ok) { + console.error("Response status:", response.status); + throw new Error("Failed to submit form"); + } + } catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } +} + +export async function addDupliArtikel(fromData: FormData) { + const artikelTyp = fromData.get("ArtikelTyp") as string; + + const payload = { + product_type: artikelTyp, + }; + + try { + const response = await fetch( + `${apiUrl}/VorlageRollenDruck/addDupliArtikel`, + { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify(payload), + cache: "no-store", + } + ); + + if (!response.ok) { + console.error("Response status:", response.status); + throw new Error("Failed to submit form"); + } + } catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } +} diff --git a/frontend/app/dashboard/vorlagenRollendruck/components/addDialog.tsx b/frontend/app/dashboard/vorlagenRollendruck/components/addDialog.tsx new file mode 100644 index 0000000..dd29bf0 --- /dev/null +++ b/frontend/app/dashboard/vorlagenRollendruck/components/addDialog.tsx @@ -0,0 +1,177 @@ +"use Client"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" + + +import { createVorlage } from "./action"; + +import { useState } from "react"; + +export default function AddDialog({ onNewEntry }: { onNewEntry: () => void }) { + const [open, setOpen] = useState(false) + const formSchema = z.object({ + Drucker: z.string().min(1, { + message: "Druckername muss mindestens 2 Zeichen lang sein.", + }), + Höhe: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Höhe muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Höhe muss mindestens 1 sein.", + }) + ), + Breite: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Breite muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Breite muss mindestens 1 sein.", + }) + ), + ArtikelTypen: z + .string() + .regex(/^(\s*\w+\s*)(,\s*\w+\s*)*$/, { + message: "Bitte geben Sie eine durch Kommas getrennte Liste ein (z.B. te, bs, s, s, s)", + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + Drucker: "", + Höhe: 0, + Breite: 0, + ArtikelTypen: "", + }, + }); + + + async function onSubmit(values: z.infer) { + const formData = new FormData(); + + formData.append("printer", values.Drucker); + formData.append("height", values.Höhe.toString()); + formData.append("width", values.Breite.toString()); + formData.append("articleTypes", values.ArtikelTypen); + try { + await createVorlage(formData); + setOpen(false); + form.reset(); + onNewEntry(); + } + catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } + + } + return ( +
+ + + + + + + Vorlage hinzufügen +
+ + ( + + Drucker Name + + + + + Der Name des Druckers, auf dem die Vorlage gedruckt wird. + + + + )} + /> + ( + + Höhe + + + + + Die Höhe der Vorlage in Millimeter. + + + + )} + /> + ( + + Breite + + + + + + Die Breite der Vorlage in Millimeter. + + + + )} + /> + ( + + Artikel Typen + + + + + Geben Sie die Artikeltypen als durch Kommas getrennte Liste ein (z.B. te, bs, s, s, s). + + + + )} + /> + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/vorlagenRollendruck/components/addDialogDupli.tsx b/frontend/app/dashboard/vorlagenRollendruck/components/addDialogDupli.tsx new file mode 100644 index 0000000..5187a75 --- /dev/null +++ b/frontend/app/dashboard/vorlagenRollendruck/components/addDialogDupli.tsx @@ -0,0 +1,99 @@ +"use Client"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" + + +import {addDupliArtikel} from "./action"; + +import { useState } from "react"; +import {Plus} from "lucide-react"; + +export default function AddDialog({ onNewEntry }: { onNewEntry: () => void }) { + const [open, setOpen] = useState(false) + const formSchema = z.object({ + ArtikelTyp: z.string().min(1, { + message: "Druckername muss mindestens 2 Zeichen lang sein.", + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + ArtikelTyp: "", + }, + }); + + + async function onSubmit(values: z.infer) { + const formData = new FormData(); + + formData.append("ArtikelTyp", values.ArtikelTyp); + try { + await addDupliArtikel(formData); + setOpen(false); + form.reset(); + onNewEntry(); + } + catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } + + } + return ( +
+ + + + + + + ArtikelTyp hinzufügen +
+ + ( + + ArtikelTyp + + + + + Der Typ des Artikels, der dupliziert gedruckt werden soll. + + + + )} + /> + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/vorlagenRollendruck/components/alterDialog.tsx b/frontend/app/dashboard/vorlagenRollendruck/components/alterDialog.tsx new file mode 100644 index 0000000..88c77e9 --- /dev/null +++ b/frontend/app/dashboard/vorlagenRollendruck/components/alterDialog.tsx @@ -0,0 +1,190 @@ +"use Client"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { zodResolver } from "@hookform/resolvers/zod" +import { useForm } from "react-hook-form" +import { z } from "zod" +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" + +import { alterVorlage } from "./action"; + +import { useState } from "react"; + +type TableData = { + id: number; + printer: string; + height: number; + width: number; + articleTypes: string; +}; + +type AlterDialogProps = { + onNewEntry: () => void; + data: TableData; +}; + +export default function AlterDialog({ onNewEntry, data }: AlterDialogProps) { + const [open, setOpen] = useState(false) + const formSchema = z.object({ + Drucker: z.string().min(1, { + message: "Druckername muss mindestens 2 Zeichen lang sein.", + }), + Höhe: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Höhe muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Höhe muss mindestens 1 sein.", + }) + ), + Breite: z.preprocess( + (value) => (typeof value === "string" ? parseFloat(value) : value), + z.number({ + invalid_type_error: "Breite muss eine gültige Zahl sein. Bspw. 1.5 .", + }).min(1, { + message: "Breite muss mindestens 1 sein.", + }) + ), + ArtikelTypen: z + .string() + .regex(/^(\s*\w+\s*)(,\s*\w+\s*)*$/, { + message: "Bitte geben Sie eine durch Kommas getrennte Liste ein (z.B. te, bs, s, s, s)", + }), + }); + + const form = useForm>({ + resolver: zodResolver(formSchema), + defaultValues: { + Drucker: data.printer, + Höhe: data.height, + Breite: data.width, + ArtikelTypen: data.articleTypes, + }, + }); + + + + async function onSubmit(values: z.infer) { + const formData = new FormData(); + formData.append("id", data.id.toString()); + formData.append("printer", values.Drucker); + formData.append("height", values.Höhe.toString()); + formData.append("width", values.Breite.toString()); + formData.append("articleTypes", values.ArtikelTypen); + try { + await alterVorlage(formData); + setOpen(false); + form.reset(); + onNewEntry(); + } + catch (error) { + console.error("Error submitting form:", error); + throw new Error("An error occurred while submitting the form."); + } + + } + return ( +
+ + + + + + + Vorlage bearbeiten +
+ + ( + + Drucker Name + + + + + Der Name des Druckers, auf dem die Vorlage gedruckt wird. + + + + )} + /> + ( + + Höhe + + + + + Die Höhe der Vorlage in Millimeter. + + + + )} + /> + ( + + Breite + + + + + + Die Breite der Vorlage in Millimeter. + + + + )} + /> + ( + + Artikel Typen + + + + + Geben Sie eine durch Kommas getrennte Liste von Artikeltypen ein (z.B. te, bs, s, s, s). + + + + )} + /> + + + +
+
+
+
+ ); +} \ No newline at end of file diff --git a/frontend/app/dashboard/vorlagenRollendruck/page.tsx b/frontend/app/dashboard/vorlagenRollendruck/page.tsx new file mode 100644 index 0000000..18c1ad0 --- /dev/null +++ b/frontend/app/dashboard/vorlagenRollendruck/page.tsx @@ -0,0 +1,227 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { Ellipsis } from "lucide-react" +import { getTableData, deleteVorlageRollendruck, getTableDataDupli, deleteDupliEntry } from "./action"; +import { + Table, + TableBody, + TableCaption, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Button } from "@/components/ui/button" + +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu" +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, + AlertDialogTrigger, +} from "@/components/ui/alert-dialog" + +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog" + +import AddDialog from "./components/addDialog"; +import AlterDialog from "./components/alterDialog"; +import AddDialogDupli from "./components/addDialogDupli"; + +type TableData = { + id: number; + printer: string; + height: number; + width: number; + articleTypes: string; +}; +export default function Page() { + const [data, setData] = useState([]); + const [dupliData, setDupliData] = useState<{ id: number; product_type: string }[]>([]); + useEffect(() => { + async function fetchData() { + try { + const result = await getTableData(); + setData(result || []); + const dupliResult = await getTableDataDupli(); + setDupliData(Array.isArray(dupliResult) ? dupliResult : []); + } catch (error) { + console.error("Fehler beim Laden der Daten:", error); + } + } + fetchData(); + }, []); + + const handleNewEntry = async () => { + try { + const result = await getTableData(); // Daten erneut abrufen + setData(result); // Tabelle aktualisieren + } catch (error) { + console.error("Fehler beim Aktualisieren der Daten:", error); + } + }; + + const handleNewEntry2 = async () => { + try { + const result = await getTableDataDupli(); // Daten erneut abrufen + setDupliData(result) // Tabelle aktualisieren + } catch (error) { + console.error("Fehler beim Aktualisieren der Daten:", error); + } + }; + + const deleteEntry = async (id: number) => { + try { + await deleteVorlageRollendruck(id); + await handleNewEntry(); + } + catch (error) { + console.error("Fehler beim Löschen der Daten:", error); + } + }; + + const deleteEntryDupli = async (id: number) => { + try { + await deleteDupliEntry(id); + await handleNewEntry2(); + } + catch (error) { + console.error("Fehler beim Löschen der Daten:", error); + } + } + + + return ( +
+
+
+ + Artikel für Duplizierung + + + Artikel Typ + + + + + + + {dupliData.map((item) => ( + + {item.product_type} + + + + + ))} + +
+
+ +
+ + Vorlagen Rollendruck + + + Drucker + Höhe + Breite + Artikel Typen + + + + + + + {data.map((item) => ( + + {item.printer} + {item.height} + {item.width} + + + + + + + + Artikel Typen + + Hier sind die Artikel Typen gelistet, die von der Vorlage unterstützt werden. + + + {item.articleTypes} + + + + + + + + + + + + + + { + event.stopPropagation(); // Verhindert das Schließen des Dropdown-Menüs + }} + > + Eintrag Löschen + + + + Sind Sie sich sicher? + + Diese Aktion ist permanent und kann nicht rückgängig gemacht werden. + + + + Abbrechen + { + deleteEntry(item.id); // Eintrag löschen + }} + > + Löschen + + + + + + + + + + ))} + +
+
+
+
+ ); +} diff --git a/frontend/app/favicon.ico b/frontend/app/favicon.ico new file mode 100644 index 0000000..718d6fe Binary files /dev/null and b/frontend/app/favicon.ico differ diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000..f127db3 --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,122 @@ +@import "tailwindcss"; +@import "tw-animate-css"; + +@custom-variant dark (&:is(.dark *)); + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); +} + +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.147 0.004 49.25); + --card: oklch(1 0 0); + --card-foreground: oklch(0.147 0.004 49.25); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.147 0.004 49.25); + --primary: oklch(0.216 0.006 56.043); + --primary-foreground: oklch(0.985 0.001 106.423); + --secondary: oklch(0.97 0.001 106.424); + --secondary-foreground: oklch(0.216 0.006 56.043); + --muted: oklch(0.97 0.001 106.424); + --muted-foreground: oklch(0.553 0.013 58.071); + --accent: oklch(0.97 0.001 106.424); + --accent-foreground: oklch(0.216 0.006 56.043); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.923 0.003 48.717); + --input: oklch(0.923 0.003 48.717); + --ring: oklch(0.709 0.01 56.259); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0.001 106.423); + --sidebar-foreground: oklch(0.147 0.004 49.25); + --sidebar-primary: oklch(0.216 0.006 56.043); + --sidebar-primary-foreground: oklch(0.985 0.001 106.423); + --sidebar-accent: oklch(0.97 0.001 106.424); + --sidebar-accent-foreground: oklch(0.216 0.006 56.043); + --sidebar-border: oklch(0.923 0.003 48.717); + --sidebar-ring: oklch(0.709 0.01 56.259); +} + +.dark { + --background: oklch(0.147 0.004 49.25); + --foreground: oklch(0.985 0.001 106.423); + --card: oklch(0.216 0.006 56.043); + --card-foreground: oklch(0.985 0.001 106.423); + --popover: oklch(0.216 0.006 56.043); + --popover-foreground: oklch(0.985 0.001 106.423); + --primary: oklch(0.923 0.003 48.717); + --primary-foreground: oklch(0.216 0.006 56.043); + --secondary: oklch(0.268 0.007 34.298); + --secondary-foreground: oklch(0.985 0.001 106.423); + --muted: oklch(0.268 0.007 34.298); + --muted-foreground: oklch(0.709 0.01 56.259); + --accent: oklch(0.268 0.007 34.298); + --accent-foreground: oklch(0.985 0.001 106.423); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.553 0.013 58.071); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.216 0.006 56.043); + --sidebar-foreground: oklch(0.985 0.001 106.423); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0.001 106.423); + --sidebar-accent: oklch(0.268 0.007 34.298); + --sidebar-accent-foreground: oklch(0.985 0.001 106.423); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.553 0.013 58.071); +} + +@layer base { + * { + @apply border-border outline-ring/50; + } + body { + @apply bg-background text-foreground; + } +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000..169dc52 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,49 @@ +import type { Metadata } from "next"; +import { Geist, Geist_Mono } from "next/font/google"; +import "./globals.css"; + +import { ToastContainer, Bounce } from "react-toastify"; + +const geistSans = Geist({ + variable: "--font-geist-sans", + subsets: ["latin"], +}); + +const geistMono = Geist_Mono({ + variable: "--font-geist-mono", + subsets: ["latin"], +}); + +export const metadata: Metadata = { + title: "ERP - Wolga Kreativ", + description: "Wolga ERP", +}; + +export default function RootLayout({ + children, +}: Readonly<{ + children: React.ReactNode; +}>) { + return ( + + + + {children} + + + ); +} diff --git a/frontend/app/login/page.tsx b/frontend/app/login/page.tsx new file mode 100644 index 0000000..613c30a --- /dev/null +++ b/frontend/app/login/page.tsx @@ -0,0 +1,9 @@ +import SignInPage from "@/components/auth/signin-form"; + +export default function Page() { + return ( + <> + + + ); +} diff --git a/frontend/components.json b/frontend/components.json new file mode 100644 index 0000000..2883c94 --- /dev/null +++ b/frontend/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": true, + "tsx": true, + "tailwind": { + "config": "", + "css": "app/globals.css", + "baseColor": "stone", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/components", + "utils": "@/lib/utils", + "ui": "@/components/ui", + "lib": "@/lib", + "hooks": "@/hooks" + }, + "iconLibrary": "lucide" +} \ No newline at end of file diff --git a/frontend/components/action.ts b/frontend/components/action.ts new file mode 100644 index 0000000..1e9b8e1 --- /dev/null +++ b/frontend/components/action.ts @@ -0,0 +1,15 @@ +"use server"; + +const apiUrl = process.env.NEXT_SERVER_API_URL; + +export async function deleteVorlageFlaechendruck(id: number) { + const res = await fetch(`${apiUrl}/vorlageFlaechendruck/delete/${id}`, { + method: "DELETE", + }); + + if (!res.ok) { + throw new Error("Failed to delete item"); + } + + return; +} diff --git a/frontend/components/admin-panel/admin-panel-layout.tsx b/frontend/components/admin-panel/admin-panel-layout.tsx new file mode 100644 index 0000000..306f339 --- /dev/null +++ b/frontend/components/admin-panel/admin-panel-layout.tsx @@ -0,0 +1,46 @@ +"use client"; + +import { Footer } from "@/components/admin-panel/footer"; +import { Sidebar } from "@/components/admin-panel/sidebar"; +import { useSidebar } from "@/hooks/use-sidebar"; +import { useStore } from "@/hooks/use-store"; +import { cn } from "@/lib/utils"; + +export default function AdminPanelLayout({ + children +}: { + children: React.ReactNode; +}) { + const sidebar = useStore(useSidebar, (x) => x); + if (!sidebar) return null; + const { getOpenState, settings } = sidebar; + return ( + <> + +
+ {children} +
+