diff --git a/desktop/.gitignore b/desktop/.gitignore new file mode 100644 index 00000000..c5df67b2 --- /dev/null +++ b/desktop/.gitignore @@ -0,0 +1,50 @@ +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ +.kotlin + +### IntelliJ IDEA ### +.idea/* +!.idea/runConfigurations/ +!.idea/encodings.xml +!.idea/misc.xml +*.iws +*.iml +*.ipr + +### Eclipse ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store + +## Database related +connectionpetstore.properties + +# Log files +*.log +log.txt +*.class +*.out +*.err +tmp/ diff --git a/desktop/.mvn/wrapper/maven-wrapper.jar b/desktop/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 00000000..c1dd12f1 Binary files /dev/null and b/desktop/.mvn/wrapper/maven-wrapper.jar differ diff --git a/desktop/.mvn/wrapper/maven-wrapper.properties b/desktop/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 00000000..e077837e --- /dev/null +++ b/desktop/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.5/apache-maven-3.8.5-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.3.2/maven-wrapper-3.3.2.jar \ No newline at end of file diff --git a/desktop/README.md b/desktop/README.md new file mode 100644 index 00000000..b525f4b2 --- /dev/null +++ b/desktop/README.md @@ -0,0 +1,48 @@ +# Pet Shop Desktop (JavaFX) + +Desktop pet shop management app built with JavaFX and MySQL. + +Made by **Group 2**, Shiv, Nikitha, Alex, Harkamal. + +## Requirements + +- IntelliJ IDEA (Community or Ultimate) +- Java 17+ +- Maven (handled through IntelliJ) +- Docker and Docker Compose (for the local MySQL container) + +## Database setup (IntelliJ) + +1. Open the project in IntelliJ. +2. Open **View → Tool Windows → Services**. +3. Add a Docker connection if needed, then open the **Docker** section in Services. +4. Start the Compose stack from `docker-compose.yml` (Compose Up, or Start). +5. Confirm the `mysql` service is running. + +The container uses `mysql:8.4`, creates the `Petstoredb` database, and imports `Petstoredata.sql`. + +## App configuration + +An example connection file is provided at `connectionpetstore.properties.example`. Copy it to `connectionpetstore.properties` and edit the values to match the local database setup. + +## Run the app (IntelliJ, Maven) + +1. Open **View → Tool Windows → Maven**. +2. Click **Reload All Maven Projects** if the dependencies have not loaded yet. +3. In the Maven tool window, expand **Plugins → javafx**. +4. Double click **javafx:run**. + +Optional, run a clean first: +- In the Maven tool window, expand **Lifecycle** and run **clean**. + +## Default accounts + +On first run, the app creates a `users` table (if missing) and seeds two accounts: + +- Admin: `admin` / `admin123` +- Staff: `staff` / `staff123` + +## Notes + +- `connectionpetstore.properties` is gitignored so credentials are not committed. +- If the app cannot connect to MySQL, confirm the Compose stack is running and MySQL is available. diff --git a/desktop/connectionpetstore.properties.example b/desktop/connectionpetstore.properties.example new file mode 100644 index 00000000..426ecafb --- /dev/null +++ b/desktop/connectionpetstore.properties.example @@ -0,0 +1,3 @@ +url=jdbc:mysql://127.0.0.1:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC +user=petapp +password=petapppass \ No newline at end of file diff --git a/desktop/mvnw b/desktop/mvnw new file mode 100755 index 00000000..8a8fb228 --- /dev/null +++ b/desktop/mvnw @@ -0,0 +1,316 @@ +#!/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 +# +# https://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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /usr/local/etc/mavenrc ] ; then + . /usr/local/etc/mavenrc + fi + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + else + JAVACMD="`\\unset -f command; \\command -v java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + $MAVEN_DEBUG_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" \ + "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/desktop/mvnw.cmd b/desktop/mvnw.cmd new file mode 100644 index 00000000..1d8ab018 --- /dev/null +++ b/desktop/mvnw.cmd @@ -0,0 +1,188 @@ +@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 https://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 Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %* +if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %* +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + +FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% ^ + %JVM_CONFIG_MAVEN_PROPS% ^ + %MAVEN_OPTS% ^ + %MAVEN_DEBUG_OPTS% ^ + -classpath %WRAPPER_JAR% ^ + "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^ + %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat" +if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%"=="on" pause + +if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE% + +cmd /C exit /B %ERROR_CODE% diff --git a/desktop/pom.xml b/desktop/pom.xml new file mode 100644 index 00000000..9842bdfa --- /dev/null +++ b/desktop/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + org.example + PetShopDesktop + 1.0-SNAPSHOT + PetShopDesktop + + UTF-8 + 25.0.1 + 5.12.1 + + + + + org.openjfx + javafx-controls + ${javafx.version} + + + org.openjfx + javafx-fxml + ${javafx.version} + + + org.openjfx + javafx-web + ${javafx.version} + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + org.junit.jupiter + junit-jupiter-engine + ${junit.version} + test + + + + com.fasterxml.jackson.core + jackson-databind + 2.18.2 + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + 2.18.2 + + + com.fasterxml.jackson.core + jackson-core + 2.18.2 + + + com.fasterxml.jackson.core + jackson-annotations + 2.18.2 + + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.13.0 + + 25 + + + + org.openjfx + javafx-maven-plugin + 0.0.8 + + + + default-cli + + org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication + app + app + app + true + true + true + + + + + + + \ No newline at end of file diff --git a/desktop/src/main/java/module-info.java b/desktop/src/main/java/module-info.java new file mode 100644 index 00000000..910be91c --- /dev/null +++ b/desktop/src/main/java/module-info.java @@ -0,0 +1,40 @@ +module org.example.petshopdesktop { + requires javafx.controls; + requires javafx.fxml; + requires javafx.web; + requires java.sql; + requires java.net.http; + requires com.fasterxml.jackson.databind; + requires com.fasterxml.jackson.core; + requires com.fasterxml.jackson.annotation; + requires com.fasterxml.jackson.datatype.jsr310; + + opens org.example.petshopdesktop.DTOs to javafx.base; + opens org.example.petshopdesktop.models to javafx.base; + opens org.example.petshopdesktop to javafx.fxml; + opens org.example.petshopdesktop.controllers.dialogcontrollers to javafx.fxml; + opens org.example.petshopdesktop.controllers to javafx.fxml; + opens org.example.petshopdesktop.auth to javafx.fxml; + opens org.example.petshopdesktop.ui to javafx.fxml; + + opens org.example.petshopdesktop.api.dto.common to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.auth to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.product to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.pet to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.service to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.supplier to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.productsupplier to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.inventory to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.appointment to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.adoption to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.chat to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.sale to com.fasterxml.jackson.databind, javafx.base; + opens org.example.petshopdesktop.api.dto.user to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.employee to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.analytics to com.fasterxml.jackson.databind; + opens org.example.petshopdesktop.api.dto.purchaseorder to com.fasterxml.jackson.databind; + + exports org.example.petshopdesktop; + exports org.example.petshopdesktop.controllers; + exports org.example.petshopdesktop.auth; +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java new file mode 100644 index 00000000..f749b5b5 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/AppointmentDTO.java @@ -0,0 +1,59 @@ +package org.example.petshopdesktop.DTOs; + +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class AppointmentDTO { + + private SimpleIntegerProperty appointmentId; + + private SimpleIntegerProperty customerId; + private SimpleStringProperty customerName; + + private SimpleIntegerProperty petId; + private SimpleStringProperty petName; + + private SimpleIntegerProperty serviceId; + private SimpleStringProperty serviceName; + + private SimpleStringProperty appointmentDate; + private SimpleStringProperty appointmentTime; + private SimpleStringProperty appointmentStatus; + + // Constructor + public AppointmentDTO(int appointmentId, + int customerId, String customerName, + int petId, String petName, + int serviceId, String serviceName, + String appointmentDate, + String appointmentTime, + String appointmentStatus) { + + this.appointmentId = new SimpleIntegerProperty(appointmentId); + this.customerId = new SimpleIntegerProperty(customerId); + this.customerName = new SimpleStringProperty(customerName); + this.petId = new SimpleIntegerProperty(petId); + this.petName = new SimpleStringProperty(petName); + this.serviceId = new SimpleIntegerProperty(serviceId); + this.serviceName = new SimpleStringProperty(serviceName); + this.appointmentDate = new SimpleStringProperty(appointmentDate); + this.appointmentTime = new SimpleStringProperty(appointmentTime); + this.appointmentStatus = new SimpleStringProperty(appointmentStatus); + } + + // Getters + public int getAppointmentId() { return appointmentId.get(); } + + public int getCustomerId() { return customerId.get(); } + public String getCustomerName() { return customerName.get(); } + + public int getPetId() { return petId.get(); } + public String getPetName() { return petName.get(); } + + public int getServiceId() { return serviceId.get(); } + public String getServiceName() { return serviceName.get(); } + + public String getAppointmentDate() { return appointmentDate.get(); } + public String getAppointmentTime() { return appointmentTime.get(); } + public String getAppointmentStatus() { return appointmentStatus.get(); } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductDTO.java new file mode 100644 index 00000000..3ea081df --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductDTO.java @@ -0,0 +1,116 @@ +package org.example.petshopdesktop.DTOs; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; +import org.example.petshopdesktop.models.Product; + +/** + * The class for productDTO, all product data is store here but also gets categoryName + */ +public class ProductDTO { + private SimpleIntegerProperty prodId; + private SimpleStringProperty prodName; + private SimpleDoubleProperty prodPrice; + private SimpleIntegerProperty categoryId; //used for edit and delete + private SimpleStringProperty categoryName; + private SimpleStringProperty prodDesc; + + //constructor + public ProductDTO(int prodId, String prodName, double prodPrice, int categoryId, String categoryName, String prodDesc) { + this.prodId = new SimpleIntegerProperty(prodId); + this.prodName = new SimpleStringProperty(prodName); + this.prodPrice = new SimpleDoubleProperty(prodPrice); + this.categoryId = new SimpleIntegerProperty(categoryId); + this.categoryName = new SimpleStringProperty(categoryName); + this.prodDesc = new SimpleStringProperty(prodDesc); + } + + //getter and setters + public int getProdId() { + return prodId.get(); + } + + public SimpleIntegerProperty prodIdProperty() { + return prodId; + } + + public void setProdId(int prodId) { + this.prodId.set(prodId); + } + + public String getProdName() { + return prodName.get(); + } + + public SimpleStringProperty prodNameProperty() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName.set(prodName); + } + + public double getProdPrice() { + return prodPrice.get(); + } + + public SimpleDoubleProperty prodPriceProperty() { + return prodPrice; + } + + public void setProdPrice(double prodPrice) { + this.prodPrice.set(prodPrice); + } + + public String getCategoryName() { + return categoryName.get(); + } + + public SimpleStringProperty categoryNameProperty() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName.set(categoryName); + } + + public String getProdDesc() { + return prodDesc.get(); + } + + public SimpleStringProperty prodDescProperty() { + return prodDesc; + } + + public void setProdDesc(String prodDesc) { + this.prodDesc.set(prodDesc); + } + + public int getCategoryId() { + return categoryId.get(); + } + + public SimpleIntegerProperty categoryIdProperty() { + return categoryId; + } + + public void setCategoryId(int categoryId) { + this.categoryId.set(categoryId); + } + + /** + * Converts DTO into product for editing and deleting + * @return + */ + public Product toProduct(){ + Product product = new Product( + getProdId(), + getProdName(), + getProdPrice(), + getCategoryId(), + getProdDesc() + ); + return product; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductSupplierDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductSupplierDTO.java new file mode 100644 index 00000000..8fb481b0 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ProductSupplierDTO.java @@ -0,0 +1,83 @@ +package org.example.petshopdesktop.DTOs; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class ProductSupplierDTO { + private SimpleIntegerProperty supId; + private SimpleIntegerProperty prodId; + private SimpleStringProperty supCompany; + private SimpleStringProperty prodName; + private SimpleDoubleProperty cost; + + //constructor + public ProductSupplierDTO(int supId, int prodId, String supCompany, String prodName, double cost) { + this.supId = new SimpleIntegerProperty(supId); + this.prodId = new SimpleIntegerProperty(prodId); + this.supCompany = new SimpleStringProperty(supCompany); + this.prodName = new SimpleStringProperty(prodName); + this.cost = new SimpleDoubleProperty(cost); + } + + //getter and setters + public int getSupId() { + return supId.get(); + } + + public SimpleIntegerProperty supIdProperty() { + return supId; + } + + public void setSupId(int supId) { + this.supId.set(supId); + } + + public int getProdId() { + return prodId.get(); + } + + public SimpleIntegerProperty prodIdProperty() { + return prodId; + } + + public void setProdId(int prodId) { + this.prodId.set(prodId); + } + + public String getSupCompany() { + return supCompany.get(); + } + + public SimpleStringProperty supCompanyProperty() { + return supCompany; + } + + public void setSupCompany(String supCompany) { + this.supCompany.set(supCompany); + } + + public String getProdName() { + return prodName.get(); + } + + public SimpleStringProperty prodNameProperty() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName.set(prodName); + } + + public double getCost() { + return cost.get(); + } + + public SimpleDoubleProperty costProperty() { + return cost; + } + + public void setCost(double cost) { + this.cost.set(cost); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/PurchaseOrderDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/PurchaseOrderDTO.java new file mode 100644 index 00000000..acc28e2a --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/PurchaseOrderDTO.java @@ -0,0 +1,25 @@ +package org.example.petshopdesktop.DTOs; + +import javafx.beans.property.*; + +public class PurchaseOrderDTO { + + private LongProperty purchaseOrderId; + private StringProperty supplierName; + private StringProperty orderDate; + private StringProperty status; + + public PurchaseOrderDTO(long id, String supplierName, + String orderDate, String status) { + + this.purchaseOrderId = new SimpleLongProperty(id); + this.supplierName = new SimpleStringProperty(supplierName); + this.orderDate = new SimpleStringProperty(orderDate); + this.status = new SimpleStringProperty(status); + } + + public long getPurchaseOrderId() { return purchaseOrderId.get(); } + public String getSupplierName() { return supplierName.get(); } + public String getOrderDate() { return orderDate.get(); } + public String getStatus() { return status.get(); } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/SaleDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/SaleDTO.java new file mode 100644 index 00000000..a20bf5fd --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/SaleDTO.java @@ -0,0 +1,93 @@ +package org.example.petshopdesktop.DTOs; + +import javafx.beans.property.*; + +public class SaleDTO { + + private IntegerProperty saleId; + private StringProperty saleDate; + private StringProperty employeeName; + private StringProperty productName; + private IntegerProperty quantity; + private DoubleProperty unitPrice; + private DoubleProperty total; + private StringProperty paymentMethod; + + public SaleDTO(int saleId, String saleDate, String employeeName, String productName, + int quantity, double unitPrice, double total, String paymentMethod) { + this.saleId = new SimpleIntegerProperty(saleId); + this.saleDate = new SimpleStringProperty(saleDate); + this.employeeName = new SimpleStringProperty(employeeName); + this.productName = new SimpleStringProperty(productName); + this.quantity = new SimpleIntegerProperty(quantity); + this.unitPrice = new SimpleDoubleProperty(unitPrice); + this.total = new SimpleDoubleProperty(total); + this.paymentMethod = new SimpleStringProperty(paymentMethod); + } + + // Getters + public int getSaleId() { + return saleId.get(); + } + + public String getSaleDate() { + return saleDate.get(); + } + + public String getEmployeeName() { + return employeeName.get(); + } + + public String getProductName() { + return productName.get(); + } + + public int getQuantity() { + return quantity.get(); + } + + public double getUnitPrice() { + return unitPrice.get(); + } + + public double getTotal() { + return total.get(); + } + + public String getPaymentMethod() { + return paymentMethod.get(); + } + + // Properties + public IntegerProperty saleIdProperty() { + return saleId; + } + + public StringProperty saleDateProperty() { + return saleDate; + } + + public StringProperty employeeNameProperty() { + return employeeName; + } + + public StringProperty productNameProperty() { + return productName; + } + + public IntegerProperty quantityProperty() { + return quantity; + } + + public DoubleProperty unitPriceProperty() { + return unitPrice; + } + + public DoubleProperty totalProperty() { + return total; + } + + public StringProperty paymentMethodProperty() { + return paymentMethod; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/DTOs/ServiceDTO.java b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ServiceDTO.java new file mode 100644 index 00000000..c876cd3f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/DTOs/ServiceDTO.java @@ -0,0 +1,110 @@ +package org.example.petshopdesktop.DTOs; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; +import org.example.petshopdesktop.models.Service; + +/** + * The class for ServiceDTO, all service data stored here + */ +public class ServiceDTO { + + private SimpleIntegerProperty serviceId; + private SimpleStringProperty serviceName; + private SimpleStringProperty serviceDesc; + private SimpleIntegerProperty serviceDuration; + private SimpleDoubleProperty servicePrice; + + // constructor + public ServiceDTO(int serviceId, + String serviceName, + String serviceDesc, + int serviceDuration, + double servicePrice) { + + this.serviceId = new SimpleIntegerProperty(serviceId); + this.serviceName = new SimpleStringProperty(serviceName); + this.serviceDesc = new SimpleStringProperty(serviceDesc); + this.serviceDuration = new SimpleIntegerProperty(serviceDuration); + this.servicePrice = new SimpleDoubleProperty(servicePrice); + } + + // getters & setters + + public int getServiceId() { + return serviceId.get(); + } + + public SimpleIntegerProperty serviceIdProperty() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId.set(serviceId); + } + + public String getServiceName() { + return serviceName.get(); + } + + public SimpleStringProperty serviceNameProperty() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName.set(serviceName); + } + + public String getServiceDesc() { + return serviceDesc.get(); + } + + public SimpleStringProperty serviceDescProperty() { + return serviceDesc; + } + + public void setServiceDesc(String serviceDesc) { + this.serviceDesc.set(serviceDesc); + } + + public int getServiceDuration() { + return serviceDuration.get(); + } + + public SimpleIntegerProperty serviceDurationProperty() { + return serviceDuration; + } + + public void setServiceDuration(int serviceDuration) { + this.serviceDuration.set(serviceDuration); + } + + public double getServicePrice() { + return servicePrice.get(); + } + + public SimpleDoubleProperty servicePriceProperty() { + return servicePrice; + } + + public void setServicePrice(double servicePrice) { + this.servicePrice.set(servicePrice); + } + + /** + * Converts DTO into Service model (for edit/delete) + */ + public Service toService() { + + Service service = new Service( + getServiceId(), + getServiceName(), + getServiceDesc(), + getServiceDuration(), + getServicePrice() + ); + + return service; + } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/Launcher.java b/desktop/src/main/java/org/example/petshopdesktop/Launcher.java new file mode 100644 index 00000000..70456482 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/Launcher.java @@ -0,0 +1,9 @@ +package org.example.petshopdesktop; + +import javafx.application.Application; + +public class Launcher { + public static void main(String[] args) { + Application.launch(PetShopApplication.class, args); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java b/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java new file mode 100644 index 00000000..0127e42c --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/PetShopApplication.java @@ -0,0 +1,19 @@ +package org.example.petshopdesktop; + +import javafx.application.Application; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.stage.Stage; + +import java.io.IOException; + +public class PetShopApplication extends Application { + @Override + public void start(Stage stage) throws IOException { + FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("login-view.fxml")); + Scene scene = new Scene(fxmlLoader.load()); + stage.setTitle("Pet Shop Manager - Login"); + stage.setScene(scene); + stage.show(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/Validator.java b/desktop/src/main/java/org/example/petshopdesktop/Validator.java new file mode 100644 index 00000000..9c6f78a6 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/Validator.java @@ -0,0 +1,130 @@ +package org.example.petshopdesktop; + +public class Validator { + + /** + * Checks if string is not blank + * @param value string to check + * @param name name of the input + * @return error msg if string is blank, otherwise empty + */ + public static String isPresent(String value, String name){ + String msg = ""; + if (value == null || value.isBlank()){ + msg += name + " is required. \n"; + } + return msg; + } + + /** + * Checks if the input is a non-negative double + * @param value input of string + * @param name name of input + * @return error msg if input is not a number or negative, otherwise empty + */ + public static String isNonNegativeDouble(String value, String name){ + String msg =""; + double result; + try{ + result = Double.parseDouble(value); + if (result < 0){ + msg += name + " must be greater than or equal 0. \n"; + } + } + catch (NumberFormatException e){ + msg += name + " must be a number.\n"; + } + return msg; + } + + /** + * Checks if the input is a double in 2 different range + * @param value input of string + * @param name name of input + * @param minValue min value of range + * @param maxValue max value of range + * @return error msg if input is out of range, otherwise empty + */ + public static String isDoubleInRange(String value, String name, double minValue, double maxValue){ + String msg =""; + double result; + try{ + result = Double.parseDouble(value); + if (result < minValue || result > maxValue){ + msg += name + " must be between " + minValue + " and " + maxValue + "\n"; + } + } + catch (NumberFormatException e){ + msg += name + " must be a number.\n"; + } + return msg; + } + + /** + * Checks if the input is a non-negative integer + * @param value input of string + * @param name name of input + * @return error msg if input is not a number or negative, otherwise empty + */ + public static String isNonNegativeInteger(String value, String name){ + String msg =""; + int result; + try{ + result = Integer.parseInt(value); + if (result < 0){ + msg += name + " must be greater than or equal 0. \n"; + } + } + catch (NumberFormatException e){ + msg += name + " must be a whole number.\n"; + } + return msg; + } + + /** + * check if the string is a given amount of characters or fewer + * @param value input of string + * @param name name of input + * @param length max allowed length + * @return error msg if input is more than given characters length + */ + public static String isLessThanVarChars(String value, String name, int length){ + String msg =""; + if (value.length() > length){ + msg += name + " must be less than " + length + " characters. \n"; + } + return msg; + } + + /** + * Checks if the input is a valid email format + * @param value input of string + * @param name name of input + * @return error msg if input is not a valid email format, otherwise empty + */ + public static String isValidEmail(String value, String name){ + String msg = ""; + String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$"; + + if (!value.matches(regex)){ + msg += name + " is not in a valid format. \n"; + } + return msg; + } + + /** + * Checks if the input is a valid phone number in format XXX-XXX-XXXX + * @param value input of string + * @param name name of input + * @return error msg if input is not in valid phone format, otherwise empty + */ + public static String isValidPhoneNumber(String value, String name){ + String msg = ""; + String regex = "^\\d{3}-\\d{3}-\\d{4}$"; + + if (!value.matches(regex)){ + msg += name + " must be in format XXX-XXX-XXXX. \n"; + } + return msg; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java new file mode 100644 index 00000000..c0fbd874 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ApiClient.java @@ -0,0 +1,222 @@ +package org.example.petshopdesktop.api; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; +import org.example.petshopdesktop.auth.UserSession; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.Duration; +import java.util.UUID; + +public class ApiClient { + private static final ApiClient INSTANCE = new ApiClient(); + private final HttpClient httpClient; + private final ObjectMapper objectMapper; + private final String baseUrl; + + private ApiClient() { + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + this.objectMapper = new ObjectMapper(); + this.objectMapper.registerModule(new JavaTimeModule()); + this.objectMapper.configure(com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + this.baseUrl = ApiConfig.getInstance().getBaseUrl(); + } + + public static ApiClient getInstance() { + return INSTANCE; + } + + public T get(String path, Class responseClass) throws Exception { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .GET() + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpRequest request = builder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return handleResponse(response, responseClass); + } + + public String getRawResponse(String path) throws Exception { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .GET() + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpRequest request = builder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() == 200 || response.statusCode() == 201) { + return response.body(); + } else if (response.statusCode() == 401) { + throw new RuntimeException("Authentication failed. Please log in again."); + } else if (response.statusCode() == 403) { + throw new RuntimeException("Access restricted. You don't have permission to perform this action."); + } else { + throw new RuntimeException(parseErrorMessage(response)); + } + } + + public T post(String path, Object requestBody, Class responseClass) throws Exception { + String jsonBody = objectMapper.writeValueAsString(requestBody); + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "application/json") + .POST(HttpRequest.BodyPublishers.ofString(jsonBody)) + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpRequest request = builder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return handleResponse(response, responseClass); + } + + public T postMultipart(String path, String partName, Path filePath, Class responseClass) throws Exception { + String boundary = "----PetShopDesktop" + UUID.randomUUID(); + String mimeType = Files.probeContentType(filePath); + if (mimeType == null || mimeType.isBlank()) { + mimeType = "application/octet-stream"; + } + + byte[] fileBytes = Files.readAllBytes(filePath); + String fileName = filePath.getFileName().toString(); + byte[] prefix = ("--" + boundary + "\r\n" + + "Content-Disposition: form-data; name=\"" + partName + "\"; filename=\"" + fileName + "\"\r\n" + + "Content-Type: " + mimeType + "\r\n\r\n").getBytes(StandardCharsets.UTF_8); + byte[] suffix = ("\r\n--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8); + byte[] body = new byte[prefix.length + fileBytes.length + suffix.length]; + System.arraycopy(prefix, 0, body, 0, prefix.length); + System.arraycopy(fileBytes, 0, body, prefix.length, fileBytes.length); + System.arraycopy(suffix, 0, body, prefix.length + fileBytes.length, suffix.length); + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "multipart/form-data; boundary=" + boundary) + .POST(HttpRequest.BodyPublishers.ofByteArray(body)) + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpResponse response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString()); + return handleResponse(response, responseClass); + } + + public T put(String path, Object requestBody, Class responseClass) throws Exception { + String jsonBody = objectMapper.writeValueAsString(requestBody); + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "application/json") + .PUT(HttpRequest.BodyPublishers.ofString(jsonBody)) + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpRequest request = builder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + return handleResponse(response, responseClass); + } + + public void delete(String path) throws Exception { + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .DELETE() + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpRequest request = builder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 204 && response.statusCode() != 200) { + throw new RuntimeException(parseErrorMessage(response)); + } + } + + public void deleteWithBody(String path, Object requestBody) throws Exception { + String jsonBody = objectMapper.writeValueAsString(requestBody); + + HttpRequest.Builder builder = HttpRequest.newBuilder() + .uri(URI.create(baseUrl + path)) + .header("Content-Type", "application/json") + .method("DELETE", HttpRequest.BodyPublishers.ofString(jsonBody)) + .timeout(Duration.ofSeconds(30)); + + addAuthHeader(builder); + + HttpRequest request = builder.build(); + HttpResponse response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()); + + if (response.statusCode() != 204 && response.statusCode() != 200) { + throw new RuntimeException(parseErrorMessage(response)); + } + } + + private void addAuthHeader(HttpRequest.Builder builder) { + String token = UserSession.getInstance().getJwtToken(); + if (token != null && !token.isEmpty()) { + builder.header("Authorization", "Bearer " + token); + } + } + + private T handleResponse(HttpResponse response, Class responseClass) throws Exception { + int statusCode = response.statusCode(); + + if (statusCode == 200 || statusCode == 201) { + if (response.body() == null || response.body().isEmpty()) { + return null; + } + return objectMapper.readValue(response.body(), responseClass); + } else if (statusCode == 204) { + return null; + } else if (statusCode == 401) { + throw new RuntimeException("Authentication failed. Please log in again."); + } else if (statusCode == 403) { + throw new RuntimeException("Access restricted. You don't have permission to perform this action."); + } else { + throw new RuntimeException(parseErrorMessage(response)); + } + } + + private String parseErrorMessage(HttpResponse response) { + try { + if (response.body() != null && !response.body().isEmpty()) { + var errorNode = objectMapper.readTree(response.body()); + if (errorNode.has("message")) { + return errorNode.get("message").asText(); + } + if (errorNode.has("errors")) { + StringBuilder sb = new StringBuilder(); + errorNode.get("errors").fields().forEachRemaining(entry -> { + sb.append(entry.getValue().asText()).append("\n"); + }); + return sb.toString().trim(); + } + } + } catch (Exception e) { + System.err.println("Error parsing error message: " + e.getMessage()); + } + return "Request failed with status " + response.statusCode(); + } + + public ObjectMapper getObjectMapper() { + return objectMapper; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ApiConfig.java b/desktop/src/main/java/org/example/petshopdesktop/api/ApiConfig.java new file mode 100644 index 00000000..3394c653 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ApiConfig.java @@ -0,0 +1,34 @@ +package org.example.petshopdesktop.api; + +import java.io.IOException; +import java.io.InputStream; +import java.util.Properties; + +public class ApiConfig { + private static final ApiConfig INSTANCE = new ApiConfig(); + private final String baseUrl; + + private ApiConfig() { + Properties props = new Properties(); + String url = "http://localhost:8080"; + + try (InputStream input = getClass().getClassLoader().getResourceAsStream("connectionpetstore.properties")) { + if (input != null) { + props.load(input); + url = props.getProperty("api.baseUrl", "http://localhost:8080"); + } + } catch (IOException e) { + System.err.println("Failed to load api.baseUrl from properties: " + e.getMessage()); + } + + this.baseUrl = url; + } + + public static ApiConfig getInstance() { + return INSTANCE; + } + + public String getBaseUrl() { + return baseUrl; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java new file mode 100644 index 00000000..fcb93c0c --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/ChatRealtimeClient.java @@ -0,0 +1,341 @@ +package org.example.petshopdesktop.api; + +import org.example.petshopdesktop.api.dto.chat.ConversationResponse; +import org.example.petshopdesktop.api.dto.chat.MessageRequest; +import org.example.petshopdesktop.api.dto.chat.MessageResponse; +import org.example.petshopdesktop.auth.UserSession; + +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.WebSocket; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; + +public class ChatRealtimeClient implements WebSocket.Listener { + private static final ChatRealtimeClient INSTANCE = new ChatRealtimeClient(); + + private final HttpClient httpClient; + private final StringBuilder frameBuffer = new StringBuilder(); + private final AtomicInteger subscriptionCounter = new AtomicInteger(1); + private final Map destinationBySubscription = new HashMap<>(); + private final Object lock = new Object(); + + private WebSocket webSocket; + private boolean connecting; + private boolean connected; + private boolean conversationsSubscribed; + private Long selectedConversationId; + private String conversationsSubscriptionId; + private String conversationMessagesSubscriptionId; + private Consumer conversationListener; + private Consumer messageListener; + private Consumer statusListener; + private volatile String currentStatus = "Chat disconnected"; + + private ChatRealtimeClient() { + this.httpClient = HttpClient.newBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .build(); + } + + public static ChatRealtimeClient getInstance() { + return INSTANCE; + } + + public void setConversationListener(Consumer conversationListener) { + this.conversationListener = conversationListener; + } + + public void setMessageListener(Consumer messageListener) { + this.messageListener = messageListener; + } + + public void setStatusListener(Consumer statusListener) { + this.statusListener = statusListener; + if (statusListener != null) { + statusListener.accept(currentStatus); + } + } + + public void connect() { + String token = UserSession.getInstance().getJwtToken(); + if (token == null || token.isBlank()) { + publishStatus("Chat disconnected"); + return; + } + + synchronized (lock) { + if (connected || connecting) { + return; + } + connecting = true; + } + + String wsUrl = ApiConfig.getInstance().getBaseUrl() + .replaceFirst("^http://", "ws://") + .replaceFirst("^https://", "wss://") + "/ws/chat"; + + publishStatus("Connecting chat..."); + + httpClient.newWebSocketBuilder() + .connectTimeout(Duration.ofSeconds(10)) + .buildAsync(URI.create(wsUrl), this) + .thenAccept(socket -> { + synchronized (lock) { + webSocket = socket; + } + socket.sendText("CONNECT\naccept-version:1.2\nhost:localhost\nAuthorization:Bearer " + token + "\n\n\0", true); + }) + .exceptionally(ex -> { + synchronized (lock) { + resetConnectionState(); + } + publishStatus("Chat unavailable"); + return null; + }); + } + + public void disconnect() { + WebSocket socket; + synchronized (lock) { + socket = webSocket; + resetConnectionState(); + selectedConversationId = null; + conversationsSubscribed = false; + } + if (socket != null) { + socket.sendText("DISCONNECT\n\n\0", true); + socket.sendClose(WebSocket.NORMAL_CLOSURE, "bye"); + } + publishStatus("Chat disconnected"); + } + + public void subscribeToConversations() { + synchronized (lock) { + conversationsSubscribed = true; + } + connect(); + synchronized (lock) { + if (connected && conversationsSubscriptionId == null) { + conversationsSubscriptionId = subscribeLocked("/topic/chat/conversations"); + } + } + } + + public void subscribeToConversation(Long conversationId) { + synchronized (lock) { + selectedConversationId = conversationId; + } + connect(); + synchronized (lock) { + if (connected) { + applySelectedConversationSubscriptionLocked(); + } + } + } + + public boolean isConnected() { + synchronized (lock) { + return connected; + } + } + + public boolean sendMessage(Long conversationId, String content) { + String token = UserSession.getInstance().getJwtToken(); + if (token == null || token.isBlank()) { + publishStatus("Chat send failed"); + return false; + } + String body; + try { + body = ApiClient.getInstance().getObjectMapper().writeValueAsString(new MessageRequest(content)); + } catch (Exception e) { + publishStatus("Chat send failed"); + return false; + } + + synchronized (lock) { + if (!connected || webSocket == null) { + connect(); + return false; + } + webSocket.sendText( + "SEND\ndestination:/app/chat/conversations/" + conversationId + "/messages\nAuthorization:Bearer " + token + "\ncontent-type:application/json\ncontent-length:" + body.getBytes(StandardCharsets.UTF_8).length + "\n\n" + body + "\0", + true + ); + return true; + } + } + + private String subscribeLocked(String destination) { + String subscriptionId = "sub-" + subscriptionCounter.getAndIncrement(); + destinationBySubscription.put(subscriptionId, destination); + webSocket.sendText("SUBSCRIBE\nid:" + subscriptionId + "\ndestination:" + destination + "\n\n\0", true); + return subscriptionId; + } + + private void unsubscribeLocked(String subscriptionId) { + destinationBySubscription.remove(subscriptionId); + if (webSocket != null) { + webSocket.sendText("UNSUBSCRIBE\nid:" + subscriptionId + "\n\n\0", true); + } + } + + private void applySubscriptionsLocked() { + if (webSocket == null || !connected) { + return; + } + if (conversationsSubscribed && conversationsSubscriptionId == null) { + conversationsSubscriptionId = subscribeLocked("/topic/chat/conversations"); + } + applySelectedConversationSubscriptionLocked(); + } + + private void applySelectedConversationSubscriptionLocked() { + if (webSocket == null || !connected) { + return; + } + + String destination = selectedConversationId == null ? null : "/topic/chat/conversations/" + selectedConversationId; + if (destination == null) { + if (conversationMessagesSubscriptionId != null) { + unsubscribeLocked(conversationMessagesSubscriptionId); + conversationMessagesSubscriptionId = null; + } + return; + } + + if (conversationMessagesSubscriptionId != null) { + String currentDestination = destinationBySubscription.get(conversationMessagesSubscriptionId); + if (destination.equals(currentDestination)) { + return; + } + unsubscribeLocked(conversationMessagesSubscriptionId); + } + + conversationMessagesSubscriptionId = subscribeLocked(destination); + } + + private void resetConnectionState() { + webSocket = null; + connecting = false; + connected = false; + destinationBySubscription.clear(); + conversationsSubscriptionId = null; + conversationMessagesSubscriptionId = null; + } + + private void handleFrame(String frame) { + String normalized = frame.replace("\r\n", "\n"); + int separator = normalized.indexOf("\n\n"); + String headerPart = separator >= 0 ? normalized.substring(0, separator) : normalized; + String bodyPart = separator >= 0 ? normalized.substring(separator + 2) : ""; + String[] headerLines = headerPart.split("\n"); + if (headerLines.length == 0) { + return; + } + + String command = headerLines[0]; + Map headers = new HashMap<>(); + for (int i = 1; i < headerLines.length; i++) { + int idx = headerLines[i].indexOf(':'); + if (idx > 0) { + headers.put(headerLines[i].substring(0, idx), headerLines[i].substring(idx + 1)); + } + } + + if ("CONNECTED".equals(command)) { + synchronized (lock) { + connecting = false; + connected = true; + applySubscriptionsLocked(); + } + publishStatus("Chat connected"); + return; + } + + if ("MESSAGE".equals(command)) { + String destination; + synchronized (lock) { + destination = destinationBySubscription.get(headers.get("subscription")); + } + try { + if (destination != null && destination.startsWith("/topic/chat/conversations/")) { + MessageResponse message = ApiClient.getInstance().getObjectMapper().readValue(bodyPart, MessageResponse.class); + if (messageListener != null) { + messageListener.accept(message); + } + } else { + ConversationResponse conversation = ApiClient.getInstance().getObjectMapper().readValue(bodyPart, ConversationResponse.class); + if (conversationListener != null) { + conversationListener.accept(conversation); + } + } + } catch (Exception e) { + publishStatus("Chat update failed"); + } + return; + } + + if ("ERROR".equals(command)) { + publishStatus("Chat error"); + } + } + + private void publishStatus(String status) { + currentStatus = status; + if (statusListener != null) { + statusListener.accept(status); + } + } + + @Override + public void onOpen(WebSocket webSocket) { + webSocket.request(1); + } + + @Override + public CompletionStage onText(WebSocket webSocket, CharSequence data, boolean last) { + synchronized (lock) { + frameBuffer.append(data); + int delimiter; + while ((delimiter = frameBuffer.indexOf("\0")) >= 0) { + String frame = frameBuffer.substring(0, delimiter); + frameBuffer.delete(0, delimiter + 1); + handleFrame(frame); + } + } + webSocket.request(1); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletionStage onBinary(WebSocket webSocket, ByteBuffer data, boolean last) { + webSocket.request(1); + return CompletableFuture.completedFuture(null); + } + + @Override + public CompletionStage onClose(WebSocket webSocket, int statusCode, String reason) { + synchronized (lock) { + resetConnectionState(); + } + publishStatus("Chat disconnected"); + return CompletableFuture.completedFuture(null); + } + + @Override + public void onError(WebSocket webSocket, Throwable error) { + synchronized (lock) { + resetConnectionState(); + } + publishStatus("Chat unavailable"); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java new file mode 100644 index 00000000..5bea7090 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionRequest.java @@ -0,0 +1,45 @@ +package org.example.petshopdesktop.api.dto.adoption; + +import java.time.LocalDate; + +public class AdoptionRequest { + private Long petId; + private Long customerId; + private LocalDate adoptionDate; + private String adoptionStatus; + + public AdoptionRequest() { + } + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public LocalDate getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(LocalDate adoptionDate) { + this.adoptionDate = adoptionDate; + } + + public String getAdoptionStatus() { + return adoptionStatus; + } + + public void setAdoptionStatus(String adoptionStatus) { + this.adoptionStatus = adoptionStatus; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java new file mode 100644 index 00000000..60667217 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/adoption/AdoptionResponse.java @@ -0,0 +1,81 @@ +package org.example.petshopdesktop.api.dto.adoption; + +import java.time.LocalDate; + +public class AdoptionResponse { + private Long adoptionId; + private Long petId; + private Long customerId; + private String petName; + private String customerName; + private LocalDate adoptionDate; + private java.math.BigDecimal adoptionFee; + private String adoptionStatus; + + public AdoptionResponse() { + } + + public Long getAdoptionId() { + return adoptionId; + } + + public void setAdoptionId(Long adoptionId) { + this.adoptionId = adoptionId; + } + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public LocalDate getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(LocalDate adoptionDate) { + this.adoptionDate = adoptionDate; + } + + public java.math.BigDecimal getAdoptionFee() { + return adoptionFee; + } + + public void setAdoptionFee(java.math.BigDecimal adoptionFee) { + this.adoptionFee = adoptionFee; + } + + public String getAdoptionStatus() { + return adoptionStatus; + } + + public void setAdoptionStatus(String adoptionStatus) { + this.adoptionStatus = adoptionStatus; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DailySales.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DailySales.java new file mode 100644 index 00000000..320a8266 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DailySales.java @@ -0,0 +1,36 @@ +package org.example.petshopdesktop.api.dto.analytics; + +import java.math.BigDecimal; + +public class DailySales { + private String date; + private BigDecimal revenue; + private Long salesCount; + + public DailySales() { + } + + public String getDate() { + return date; + } + + public void setDate(String date) { + this.date = date; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } + + public Long getSalesCount() { + return salesCount; + } + + public void setSalesCount(Long salesCount) { + this.salesCount = salesCount; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java new file mode 100644 index 00000000..1b29b2b3 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/DashboardResponse.java @@ -0,0 +1,121 @@ +package org.example.petshopdesktop.api.dto.analytics; + +import java.math.BigDecimal; +import java.util.List; + +public class DashboardResponse { + private SalesSummary salesSummary; + private InventorySummary inventorySummary; + private List topProducts; + private List dailySales; + + public DashboardResponse() { + } + + public SalesSummary getSalesSummary() { + return salesSummary; + } + + public void setSalesSummary(SalesSummary salesSummary) { + this.salesSummary = salesSummary; + } + + public InventorySummary getInventorySummary() { + return inventorySummary; + } + + public void setInventorySummary(InventorySummary inventorySummary) { + this.inventorySummary = inventorySummary; + } + + public List getTopProducts() { + return topProducts; + } + + public void setTopProducts(List topProducts) { + this.topProducts = topProducts; + } + + public List getDailySales() { + return dailySales; + } + + public void setDailySales(List dailySales) { + this.dailySales = dailySales; + } + + public static class SalesSummary { + private BigDecimal totalRevenue; + private Long totalSales; + private BigDecimal totalRefunds; + private Long totalRefundCount; + + public SalesSummary() { + } + + public BigDecimal getTotalRevenue() { + return totalRevenue; + } + + public void setTotalRevenue(BigDecimal totalRevenue) { + this.totalRevenue = totalRevenue; + } + + public Long getTotalSales() { + return totalSales; + } + + public void setTotalSales(Long totalSales) { + this.totalSales = totalSales; + } + + public BigDecimal getTotalRefunds() { + return totalRefunds; + } + + public void setTotalRefunds(BigDecimal totalRefunds) { + this.totalRefunds = totalRefunds; + } + + public Long getTotalRefundCount() { + return totalRefundCount; + } + + public void setTotalRefundCount(Long totalRefundCount) { + this.totalRefundCount = totalRefundCount; + } + } + + public static class InventorySummary { + private Long totalProducts; + private Long lowStockProducts; + private Long outOfStockProducts; + + public InventorySummary() { + } + + public Long getTotalProducts() { + return totalProducts; + } + + public void setTotalProducts(Long totalProducts) { + this.totalProducts = totalProducts; + } + + public Long getLowStockProducts() { + return lowStockProducts; + } + + public void setLowStockProducts(Long lowStockProducts) { + this.lowStockProducts = lowStockProducts; + } + + public Long getOutOfStockProducts() { + return outOfStockProducts; + } + + public void setOutOfStockProducts(Long outOfStockProducts) { + this.outOfStockProducts = outOfStockProducts; + } + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/TopProduct.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/TopProduct.java new file mode 100644 index 00000000..a62ccce9 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/analytics/TopProduct.java @@ -0,0 +1,45 @@ +package org.example.petshopdesktop.api.dto.analytics; + +import java.math.BigDecimal; + +public class TopProduct { + private Long productId; + private String productName; + private Long quantitySold; + private BigDecimal revenue; + + public TopProduct() { + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public Long getQuantitySold() { + return quantitySold; + } + + public void setQuantitySold(Long quantitySold) { + this.quantitySold = quantitySold; + } + + public BigDecimal getRevenue() { + return revenue; + } + + public void setRevenue(BigDecimal revenue) { + this.revenue = revenue; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java new file mode 100644 index 00000000..a81faaff --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentRequest.java @@ -0,0 +1,74 @@ +package org.example.petshopdesktop.api.dto.appointment; + +import java.time.LocalDate; +import java.time.LocalTime; +import java.util.List; + +public class AppointmentRequest { + private List petIds; + private Long customerId; + private Long storeId; + private Long serviceId; + private LocalDate appointmentDate; + private LocalTime appointmentTime; + private String appointmentStatus; + + public AppointmentRequest() { + } + + public List getPetIds() { + return petIds; + } + + public void setPetIds(List petIds) { + this.petIds = petIds; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public Long getServiceId() { + return serviceId; + } + + public void setServiceId(Long serviceId) { + this.serviceId = serviceId; + } + + public LocalDate getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(LocalDate appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public LocalTime getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(LocalTime appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public String getAppointmentStatus() { + return appointmentStatus; + } + + public void setAppointmentStatus(String appointmentStatus) { + this.appointmentStatus = appointmentStatus; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java new file mode 100644 index 00000000..1d904bd0 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/appointment/AppointmentResponse.java @@ -0,0 +1,118 @@ +package org.example.petshopdesktop.api.dto.appointment; + +import java.time.LocalDate; +import java.time.LocalTime; + +public class AppointmentResponse { + private Long appointmentId; + private Long customerId; + private String customerName; + private Long storeId; + private String storeName; + private Long serviceId; + private java.util.List petNames; + private java.util.List petIds; + private String serviceName; + private LocalDate appointmentDate; + private LocalTime appointmentTime; + private String appointmentStatus; + + public AppointmentResponse() { + } + + public Long getAppointmentId() { + return appointmentId; + } + + public void setAppointmentId(Long appointmentId) { + this.appointmentId = appointmentId; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + + public Long getServiceId() { + return serviceId; + } + + public void setServiceId(Long serviceId) { + this.serviceId = serviceId; + } + + public java.util.List getPetNames() { + return petNames; + } + + public void setPetNames(java.util.List petNames) { + this.petNames = petNames; + } + + public java.util.List getPetIds() { + return petIds; + } + + public void setPetIds(java.util.List petIds) { + this.petIds = petIds; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public LocalDate getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(LocalDate appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public LocalTime getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(LocalTime appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public String getAppointmentStatus() { + return appointmentStatus; + } + + public void setAppointmentStatus(String appointmentStatus) { + this.appointmentStatus = appointmentStatus; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/AvatarUploadResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/AvatarUploadResponse.java new file mode 100644 index 00000000..24dadcca --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/AvatarUploadResponse.java @@ -0,0 +1,25 @@ +package org.example.petshopdesktop.api.dto.auth; + +public class AvatarUploadResponse { + private String avatarUrl; + private String message; + + public AvatarUploadResponse() { + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/LoginRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/LoginRequest.java new file mode 100644 index 00000000..89d6a98f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/LoginRequest.java @@ -0,0 +1,30 @@ +package org.example.petshopdesktop.api.dto.auth; + +public class LoginRequest { + private String username; + private String password; + + public LoginRequest() { + } + + public LoginRequest(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/LoginResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/LoginResponse.java new file mode 100644 index 00000000..50354d48 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/LoginResponse.java @@ -0,0 +1,34 @@ +package org.example.petshopdesktop.api.dto.auth; + +public class LoginResponse { + private String token; + private String username; + private String role; + + public LoginResponse() { + } + + public String getToken() { + return token; + } + + public void setToken(String token) { + this.token = token; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/UserInfoResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/UserInfoResponse.java new file mode 100644 index 00000000..fe83893c --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/auth/UserInfoResponse.java @@ -0,0 +1,88 @@ +package org.example.petshopdesktop.api.dto.auth; + +public class UserInfoResponse { + private Long id; + private String username; + private String email; + private String fullName; + private String phone; + private String avatarUrl; + private String role; + private Long storeId; + private String storeName; + + public UserInfoResponse() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/ConversationRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/ConversationRequest.java new file mode 100644 index 00000000..cd5ef6ee --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/ConversationRequest.java @@ -0,0 +1,20 @@ +package org.example.petshopdesktop.api.dto.chat; + +public class ConversationRequest { + private String message; + + public ConversationRequest() { + } + + public ConversationRequest(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/ConversationResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/ConversationResponse.java new file mode 100644 index 00000000..1bcdba17 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/ConversationResponse.java @@ -0,0 +1,90 @@ +package org.example.petshopdesktop.api.dto.chat; + +import java.time.LocalDateTime; + +public class ConversationResponse { + private Long id; + private Long customerId; + private Long staffId; + private String status; + private String mode; + private String lastMessage; + private LocalDateTime humanRequestedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public ConversationResponse() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStaffId() { + return staffId; + } + + public void setStaffId(Long staffId) { + this.staffId = staffId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getLastMessage() { + return lastMessage; + } + + public void setLastMessage(String lastMessage) { + this.lastMessage = lastMessage; + } + + public LocalDateTime getHumanRequestedAt() { + return humanRequestedAt; + } + + public void setHumanRequestedAt(LocalDateTime humanRequestedAt) { + this.humanRequestedAt = humanRequestedAt; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageRequest.java new file mode 100644 index 00000000..a5c17ca4 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageRequest.java @@ -0,0 +1,20 @@ +package org.example.petshopdesktop.api.dto.chat; + +public class MessageRequest { + private String content; + + public MessageRequest() { + } + + public MessageRequest(String content) { + this.content = content; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java new file mode 100644 index 00000000..f81db82d --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/chat/MessageResponse.java @@ -0,0 +1,63 @@ +package org.example.petshopdesktop.api.dto.chat; + +import java.time.LocalDateTime; + +public class MessageResponse { + private Long id; + private Long conversationId; + private Long senderId; + private String content; + private LocalDateTime timestamp; + private Boolean isRead; + + public MessageResponse() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getConversationId() { + return conversationId; + } + + public void setConversationId(Long conversationId) { + this.conversationId = conversationId; + } + + public Long getSenderId() { + return senderId; + } + + public void setSenderId(Long senderId) { + this.senderId = senderId; + } + + public String getContent() { + return content; + } + + public void setContent(String content) { + this.content = content; + } + + public LocalDateTime getTimestamp() { + return timestamp; + } + + public void setTimestamp(LocalDateTime timestamp) { + this.timestamp = timestamp; + } + + public Boolean getIsRead() { + return isRead; + } + + public void setIsRead(Boolean isRead) { + this.isRead = isRead; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/BulkDeleteRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/BulkDeleteRequest.java new file mode 100644 index 00000000..d95798ef --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/BulkDeleteRequest.java @@ -0,0 +1,22 @@ +package org.example.petshopdesktop.api.dto.common; + +import java.util.List; + +public class BulkDeleteRequest { + private List ids; + + public BulkDeleteRequest() { + } + + public BulkDeleteRequest(List ids) { + this.ids = ids; + } + + public List getIds() { + return ids; + } + + public void setIds(List ids) { + this.ids = ids; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/DropdownOption.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/DropdownOption.java new file mode 100644 index 00000000..bacbad0b --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/DropdownOption.java @@ -0,0 +1,30 @@ +package org.example.petshopdesktop.api.dto.common; + +public class DropdownOption { + private Long id; + private String label; + + public DropdownOption() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getLabel() { + return label; + } + + public void setLabel(String label) { + this.label = label; + } + + @Override + public String toString() { + return label == null ? "" : label; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java new file mode 100644 index 00000000..bbcc467c --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/common/PageResponse.java @@ -0,0 +1,72 @@ +package org.example.petshopdesktop.api.dto.common; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class PageResponse { + private List content; + + @JsonProperty("number") + private int pageNumber; + + @JsonProperty("size") + private int pageSize; + + private long totalElements; + private int totalPages; + private boolean last; + + public PageResponse() { + } + + public List getContent() { + return content; + } + + public void setContent(List content) { + this.content = content; + } + + public int getPageNumber() { + return pageNumber; + } + + public void setPageNumber(int pageNumber) { + this.pageNumber = pageNumber; + } + + public int getPageSize() { + return pageSize; + } + + public void setPageSize(int pageSize) { + this.pageSize = pageSize; + } + + public long getTotalElements() { + return totalElements; + } + + public void setTotalElements(long totalElements) { + this.totalElements = totalElements; + } + + public int getTotalPages() { + return totalPages; + } + + public void setTotalPages(int totalPages) { + this.totalPages = totalPages; + } + + public boolean isLast() { + return last; + } + + public void setLast(boolean last) { + this.last = last; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java new file mode 100644 index 00000000..f047f641 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeRequest.java @@ -0,0 +1,29 @@ +package org.example.petshopdesktop.api.dto.employee; + +public class EmployeeRequest { + private String username; + private String password; + private String firstName; + private String lastName; + private String email; + private String phone; + private String role; + private Boolean active; + + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getPassword() { return password; } + public void setPassword(String password) { this.password = password; } + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java new file mode 100644 index 00000000..030488c1 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/employee/EmployeeResponse.java @@ -0,0 +1,43 @@ +package org.example.petshopdesktop.api.dto.employee; + +import java.time.LocalDateTime; + +public class EmployeeResponse { + private Long employeeId; + private Long userId; + private String username; + private String firstName; + private String lastName; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public Long getEmployeeId() { return employeeId; } + public void setEmployeeId(Long employeeId) { this.employeeId = employeeId; } + public Long getUserId() { return userId; } + public void setUserId(Long userId) { this.userId = userId; } + public String getUsername() { return username; } + public void setUsername(String username) { this.username = username; } + public String getFirstName() { return firstName; } + public void setFirstName(String firstName) { this.firstName = firstName; } + public String getLastName() { return lastName; } + public void setLastName(String lastName) { this.lastName = lastName; } + public String getFullName() { return fullName; } + public void setFullName(String fullName) { this.fullName = fullName; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + public String getPhone() { return phone; } + public void setPhone(String phone) { this.phone = phone; } + public String getRole() { return role; } + public void setRole(String role) { this.role = role; } + public Boolean getActive() { return active; } + public void setActive(Boolean active) { this.active = active; } + public LocalDateTime getCreatedAt() { return createdAt; } + public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } + public LocalDateTime getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryRequest.java new file mode 100644 index 00000000..41196003 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryRequest.java @@ -0,0 +1,25 @@ +package org.example.petshopdesktop.api.dto.inventory; + +public class InventoryRequest { + private Long prodId; + private Integer quantity; + + public InventoryRequest() { + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryResponse.java new file mode 100644 index 00000000..1767f751 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/inventory/InventoryResponse.java @@ -0,0 +1,72 @@ +package org.example.petshopdesktop.api.dto.inventory; + +import java.time.LocalDateTime; + +public class InventoryResponse { + private Long inventoryId; + private Long prodId; + private String productName; + private String categoryName; + private Integer quantity; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public InventoryResponse() { + } + + public Long getInventoryId() { + return inventoryId; + } + + public void setInventoryId(Long inventoryId) { + this.inventoryId = inventoryId; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java new file mode 100644 index 00000000..be71c214 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetRequest.java @@ -0,0 +1,63 @@ +package org.example.petshopdesktop.api.dto.pet; + +import java.math.BigDecimal; + +public class PetRequest { + private String petName; + private String petSpecies; + private String petBreed; + private Integer petAge; + private String petStatus; + private BigDecimal petPrice; + + public PetRequest() { + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getPetSpecies() { + return petSpecies; + } + + public void setPetSpecies(String petSpecies) { + this.petSpecies = petSpecies; + } + + public String getPetBreed() { + return petBreed; + } + + public void setPetBreed(String petBreed) { + this.petBreed = petBreed; + } + + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } + + public String getPetStatus() { + return petStatus; + } + + public void setPetStatus(String petStatus) { + this.petStatus = petStatus; + } + + public BigDecimal getPetPrice() { + return petPrice; + } + + public void setPetPrice(BigDecimal petPrice) { + this.petPrice = petPrice; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java new file mode 100644 index 00000000..a7932253 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/pet/PetResponse.java @@ -0,0 +1,91 @@ +package org.example.petshopdesktop.api.dto.pet; + +import java.math.BigDecimal; +import java.time.LocalDateTime; + +public class PetResponse { + private Long petId; + private String petName; + private String petSpecies; + private String petBreed; + private Integer petAge; + private String petStatus; + private BigDecimal petPrice; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public PetResponse() { + } + + public Long getPetId() { + return petId; + } + + public void setPetId(Long petId) { + this.petId = petId; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getPetSpecies() { + return petSpecies; + } + + public void setPetSpecies(String petSpecies) { + this.petSpecies = petSpecies; + } + + public String getPetBreed() { + return petBreed; + } + + public void setPetBreed(String petBreed) { + this.petBreed = petBreed; + } + + public Integer getPetAge() { + return petAge; + } + + public void setPetAge(Integer petAge) { + this.petAge = petAge; + } + + public String getPetStatus() { + return petStatus; + } + + public void setPetStatus(String petStatus) { + this.petStatus = petStatus; + } + + public BigDecimal getPetPrice() { + return petPrice; + } + + public void setPetPrice(BigDecimal petPrice) { + this.petPrice = petPrice; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/product/ProductRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/product/ProductRequest.java new file mode 100644 index 00000000..020afca6 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/product/ProductRequest.java @@ -0,0 +1,45 @@ +package org.example.petshopdesktop.api.dto.product; + +import java.math.BigDecimal; + +public class ProductRequest { + private String prodName; + private Long categoryId; + private BigDecimal prodPrice; + private String prodDesc; + + public ProductRequest() { + } + + public String getProdName() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName = prodName; + } + + public Long getCategoryId() { + return categoryId; + } + + public void setCategoryId(Long categoryId) { + this.categoryId = categoryId; + } + + public BigDecimal getProdPrice() { + return prodPrice; + } + + public void setProdPrice(BigDecimal prodPrice) { + this.prodPrice = prodPrice; + } + + public String getProdDesc() { + return prodDesc; + } + + public void setProdDesc(String prodDesc) { + this.prodDesc = prodDesc; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/product/ProductResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/product/ProductResponse.java new file mode 100644 index 00000000..c989fdf5 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/product/ProductResponse.java @@ -0,0 +1,54 @@ +package org.example.petshopdesktop.api.dto.product; + +import java.math.BigDecimal; + +public class ProductResponse { + private Long prodId; + private String prodName; + private String categoryName; + private BigDecimal prodPrice; + private String prodDesc; + + public ProductResponse() { + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProdName() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName = prodName; + } + + public String getCategoryName() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName = categoryName; + } + + public BigDecimal getProdPrice() { + return prodPrice; + } + + public void setProdPrice(BigDecimal prodPrice) { + this.prodPrice = prodPrice; + } + + public String getProdDesc() { + return prodDesc; + } + + public void setProdDesc(String prodDesc) { + this.prodDesc = prodDesc; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/productsupplier/ProductSupplierRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/productsupplier/ProductSupplierRequest.java new file mode 100644 index 00000000..69f4ad61 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/productsupplier/ProductSupplierRequest.java @@ -0,0 +1,36 @@ +package org.example.petshopdesktop.api.dto.productsupplier; + +import java.math.BigDecimal; + +public class ProductSupplierRequest { + private Long productId; + private Long supplierId; + private BigDecimal cost; + + public ProductSupplierRequest() { + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public Long getSupplierId() { + return supplierId; + } + + public void setSupplierId(Long supplierId) { + this.supplierId = supplierId; + } + + public BigDecimal getCost() { + return cost; + } + + public void setCost(BigDecimal cost) { + this.cost = cost; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/productsupplier/ProductSupplierResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/productsupplier/ProductSupplierResponse.java new file mode 100644 index 00000000..b601efe3 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/productsupplier/ProductSupplierResponse.java @@ -0,0 +1,54 @@ +package org.example.petshopdesktop.api.dto.productsupplier; + +import java.math.BigDecimal; + +public class ProductSupplierResponse { + private Long productId; + private Long supplierId; + private String productName; + private String supplierName; + private BigDecimal cost; + + public ProductSupplierResponse() { + } + + public Long getProductId() { + return productId; + } + + public void setProductId(Long productId) { + this.productId = productId; + } + + public Long getSupplierId() { + return supplierId; + } + + public void setSupplierId(Long supplierId) { + this.supplierId = supplierId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getSupplierName() { + return supplierName; + } + + public void setSupplierName(String supplierName) { + this.supplierName = supplierName; + } + + public BigDecimal getCost() { + return cost; + } + + public void setCost(BigDecimal cost) { + this.cost = cost; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/purchaseorder/PurchaseOrderResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/purchaseorder/PurchaseOrderResponse.java new file mode 100644 index 00000000..4b96eedc --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/purchaseorder/PurchaseOrderResponse.java @@ -0,0 +1,64 @@ +package org.example.petshopdesktop.api.dto.purchaseorder; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public class PurchaseOrderResponse { + private Long purchaseOrderId; + private String supplierName; + private LocalDate orderDate; + private LocalDate expectedDeliveryDate; + private String orderStatus; + private BigDecimal totalAmount; + + public PurchaseOrderResponse() { + } + + public Long getPurchaseOrderId() { + return purchaseOrderId; + } + + public void setPurchaseOrderId(Long purchaseOrderId) { + this.purchaseOrderId = purchaseOrderId; + } + + public String getSupplierName() { + return supplierName; + } + + public void setSupplierName(String supplierName) { + this.supplierName = supplierName; + } + + public LocalDate getOrderDate() { + return orderDate; + } + + public void setOrderDate(LocalDate orderDate) { + this.orderDate = orderDate; + } + + public LocalDate getExpectedDeliveryDate() { + return expectedDeliveryDate; + } + + public void setExpectedDeliveryDate(LocalDate expectedDeliveryDate) { + this.expectedDeliveryDate = expectedDeliveryDate; + } + + public String getOrderStatus() { + return orderStatus; + } + + public void setOrderStatus(String orderStatus) { + this.orderStatus = orderStatus; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleItemRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleItemRequest.java new file mode 100644 index 00000000..03401958 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleItemRequest.java @@ -0,0 +1,27 @@ +package org.example.petshopdesktop.api.dto.sale; + +import java.math.BigDecimal; + +public class SaleItemRequest { + private Long prodId; + private Integer quantity; + + public SaleItemRequest() { + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleItemResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleItemResponse.java new file mode 100644 index 00000000..8bed99b1 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleItemResponse.java @@ -0,0 +1,61 @@ +package org.example.petshopdesktop.api.dto.sale; + +import java.math.BigDecimal; + +public class SaleItemResponse { + private Long saleItemId; + private Long prodId; + private String productName; + private Integer quantity; + private BigDecimal unitPrice; + + public SaleItemResponse() { + } + + public Long getSaleItemId() { + return saleItemId; + } + + public void setSaleItemId(Long saleItemId) { + this.saleItemId = saleItemId; + } + + public Long getProdId() { + return prodId; + } + + public void setProdId(Long prodId) { + this.prodId = prodId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public Integer getQuantity() { + return quantity; + } + + public void setQuantity(Integer quantity) { + this.quantity = quantity; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(BigDecimal unitPrice) { + this.unitPrice = unitPrice; + } + + public BigDecimal getLineTotal() { + if (unitPrice == null || quantity == null) { + return BigDecimal.ZERO; + } + return unitPrice.multiply(BigDecimal.valueOf(quantity)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleRequest.java new file mode 100644 index 00000000..991f7810 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleRequest.java @@ -0,0 +1,54 @@ +package org.example.petshopdesktop.api.dto.sale; + +import java.util.List; + +public class SaleRequest { + private Long storeId; + private String paymentMethod; + private List items; + private Boolean isRefund; + private Long originalSaleId; + + public SaleRequest() { + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } + + public Boolean getIsRefund() { + return isRefund; + } + + public void setIsRefund(Boolean isRefund) { + this.isRefund = isRefund; + } + + public Long getOriginalSaleId() { + return originalSaleId; + } + + public void setOriginalSaleId(Long originalSaleId) { + this.originalSaleId = originalSaleId; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleResponse.java new file mode 100644 index 00000000..7ee8eb45 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/sale/SaleResponse.java @@ -0,0 +1,92 @@ +package org.example.petshopdesktop.api.dto.sale; + +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.util.List; + +public class SaleResponse { + private Long saleId; + private String employeeName; + private String storeName; + private LocalDateTime saleDate; + private BigDecimal totalAmount; + private String paymentMethod; + private Boolean isRefund; + private Long originalSaleId; + private List items; + + public SaleResponse() { + } + + public Long getSaleId() { + return saleId; + } + + public void setSaleId(Long saleId) { + this.saleId = saleId; + } + + public String getEmployeeName() { + return employeeName; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public String getStoreName() { + return storeName; + } + + public void setStoreName(String storeName) { + this.storeName = storeName; + } + + public LocalDateTime getSaleDate() { + return saleDate; + } + + public void setSaleDate(LocalDateTime saleDate) { + this.saleDate = saleDate; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } + + public void setTotalAmount(BigDecimal totalAmount) { + this.totalAmount = totalAmount; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public void setPaymentMethod(String paymentMethod) { + this.paymentMethod = paymentMethod; + } + + public Boolean getIsRefund() { + return isRefund; + } + + public void setIsRefund(Boolean isRefund) { + this.isRefund = isRefund; + } + + public Long getOriginalSaleId() { + return originalSaleId; + } + + public void setOriginalSaleId(Long originalSaleId) { + this.originalSaleId = originalSaleId; + } + + public List getItems() { + return items; + } + + public void setItems(List items) { + this.items = items; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/service/ServiceRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/service/ServiceRequest.java new file mode 100644 index 00000000..8fd68a1b --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/service/ServiceRequest.java @@ -0,0 +1,45 @@ +package org.example.petshopdesktop.api.dto.service; + +import java.math.BigDecimal; + +public class ServiceRequest { + private String serviceName; + private BigDecimal servicePrice; + private String serviceDesc; + private Integer serviceDuration; + + public ServiceRequest() { + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public BigDecimal getServicePrice() { + return servicePrice; + } + + public void setServicePrice(BigDecimal servicePrice) { + this.servicePrice = servicePrice; + } + + public String getServiceDesc() { + return serviceDesc; + } + + public void setServiceDesc(String serviceDesc) { + this.serviceDesc = serviceDesc; + } + + public Integer getServiceDuration() { + return serviceDuration; + } + + public void setServiceDuration(Integer serviceDuration) { + this.serviceDuration = serviceDuration; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/service/ServiceResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/service/ServiceResponse.java new file mode 100644 index 00000000..caf2e3ff --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/service/ServiceResponse.java @@ -0,0 +1,54 @@ +package org.example.petshopdesktop.api.dto.service; + +import java.math.BigDecimal; + +public class ServiceResponse { + private Long serviceId; + private String serviceName; + private BigDecimal servicePrice; + private String serviceDesc; + private Integer serviceDuration; + + public ServiceResponse() { + } + + public Long getServiceId() { + return serviceId; + } + + public void setServiceId(Long serviceId) { + this.serviceId = serviceId; + } + + public String getServiceName() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName = serviceName; + } + + public BigDecimal getServicePrice() { + return servicePrice; + } + + public void setServicePrice(BigDecimal servicePrice) { + this.servicePrice = servicePrice; + } + + public String getServiceDesc() { + return serviceDesc; + } + + public void setServiceDesc(String serviceDesc) { + this.serviceDesc = serviceDesc; + } + + public Integer getServiceDuration() { + return serviceDuration; + } + + public void setServiceDuration(Integer serviceDuration) { + this.serviceDuration = serviceDuration; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/supplier/SupplierRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/supplier/SupplierRequest.java new file mode 100644 index 00000000..fb3dcd92 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/supplier/SupplierRequest.java @@ -0,0 +1,61 @@ +package org.example.petshopdesktop.api.dto.supplier; + +public class SupplierRequest { + private String supCompany; + private String supContactFirstName; + private String supContactLastName; + private String supPhone; + private String supEmail; + private String address; + + public SupplierRequest() { + } + + public String getSupCompany() { + return supCompany; + } + + public void setSupCompany(String supCompany) { + this.supCompany = supCompany; + } + + public String getSupContactFirstName() { + return supContactFirstName; + } + + public void setSupContactFirstName(String supContactFirstName) { + this.supContactFirstName = supContactFirstName; + } + + public String getSupContactLastName() { + return supContactLastName; + } + + public void setSupContactLastName(String supContactLastName) { + this.supContactLastName = supContactLastName; + } + + public String getSupPhone() { + return supPhone; + } + + public void setSupPhone(String supPhone) { + this.supPhone = supPhone; + } + + public String getSupEmail() { + return supEmail; + } + + public void setSupEmail(String supEmail) { + this.supEmail = supEmail; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/supplier/SupplierResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/supplier/SupplierResponse.java new file mode 100644 index 00000000..64db982f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/supplier/SupplierResponse.java @@ -0,0 +1,70 @@ +package org.example.petshopdesktop.api.dto.supplier; + +public class SupplierResponse { + private Long supId; + private String supCompany; + private String supContactFirstName; + private String supContactLastName; + private String supPhone; + private String supEmail; + private String address; + + public SupplierResponse() { + } + + public Long getSupId() { + return supId; + } + + public void setSupId(Long supId) { + this.supId = supId; + } + + public String getSupCompany() { + return supCompany; + } + + public void setSupCompany(String supCompany) { + this.supCompany = supCompany; + } + + public String getSupContactFirstName() { + return supContactFirstName; + } + + public void setSupContactFirstName(String supContactFirstName) { + this.supContactFirstName = supContactFirstName; + } + + public String getSupContactLastName() { + return supContactLastName; + } + + public void setSupContactLastName(String supContactLastName) { + this.supContactLastName = supContactLastName; + } + + public String getSupPhone() { + return supPhone; + } + + public void setSupPhone(String supPhone) { + this.supPhone = supPhone; + } + + public String getSupEmail() { + return supEmail; + } + + public void setSupEmail(String supEmail) { + this.supEmail = supEmail; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java new file mode 100644 index 00000000..a6c9f669 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserRequest.java @@ -0,0 +1,70 @@ +package org.example.petshopdesktop.api.dto.user; + +public class UserRequest { + private String username; + private String password; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + + public UserRequest() { + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java new file mode 100644 index 00000000..3a42f128 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/dto/user/UserResponse.java @@ -0,0 +1,90 @@ +package org.example.petshopdesktop.api.dto.user; + +import java.time.LocalDateTime; + +public class UserResponse { + private Long id; + private String username; + private String fullName; + private String email; + private String phone; + private String role; + private Boolean active; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; + + public UserResponse() { + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getFullName() { + return fullName; + } + + public void setFullName(String fullName) { + this.fullName = fullName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getRole() { + return role; + } + + public void setRole(String role) { + this.role = role; + } + + public Boolean getActive() { + return active; + } + + public void setActive(Boolean active) { + this.active = active; + } + + public LocalDateTime getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(LocalDateTime createdAt) { + this.createdAt = createdAt; + } + + public LocalDateTime getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(LocalDateTime updatedAt) { + this.updatedAt = updatedAt; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AdoptionApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AdoptionApi.java new file mode 100644 index 00000000..d4f90c85 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AdoptionApi.java @@ -0,0 +1,53 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest; +import org.example.petshopdesktop.api.dto.adoption.AdoptionResponse; +import org.example.petshopdesktop.api.dto.common.BulkDeleteRequest; +import org.example.petshopdesktop.api.dto.common.PageResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class AdoptionApi { + private static final AdoptionApi INSTANCE = new AdoptionApi(); + private final ApiClient apiClient; + + private AdoptionApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static AdoptionApi getInstance() { + return INSTANCE; + } + + public List listAdoptions(String query) throws Exception { + String path = "/api/v1/adoptions?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from adoptions endpoint"); + } + return pageResponse.getContent(); + } + + public AdoptionResponse createAdoption(AdoptionRequest request) throws Exception { + return apiClient.post("/api/v1/adoptions", request, AdoptionResponse.class); + } + + public AdoptionResponse updateAdoption(Long id, AdoptionRequest request) throws Exception { + return apiClient.put("/api/v1/adoptions/" + id, request, AdoptionResponse.class); + } + + public void deleteAdoptions(List ids) throws Exception { + apiClient.deleteWithBody("/api/v1/adoptions", new BulkDeleteRequest(ids)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AnalyticsApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AnalyticsApi.java new file mode 100644 index 00000000..ebecd9c2 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AnalyticsApi.java @@ -0,0 +1,22 @@ +package org.example.petshopdesktop.api.endpoints; + +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.analytics.DashboardResponse; + +public class AnalyticsApi { + private static final AnalyticsApi INSTANCE = new AnalyticsApi(); + private final ApiClient apiClient; + + private AnalyticsApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static AnalyticsApi getInstance() { + return INSTANCE; + } + + public DashboardResponse getDashboard(int days, int top) throws Exception { + String path = "/api/v1/analytics/dashboard?days=" + days + "&top=" + top; + return apiClient.get(path, DashboardResponse.class); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AppointmentApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AppointmentApi.java new file mode 100644 index 00000000..4dd65bf0 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AppointmentApi.java @@ -0,0 +1,53 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.appointment.AppointmentRequest; +import org.example.petshopdesktop.api.dto.appointment.AppointmentResponse; +import org.example.petshopdesktop.api.dto.common.BulkDeleteRequest; +import org.example.petshopdesktop.api.dto.common.PageResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class AppointmentApi { + private static final AppointmentApi INSTANCE = new AppointmentApi(); + private final ApiClient apiClient; + + private AppointmentApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static AppointmentApi getInstance() { + return INSTANCE; + } + + public List listAppointments(String query) throws Exception { + String path = "/api/v1/appointments?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from appointments endpoint"); + } + return pageResponse.getContent(); + } + + public AppointmentResponse createAppointment(AppointmentRequest request) throws Exception { + return apiClient.post("/api/v1/appointments", request, AppointmentResponse.class); + } + + public AppointmentResponse updateAppointment(Long id, AppointmentRequest request) throws Exception { + return apiClient.put("/api/v1/appointments/" + id, request, AppointmentResponse.class); + } + + public void deleteAppointments(List ids) throws Exception { + apiClient.deleteWithBody("/api/v1/appointments", new BulkDeleteRequest(ids)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java new file mode 100644 index 00000000..a273a738 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/AuthApi.java @@ -0,0 +1,32 @@ +package org.example.petshopdesktop.api.endpoints; + +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse; +import org.example.petshopdesktop.api.dto.auth.UserInfoResponse; + +import java.nio.file.Path; + +public class AuthApi { + private static final AuthApi INSTANCE = new AuthApi(); + private final ApiClient apiClient; + + private AuthApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static AuthApi getInstance() { + return INSTANCE; + } + + public UserInfoResponse getCurrentUser() throws Exception { + return apiClient.get("/api/v1/auth/me", UserInfoResponse.class); + } + + public AvatarUploadResponse uploadAvatar(Path filePath) throws Exception { + return apiClient.postMultipart("/api/v1/auth/me/avatar", "avatar", filePath, AvatarUploadResponse.class); + } + + public void deleteAvatar() throws Exception { + apiClient.delete("/api/v1/auth/me/avatar"); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ChatApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ChatApi.java new file mode 100644 index 00000000..f6429731 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ChatApi.java @@ -0,0 +1,45 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.chat.ConversationRequest; +import org.example.petshopdesktop.api.dto.chat.ConversationResponse; +import org.example.petshopdesktop.api.dto.chat.MessageRequest; +import org.example.petshopdesktop.api.dto.chat.MessageResponse; + +import java.util.List; + +public class ChatApi { + private static final ChatApi INSTANCE = new ChatApi(); + private final ApiClient apiClient; + + private ChatApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static ChatApi getInstance() { + return INSTANCE; + } + + public List listConversations() throws Exception { + String response = apiClient.getRawResponse("/api/v1/chat/conversations"); + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public ConversationResponse createConversation(ConversationRequest request) throws Exception { + return apiClient.post("/api/v1/chat/conversations", request, ConversationResponse.class); + } + + public ConversationResponse getConversation(Long id) throws Exception { + return apiClient.get("/api/v1/chat/conversations/" + id, ConversationResponse.class); + } + + public List listMessages(Long conversationId) throws Exception { + String response = apiClient.getRawResponse("/api/v1/chat/conversations/" + conversationId + "/messages"); + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public MessageResponse sendMessage(Long conversationId, MessageRequest request) throws Exception { + return apiClient.post("/api/v1/chat/conversations/" + conversationId + "/messages", request, MessageResponse.class); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java new file mode 100644 index 00000000..6c20526e --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/DropdownApi.java @@ -0,0 +1,76 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.DropdownOption; + +import java.util.List; + +public class DropdownApi { + private static final DropdownApi INSTANCE = new DropdownApi(); + private final ApiClient apiClient; + + private DropdownApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static DropdownApi getInstance() { + return INSTANCE; + } + + public List getCategories() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/categories"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from categories endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public List getProducts() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/products"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from products endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public List getSuppliers() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/suppliers"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from suppliers endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public List getServices() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/services"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from services endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public List getCustomers() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/customers"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from customers endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public List getPets() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/pets"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from pets endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } + + public List getStores() throws Exception { + String response = apiClient.getRawResponse("/api/v1/dropdowns/stores"); + if (response == null || response.isEmpty()) { + throw new IllegalStateException("Empty response from stores endpoint"); + } + return apiClient.getObjectMapper().readValue(response, new TypeReference>() {}); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java new file mode 100644 index 00000000..e3a8ad52 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/EmployeeApi.java @@ -0,0 +1,48 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; +import org.example.petshopdesktop.api.dto.employee.EmployeeResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class EmployeeApi { + private static final EmployeeApi INSTANCE = new EmployeeApi(); + private final ApiClient apiClient; + + private EmployeeApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static EmployeeApi getInstance() { + return INSTANCE; + } + + public List listEmployees(String query) throws Exception { + String path = "/api/v1/employees?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from employees endpoint"); + } + return pageResponse.getContent(); + } + + public EmployeeResponse createEmployee(EmployeeRequest request) throws Exception { + return apiClient.post("/api/v1/employees", request, EmployeeResponse.class); + } + + public EmployeeResponse updateEmployee(Long id, EmployeeRequest request) throws Exception { + return apiClient.put("/api/v1/employees/" + id, request, EmployeeResponse.class); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/InventoryApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/InventoryApi.java new file mode 100644 index 00000000..dbf08be7 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/InventoryApi.java @@ -0,0 +1,52 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.inventory.InventoryRequest; +import org.example.petshopdesktop.api.dto.inventory.InventoryResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class InventoryApi { + private static final InventoryApi INSTANCE = new InventoryApi(); + private final ApiClient apiClient; + + private InventoryApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static InventoryApi getInstance() { + return INSTANCE; + } + + public List listInventory(String query) throws Exception { + String path = "/api/v1/inventory?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from inventory endpoint"); + } + return pageResponse.getContent(); + } + + public InventoryResponse createInventory(InventoryRequest request) throws Exception { + return apiClient.post("/api/v1/inventory", request, InventoryResponse.class); + } + + public InventoryResponse updateInventory(Long id, InventoryRequest request) throws Exception { + return apiClient.put("/api/v1/inventory/" + id, request, InventoryResponse.class); + } + + public void deleteInventory(Long id) throws Exception { + apiClient.delete("/api/v1/inventory/" + id); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java new file mode 100644 index 00000000..b5fe23e9 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PetApi.java @@ -0,0 +1,53 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.BulkDeleteRequest; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.pet.PetRequest; +import org.example.petshopdesktop.api.dto.pet.PetResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class PetApi { + private static final PetApi INSTANCE = new PetApi(); + private final ApiClient apiClient; + + private PetApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static PetApi getInstance() { + return INSTANCE; + } + + public List listPets(String query) throws Exception { + String path = "/api/v1/pets?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from pets endpoint"); + } + return pageResponse.getContent(); + } + + public PetResponse createPet(PetRequest request) throws Exception { + return apiClient.post("/api/v1/pets", request, PetResponse.class); + } + + public PetResponse updatePet(Long id, PetRequest request) throws Exception { + return apiClient.put("/api/v1/pets/" + id, request, PetResponse.class); + } + + public void deletePets(List ids) throws Exception { + apiClient.deleteWithBody("/api/v1/pets", new BulkDeleteRequest(ids)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java new file mode 100644 index 00000000..5bffd489 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductApi.java @@ -0,0 +1,53 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.BulkDeleteRequest; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.product.ProductRequest; +import org.example.petshopdesktop.api.dto.product.ProductResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class ProductApi { + private static final ProductApi INSTANCE = new ProductApi(); + private final ApiClient apiClient; + + private ProductApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static ProductApi getInstance() { + return INSTANCE; + } + + public List listProducts(String query) throws Exception { + String path = "/api/v1/products?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from products endpoint"); + } + return pageResponse.getContent(); + } + + public ProductResponse createProduct(ProductRequest request) throws Exception { + return apiClient.post("/api/v1/products", request, ProductResponse.class); + } + + public ProductResponse updateProduct(Long id, ProductRequest request) throws Exception { + return apiClient.put("/api/v1/products/" + id, request, ProductResponse.class); + } + + public void deleteProducts(List ids) throws Exception { + apiClient.deleteWithBody("/api/v1/products", new BulkDeleteRequest(ids)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductSupplierApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductSupplierApi.java new file mode 100644 index 00000000..74143c0f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ProductSupplierApi.java @@ -0,0 +1,53 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.BulkDeleteRequest; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierRequest; +import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class ProductSupplierApi { + private static final ProductSupplierApi INSTANCE = new ProductSupplierApi(); + private final ApiClient apiClient; + + private ProductSupplierApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static ProductSupplierApi getInstance() { + return INSTANCE; + } + + public List listProductSuppliers(String query) throws Exception { + String path = "/api/v1/product-suppliers?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from product-suppliers endpoint"); + } + return pageResponse.getContent(); + } + + public ProductSupplierResponse createProductSupplier(ProductSupplierRequest request) throws Exception { + return apiClient.post("/api/v1/product-suppliers", request, ProductSupplierResponse.class); + } + + public ProductSupplierResponse updateProductSupplier(Long productId, Long supplierId, ProductSupplierRequest request) throws Exception { + return apiClient.put("/api/v1/product-suppliers/" + productId + "/" + supplierId, request, ProductSupplierResponse.class); + } + + public void deleteProductSupplier(Long productId, Long supplierId) throws Exception { + apiClient.delete("/api/v1/product-suppliers/" + productId + "/" + supplierId); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PurchaseOrderApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PurchaseOrderApi.java new file mode 100644 index 00000000..accea0eb --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/PurchaseOrderApi.java @@ -0,0 +1,39 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.purchaseorder.PurchaseOrderResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class PurchaseOrderApi { + private static final PurchaseOrderApi INSTANCE = new PurchaseOrderApi(); + private final ApiClient apiClient; + + private PurchaseOrderApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static PurchaseOrderApi getInstance() { + return INSTANCE; + } + + public List listPurchaseOrders(String query) throws Exception { + String path = "/api/v1/purchase-orders?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from purchase-orders endpoint"); + } + return pageResponse.getContent(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java new file mode 100644 index 00000000..d3355012 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SaleApi.java @@ -0,0 +1,48 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.sale.SaleRequest; +import org.example.petshopdesktop.api.dto.sale.SaleResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class SaleApi { + private static final SaleApi INSTANCE = new SaleApi(); + private final ApiClient apiClient; + + private SaleApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static SaleApi getInstance() { + return INSTANCE; + } + + public List listSales(int page, int size, String query) throws Exception { + String path = "/api/v1/sales?page=" + page + "&size=" + size; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from sales endpoint"); + } + return pageResponse.getContent(); + } + + public SaleResponse getSale(Long id) throws Exception { + return apiClient.get("/api/v1/sales/" + id, SaleResponse.class); + } + + public SaleResponse createSale(SaleRequest request) throws Exception { + return apiClient.post("/api/v1/sales", request, SaleResponse.class); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ServiceApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ServiceApi.java new file mode 100644 index 00000000..66b348dc --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/ServiceApi.java @@ -0,0 +1,53 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.BulkDeleteRequest; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.service.ServiceRequest; +import org.example.petshopdesktop.api.dto.service.ServiceResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class ServiceApi { + private static final ServiceApi INSTANCE = new ServiceApi(); + private final ApiClient apiClient; + + private ServiceApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static ServiceApi getInstance() { + return INSTANCE; + } + + public List listServices(String query) throws Exception { + String path = "/api/v1/services?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from services endpoint"); + } + return pageResponse.getContent(); + } + + public ServiceResponse createService(ServiceRequest request) throws Exception { + return apiClient.post("/api/v1/services", request, ServiceResponse.class); + } + + public ServiceResponse updateService(Long id, ServiceRequest request) throws Exception { + return apiClient.put("/api/v1/services/" + id, request, ServiceResponse.class); + } + + public void deleteServices(List ids) throws Exception { + apiClient.deleteWithBody("/api/v1/services", new BulkDeleteRequest(ids)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SupplierApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SupplierApi.java new file mode 100644 index 00000000..bcfb8acb --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/SupplierApi.java @@ -0,0 +1,53 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.BulkDeleteRequest; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.supplier.SupplierRequest; +import org.example.petshopdesktop.api.dto.supplier.SupplierResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class SupplierApi { + private static final SupplierApi INSTANCE = new SupplierApi(); + private final ApiClient apiClient; + + private SupplierApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static SupplierApi getInstance() { + return INSTANCE; + } + + public List listSuppliers(String query) throws Exception { + String path = "/api/v1/suppliers?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from suppliers endpoint"); + } + return pageResponse.getContent(); + } + + public SupplierResponse createSupplier(SupplierRequest request) throws Exception { + return apiClient.post("/api/v1/suppliers", request, SupplierResponse.class); + } + + public SupplierResponse updateSupplier(Long id, SupplierRequest request) throws Exception { + return apiClient.put("/api/v1/suppliers/" + id, request, SupplierResponse.class); + } + + public void deleteSuppliers(List ids) throws Exception { + apiClient.deleteWithBody("/api/v1/suppliers", new BulkDeleteRequest(ids)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/UserApi.java b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/UserApi.java new file mode 100644 index 00000000..315d53a1 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/api/endpoints/UserApi.java @@ -0,0 +1,44 @@ +package org.example.petshopdesktop.api.endpoints; + +import com.fasterxml.jackson.core.type.TypeReference; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.common.PageResponse; +import org.example.petshopdesktop.api.dto.user.UserRequest; +import org.example.petshopdesktop.api.dto.user.UserResponse; + +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.util.List; + +public class UserApi { + private static final UserApi INSTANCE = new UserApi(); + private final ApiClient apiClient; + + private UserApi() { + this.apiClient = ApiClient.getInstance(); + } + + public static UserApi getInstance() { + return INSTANCE; + } + + public List listUsers(String query) throws Exception { + String path = "/api/v1/users?page=0&size=1000"; + if (query != null && !query.isEmpty()) { + path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8); + } + String response = apiClient.getRawResponse(path); + PageResponse pageResponse = apiClient.getObjectMapper().readValue( + response, + new TypeReference>() {} + ); + if (pageResponse == null) { + throw new IllegalStateException("Null response from users endpoint"); + } + return pageResponse.getContent(); + } + + public UserResponse createUser(UserRequest request) throws Exception { + return apiClient.post("/api/v1/users", request, UserResponse.class); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/auth/Role.java b/desktop/src/main/java/org/example/petshopdesktop/auth/Role.java new file mode 100644 index 00000000..64f38459 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/auth/Role.java @@ -0,0 +1,6 @@ +package org.example.petshopdesktop.auth; + +public enum Role { + ADMIN, + STAFF +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java b/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java new file mode 100644 index 00000000..a578d0e4 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/auth/UserSession.java @@ -0,0 +1,95 @@ +package org.example.petshopdesktop.auth; + +public class UserSession { + private static UserSession instance; + + private Long userId; + private Long employeeId; + private String username; + private String employeeName; + private Role role; + private String jwtToken; + private Long storeId; + private String avatarUrl; + + private UserSession() {} + + public static UserSession getInstance() { + if (instance == null) { + instance = new UserSession(); + } + return instance; + } + + public void login(Long userId, String username, Role role, String jwtToken) { + this.userId = userId; + this.employeeId = userId; + this.username = username; + this.employeeName = username; + this.role = role; + this.jwtToken = jwtToken; + } + + public void logout() { + this.userId = null; + this.employeeId = null; + this.username = null; + this.employeeName = null; + this.role = null; + this.jwtToken = null; + this.storeId = null; + this.avatarUrl = null; + } + + public Long getUserId() { + return userId; + } + + public Long getEmployeeId() { + return employeeId; + } + + public String getUsername() { + return username; + } + + public String getEmployeeName() { + return employeeName; + } + + public Role getRole() { + return role; + } + + public String getJwtToken() { + return jwtToken; + } + + public Long getStoreId() { + return storeId; + } + + public void setStoreId(Long storeId) { + this.storeId = storeId; + } + + public String getAvatarUrl() { + return avatarUrl; + } + + public void setAvatarUrl(String avatarUrl) { + this.avatarUrl = avatarUrl; + } + + public void setEmployeeName(String employeeName) { + this.employeeName = employeeName; + } + + public boolean isLoggedIn() { + return username != null && role != null; + } + + public boolean isAdmin() { + return Role.ADMIN.equals(role); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java new file mode 100644 index 00000000..65edcebd --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AdoptionController.java @@ -0,0 +1,259 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.adoption.AdoptionResponse; +import org.example.petshopdesktop.api.endpoints.AdoptionApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.AdoptionDialogController; +import org.example.petshopdesktop.models.Adoption; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class AdoptionController { + + @FXML + private Button btnAdd; + + @FXML + private Button btnDelete; + + @FXML + private Button btnEdit; + + @FXML + private TableColumn colAdoptionId; + + @FXML + private TableColumn colPetId; + + @FXML + private TableColumn colCustomerName; + + @FXML + private TableColumn colAdoptionDate; + + @FXML + private TableColumn colAdoptionFee; + + @FXML + private TableColumn colAdoptionStatus; + + @FXML + private TableView tvAdoptions; + + @FXML + private TextField txtSearch; + + private ObservableList data = FXCollections.observableArrayList(); + private String mode = null; + + @FXML + void initialize() { + btnEdit.setDisable(true); + btnDelete.setDisable(true); + //Enable multiple selection + tvAdoptions.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + + colAdoptionId.setCellValueFactory(new PropertyValueFactory<>("adoptionId")); + colPetId.setCellValueFactory(new PropertyValueFactory<>("petName")); + colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); + colAdoptionDate.setCellValueFactory(new PropertyValueFactory<>("adoptionDate")); + colAdoptionFee.setCellValueFactory(new PropertyValueFactory<>("adoptionFee")); + colAdoptionStatus.setCellValueFactory(new PropertyValueFactory<>("adoptionStatus")); + + displayAdoptions(); + + tvAdoptions.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + }); + + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredAdoptions(newValue); + }); + + //EventListener for DELETE key + tvAdoptions.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvAdoptions.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + } + + @FXML + void btnAddClicked(ActionEvent event) { + mode = "Add"; + openDialog(null, mode); + } + + @FXML + void btnDeleteClicked(ActionEvent event) { + //get selected adoptions + var selectedAdoptions = tvAdoptions.getSelectionModel().getSelectedItems(); + if (selectedAdoptions.isEmpty()) return; + + //ask user to confirm + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + String message = selectedAdoptions.size() == 1 + ? "Are you sure you want to delete this adoption record?" + : "Are you sure you want to delete " + selectedAdoptions.size() + " adoption records?"; + question.setContentText(message); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + Optional result = question.showAndWait(); + + //if confirmed, start deletion + if (result.isPresent() && result.get() == ButtonType.OK) { + List ids = selectedAdoptions.stream() + .map(a -> (long) a.getAdoptionId()) + .collect(Collectors.toList()); + + try { + AdoptionApi.getInstance().deleteAdoptions(ids); + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted " + ids.size() + " adoption record(s)"); + alert.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "AdoptionController.btnDeleteClicked", + e, + "Deleting adoptions"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Failed"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + + //refresh display and reset inputs + displayAdoptions(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } + + @FXML + void btnEditClicked(ActionEvent event) { + Adoption selectedAdoption = tvAdoptions.getSelectionModel().getSelectedItem(); + + if (selectedAdoption != null) { + mode = "Edit"; + openDialog(selectedAdoption, mode); + } + } + + private void displayFilteredAdoptions(String filter) { + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()) { + displayAdoptions(); + } else { + new Thread(() -> { + try { + List adoptions = AdoptionApi.getInstance().listAdoptions(filter); + List adoptionList = adoptions.stream() + .map(this::mapToAdoption) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(adoptionList); + tvAdoptions.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "AdoptionController.displayFilteredAdoptions", + e, + "Filtering adoptions with filter: " + filter); + }); + } + }).start(); + } + } + + private void displayAdoptions() { + new Thread(() -> { + try { + List adoptions = AdoptionApi.getInstance().listAdoptions(null); + List adoptionList = adoptions.stream() + .map(this::mapToAdoption) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(adoptionList); + tvAdoptions.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "AdoptionController.displayAdoptions", + e, + "Fetching adoption data for table display"); + }); + } + }).start(); + } + + private void openDialog(Adoption adoption, String mode) { + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource( + "/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml")); + Scene scene = null; + try { + scene = new Scene(fxmlLoader.load()); + } catch (IOException e) { + ActivityLogger.getInstance().logException( + "AdoptionController.openDialog", + e, + "Loading adoption dialog in " + mode + " mode"); + throw new RuntimeException(e); + } + + AdoptionDialogController dialogController = fxmlLoader.getController(); + dialogController.setMode(mode); + + if (mode.equals("Edit")) { + dialogController.displayAdoptionDetails(adoption); + } + + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); + dialogStage.setTitle(mode.equals("Add") ? "Add Adoption" : "Edit Adoption"); + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + displayAdoptions(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + + private Adoption mapToAdoption(AdoptionResponse response) { + return new Adoption( + response.getAdoptionId().intValue(), + response.getPetId() != null ? response.getPetId().intValue() : 0, + response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, + response.getPetName(), + response.getCustomerName(), + response.getAdoptionDate() != null ? response.getAdoptionDate().toString() : "", + response.getAdoptionFee() != null ? response.getAdoptionFee().doubleValue() : 0.0, + response.getAdoptionStatus() + ); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java new file mode 100644 index 00000000..1ab0ffbe --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AnalyticsController.java @@ -0,0 +1,339 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.chart.*; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import org.example.petshopdesktop.api.dto.analytics.DailySales; +import org.example.petshopdesktop.api.dto.analytics.DashboardResponse; +import org.example.petshopdesktop.api.dto.analytics.TopProduct; +import org.example.petshopdesktop.api.dto.sale.SaleResponse; +import org.example.petshopdesktop.api.endpoints.AnalyticsApi; +import org.example.petshopdesktop.api.endpoints.SaleApi; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.text.NumberFormat; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import javafx.util.StringConverter; + +import java.util.*; +import java.util.stream.Collectors; + +public class AnalyticsController { + + @FXML + private Button btnRefresh; + + @FXML + private Label lblError; + + @FXML + private Label lblTotalRevenue; + + @FXML + private Label lblTotalTransactions; + + @FXML + private Label lblAvgTransaction; + + @FXML + private Label lblTotalItems; + + @FXML + private LineChart chartSalesOverTime; + + @FXML + private NumberAxis axisSalesDate; + + @FXML + private BarChart chartTopRevenue; + + @FXML + private BarChart chartTopQuantity; + + @FXML + private PieChart chartPaymentMethods; + + @FXML + private BarChart chartEmployeePerformance; + + private static final String SALES_COLOR = "#ff6b35"; + private static final String REVENUE_COLOR = "#4ecdc4"; + private static final String QUANTITY_COLOR = "#ff9f1c"; + private static final String EMPLOYEE_COLOR = "#1a759f"; + private static final List PIE_COLORS = List.of("#ff6b35", "#4ecdc4", "#1a759f", "#ff9f1c", "#577590", "#90be6d"); + + private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + private final NumberFormat wholeNumber = NumberFormat.getIntegerInstance(); + private final DateTimeFormatter chartDateFormatter = DateTimeFormatter.ofPattern("MMM d", Locale.CANADA); + private final List salesDateLabels = new ArrayList<>(); + + @FXML + public void initialize() { + configureCharts(); + loadAnalyticsData(); + } + + private void disableAllCharts() { + chartSalesOverTime.setVisible(false); + chartTopRevenue.setVisible(false); + chartTopQuantity.setVisible(false); + chartPaymentMethods.setVisible(false); + chartEmployeePerformance.setVisible(false); + btnRefresh.setDisable(true); + } + + private void configureCharts() { + chartSalesOverTime.setAnimated(false); + axisSalesDate.setAnimated(false); + axisSalesDate.setAutoRanging(false); + axisSalesDate.setForceZeroInRange(false); + axisSalesDate.setMinorTickVisible(false); + axisSalesDate.setTickUnit(1); + axisSalesDate.setTickLabelRotation(-45); + axisSalesDate.setTickLabelFormatter(new StringConverter<>() { + @Override + public String toString(Number value) { + int index = value.intValue(); + if (index < 0 || index >= salesDateLabels.size()) { + return ""; + } + int labelStep = Math.max(1, (salesDateLabels.size() + 14) / 15); + int lastIndex = salesDateLabels.size() - 1; + boolean nearEnd = index >= Math.max(0, lastIndex - 2); + boolean showLabel = index == 0 || nearEnd || index % labelStep == 0; + return showLabel ? salesDateLabels.get(index) : ""; + } + + @Override + public Number fromString(String string) { + return 0; + } + }); + chartTopRevenue.setAnimated(true); + chartTopQuantity.setAnimated(true); + chartPaymentMethods.setAnimated(true); + chartEmployeePerformance.setAnimated(true); + } + + private void loadAnalyticsData() { + lblError.setVisible(false); + new Thread(() -> { + try { + DashboardResponse dashboard = AnalyticsApi.getInstance().getDashboard(30, 10); + List sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null); + + Platform.runLater(() -> { + try { + loadSummaryData(dashboard); + loadSalesOverTime(dashboard); + loadTopProductsByRevenue(dashboard); + loadTopProductsByQuantity(dashboard); + loadPaymentMethodDistribution(sales); + loadEmployeePerformance(sales); + } catch (Exception e) { + ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data"); + lblError.setText("Error loading analytics data. Please try again."); + lblError.setVisible(true); + } + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data"); + lblError.setText("Error loading analytics data. Please try again."); + lblError.setVisible(true); + }); + } + }).start(); + } + + private void loadSummaryData(DashboardResponse dashboard) throws Exception { + if (dashboard != null) { + BigDecimal totalRevenue = BigDecimal.ZERO; + Long totalSales = 0L; + Long totalProducts = 0L; + + if (dashboard.getSalesSummary() != null) { + totalRevenue = dashboard.getSalesSummary().getTotalRevenue() != null ? dashboard.getSalesSummary().getTotalRevenue() : BigDecimal.ZERO; + totalSales = dashboard.getSalesSummary().getTotalSales() != null ? dashboard.getSalesSummary().getTotalSales() : 0L; + } + + if (dashboard.getInventorySummary() != null) { + totalProducts = dashboard.getInventorySummary().getTotalProducts() != null ? dashboard.getInventorySummary().getTotalProducts() : 0L; + } + + lblTotalRevenue.setText(currency.format(totalRevenue)); + lblTotalTransactions.setText(wholeNumber.format(totalSales)); + + BigDecimal avgTransaction = BigDecimal.ZERO; + if (totalSales > 0) { + avgTransaction = totalRevenue.divide(BigDecimal.valueOf(totalSales), 2, RoundingMode.HALF_UP); + } + lblAvgTransaction.setText(currency.format(avgTransaction)); + lblTotalItems.setText(wholeNumber.format(totalProducts)); + } + } + + private void loadSalesOverTime(DashboardResponse dashboard) throws Exception { + List dailySales = dashboard.getDailySales() != null ? dashboard.getDailySales() : new ArrayList<>(); + XYChart.Series series = new XYChart.Series<>(); + series.setName("Daily Revenue"); + + salesDateLabels.clear(); + for (int i = 0; i < dailySales.size(); i++) { + DailySales dailySale = dailySales.get(i); + salesDateLabels.add(formatChartDate(dailySale.getDate())); + BigDecimal revenue = dailySale.getRevenue() != null ? dailySale.getRevenue() : BigDecimal.ZERO; + series.getData().add(new XYChart.Data<>(i, revenue)); + } + + int upperBound = Math.max(0, salesDateLabels.size() - 1); + axisSalesDate.setLowerBound(0); + axisSalesDate.setUpperBound(upperBound); + chartSalesOverTime.getData().clear(); + chartSalesOverTime.getData().add(series); + applyLineChartColor(chartSalesOverTime, SALES_COLOR); + } + + private String formatChartDate(String date) { + if (date == null || date.isBlank()) { + return ""; + } + + try { + return LocalDate.parse(date).format(chartDateFormatter); + } catch (DateTimeParseException ignored) { + return date; + } + } + + private void loadTopProductsByRevenue(DashboardResponse dashboard) throws Exception { + List topProducts = dashboard.getTopProducts() != null ? dashboard.getTopProducts() : new ArrayList<>(); + XYChart.Series series = new XYChart.Series<>(); + series.setName("Revenue"); + + for (TopProduct product : topProducts) { + BigDecimal revenue = product.getRevenue() != null ? product.getRevenue() : BigDecimal.ZERO; + series.getData().add(new XYChart.Data<>(revenue, product.getProductName())); + } + + chartTopRevenue.getData().clear(); + chartTopRevenue.getData().add(series); + applyBarChartColor(chartTopRevenue, REVENUE_COLOR); + } + + private void loadTopProductsByQuantity(DashboardResponse dashboard) throws Exception { + List topProducts = dashboard.getTopProducts() != null ? dashboard.getTopProducts() : new ArrayList<>(); + XYChart.Series series = new XYChart.Series<>(); + series.setName("Quantity"); + + for (TopProduct product : topProducts) { + Long quantitySold = product.getQuantitySold() != null ? product.getQuantitySold() : 0L; + series.getData().add(new XYChart.Data<>(quantitySold, product.getProductName())); + } + + chartTopQuantity.getData().clear(); + chartTopQuantity.getData().add(series); + applyBarChartColor(chartTopQuantity, QUANTITY_COLOR); + } + + private void loadPaymentMethodDistribution(List sales) throws Exception { + Map paymentMethodCount = sales.stream() + .filter(sale -> sale.getIsRefund() == null || !sale.getIsRefund()) + .collect(Collectors.groupingBy( + sale -> sale.getPaymentMethod() != null ? sale.getPaymentMethod() : "Unknown", + Collectors.counting() + )); + + chartPaymentMethods.getData().clear(); + + List> paymentEntries = paymentMethodCount.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .toList(); + + for (Map.Entry entry : paymentEntries) { + PieChart.Data slice = new PieChart.Data( + entry.getKey() + " (" + entry.getValue() + ")", + entry.getValue() + ); + chartPaymentMethods.getData().add(slice); + } + + chartPaymentMethods.setLabelsVisible(false); + applyPieChartColors(); + } + + private void loadEmployeePerformance(List sales) throws Exception { + Map employeeRevenue = sales.stream() + .filter(sale -> sale.getIsRefund() == null || !sale.getIsRefund()) + .filter(sale -> sale.getEmployeeName() != null) + .collect(Collectors.groupingBy( + SaleResponse::getEmployeeName, + Collectors.summingDouble(sale -> sale.getTotalAmount() != null ? sale.getTotalAmount().doubleValue() : 0.0) + )); + + XYChart.Series series = new XYChart.Series<>(); + series.setName("Revenue"); + + List> employeeEntries = employeeRevenue.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .toList(); + + for (Map.Entry entry : employeeEntries) { + series.getData().add(new XYChart.Data<>(entry.getKey(), entry.getValue())); + } + + chartEmployeePerformance.getData().clear(); + chartEmployeePerformance.getData().add(series); + applyBarChartColor(chartEmployeePerformance, EMPLOYEE_COLOR); + } + + private void applyLineChartColor(LineChart chart, String color) { + Platform.runLater(() -> { + for (XYChart.Series series : chart.getData()) { + if (series.getNode() != null) { + series.getNode().setStyle("-fx-stroke: " + color + ";"); + } + for (XYChart.Data data : series.getData()) { + if (data.getNode() != null) { + data.getNode().setStyle("-fx-background-color: white, " + color + ";"); + } + } + } + }); + } + + private void applyBarChartColor(XYChart chart, String color) { + Platform.runLater(() -> { + for (XYChart.Series series : chart.getData()) { + for (XYChart.Data data : series.getData()) { + if (data.getNode() != null) { + data.getNode().setStyle("-fx-bar-fill: " + color + ";"); + } + } + } + }); + } + + private void applyPieChartColors() { + Platform.runLater(() -> { + for (int i = 0; i < chartPaymentMethods.getData().size(); i++) { + PieChart.Data slice = chartPaymentMethods.getData().get(i); + if (slice.getNode() != null) { + slice.getNode().setStyle("-fx-pie-color: " + PIE_COLORS.get(i % PIE_COLORS.size()) + ";"); + } + } + }); + } + + @FXML + void handleRefresh(ActionEvent event) { + loadAnalyticsData(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java new file mode 100644 index 00000000..221140b0 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/AppointmentController.java @@ -0,0 +1,247 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; + +import org.example.petshopdesktop.DTOs.AppointmentDTO; +import org.example.petshopdesktop.api.dto.appointment.AppointmentResponse; +import org.example.petshopdesktop.api.endpoints.AppointmentApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialogController; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.util.List; +import java.util.stream.Collectors; + +public class AppointmentController { + + @FXML private TableView tvAppointments; + + @FXML private TableColumn colAppointmentId; + @FXML private TableColumn colPetName; + @FXML private TableColumn colServiceName; + @FXML private TableColumn colAppointmentDate; + @FXML private TableColumn colAppointmentTime; + @FXML private TableColumn colCustomerName; + @FXML private TableColumn colAppointmentStatus; + + @FXML private Button btnAdd; + @FXML private Button btnEdit; + @FXML private Button btnDelete; + + @FXML private TextField txtSearch; + + private final ObservableList appointments = FXCollections.observableArrayList(); + private FilteredList filtered; + + @FXML + public void initialize(){ + //Enable multiple selection + tvAppointments.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + + colAppointmentId.setCellValueFactory(new PropertyValueFactory<>("appointmentId")); + colPetName.setCellValueFactory(new PropertyValueFactory<>("petName")); + colServiceName.setCellValueFactory(new PropertyValueFactory<>("serviceName")); + colAppointmentDate.setCellValueFactory(new PropertyValueFactory<>("appointmentDate")); + colAppointmentTime.setCellValueFactory(new PropertyValueFactory<>("appointmentTime")); + colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName")); + colAppointmentStatus.setCellValueFactory(new PropertyValueFactory<>("appointmentStatus")); + + filtered = new FilteredList<>(appointments, a -> true); + tvAppointments.setItems(filtered); + + if (txtSearch != null) { + txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); + } + + //EventListener for DELETE key + tvAppointments.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvAppointments.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + + loadAppointments(); + } + + private void loadAppointments(){ + new Thread(() -> { + try{ + List responses = AppointmentApi.getInstance().listAppointments(null); + List appointmentDTOs = responses.stream() + .map(this::mapToAppointmentDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + appointments.setAll(appointmentDTOs); + }); + }catch(Exception e){ + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentController.loadAppointments", + e, + "Loading appointments for table display"); + e.printStackTrace(); + }); + } + }).start(); + } + + private void applyFilter(String text) { + String query = text == null || text.trim().isEmpty() ? null : text.trim(); + new Thread(() -> { + try { + List responses = AppointmentApi.getInstance().listAppointments(query); + List appointmentDTOs = responses.stream() + .map(this::mapToAppointmentDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + appointments.setAll(appointmentDTOs); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentController.applyFilter", + e, + String.format("Filtering appointments with query: %s", query)); + e.printStackTrace(); + }); + } + }).start(); + } + + @FXML + void btnAddClicked(ActionEvent event){ + openDialog(null, "Add"); + } + + @FXML + void btnEditClicked(ActionEvent event){ + + AppointmentDTO selected = + tvAppointments.getSelectionModel().getSelectedItem(); + + if(selected == null){ + showAlert("Select Appointment", "Please select appointment to edit."); + return; + } + + openDialog(selected, "Edit"); + } + + @FXML + void btnDeleteClicked(ActionEvent event){ + //get selected appointments + var selectedAppointments = tvAppointments.getSelectionModel().getSelectedItems(); + if (selectedAppointments.isEmpty()) return; + + //ask user to confirm + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + String message = selectedAppointments.size() == 1 + ? "Are you sure you want to delete this appointment?" + : "Are you sure you want to delete " + selectedAppointments.size() + " appointments?"; + question.setContentText(message); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + java.util.Optional result = question.showAndWait(); + + //if confirmed, start deletion + if (result.isPresent() && result.get() == ButtonType.OK) { + List ids = selectedAppointments.stream() + .map(a -> (long) a.getAppointmentId()) + .collect(Collectors.toList()); + + try { + AppointmentApi.getInstance().deleteAppointments(ids); + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted " + ids.size() + " appointment(s)"); + alert.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "AppointmentController.btnDeleteClicked", + e, + "Deleting appointments"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Failed"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + + //refresh display + loadAppointments(); + } + } + + private void openDialog(AppointmentDTO appt, String mode){ + + try{ + FXMLLoader loader = new FXMLLoader( + getClass().getResource( + "/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml" + ) + ); + + Scene scene = new Scene(loader.load()); + + AppointmentDialogController controller = + loader.getController(); + + controller.setMode(mode); + + if(mode.equals("Edit")){ + controller.displayAppointmentDetails(appt); + } + + Stage stage = new Stage(); + stage.initModality(Modality.APPLICATION_MODAL); + stage.setScene(scene); + stage.showAndWait(); + + loadAppointments(); + + }catch(Exception e){ + ActivityLogger.getInstance().logException( + "AppointmentController.openDialog", + e, + "Opening appointment dialog in " + mode + " mode"); + e.printStackTrace(); + } + } + + private void showAlert(String title, String msg){ + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(msg); + alert.showAndWait(); + } + + private AppointmentDTO mapToAppointmentDTO(AppointmentResponse response) { + Long petId = response.getPetIds() != null && !response.getPetIds().isEmpty() ? response.getPetIds().get(0) : null; + return new AppointmentDTO( + response.getAppointmentId().intValue(), + response.getCustomerId() != null ? response.getCustomerId().intValue() : 0, + response.getCustomerName(), + petId != null ? petId.intValue() : 0, + String.join(", ", response.getPetNames()), + response.getServiceId() != null ? response.getServiceId().intValue() : 0, + response.getServiceName(), + response.getAppointmentDate().toString(), + response.getAppointmentTime().toString(), + response.getAppointmentStatus() + ); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java new file mode 100644 index 00000000..bc343639 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ChatController.java @@ -0,0 +1,350 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.ListCell; +import javafx.scene.control.ListView; +import javafx.scene.control.ScrollPane; +import javafx.scene.control.TextArea; +import javafx.scene.input.KeyCode; +import javafx.scene.layout.HBox; +import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; +import javafx.scene.layout.VBox; +import org.example.petshopdesktop.api.ChatRealtimeClient; +import org.example.petshopdesktop.api.dto.chat.ConversationResponse; +import org.example.petshopdesktop.api.dto.chat.MessageRequest; +import org.example.petshopdesktop.api.dto.chat.MessageResponse; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.endpoints.ChatApi; +import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.time.format.DateTimeFormatter; +import java.util.Comparator; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +public class ChatController { + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("MMM d, HH:mm"); + + @FXML + private ListView lvConversations; + + @FXML + private VBox vbMessages; + + @FXML + private ScrollPane spMessages; + + @FXML + private TextArea txtMessage; + + @FXML + private Button btnSend; + + @FXML + private Button btnRefresh; + + @FXML + private Label lblConversationTitle; + + @FXML + private Label lblChatStatus; + + private final ObservableList conversations = FXCollections.observableArrayList(); + private final Map customerLabels = new HashMap<>(); + private final ChatRealtimeClient realtimeClient = ChatRealtimeClient.getInstance(); + private ConversationResponse selectedConversation; + + @FXML + public void initialize() { + lvConversations.setItems(conversations); + lvConversations.setCellFactory(list -> new ListCell<>() { + @Override + protected void updateItem(ConversationResponse item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + setGraphic(null); + return; + } + + Label title = new Label(getConversationTitle(item)); + title.setStyle("-fx-font-weight: bold; -fx-text-fill: #1f2937;"); + Label preview = new Label(item.getLastMessage() == null ? "" : item.getLastMessage()); + preview.setStyle("-fx-text-fill: #64748b;"); + preview.setWrapText(true); + Label meta = new Label(buildConversationMeta(item)); + meta.setStyle("-fx-text-fill: #94a3b8; -fx-font-size: 11px;"); + VBox box = new VBox(4, title, preview, meta); + setGraphic(box); + } + }); + + lvConversations.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> { + if (newValue != null) { + selectedConversation = newValue; + lblConversationTitle.setText(getConversationTitle(newValue)); + loadMessages(newValue.getId()); + realtimeClient.subscribeToConversation(newValue.getId()); + } + }); + + txtMessage.setOnKeyPressed(event -> { + if (event.getCode() == KeyCode.ENTER && event.isControlDown()) { + btnSendClicked(); + } + }); + + realtimeClient.setConversationListener(conversation -> Platform.runLater(() -> upsertConversation(conversation))); + realtimeClient.setMessageListener(message -> Platform.runLater(() -> appendMessageIfSelected(message))); + realtimeClient.setStatusListener(status -> Platform.runLater(() -> lblChatStatus.setText(status))); + realtimeClient.subscribeToConversations(); + + loadCustomers(); + loadConversations(); + } + + @FXML + void btnRefreshClicked() { + loadConversations(); + if (selectedConversation != null) { + loadMessages(selectedConversation.getId()); + } + } + + @FXML + void btnSendClicked() { + if (selectedConversation == null) { + lblChatStatus.setText("Select a conversation"); + return; + } + + String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim(); + if (content.isEmpty()) { + return; + } + + txtMessage.clear(); + boolean sent = realtimeClient.sendMessage(selectedConversation.getId(), content); + if (!sent) { + sendMessageFallback(selectedConversation.getId(), content); + } + } + + private void loadCustomers() { + new Thread(() -> { + try { + List customers = DropdownApi.getInstance().getCustomers(); + Map labels = new HashMap<>(); + for (DropdownOption option : customers) { + labels.put(option.getId(), option.getLabel()); + } + Platform.runLater(() -> { + customerLabels.clear(); + customerLabels.putAll(labels); + lvConversations.refresh(); + if (selectedConversation != null) { + lblConversationTitle.setText(getConversationTitle(selectedConversation)); + } + }); + } catch (Exception e) { + Platform.runLater(() -> ActivityLogger.getInstance().logException( + "ChatController.loadCustomers", + e, + "Loading customer labels for chat")); + } + }).start(); + } + + private void loadConversations() { + new Thread(() -> { + try { + List response = ChatApi.getInstance().listConversations(); + response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); + Platform.runLater(() -> { + conversations.setAll(response); + restoreSelection(); + if (selectedConversation == null && !conversations.isEmpty()) { + lvConversations.getSelectionModel().selectFirst(); + } + }); + } catch (Exception e) { + Platform.runLater(() -> { + lblChatStatus.setText("Chat unavailable"); + ActivityLogger.getInstance().logException( + "ChatController.loadConversations", + e, + "Loading conversations"); + }); + } + }).start(); + } + + private void loadMessages(Long conversationId) { + new Thread(() -> { + try { + List messages = ChatApi.getInstance().listMessages(conversationId); + Platform.runLater(() -> renderMessages(messages)); + } catch (Exception e) { + Platform.runLater(() -> ActivityLogger.getInstance().logException( + "ChatController.loadMessages", + e, + "Loading messages for conversation " + conversationId)); + } + }).start(); + } + + private void sendMessageFallback(Long conversationId, String content) { + new Thread(() -> { + try { + MessageResponse response = ChatApi.getInstance().sendMessage(conversationId, new MessageRequest(content)); + Platform.runLater(() -> { + lblChatStatus.setText("Chat fallback active"); + appendMessageIfSelected(response); + }); + } catch (Exception e) { + Platform.runLater(() -> ActivityLogger.getInstance().logException( + "ChatController.sendMessageFallback", + e, + "Sending chat message for conversation " + conversationId)); + } + }).start(); + } + + private void renderMessages(List messages) { + vbMessages.getChildren().clear(); + for (MessageResponse message : messages) { + vbMessages.getChildren().add(createMessageBubble(message)); + } + scrollMessagesToBottom(); + } + + private void appendMessageIfSelected(MessageResponse message) { + upsertConversationForMessage(message); + if (selectedConversation != null && selectedConversation.getId().equals(message.getConversationId())) { + vbMessages.getChildren().add(createMessageBubble(message)); + scrollMessagesToBottom(); + } + } + + private void upsertConversation(ConversationResponse conversation) { + Optional existing = conversations.stream() + .filter(item -> item.getId().equals(conversation.getId())) + .findFirst(); + + if (existing.isPresent()) { + ConversationResponse current = existing.get(); + int index = conversations.indexOf(current); + conversations.set(index, conversation); + } else { + conversations.add(conversation); + } + + conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); + restoreSelection(); + lvConversations.refresh(); + } + + private void upsertConversationForMessage(MessageResponse message) { + conversations.stream() + .filter(conversation -> conversation.getId().equals(message.getConversationId())) + .findFirst() + .ifPresent(conversation -> { + conversation.setLastMessage(message.getContent()); + conversation.setUpdatedAt(message.getTimestamp()); + conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder()))); + lvConversations.refresh(); + }); + } + + private void restoreSelection() { + if (selectedConversation == null) { + return; + } + conversations.stream() + .filter(item -> item.getId().equals(selectedConversation.getId())) + .findFirst() + .ifPresent(match -> { + selectedConversation = match; + lvConversations.getSelectionModel().select(match); + }); + } + + private HBox createMessageBubble(MessageResponse message) { + boolean mine = message.getSenderId() != null && message.getSenderId().equals(UserSession.getInstance().getUserId()); + Label author = new Label(resolveAuthorLabel(message)); + author.setStyle("-fx-font-weight: bold; -fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";"); + + Label content = new Label(message.getContent()); + content.setWrapText(true); + content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";"); + + String timestampText = message.getTimestamp() == null ? "" : TIME_FORMATTER.format(message.getTimestamp()); + Label timestamp = new Label(timestampText); + timestamp.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#94a3b8") + "; -fx-font-size: 11px;"); + + VBox bubble = new VBox(4, author, content, timestamp); + bubble.setMaxWidth(420); + bubble.setStyle(mine + ? "-fx-background-color: #0f766e; -fx-background-radius: 14; -fx-padding: 12;" + : "-fx-background-color: #e2e8f0; -fx-background-radius: 14; -fx-padding: 12;"); + + Region spacer = new Region(); + HBox.setHgrow(spacer, Priority.ALWAYS); + HBox container = new HBox(12); + if (mine) { + container.getChildren().addAll(spacer, bubble); + } else { + container.getChildren().addAll(bubble, spacer); + } + return container; + } + + private String resolveAuthorLabel(MessageResponse message) { + Long currentUserId = UserSession.getInstance().getUserId(); + if (message.getSenderId() != null && message.getSenderId().equals(currentUserId)) { + return "You"; + } + if (selectedConversation != null && selectedConversation.getStaffId() != null && selectedConversation.getStaffId().equals(message.getSenderId())) { + return "Staff"; + } + return "Customer"; + } + + private String getConversationTitle(ConversationResponse conversation) { + String customerLabel = customerLabels.get(conversation.getCustomerId()); + return customerLabel != null ? customerLabel : "Customer #" + conversation.getCustomerId(); + } + + private String buildConversationMeta(ConversationResponse conversation) { + String assignee; + if (conversation.getStaffId() != null) { + assignee = "Assigned"; + } else if (conversation.getHumanRequestedAt() != null) { + assignee = "Takeover requested"; + } else if ("AUTOMATED".equals(conversation.getMode())) { + assignee = "Automated"; + } else { + assignee = "Open"; + } + String updated = conversation.getUpdatedAt() == null ? "" : TIME_FORMATTER.format(conversation.getUpdatedAt()); + return assignee + (updated.isBlank() ? "" : " · " + updated); + } + + private static java.time.LocalDateTime conversationSortTime(ConversationResponse conversation) { + return conversation.getUpdatedAt() != null ? conversation.getUpdatedAt() : conversation.getCreatedAt(); + } + + private void scrollMessagesToBottom() { + Platform.runLater(() -> spMessages.setVvalue(1.0)); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java new file mode 100644 index 00000000..f5f83461 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/InventoryController.java @@ -0,0 +1,244 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.inventory.InventoryResponse; +import org.example.petshopdesktop.api.endpoints.InventoryApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.InventoryDialogController; +import org.example.petshopdesktop.models.Inventory; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class InventoryController { + + //FXML elements + @FXML + private Button btnAdd; + + @FXML + private Button btnDelete; + + @FXML + private Button btnEdit; + + @FXML + private TableColumn colInventoryId; + + @FXML + private TableColumn colProductId; + + @FXML + private TableColumn colProductName; + + @FXML + private TableColumn colQuantity; + + @FXML + private TableView tvInventory; + + @FXML + private TextField txtSearch; + + private ObservableList data = FXCollections.observableArrayList(); + + //Determines if in add/edit mode + private String mode = null; + + //Loads upon view bootup + @FXML + void initialize() { + btnEdit.setDisable(true); + btnDelete.setDisable(true); + tvInventory.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.SINGLE); + + colInventoryId.setCellValueFactory(new PropertyValueFactory<>("inventoryId")); + colProductId.setCellValueFactory(new PropertyValueFactory<>("prodId")); + colProductName.setCellValueFactory(new PropertyValueFactory<>("prodName")); + colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); + + displayInventory(); + + tvInventory.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + }); + + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredInventory(newValue); + }); + + tvInventory.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvInventory.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + } + + //Opens dialog in add mode + @FXML + void btnAddClicked(ActionEvent event) { + mode = "Add"; + openDialog(null, mode); + } + + @FXML + void btnDeleteClicked(ActionEvent event) { + Inventory selectedInventory = tvInventory.getSelectionModel().getSelectedItem(); + if (selectedInventory == null) return; + + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + question.setContentText("Are you sure you want to delete this inventory record?"); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + Optional result = question.showAndWait(); + + if (result.isPresent() && result.get() == ButtonType.OK) { + try { + InventoryApi.getInstance().deleteInventory((long) selectedInventory.getInventoryId()); + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted inventory record"); + alert.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "InventoryController.btnDeleteClicked", + e, + "Deleting inventory"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Failed"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + + displayInventory(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } + + //Editing a record + @FXML + void btnEditClicked(ActionEvent event) { + Inventory selectedInventory = tvInventory.getSelectionModel().getSelectedItem(); + + if (selectedInventory != null) { + mode = "Edit"; + openDialog(selectedInventory, mode); + } + } + + private void displayFilteredInventory(String filter) { + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()) { + displayInventory(); + } else { + new Thread(() -> { + try { + List inventories = InventoryApi.getInstance().listInventory(filter); + List inventoryList = inventories.stream() + .map(this::mapToInventory) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(inventoryList); + tvInventory.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "InventoryController.displayFilteredInventory", + e, + String.format("Filtering inventory with keyword: %s", filter)); + }); + } + }).start(); + } + } + + private void displayInventory() { + new Thread(() -> { + try { + List inventories = InventoryApi.getInstance().listInventory(null); + List inventoryList = inventories.stream() + .map(this::mapToInventory) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(inventoryList); + tvInventory.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "InventoryController.displayInventory", + e, + "Fetching inventory data for table display"); + }); + } + }).start(); + } + + private void openDialog(Inventory inventory, String mode) { + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/inventory-dialog-view.fxml")); + Scene scene = null; + + try { + scene = new Scene(fxmlLoader.load()); + } catch (IOException e) { + ActivityLogger.getInstance().logException( + "InventoryController.openDialog", + e, + String.format("Loading inventory dialog view in %s mode", mode)); + throw new RuntimeException(e); + } + + InventoryDialogController dialogController = fxmlLoader.getController(); + dialogController.setMode(mode); + + if (mode.equals("Edit")) { + dialogController.displayInventoryDetails(inventory); + } + + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); + dialogStage.setTitle(mode.equals("Add") ? "Add Inventory" : "Edit Inventory"); + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + displayInventory(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + + private Inventory mapToInventory(InventoryResponse response) { + return new Inventory( + response.getInventoryId().intValue(), + response.getProdId() != null ? response.getProdId().intValue() : 0, + response.getProductName(), + response.getCategoryName() != null ? response.getCategoryName() : "", + 0, + "N/A", + response.getQuantity() != null ? response.getQuantity() : 0, + 0 + ); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/LoginController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/LoginController.java new file mode 100644 index 00000000..70c70e12 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/LoginController.java @@ -0,0 +1,125 @@ +package org.example.petshopdesktop.controllers; + +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.scene.layout.StackPane; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.ApiClient; +import org.example.petshopdesktop.api.dto.auth.LoginRequest; +import org.example.petshopdesktop.api.dto.auth.LoginResponse; +import org.example.petshopdesktop.api.dto.auth.UserInfoResponse; +import org.example.petshopdesktop.auth.Role; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.ui.SvgWebViewFactory; +import org.example.petshopdesktop.util.ActivityLogger; + +public class LoginController { + + @FXML + private TextField txtUsername; + + @FXML + private PasswordField txtPassword; + + @FXML + private Label lblError; + + @FXML + private StackPane logoContainer; + + @FXML + public void initialize() { + lblError.setText(""); + logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-text-light.svg", 142)); + } + + @FXML + void btnLoginClicked(ActionEvent event) { + String username = txtUsername.getText().trim(); + String password = txtPassword.getText(); + + if (username.isEmpty() || password.isEmpty()) { + lblError.setText("Please enter username and password."); + return; + } + + try { + ApiClient apiClient = ApiClient.getInstance(); + + LoginRequest loginRequest = new LoginRequest(username, password); + LoginResponse loginResponse = apiClient.post("/api/v1/auth/login", loginRequest, LoginResponse.class); + + if (loginResponse == null) { + throw new IllegalStateException("Login response is null"); + } + + String token = loginResponse.getToken(); + String roleStr = loginResponse.getRole(); + if (token == null || roleStr == null) { + throw new IllegalStateException("Token or role is null"); + } + + if ("CUSTOMER".equalsIgnoreCase(roleStr)) { + lblError.setText("Access Denied: Customer accounts cannot access the desktop application."); + txtPassword.clear(); + return; + } + + Role role = Role.valueOf(roleStr.toUpperCase()); + + UserSession.getInstance().login(null, username, role, token); + + UserInfoResponse userInfo = apiClient.get("/api/v1/auth/me", UserInfoResponse.class); + if (userInfo == null) { + throw new IllegalStateException("User info is null"); + } + UserSession.getInstance().login(userInfo.getId(), username, role, token); + UserSession.getInstance().setStoreId(userInfo.getStoreId()); + UserSession.getInstance().setEmployeeName(userInfo.getFullName() == null || userInfo.getFullName().isBlank() ? username : userInfo.getFullName()); + UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl()); + + openMainLayout(); + + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "LoginController.btnLoginClicked", + e, + "Authentication attempt for username: " + username); + + String errorMsg = e.getMessage(); + if (errorMsg != null && errorMsg.contains("Authentication failed")) { + lblError.setText("Invalid username or password."); + txtPassword.clear(); + } else if (e.getCause() instanceof java.net.ConnectException || + e instanceof java.net.http.HttpConnectTimeoutException) { + lblError.setText("Backend is not reachable, check backend docker compose and port 8080."); + } else { + lblError.setText(errorMsg != null ? errorMsg : "Login failed. Please try again."); + } + } + } + + private void openMainLayout() { + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/org/example/petshopdesktop/main-layout-view.fxml")); + Scene scene = new Scene(loader.load()); + Stage stage = (Stage) txtUsername.getScene().getWindow(); + stage.setScene(scene); + stage.setTitle("Pet Shop Manager"); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "LoginController.openMainLayout", + e, + "Loading main application layout after successful login"); + lblError.setText("Error loading application: " + e.getMessage()); + e.printStackTrace(); + } + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java new file mode 100644 index 00000000..b262be0b --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/MainLayoutController.java @@ -0,0 +1,448 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Parent; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.Separator; +import javafx.scene.image.Image; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.paint.ImagePattern; +import javafx.scene.shape.Circle; +import javafx.stage.FileChooser; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.ApiConfig; +import org.example.petshopdesktop.api.ChatRealtimeClient; +import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse; +import org.example.petshopdesktop.api.dto.auth.UserInfoResponse; +import org.example.petshopdesktop.api.endpoints.AuthApi; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.ui.SvgWebViewFactory; +import org.example.petshopdesktop.util.ActivityLogger; + +public class MainLayoutController { + + private static final String NAV_BASE_STYLE = "-fx-background-color: transparent; " + + "-fx-text-fill: #cbd5e1; " + + "-fx-background-radius: 10; " + + "-fx-cursor: hand; " + + "-fx-focus-color: transparent; " + + "-fx-faint-focus-color: transparent;"; + + private static final String NAV_ACTIVE_STYLE = "-fx-background-color: #FF6B6B; " + + "-fx-text-fill: white; " + + "-fx-background-radius: 10; " + + "-fx-cursor: hand; " + + "-fx-focus-color: transparent; " + + "-fx-faint-focus-color: transparent; " + + "-fx-effect: dropshadow(gaussian, rgba(0,0,0,0.22), 10, 0.15, 0, 2);"; + + @FXML + private Button btnAdoptions; + + @FXML + private Button btnAppointments; + + @FXML + private Button btnInventory; + + @FXML + private Button btnLogout; + + @FXML + private Button btnPets; + + @FXML + private Button btnProductSuppliers; + + @FXML + private Button btnProducts; + + @FXML + private Button btnSalesHistory; + + @FXML + private Button btnPurchaseOrders; + + @FXML + private Button btnServices; + + @FXML + private Button btnSuppliers; + + @FXML + private Button btnStaffAccounts; + + @FXML + private Button btnAnalytics; + + @FXML + private Button btnChat; + + @FXML + private StackPane logoContainer; + + @FXML + private StackPane avatarPreview; + + @FXML + private Button btnChangeAvatar; + + @FXML + private Button btnRemoveAvatar; + + @FXML + private Label lblUsername; + + @FXML + private Label lblRole; + + @FXML + private Label lblAdminSection; + + @FXML + private Separator separatorAdmin; + + @FXML + private StackPane spContentArea; + + @FXML + void btnAdoptionsClicked(ActionEvent event) { + loadView("adoption-view.fxml"); + updateButtons(btnAdoptions); + } + + @FXML + void btnAppointmentsClicked(ActionEvent event) { + loadView("appointment-view.fxml"); + updateButtons(btnAppointments); + } + + @FXML + void btnInventoryClicked(ActionEvent event) { + loadView("inventory-view.fxml"); + updateButtons(btnInventory); + } + + @FXML + void btnPetsClicked(ActionEvent event) { + loadView("pet-view.fxml"); + updateButtons(btnPets); + } + + @FXML + void btnProductSuppliersClicked(ActionEvent event) { + loadView("product-supplier-view.fxml"); + updateButtons(btnProductSuppliers); + } + + @FXML + void btnProductsClicked(ActionEvent event) { + loadView("product-view.fxml"); + updateButtons(btnProducts); + } + + @FXML + void btnSalesHistoryClicked(ActionEvent event) { + loadView("sale-view.fxml"); + updateButtons(btnSalesHistory); + } + + @FXML + void btnPurchaseOrdersClicked(ActionEvent event) { + loadView("purchase-order-view.fxml"); + updateButtons(btnPurchaseOrders); + } + + @FXML + void btnStaffAccountsClicked(ActionEvent event) { + loadView("staff-accounts-view.fxml"); + updateButtons(btnStaffAccounts); + } + + @FXML + void btnAnalyticsClicked(ActionEvent event) { + loadView("analytics-view.fxml"); + updateButtons(btnAnalytics); + } + + @FXML + void btnServicesClicked(ActionEvent event) { + loadView("service-view.fxml"); + updateButtons(btnServices); + } + + @FXML + void btnSuppliersClicked(ActionEvent event) { + loadView("supplier-view.fxml"); + updateButtons(btnSuppliers); + } + + @FXML + void btnChatClicked(ActionEvent event) { + loadView("chat-view.fxml"); + updateButtons(btnChat); + } + + @FXML + void logoClicked(MouseEvent event) { + UserSession session = UserSession.getInstance(); + if (session.isAdmin()) { + loadView("analytics-view.fxml"); + updateButtons(btnAnalytics); + } else { + loadView("sale-view.fxml"); + updateButtons(btnSalesHistory); + } + } + + @FXML + void btnChangeAvatarClicked(ActionEvent event) { + FileChooser chooser = new FileChooser(); + chooser.setTitle("Choose Profile Picture"); + chooser.getExtensionFilters().addAll( + new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif") + ); + java.io.File file = chooser.showOpenDialog(btnChangeAvatar.getScene().getWindow()); + if (file == null) { + return; + } + + try { + AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath()); + UserSession.getInstance().setAvatarUrl(response.getAvatarUrl()); + renderAvatar(UserSession.getInstance().getEmployeeName(), response.getAvatarUrl()); + btnRemoveAvatar.setDisable(response.getAvatarUrl() == null || response.getAvatarUrl().isBlank()); + } catch (Exception e) { + ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar"); + showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture."); + } + } + + @FXML + void btnRemoveAvatarClicked(ActionEvent event) { + try { + AuthApi.getInstance().deleteAvatar(); + UserSession.getInstance().setAvatarUrl(null); + renderAvatar(UserSession.getInstance().getEmployeeName(), null); + btnRemoveAvatar.setDisable(true); + } catch (Exception e) { + ActivityLogger.getInstance().logException("MainLayoutController.btnRemoveAvatarClicked", e, "Deleting avatar"); + showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not remove profile picture."); + } + } + + @FXML + void btnLogoutClicked(ActionEvent event) { + ChatRealtimeClient.getInstance().disconnect(); + UserSession.getInstance().logout(); + try { + FXMLLoader loader = new FXMLLoader( + getClass().getResource("/org/example/petshopdesktop/login-view.fxml")); + Scene scene = new Scene(loader.load()); + Stage stage = (Stage) btnLogout.getScene().getWindow(); + stage.setScene(scene); + stage.setTitle("Pet Shop Manager - Login"); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "MainLayoutController.btnLogoutClicked", + e, + "Loading login view after logout"); + System.err.println("Error loading login view: " + e.getMessage()); + e.printStackTrace(); + } + } + + @FXML + public void initialize() { + logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg", 94)); + renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl()); + btnRemoveAvatar.setDisable(UserSession.getInstance().getAvatarUrl() == null || UserSession.getInstance().getAvatarUrl().isBlank()); + refreshProfileHeader(); + applyRBAC(); + + UserSession session = UserSession.getInstance(); + if (session.isAdmin()) { + loadView("analytics-view.fxml"); + updateButtons(btnAnalytics); + } else { + loadView("sale-view.fxml"); + updateButtons(btnSalesHistory); + } + } + + private void refreshProfileHeader() { + new Thread(() -> { + try { + UserInfoResponse userInfo = AuthApi.getInstance().getCurrentUser(); + String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank() + ? UserSession.getInstance().getUsername() + : userInfo.getFullName(); + Platform.runLater(() -> { + UserSession.getInstance().setEmployeeName(displayName); + UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl()); + lblUsername.setText(displayName); + renderAvatar(displayName, userInfo.getAvatarUrl()); + btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank()); + }); + } catch (Exception e) { + Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl())); + } + }).start(); + } + + private void renderAvatar(String displayName, String avatarUrl) { + Circle border = new Circle(29); + border.setFill(Color.web("#dbe4ee")); + + Circle circle = new Circle(26); + Label initials = new Label(initials(displayName)); + initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;"); + + if (avatarUrl != null && !avatarUrl.isBlank()) { + try { + String resolvedUrl = avatarUrl.startsWith("http") ? avatarUrl : ApiConfig.getInstance().getBaseUrl() + avatarUrl; + Image image = new Image(resolvedUrl, 52, 52, true, true, true); + if (!image.isError()) { + circle.setFill(new ImagePattern(image)); + initials.setVisible(false); + } else { + circle.setFill(Color.web("#4ECDC4")); + initials.setVisible(true); + } + } catch (Exception e) { + circle.setFill(Color.web("#4ECDC4")); + initials.setVisible(true); + } + } else { + circle.setFill(Color.web("#4ECDC4")); + initials.setVisible(true); + } + + avatarPreview.getChildren().setAll(border, circle, initials); + } + + private String initials(String displayName) { + if (displayName == null || displayName.isBlank()) { + return "?"; + } + + String[] parts = displayName.trim().split("\\s+"); + if (parts.length == 1) { + return parts[0].substring(0, 1).toUpperCase(); + } + return (parts[0].substring(0, 1) + parts[parts.length - 1].substring(0, 1)).toUpperCase(); + } + + private void showAvatarError(String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("Profile Picture"); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + private void applyRBAC() { + UserSession session = UserSession.getInstance(); + + String displayName = session.getEmployeeName(); + if (displayName == null || displayName.isBlank()) { + displayName = session.getUsername(); + } + lblUsername.setText(displayName == null ? "" : displayName); + lblRole.setText("Leon's Petstore"); + + boolean isAdmin = session.isAdmin(); + btnInventory.setVisible(isAdmin); + btnInventory.setManaged(isAdmin); + btnSuppliers.setVisible(isAdmin); + btnSuppliers.setManaged(isAdmin); + btnProductSuppliers.setVisible(isAdmin); + btnProductSuppliers.setManaged(isAdmin); + + btnPurchaseOrders.setVisible(isAdmin); + btnPurchaseOrders.setManaged(isAdmin); + + if (btnStaffAccounts != null) { + btnStaffAccounts.setVisible(isAdmin); + btnStaffAccounts.setManaged(isAdmin); + } + + if (lblAdminSection != null) { + lblAdminSection.setVisible(isAdmin); + lblAdminSection.setManaged(isAdmin); + } + + if (separatorAdmin != null) { + separatorAdmin.setVisible(isAdmin); + separatorAdmin.setManaged(isAdmin); + } + + if (btnAnalytics != null) { + btnAnalytics.setVisible(isAdmin); + btnAnalytics.setManaged(isAdmin); + } + + btnSalesHistory.setText(isAdmin ? "Sales History" : "Sales"); + + + } + + private void loadView(String fxmlFile) { + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/modelviews/" + fxmlFile)); + Parent view = loader.load(); + spContentArea.getChildren().clear(); + spContentArea.getChildren().add(view); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "MainLayoutController.loadView", + e, + "Loading view: " + fxmlFile); + System.err.println("Error loading view: " + fxmlFile); + e.printStackTrace(); + + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle("View Load Error"); + alert.setHeaderText("Failed to load: " + fxmlFile); + alert.setContentText("Error: " + e.getMessage() + "\n\nCheck console for details."); + alert.showAndWait(); + } + } + + private void updateButtons(Button activeButton) { + Button[] buttons = { + btnAdoptions, + btnPets, + btnAppointments, + btnInventory, + btnSalesHistory, + btnServices, + btnSuppliers, + btnProductSuppliers, + btnProducts, + btnPurchaseOrders, + btnStaffAccounts, + btnAnalytics, + btnChat + }; + + for (Button button : buttons) { + if (button != null) { + button.setStyle(NAV_BASE_STYLE); + } + } + + if (activeButton != null) { + activeButton.setStyle(NAV_ACTIVE_STYLE); + } + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java new file mode 100644 index 00000000..55ccb3e1 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PetController.java @@ -0,0 +1,269 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.pet.PetResponse; +import org.example.petshopdesktop.api.endpoints.PetApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController; +import org.example.petshopdesktop.models.Pet; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class PetController { + + @FXML + private Button btnAdd; + + @FXML + private Button btnDelete; + + @FXML + private Button btnEdit; + + @FXML + private TableColumn colPetAge; + + @FXML + private TableColumn colPetBreed; + + @FXML + private TableColumn colPetId; + + @FXML + private TableColumn colPetName; + + @FXML + private TableColumn colPetPrice; + + @FXML + private TableColumn colPetSpecies; + + @FXML + private TableColumn colPetStatus; + + @FXML + private TableView tvPets; + + @FXML + private TextField txtSearch; + + @FXML + void btnAddClicked(ActionEvent event) { + mode = "Add"; + openDialog(null,mode); + } + + @FXML + void btnDeleteClicked(ActionEvent event) { + //get selected pets + var selectedPets = tvPets.getSelectionModel().getSelectedItems(); + if (selectedPets.isEmpty()) return; + + //ask user to confirm + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + String message = selectedPets.size() == 1 + ? "Are you sure you want to delete this pet?" + : "Are you sure you want to delete " + selectedPets.size() + " pets?"; + question.setContentText(message); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + Optional result = question.showAndWait(); + + //if confirmed, start deletion + if (result.isPresent() && result.get() == ButtonType.OK) { + List ids = selectedPets.stream() + .map(p -> (long) p.getPetId()) + .collect(Collectors.toList()); + + try { + PetApi.getInstance().deletePets(ids); + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted " + ids.size() + " pet(s)"); + alert.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "PetController.btnDeleteClicked", + e, + "Deleting pets"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Failed"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + + //refresh display and reset inputs + displayPets(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } + + @FXML + void btnEditClicked(ActionEvent event) { + Pet selectedPet = tvPets.getSelectionModel().getSelectedItem(); + + if(selectedPet != null){ + mode = "Edit"; + openDialog(selectedPet,mode); + } + } + + private ObservableList data = FXCollections.observableArrayList(); + String mode = null; + + @FXML + void initialize() { + btnEdit.setDisable(true); + btnDelete.setDisable(true); + //Enable multiple selection + tvPets.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + + colPetId.setCellValueFactory(new PropertyValueFactory("petId")); + colPetName.setCellValueFactory(new PropertyValueFactory("petName")); + colPetSpecies.setCellValueFactory(new PropertyValueFactory("petSpecies")); + colPetBreed.setCellValueFactory(new PropertyValueFactory("petBreed")); + colPetAge.setCellValueFactory(new PropertyValueFactory("petAge")); + colPetStatus.setCellValueFactory(new PropertyValueFactory("petStatus")); + colPetPrice.setCellValueFactory(new PropertyValueFactory("petPrice")); + + displayPets(); + + tvPets.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + }); + + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredPet(newValue); + }); + + //EventListener for DELETE key + tvPets.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvPets.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + } + + private void displayFilteredPet(String filter) { + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ + displayPets(); + } else { + new Thread(() -> { + try { + List pets = PetApi.getInstance().listPets(filter); + List petList = pets.stream() + .map(this::mapToPet) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(petList); + tvPets.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "PetController.displayFilteredPet", + e, + String.format("Filtering pets with keyword: %s", filter)); + }); + } + }).start(); + } + } + + private void displayPets() { + new Thread(() -> { + try { + List pets = PetApi.getInstance().listPets(null); + List petList = pets.stream() + .map(this::mapToPet) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(petList); + tvPets.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "PetController.displayPets", + e, + "Fetching pet data for table display"); + }); + } + }).start(); + } + + private void openDialog(Pet pet, String mode){ + //Get new view + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml")); + Scene scene = null; + try{ + scene = new Scene(fxmlLoader.load()); + } catch (IOException e) { + ActivityLogger.getInstance().logException( + "PetController.openDialog", + e, + "Loading pet dialog in " + mode + " mode"); + throw new RuntimeException(e); + } + PetDialogController dialogController = fxmlLoader.getController(); //controller associated with this view + dialogController.setMode(mode); + + //Open the dialog depending on the mode + if(mode.equals("Edit")){ + //Make it display pet details in dialog + dialogController.displayPetDetails(pet); + } + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal + if(mode.equals("Add")){ + dialogStage.setTitle("Add Pet"); + } + else { + dialogStage.setTitle("Edit Pet"); + } + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + //When dialog closes update table view and disable edit and delete buttons, and reset search bar + displayPets(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + + private Pet mapToPet(PetResponse response) { + return new Pet( + response.getPetId().intValue(), + response.getPetName(), + response.getPetSpecies(), + response.getPetBreed(), + response.getPetAge() != null ? response.getPetAge() : 0, + response.getPetStatus(), + response.getPetPrice().doubleValue() + ); + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java new file mode 100644 index 00000000..e6168911 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductController.java @@ -0,0 +1,299 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.ProductDTO; +import org.example.petshopdesktop.api.dto.product.ProductResponse; +import org.example.petshopdesktop.api.endpoints.ProductApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * The controller for any operations in the products view + */ +public class ProductController { + + @FXML + private Button btnAdd; + + @FXML + private Button btnDelete; + + @FXML + private Button btnEdit; + + @FXML + private TableColumn colProductCategory; + + @FXML + private TableColumn colProductDesc; + + @FXML + private TableColumn colProductId; + + @FXML + private TableColumn colProductName; + + @FXML + private TableColumn colProductPrice; + + @FXML + private TableView tvProducts; + + @FXML + private TextField txtSearch; + + //data declaration + private ObservableList data = FXCollections.observableArrayList(); //empty + private String mode = null; + + /** + * Set up the table view for products and display it when starting up + */ + @FXML + void initialize() { + //Disable buttons until a row is selected + btnEdit.setDisable(true); + btnDelete.setDisable(true); + //Enable multiple selection + tvProducts.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + //set up table columns + colProductId.setCellValueFactory(new PropertyValueFactory("prodId")); + colProductName.setCellValueFactory(new PropertyValueFactory("prodName")); + colProductPrice.setCellValueFactory(new PropertyValueFactory("prodPrice")); + colProductCategory.setCellValueFactory(new PropertyValueFactory("categoryName")); + colProductDesc.setCellValueFactory(new PropertyValueFactory("prodDesc")); + + displayProduct(); + + //EventListener to Enable buttons when a row is selected + tvProducts.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + } + ); + + //EventListener to search when text is changed on searchbar + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredProduct(newValue); + }); + + //EventListener for DELETE key press + tvProducts.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvProducts.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + + } + + /** + * Display the productDTO to table view + */ + private void displayProduct(){ + new Thread(() -> { + try { + List products = ProductApi.getInstance().listProducts(null); + List productDTOs = products.stream() + .map(this::mapToProductDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(productDTOs); + tvProducts.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "ProductController.displayProduct", + e, + "Fetching product data for table display"); + }); + } + }).start(); + } + + /** + * open a new dialog for adding a product + * @param event click event for button + */ + @FXML + void btnAddClicked(ActionEvent event) { + mode = "Add"; + openDialog(null,mode); + } + + /** + * Delete selected product(s) when delete is clicked + * @param event click event for button + */ + @FXML + void btnDeleteClicked(ActionEvent event) { + //get selected products + var selectedProducts = tvProducts.getSelectionModel().getSelectedItems(); + if (selectedProducts.isEmpty()) return; + + //ask user to confirm + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + String message = selectedProducts.size() == 1 + ? "Are you sure you want to delete this product?" + : "Are you sure you want to delete " + selectedProducts.size() + " products?"; + question.setContentText(message); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + Optional result = question.showAndWait(); + + //if confirmed, start deletion + if (result.isPresent() && result.get() == ButtonType.OK) { + List ids = selectedProducts.stream() + .map(p -> (long) p.getProdId()) + .collect(Collectors.toList()); + + try { + ProductApi.getInstance().deleteProducts(ids); + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted " + ids.size() + " product(s)"); + alert.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ProductController.btnDeleteClicked", + e, + "Deleting products"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Failed"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + + //refresh display and reset inputs + displayProduct(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } + + /** + * Open a new dialog for editing a product + * @param event click event for button + */ + @FXML + void btnEditClicked(ActionEvent event) { + //set selected product + ProductDTO selectedProduct = tvProducts.getSelectionModel().getSelectedItem(); + + if (selectedProduct != null) { + mode = "Edit"; + openDialog(selectedProduct, mode); + } + } + + /** + * Filter the table given the string from the searchbar + * @param filter word to filter table + */ + private void displayFilteredProduct(String filter){ + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ + displayProduct(); + } else { + new Thread(() -> { + try { + List products = ProductApi.getInstance().listProducts(filter); + List productDTOs = products.stream() + .map(this::mapToProductDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(productDTOs); + tvProducts.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "ProductController.displayFilteredProduct", + e, + String.format("Filtering products with keyword: %s", filter)); + }); + } + }).start(); + } + } + + /** + * Function to open the new Dialog for edit or adding + * depending on the mode given + * @param product the product entity for editing, null if adding + * @param mode the mode the dialog should be in + */ + private void openDialog(ProductDTO product, String mode){ + //Get new view + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/product-dialog-view.fxml")); + Scene scene = null; + try{ + scene = new Scene(fxmlLoader.load()); + } catch (IOException e) { + ActivityLogger.getInstance().logException( + "ProductController.openDialog", + e, + String.format("Loading product dialog view in %s mode", mode)); + throw new RuntimeException(e); + } + ProductDialogController dialogController = fxmlLoader.getController(); //controller associated with this view + dialogController.setMode(mode); + + //Open the dialog depending on the mode + if(mode.equals("Edit")){ + //Make it display suppliers details in dialog + dialogController.displayProductDetails(product); + } + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal + if(mode.equals("Add")){ + dialogStage.setTitle("Add Product"); + } + else { + dialogStage.setTitle("Edit Product"); + } + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + //When dialog closes update table view and disable edit and delete buttons, and reset search bar + displayProduct(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + + private ProductDTO mapToProductDTO(ProductResponse response) { + return new ProductDTO( + response.getProdId().intValue(), + response.getProdName(), + response.getProdPrice().doubleValue(), + 0, + response.getCategoryName(), + response.getProdDesc() + ); + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java new file mode 100644 index 00000000..63bba3ae --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ProductSupplierController.java @@ -0,0 +1,305 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.ProductSupplierDTO; +import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierResponse; +import org.example.petshopdesktop.api.endpoints.ProductSupplierApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.ProductSupplierDialogController; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +public class ProductSupplierController { + + @FXML + private Button btnAdd; + + @FXML + private Button btnDelete; + + @FXML + private Button btnEdit; + + @FXML + private TableColumn colCost; + + @FXML + private TableColumn colProductId; + + @FXML + private TableColumn colProductName; + + @FXML + private TableColumn colSupplierId; + + @FXML + private TableColumn colSupplierName; + + @FXML + private TableView tvProductSuppliers; + + @FXML + private TextField txtSearch; + + //data declaration + private ObservableList data = FXCollections.observableArrayList(); + private String mode = null; + + /** + * Set up the table view for table and display it when starting up + */ + @FXML + public void initialize() { + //Disable buttons until a row is selected + btnEdit.setDisable(true); + btnDelete.setDisable(true); + //Enable multiple selection + tvProductSuppliers.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + //set up table columns + colProductId.setCellValueFactory(new PropertyValueFactory("prodId")); + colProductName.setCellValueFactory(new PropertyValueFactory("prodName")); + colSupplierId.setCellValueFactory(new PropertyValueFactory("supId")); + colSupplierName.setCellValueFactory(new PropertyValueFactory("supCompany")); + colCost.setCellValueFactory(new PropertyValueFactory("cost")); + + displayProductSupplier(); + + //EventListener to Enable buttons when a row is selected + tvProductSuppliers.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + } + ); + + //EventListener to search when text is changed on searchbar + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredProductSupplier(newValue); + }); + + //EventListener for DELETE key + tvProductSuppliers.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvProductSuppliers.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + + } + + /** + * Display the ProductSupplierDTO to table view + */ + private void displayProductSupplier() { + new Thread(() -> { + try { + List productSuppliers = ProductSupplierApi.getInstance().listProductSuppliers(null); + List productSupplierDTOs = productSuppliers.stream() + .map(this::mapToProductSupplierDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(productSupplierDTOs); + tvProductSuppliers.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "ProductSupplierController.displayProductSupplier", + e, + "Fetching product-supplier data for table display"); + }); + } + }).start(); + } + + /** + * Filter the table given the string from the searchbar + * @param filter word to filter table + */ + private void displayFilteredProductSupplier(String filter){ + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ + displayProductSupplier(); + } else { + new Thread(() -> { + try { + List productSuppliers = ProductSupplierApi.getInstance().listProductSuppliers(filter); + List productSupplierDTOs = productSuppliers.stream() + .map(this::mapToProductSupplierDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(productSupplierDTOs); + tvProductSuppliers.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "ProductSupplierController.displayFilteredProductSupplier", + e, + "Filtering product-supplier data with filter: " + filter); + }); + } + }).start(); + } + } + + /** + * open a new dialog for adding a productSupplier + * @param event click event for button + */ + @FXML + void btnAddClicked(ActionEvent event) { + mode = "Add"; + openDialog(null,mode); + } + + /** + * Delete selected product-supplier(s) when delete is clicked + * @param event click event for button + */ + @FXML + void btnDeleteClicked(ActionEvent event) { + //get selected product-suppliers + var selectedProductSuppliers = tvProductSuppliers.getSelectionModel().getSelectedItems(); + if (selectedProductSuppliers.isEmpty()) return; + + //ask user to confirm + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + String message = selectedProductSuppliers.size() == 1 + ? "Are you sure you want to delete this product-supplier?" + : "Are you sure you want to delete " + selectedProductSuppliers.size() + " product-suppliers?"; + question.setContentText(message); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + Optional result = question.showAndWait(); + + //if confirmed, start deletion + if (result.isPresent() && result.get() == ButtonType.OK) { + int deleteCount = 0; + Exception lastException = null; + + for (ProductSupplierDTO ps : selectedProductSuppliers) { + try { + ProductSupplierApi.getInstance().deleteProductSupplier( + (long) ps.getProdId(), + (long) ps.getSupId() + ); + deleteCount++; + } catch (Exception e) { + lastException = e; + ActivityLogger.getInstance().logException( + "ProductSupplierController.btnDeleteClicked", + e, + "Deleting product-supplier with productId=" + ps.getProdId() + ", supplierId=" + ps.getSupId()); + } + } + + if (deleteCount > 0) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted " + deleteCount + " product-supplier(s)"); + alert.showAndWait(); + } + + if (lastException != null && deleteCount < selectedProductSuppliers.size()) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Partially Failed"); + alert.setContentText("Deleted " + deleteCount + " of " + selectedProductSuppliers.size() + " product-supplier(s). Last error: " + lastException.getMessage()); + alert.showAndWait(); + } + + //refresh display and reset inputs + displayProductSupplier(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } + + @FXML + void btnEditClicked(ActionEvent event) { + //set selected item + ProductSupplierDTO selectedProductSupplier = tvProductSuppliers.getSelectionModel().getSelectedItem(); + + if (selectedProductSupplier != null) { + mode = "Edit"; + openDialog(selectedProductSupplier,mode); + } + } + + /** + * Function to open the new Dialog for edit or adding + * depending on the mode given + * @param productSupplier the productSupplier entity for editing, null if adding + * @param mode the mode the dialog should be in + */ + private void openDialog(ProductSupplierDTO productSupplier, String mode){ + //Get new view + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/product-supplier-dialog-view.fxml")); + Scene scene = null; + try{ + scene = new Scene(fxmlLoader.load()); + } catch (IOException e) { + ActivityLogger.getInstance().logException( + "ProductSupplierController.openDialog", + e, + "Loading product-supplier dialog in " + mode + " mode"); + throw new RuntimeException(e); + } + ProductSupplierDialogController dialogController = fxmlLoader.getController(); //controller associated with this view + dialogController.setMode(mode); + if (productSupplier != null) { + dialogController.setSelectedIds(productSupplier.getSupId(), productSupplier.getProdId()); + } + + //Open the dialog depending on the mode + if(mode.equals("Edit")){ + //Make it display suppliers details in dialog + dialogController.displayProductSupplierDetails(productSupplier); + } + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal + if(mode.equals("Add")){ + dialogStage.setTitle("Add Product-Supplier"); + } + else { + dialogStage.setTitle("Edit Product-Supplier"); + } + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + //When dialog closes update table view and disable edit and delete buttons, and reset search bar + displayProductSupplier(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + + private ProductSupplierDTO mapToProductSupplierDTO(ProductSupplierResponse response) { + return new ProductSupplierDTO( + response.getSupplierId().intValue(), + response.getProductId().intValue(), + response.getSupplierName(), + response.getProductName(), + response.getCost().doubleValue() + ); + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java new file mode 100644 index 00000000..71ec81ec --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/PurchaseOrderController.java @@ -0,0 +1,121 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import org.example.petshopdesktop.DTOs.PurchaseOrderDTO; +import org.example.petshopdesktop.api.dto.purchaseorder.PurchaseOrderResponse; +import org.example.petshopdesktop.api.endpoints.PurchaseOrderApi; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.util.List; +import java.util.stream.Collectors; + +public class PurchaseOrderController { + + @FXML private Button btnRefresh; + + @FXML + private TextField txtSearch; + + @FXML private TableView tvPurchaseOrders; + + @FXML private TableColumn colOrderId; + @FXML private TableColumn colSupplier; + @FXML private TableColumn colOrderDate; + @FXML private TableColumn colStatus; + + private final ObservableList purchaseOrders = FXCollections.observableArrayList(); + private FilteredList filtered; + + @FXML + public void initialize() { + + colOrderId.setCellValueFactory( + new PropertyValueFactory<>("purchaseOrderId")); + + colSupplier.setCellValueFactory( + new PropertyValueFactory<>("supplierName")); + + colOrderDate.setCellValueFactory( + new PropertyValueFactory<>("orderDate")); + + colStatus.setCellValueFactory( + new PropertyValueFactory<>("status")); + + filtered = new FilteredList<>(purchaseOrders, p -> true); + tvPurchaseOrders.setItems(filtered); + + if (txtSearch != null) { + txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); + } + + loadPurchaseOrders(); + } + + private void loadPurchaseOrders() { + new Thread(() -> { + try { + List responses = PurchaseOrderApi.getInstance().listPurchaseOrders(null); + List dtos = responses.stream() + .map(this::mapToPurchaseOrderDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + purchaseOrders.setAll(dtos); + tvPurchaseOrders.setItems(filtered); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "PurchaseOrderController.loadPurchaseOrders", + e, + "Loading purchase orders for table display"); + new Alert(Alert.AlertType.ERROR, + "Unable to load purchase orders").showAndWait(); + }); + } + }).start(); + } + + private void applyFilter(String text) { + if (filtered == null) { + return; + } + + String q = text == null ? "" : text.trim().toLowerCase(); + if (q.isEmpty()) { + filtered.setPredicate(p -> true); + return; + } + + filtered.setPredicate(p -> + String.valueOf(p.getPurchaseOrderId()).contains(q) + || safe(p.getSupplierName()).contains(q) + || safe(p.getOrderDate()).contains(q) + || safe(p.getStatus()).contains(q) + ); + } + + private static String safe(String v) { + return v == null ? "" : v.toLowerCase(); + } + + @FXML + void btnRefresh() { + loadPurchaseOrders(); + } + + private PurchaseOrderDTO mapToPurchaseOrderDTO(PurchaseOrderResponse response) { + return new PurchaseOrderDTO( + response.getPurchaseOrderId(), + response.getSupplierName(), + response.getOrderDate() != null ? response.getOrderDate().toString() : "", + response.getOrderStatus() + ); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java new file mode 100644 index 00000000..c3866b1e --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SaleController.java @@ -0,0 +1,440 @@ +package org.example.petshopdesktop.controllers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.SelectionMode; +import javafx.scene.control.Spinner; +import javafx.scene.control.SpinnerValueFactory; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.scene.layout.VBox; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.auth.UserSession; +import javafx.concurrent.Task; +import org.example.petshopdesktop.api.endpoints.ProductApi; +import org.example.petshopdesktop.api.endpoints.SaleApi; +import org.example.petshopdesktop.api.dto.product.ProductResponse; +import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; +import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; +import org.example.petshopdesktop.api.dto.sale.SaleRequest; +import org.example.petshopdesktop.api.dto.sale.SaleResponse; +import org.example.petshopdesktop.models.Product; +import org.example.petshopdesktop.models.SaleCartItem; +import org.example.petshopdesktop.models.SaleLineItem; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +public class SaleController { + + @FXML + private Button btnRefresh; + + @FXML + private Button btnRefund; + + @FXML + private Label lblModeNote; + + @FXML + private VBox vbCreateSale; + + @FXML + private ComboBox cbProduct; + + @FXML + private Spinner spQuantity; + + @FXML + private Button btnAddToCart; + + @FXML + private Button btnRemoveSelected; + + @FXML + private TableView tvCart; + + @FXML + private TableColumn colCartProduct; + + @FXML + private TableColumn colCartQty; + + @FXML + private TableColumn colCartUnitPrice; + + @FXML + private TableColumn colCartTotal; + + @FXML + private ComboBox cbPaymentMethod; + + @FXML + private Label lblCartTotal; + + @FXML + private Button btnClearCart; + + @FXML + private Button btnSaveSale; + + @FXML + private TableColumn colSaleId; + + @FXML + private TableColumn colSaleDate; + + @FXML + private TableColumn colEmployeeName; + + @FXML + private TableColumn colServiceProduct; + + @FXML + private TableColumn colSaleQuantity; + + @FXML + private TableColumn colSaleUnitPrice; + + @FXML + private TableColumn colSaleTotal; + + @FXML + private TableColumn colSalePaymentType; + + @FXML + private TableView tvSales; + + @FXML + private TextField txtSearch; + + private final ObservableList cartItems = FXCollections.observableArrayList(); + private final ObservableList saleItems = FXCollections.observableArrayList(); + private FilteredList filteredSales; + + private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + + @FXML + public void initialize() { + setupTables(); + setupCreateSale(); + applyRoleMode(); + + refreshSales(); + } + + private void setupTables() { + tvCart.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tvCart.setFixedCellSize(34); + colCartProduct.setCellValueFactory(new PropertyValueFactory<>("prodName")); + colCartQty.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colCartUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colCartTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + tvCart.setItems(cartItems); + tvCart.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + tvSales.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY); + tvSales.setFixedCellSize(34); + colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId")); + colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate")); + colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName")); + colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName")); + colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colSaleTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + colSalePaymentType.setCellValueFactory(new PropertyValueFactory<>("paymentMethod")); + + filteredSales = new FilteredList<>(saleItems, s -> true); + tvSales.setItems(filteredSales); + + txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal)); + } + + private void setupCreateSale() { + spQuantity.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 999, 1)); + spQuantity.setEditable(true); + + cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card")); + cbPaymentMethod.getSelectionModel().selectFirst(); + + updateCartTotal(); + + try { + List productResponses = ProductApi.getInstance().listProducts(null); + ObservableList products = FXCollections.observableArrayList(); + for (ProductResponse pr : productResponses) { + products.add(new Product( + pr.getProdId().intValue(), + pr.getProdName(), + pr.getProdPrice().doubleValue(), + 0, + pr.getProdDesc() + )); + } + cbProduct.setItems(products); + } catch (Exception e) { + ActivityLogger.getInstance().logException("SaleController.setupCreateSale", e, "Loading products"); + } + } + + private void applyRoleMode() { + boolean isAdmin = UserSession.getInstance().isAdmin(); + vbCreateSale.setVisible(!isAdmin); + vbCreateSale.setManaged(!isAdmin); + lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)"); + } + + private void refreshSales() { + refreshSales(false); + } + + private void refreshSales(boolean showErrorDialog) { + Task> task = new Task>() { + @Override + protected List call() throws Exception { + List sales = SaleApi.getInstance().listSales(0, 1000, null); + List lineItems = new ArrayList<>(); + + for (SaleResponse sale : sales) { + String saleDate = sale.getSaleDate() != null + ? sale.getSaleDate().format(DATE_FORMATTER) + : ""; + + if (sale.getItems() != null && !sale.getItems().isEmpty()) { + for (SaleItemResponse item : sale.getItems()) { + double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0; + double lineTotal = unitPrice * item.getQuantity(); + lineItems.add(new SaleLineItem( + sale.getSaleId().intValue(), + saleDate, + sale.getEmployeeName(), + item.getProductName(), + item.getQuantity(), + unitPrice, + lineTotal, + sale.getPaymentMethod(), + sale.getIsRefund() != null && sale.getIsRefund() + )); + } + } + } + return lineItems; + } + }; + + task.setOnSucceeded(event -> { + saleItems.setAll(task.getValue()); + }); + + task.setOnFailed(event -> { + Throwable e = task.getException(); + ActivityLogger.getInstance().logException("SaleController.refreshSales", (Exception) e, "Loading sales"); + if (showErrorDialog) { + showError("Sales", "Could not load sales: " + e.getMessage()); + } + }); + + new Thread(task).start(); + } + + @FXML + void btnRefresh(ActionEvent event) { + refreshSales(true); + } + + @FXML + void btnAddToCart(ActionEvent event) { + Product product = cbProduct.getSelectionModel().getSelectedItem(); + if (product == null) { + showError("Create Sale", "Select a product."); + return; + } + + int requestedQty; + try { + requestedQty = spQuantity.getValue(); + } catch (Exception e) { + showError("Create Sale", "Enter a valid quantity."); + return; + } + if (requestedQty <= 0) { + showError("Create Sale", "Quantity must be at least 1."); + return; + } + + for (SaleCartItem item : cartItems) { + if (item.getProdId() == product.getProdId()) { + item.setQuantity(item.getQuantity() + requestedQty); + tvCart.refresh(); + updateCartTotal(); + return; + } + } + + cartItems.add(new SaleCartItem(product.getProdId(), product.getProdName(), requestedQty, product.getProdPrice())); + updateCartTotal(); + } + + @FXML + void btnRemoveSelected(ActionEvent event) { + SaleCartItem selected = tvCart.getSelectionModel().getSelectedItem(); + if (selected != null) { + cartItems.remove(selected); + updateCartTotal(); + } + } + + @FXML + void btnClearCart(ActionEvent event) { + cartItems.clear(); + updateCartTotal(); + } + + @FXML + void btnSaveSale(ActionEvent event) { + if (UserSession.getInstance().isAdmin()) { + showError("Create Sale", "This action is restricted to staff."); + return; + } + + Long storeId = UserSession.getInstance().getStoreId(); + if (storeId == null || storeId <= 0) { + showError("Create Sale", "Store is not set for this account."); + return; + } + + if (cartItems.isEmpty()) { + showError("Create Sale", "Add at least one item."); + return; + } + + String payment = cbPaymentMethod.getSelectionModel().getSelectedItem(); + if (payment == null || payment.isBlank()) { + showError("Create Sale", "Select a payment method."); + return; + } + + try { + SaleRequest request = new SaleRequest(); + request.setStoreId(storeId); + request.setPaymentMethod(payment); + + List itemRequests = new ArrayList<>(); + for (SaleCartItem cartItem : cartItems) { + SaleItemRequest itemRequest = new SaleItemRequest(); + itemRequest.setProdId((long) cartItem.getProdId()); + itemRequest.setQuantity(cartItem.getQuantity()); + itemRequests.add(itemRequest); + } + request.setItems(itemRequests); + + SaleResponse response = SaleApi.getInstance().createSale(request); + showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created."); + + cartItems.clear(); + updateCartTotal(); + + refreshSales(true); + } catch (Exception e) { + ActivityLogger.getInstance().logException("SaleController.btnSaveSale", e, "Creating sale"); + String errorMsg = e.getMessage(); + if (errorMsg != null && errorMsg.contains("Insufficient inventory")) { + showError("Create Sale", "Insufficient stock for one or more items."); + } else { + showError("Create Sale", errorMsg != null ? errorMsg : "Could not save the sale."); + } + } + } + + @FXML + void btnRefund(ActionEvent event) { + openRefundDialog(); + } + + private void openRefundDialog() { + try { + SaleLineItem selectedSale = tvSales.getSelectionModel().getSelectedItem(); + if (selectedSale != null && selectedSale.isRefund()) { + showError("Refund", "Select an original sale, not an existing refund."); + return; + } + + FXMLLoader loader = new FXMLLoader(getClass().getResource( + "/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(btnRefund.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Process Refund"); + dialog.setScene(new Scene(loader.load())); + if (selectedSale != null) { + loader.getController() + .prefillSale((long) selectedSale.getSaleId()); + } + dialog.setResizable(false); + dialog.showAndWait(); + + refreshSales(true); + } catch (Exception e) { + ActivityLogger.getInstance().logException("SaleController.openRefundDialog", e, "Opening refund dialog"); + } + } + + private void updateCartTotal() { + double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum(); + lblCartTotal.setText(currency.format(total)); + } + + private void applySalesFilter(String filter) { + String f = filter == null ? "" : filter.trim().toLowerCase(); + if (f.isEmpty()) { + filteredSales.setPredicate(s -> true); + return; + } + + filteredSales.setPredicate(s -> + String.valueOf(s.getSaleId()).contains(f) + || safe(s.getSaleDate()).contains(f) + || safe(s.getEmployeeName()).contains(f) + || safe(s.getItemName()).contains(f) + || safe(s.getPaymentMethod()).contains(f) + ); + } + + private static String safe(String v) { + return v == null ? "" : v.toLowerCase(); + } + + private void showError(String title, String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + private void showInfo(String title, String message) { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java new file mode 100644 index 00000000..d0a3049e --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/ServiceController.java @@ -0,0 +1,233 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.ServiceDTO; +import org.example.petshopdesktop.api.dto.service.ServiceResponse; +import org.example.petshopdesktop.api.endpoints.ServiceApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.ServiceDialogController; +import org.example.petshopdesktop.util.ActivityLogger; +import javafx.stage.Modality; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + + +public class ServiceController { + + @FXML private Button btnAdd; + @FXML private Button btnDelete; + @FXML private Button btnEdit; + + @FXML private TableColumn colServiceId; + @FXML private TableColumn colServiceName; + @FXML private TableColumn colServiceDesc; + @FXML private TableColumn colServiceDuration; + @FXML private TableColumn colServicePrice; + + @FXML private TableView tvServices; + + @FXML private TextField txtSearch; + + private ObservableList data = FXCollections.observableArrayList(); + private String mode = null; + + @FXML + public void initialize() { + btnEdit.setDisable(true); + btnDelete.setDisable(true); + tvServices.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + + colServiceId.setCellValueFactory(new PropertyValueFactory<>("serviceId")); + colServiceName.setCellValueFactory(new PropertyValueFactory<>("serviceName")); + colServiceDesc.setCellValueFactory(new PropertyValueFactory<>("serviceDesc")); + colServiceDuration.setCellValueFactory(new PropertyValueFactory<>("serviceDuration")); + colServicePrice.setCellValueFactory(new PropertyValueFactory<>("servicePrice")); + + displayServices(); + + tvServices.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + } + ); + + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredServices(newValue); + }); + + tvServices.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvServices.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + } + + private void displayServices() { + new Thread(() -> { + try { + List services = ServiceApi.getInstance().listServices(null); + List serviceDTOs = services.stream() + .map(this::mapToServiceDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(serviceDTOs); + tvServices.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "ServiceController.displayServices", + e, + "Fetching service data for table display"); + }); + } + }).start(); + } + + private void displayFilteredServices(String filter) { + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()) { + displayServices(); + } else { + new Thread(() -> { + try { + List services = ServiceApi.getInstance().listServices(filter); + List serviceDTOs = services.stream() + .map(this::mapToServiceDTO) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(serviceDTOs); + tvServices.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "ServiceController.displayFilteredServices", + e, + String.format("Filtering services with keyword: %s", filter)); + }); + } + }).start(); + } + } + + + @FXML + void btnAddClicked(ActionEvent event) { + mode = "Add"; + openDialog(null, mode); + } + + @FXML + void btnEditClicked(ActionEvent event) { + ServiceDTO selected = tvServices.getSelectionModel().getSelectedItem(); + + if (selected != null) { + mode = "Edit"; + openDialog(selected, mode); + } + } + + @FXML + void btnDeleteClicked(ActionEvent event) { + var selectedServices = tvServices.getSelectionModel().getSelectedItems(); + if (selectedServices.isEmpty()) return; + + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + String message = selectedServices.size() == 1 + ? "Are you sure you want to delete this service?" + : "Are you sure you want to delete " + selectedServices.size() + " services?"; + question.setContentText(message); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + Optional result = question.showAndWait(); + + if (result.isPresent() && result.get() == ButtonType.OK) { + List ids = selectedServices.stream() + .map(s -> (long) s.getServiceId()) + .collect(Collectors.toList()); + + try { + ServiceApi.getInstance().deleteServices(ids); + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted " + ids.size() + " service(s)"); + alert.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ServiceController.btnDeleteClicked", + e, + "Deleting services"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Failed"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + + displayServices(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } + + private void openDialog(ServiceDTO service, String mode) { + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/service-dialog-view.fxml")); + Scene scene = null; + try { + scene = new Scene(fxmlLoader.load()); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ServiceController.openDialog", + e, + String.format("Loading service dialog view in %s mode", mode)); + throw new RuntimeException(e); + } + ServiceDialogController dialogController = fxmlLoader.getController(); + dialogController.setMode(mode); + + if (mode.equals("Edit")) { + dialogController.setService(service); + } + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); + if (mode.equals("Add")) { + dialogStage.setTitle("Add Service"); + } else { + dialogStage.setTitle("Edit Service"); + } + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + displayServices(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + + private ServiceDTO mapToServiceDTO(ServiceResponse response) { + return new ServiceDTO( + response.getServiceId().intValue(), + response.getServiceName(), + response.getServiceDesc(), + response.getServiceDuration() != null ? response.getServiceDuration() : 0, + response.getServicePrice().doubleValue() + ); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java new file mode 100644 index 00000000..a02674e4 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/StaffAccountsController.java @@ -0,0 +1,231 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.collections.transformation.FilteredList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TableColumn; +import javafx.scene.control.TableView; +import javafx.scene.control.TextField; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.employee.EmployeeResponse; +import org.example.petshopdesktop.api.endpoints.EmployeeApi; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.models.StaffAccount; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.sql.Timestamp; +import java.time.ZoneId; +import java.util.List; +import java.util.stream.Collectors; + +public class StaffAccountsController { + + @FXML + private TableView tvStaff; + + @FXML + private TableColumn colUsername; + + @FXML + private TableColumn colName; + + @FXML + private TableColumn colEmail; + + @FXML + private TableColumn colPhone; + + @FXML + private TableColumn colStatus; + + @FXML + private TableColumn colCreated; + + @FXML + private TextField txtSearch; + + @FXML + private Label lblError; + + @FXML + private Button btnCreateAccount; + + @FXML + private Button btnEditAccount; + + private final ObservableList staffAccounts = FXCollections.observableArrayList(); + private FilteredList filtered; + + @FXML + public void initialize() { + colUsername.setCellValueFactory(new PropertyValueFactory<>("username")); + colName.setCellValueFactory(new PropertyValueFactory<>("fullName")); + colEmail.setCellValueFactory(new PropertyValueFactory<>("email")); + colPhone.setCellValueFactory(new PropertyValueFactory<>("phone")); + colStatus.setCellValueFactory(new PropertyValueFactory<>("status")); + colCreated.setCellValueFactory(new PropertyValueFactory<>("createdAt")); + + filtered = new FilteredList<>(staffAccounts, a -> true); + tvStaff.setItems(filtered); + + txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n)); + + tvStaff.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> { + if (btnEditAccount != null) { + btnEditAccount.setDisable(newValue == null); + } + }); + + if (!UserSession.getInstance().isAdmin()) { + lblError.setText("Access restricted."); + tvStaff.setDisable(true); + btnCreateAccount.setDisable(true); + if (btnEditAccount != null) { + btnEditAccount.setDisable(true); + } + return; + } + + if (btnEditAccount != null) { + btnEditAccount.setDisable(true); + } + + refresh(); + } + + @FXML + void btnRefreshClicked(ActionEvent event) { + refresh(); + } + + @FXML + void btnCreateAccountClicked(ActionEvent event) { + lblError.setText(""); + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-register-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvStaff.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Create Staff Account"); + dialog.setScene(new Scene(loader.load())); + dialog.setResizable(false); + dialog.showAndWait(); + refresh(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("StaffAccountsController.btnCreateAccountClicked", e, "Opening staff register dialog"); + lblError.setText("Could not open staff account creation."); + } + } + + @FXML + void btnEditAccountClicked(ActionEvent event) { + lblError.setText(""); + StaffAccount selected = tvStaff.getSelectionModel().getSelectedItem(); + if (selected == null) { + lblError.setText("Select a staff account to edit."); + return; + } + try { + FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml")); + Stage dialog = new Stage(); + dialog.initOwner(tvStaff.getScene().getWindow()); + dialog.initModality(Modality.APPLICATION_MODAL); + dialog.setTitle("Edit Staff Account"); + dialog.setScene(new Scene(loader.load())); + dialog.setResizable(false); + var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController(); + controller.setStaffAccount(selected); + dialog.showAndWait(); + refresh(); + } catch (Exception e) { + ActivityLogger.getInstance().logException("StaffAccountsController.btnEditAccountClicked", e, "Opening staff edit dialog"); + lblError.setText("Could not open staff account editor."); + } + } + + private void refresh() { + lblError.setText(""); + tvStaff.setDisable(true); + + new Thread(() -> { + try { + List employees = EmployeeApi.getInstance().listEmployees(null); + List accounts = employees.stream() + .map(this::mapToStaffAccount) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + staffAccounts.setAll(accounts); + tvStaff.setDisable(false); + }); + } catch (Exception e) { + ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts"); + Platform.runLater(() -> { + String message = e.getMessage(); + lblError.setText(message == null || message.isBlank() + ? "Could not load staff accounts." + : "Could not load staff accounts: " + message); + tvStaff.setDisable(false); + }); + } + }).start(); + } + + private StaffAccount mapToStaffAccount(EmployeeResponse employee) { + long userId = employee.getUserId() != null ? employee.getUserId() : 0L; + long employeeId = employee.getEmployeeId() != null ? employee.getEmployeeId() : 0L; + String username = employee.getUsername(); + String fullName = employee.getFullName() != null ? employee.getFullName() : ""; + String[] names = splitFullName(fullName); + String firstName = names[0]; + String lastName = names[1]; + String email = employee.getEmail() != null ? employee.getEmail() : ""; + String phone = employee.getPhone() != null ? employee.getPhone() : ""; + String role = employee.getRole() != null ? employee.getRole() : "STAFF"; + boolean active = employee.getActive() != null ? employee.getActive() : false; + Timestamp createdAt = employee.getCreatedAt() != null + ? Timestamp.from(employee.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant()) + : null; + + return new StaffAccount(userId, employeeId, username, firstName, lastName, email, phone, role, active, createdAt); + } + + private String[] splitFullName(String fullName) { + if (fullName == null || fullName.trim().isEmpty()) { + return new String[]{"", ""}; + } + String[] parts = fullName.trim().split("\\s+", 2); + String firstName = parts.length > 0 ? parts[0] : ""; + String lastName = parts.length > 1 ? parts[1] : ""; + return new String[]{firstName, lastName}; + } + + private void applyFilter(String text) { + String q = text == null ? "" : text.trim().toLowerCase(); + if (q.isEmpty()) { + filtered.setPredicate(a -> true); + return; + } + + filtered.setPredicate(a -> + safe(a.getUsername()).contains(q) + || safe(a.getFullName()).contains(q) + || safe(a.getEmail()).contains(q) + || safe(a.getPhone()).contains(q) + || safe(a.getStatus()).contains(q) + ); + } + + private static String safe(String v) { + return v == null ? "" : v.toLowerCase(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java new file mode 100644 index 00000000..4df26941 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/SupplierController.java @@ -0,0 +1,303 @@ +package org.example.petshopdesktop.controllers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.fxml.FXMLLoader; +import javafx.scene.Scene; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Modality; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.supplier.SupplierResponse; +import org.example.petshopdesktop.api.endpoints.SupplierApi; +import org.example.petshopdesktop.controllers.dialogcontrollers.SupplierDialogController; +import org.example.petshopdesktop.models.Supplier; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.io.IOException; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +/** + * The controller for any operations in the supplier view + */ +public class SupplierController { + + @FXML + private Button btnAdd; + + @FXML + private Button btnDelete; + + @FXML + private Button btnEdit; + + @FXML + private TableColumn colContactPerson; + + @FXML + private TableColumn colSupplierEmail; + + @FXML + private TableColumn colSupplierId; + + @FXML + private TableColumn colSupplierName; + + @FXML + private TableColumn colSupplierPhone; + + @FXML + private TableView tvSuppliers; + + @FXML + private TextField txtSearch; + + private ObservableList data = FXCollections.observableArrayList(); + private String mode = null; + + /** + * Set up the table view for suppliers and display it when starting up + */ + @FXML + void initialize(){ + //Disable buttons until a row is selected + btnEdit.setDisable(true); + btnDelete.setDisable(true); + //Enable multiple selection + tvSuppliers.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE); + //set columns for table view + colSupplierId.setCellValueFactory(new PropertyValueFactory("supId")); + colSupplierName.setCellValueFactory(new PropertyValueFactory("supCompany")); + colContactPerson.setCellValueFactory(new PropertyValueFactory("supFullName")); + colSupplierEmail.setCellValueFactory(new PropertyValueFactory("supEmail")); + colSupplierPhone.setCellValueFactory(new PropertyValueFactory("supPhone")); + + displaySupplier(); + + //EventListener to Enable buttons when a row is selected + tvSuppliers.getSelectionModel().selectedItemProperty().addListener( + (observable, oldValue, newValue) -> { + btnEdit.setDisable(false); + btnDelete.setDisable(false); + } + ); + + //EventListener to search when text is changed on searchbar + txtSearch.textProperty().addListener((observable, oldValue, newValue) -> { + displayFilteredSupplier(newValue); + }); + + //EventListener for DELETE key + tvSuppliers.setOnKeyPressed(event -> { + if (event.getCode() == javafx.scene.input.KeyCode.DELETE) { + if (tvSuppliers.getSelectionModel().getSelectedItem() != null) { + btnDeleteClicked(null); + } + } + }); + + } + + /** + * Display the suppliers to table view + */ + private void displaySupplier(){ + new Thread(() -> { + try { + List suppliers = SupplierApi.getInstance().listSuppliers(null); + List supplierList = suppliers.stream() + .map(this::mapToSupplier) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(supplierList); + tvSuppliers.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "SupplierController.displaySupplier", + e, + "Fetching supplier data for table display"); + }); + } + }).start(); + } + + /** + * Filter the table given the string from the searchbar + * @param filter word to filter table + */ + private void displayFilteredSupplier(String filter){ + if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){ + displaySupplier(); + } else { + new Thread(() -> { + try { + List suppliers = SupplierApi.getInstance().listSuppliers(filter); + List supplierList = suppliers.stream() + .map(this::mapToSupplier) + .collect(Collectors.toList()); + + Platform.runLater(() -> { + data.setAll(supplierList); + tvSuppliers.setItems(data); + }); + } catch (Exception e) { + Platform.runLater(() -> { + System.out.println("Error while fetching table data: " + e.getMessage()); + ActivityLogger.getInstance().logException( + "SupplierController.displayFilteredSupplier", + e, + "Filtering suppliers with filter: " + filter); + }); + } + }).start(); + } + } + + + /** + * open a new dialog for adding a supplier + * @param event click event for button + */ + @FXML + void btnAddClicked(ActionEvent event) { + mode = "Add"; + openDialog(null,mode); + } + + /** + * Delete selected supplier(s) when delete is clicked + * @param event click event for button + */ + @FXML + void btnDeleteClicked(ActionEvent event) { + //get selected suppliers + var selectedSuppliers = tvSuppliers.getSelectionModel().getSelectedItems(); + if (selectedSuppliers.isEmpty()) return; + + //ask user to confirm + Alert question = new Alert(Alert.AlertType.CONFIRMATION); + question.setHeaderText("Please confirm delete"); + String message = selectedSuppliers.size() == 1 + ? "Are you sure you want to delete this supplier?" + : "Are you sure you want to delete " + selectedSuppliers.size() + " suppliers?"; + question.setContentText(message); + question.getDialogPane().lookupButton(ButtonType.OK).requestFocus(); + Optional result = question.showAndWait(); + + //if confirmed, start deletion + if (result.isPresent() && result.get() == ButtonType.OK) { + List ids = selectedSuppliers.stream() + .map(s -> (long) s.getSupId()) + .collect(Collectors.toList()); + + try { + SupplierApi.getInstance().deleteSuppliers(ids); + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Database Operation Confirmed"); + alert.setContentText("Successfully deleted " + ids.size() + " supplier(s)"); + alert.showAndWait(); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "SupplierController.btnDeleteClicked", + e, + "Deleting suppliers"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Delete Operation Failed"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + + //refresh display and reset inputs + displaySupplier(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + } + + /** + * Open a new dialog for editing a supplier + * @param event click event for button + */ + @FXML + void btnEditClicked(ActionEvent event) { + //set selected supplier + Supplier selectedSupplier = tvSuppliers.getSelectionModel().getSelectedItem(); + + if (selectedSupplier != null) { + mode = "Edit"; + openDialog(selectedSupplier, mode); + } + } + + /** + * Function to open the new Dialog for edit or adding + * depending on the mode given + * @param supplier the supplier entity for editing, null if adding + * @param mode the mode the dialog should be in + */ + private void openDialog(Supplier supplier, String mode){ + //Get new view + FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/supplier-dialog-view.fxml")); + Scene scene = null; + try{ + scene = new Scene(fxmlLoader.load()); + } catch (IOException e) { + ActivityLogger.getInstance().logException( + "SupplierController.openDialog", + e, + "Loading supplier dialog in " + mode + " mode"); + throw new RuntimeException(e); + } + SupplierDialogController dialogController = fxmlLoader.getController(); //controller associated with this view + dialogController.setMode(mode); + + //Open the dialog depending on the mode + if(mode.equals("Edit")){ + //Make it display suppliers details in dialog + dialogController.displaySupplierDetails(supplier); + } + Stage dialogStage = new Stage(); + dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal + if(mode.equals("Add")){ + dialogStage.setTitle("Add Supplier"); + } + else { + dialogStage.setTitle("Edit Supplier"); + } + dialogStage.setScene(scene); + dialogStage.showAndWait(); + + //When dialog closes update table view and disable edit and delete buttons, and reset search bar + displaySupplier(); + btnDelete.setDisable(true); + btnEdit.setDisable(true); + txtSearch.setText(""); + } + + private Supplier mapToSupplier(SupplierResponse response) { + String contactPerson = response.getSupContactFirstName() + " " + response.getSupContactLastName() != null ? response.getSupContactFirstName() + " " + response.getSupContactLastName() : ""; + String[] nameParts = contactPerson.split(" ", 2); + String firstName = nameParts.length > 0 ? nameParts[0] : ""; + String lastName = nameParts.length > 1 ? nameParts[1] : ""; + + return new Supplier( + response.getSupId().intValue(), + response.getSupCompany(), + firstName, + lastName, + response.getSupEmail(), + response.getSupPhone() + ); + } + +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java new file mode 100644 index 00000000..a1ed25fb --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AdoptionDialogController.java @@ -0,0 +1,232 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.DatePicker; +import javafx.scene.control.Label; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.endpoints.AdoptionApi; +import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.models.Adoption; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.time.LocalDate; +import java.util.List; + +public class AdoptionDialogController { + + //FXML elements + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private ComboBox cbAdoptionStatus; + + @FXML + private ComboBox cbCustomer; + + @FXML + private ComboBox cbPet; + + @FXML + private DatePicker dpAdoptionDate; + + @FXML + private Label lblAdoptionId; + + @FXML + private Label lblMode; + + //Stores if the dialog view is in add/edit mode + private String mode = null; + + //Adoption statuses + private ObservableList statusList = FXCollections.observableArrayList( + "Pending", "Completed", "Cancelled" + ); + + @FXML + void initialize() { + + cbAdoptionStatus.setItems(statusList); + + new Thread(() -> { + try { + List pets = DropdownApi.getInstance().getPets(); + Platform.runLater(() -> { + if (pets != null) { + ObservableList petsObs = FXCollections.observableArrayList(pets); + cbPet.setItems(petsObs); + } + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.initialize", + e, + "Loading pets for combo box"); + System.out.println("Error loading pets: " + e.getMessage()); + }); + } + }).start(); + + new Thread(() -> { + try { + List customers = DropdownApi.getInstance().getCustomers(); + Platform.runLater(() -> { + if (customers != null) { + ObservableList customersObs = FXCollections.observableArrayList(customers); + cbCustomer.setItems(customersObs); + } + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.initialize", + e, + "Loading customers for combo box"); + System.out.println("Error loading customers: " + e.getMessage()); + }); + } + }).start(); + + btnSave.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + buttonSaveClicked(mouseEvent); + } + }); + + btnCancel.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + closeStage(mouseEvent); + } + }); + } + + private void buttonSaveClicked(MouseEvent mouseEvent) { + String errorMsg = ""; + + if (cbPet.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Pet is required.\n"; + } + + if (cbCustomer.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Customer is required.\n"; + } + + if (dpAdoptionDate.getValue() == null) { + errorMsg += "Adoption Date is required.\n"; + } + + if (cbAdoptionStatus.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Status is required.\n"; + } + + if (errorMsg.isEmpty()) { + try { + AdoptionRequest request = new AdoptionRequest(); + request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId()); + request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId()); + request.setAdoptionDate(dpAdoptionDate.getValue()); + request.setAdoptionStatus(cbAdoptionStatus.getValue()); + + if (mode.equals("Add")) { + AdoptionApi.getInstance().createAdoption(request); + } else { + String[] parts = lblAdoptionId.getText().split(": "); + if (parts.length < 2) { + throw new IllegalStateException("Invalid adoption ID format"); + } + Long adoptionId = Long.parseLong(parts[1]); + AdoptionApi.getInstance().updateAdoption(adoptionId, request); + } + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Saved"); + alert.setContentText(mode + " succeeded"); + alert.showAndWait(); + closeStage(mouseEvent); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.buttonSaveClicked", + e, + mode + " adoption"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + } else { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(errorMsg); + alert.showAndWait(); + } + } + + + private void closeStage(MouseEvent mouseEvent) { + Node node = (Node) mouseEvent.getSource(); + Stage stage = (Stage) node.getScene().getWindow(); + stage.close(); + } + + public void displayAdoptionDetails(Adoption adoption) { + if (adoption != null) { + lblAdoptionId.setText("ID: " + adoption.getAdoptionId()); + + for (DropdownOption pet : cbPet.getItems()) { + if (pet.getLabel().equals(adoption.getPetName())) { + cbPet.getSelectionModel().select(pet); + break; + } + } + + for (DropdownOption customer : cbCustomer.getItems()) { + if (customer.getLabel().equals(adoption.getCustomerName())) { + cbCustomer.getSelectionModel().select(customer); + break; + } + } + + if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) { + try { + dpAdoptionDate.setValue(LocalDate.parse(adoption.getAdoptionDate())); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "AdoptionDialogController.displayAdoptionDetails", + e, + "Parsing adoption date"); + } + } + + for (String status : cbAdoptionStatus.getItems()) { + if (status.equals(adoption.getAdoptionStatus())) { + cbAdoptionStatus.getSelectionModel().select(status); + break; + } + } + } + } + + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Adoption"); + lblAdoptionId.setVisible(mode.equals("Edit")); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java new file mode 100644 index 00000000..932d10d5 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/AppointmentDialogController.java @@ -0,0 +1,291 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import javafx.scene.control.ListCell; + +import org.example.petshopdesktop.DTOs.AppointmentDTO; +import org.example.petshopdesktop.api.dto.appointment.AppointmentRequest; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.endpoints.AppointmentApi; +import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.time.LocalTime; +import java.util.Collections; +import java.util.List; + +public class AppointmentDialogController { + + // ============================ + // FXML + // ============================ + + @FXML private Button btnCancel; + @FXML private Button btnSave; + + @FXML private ComboBox cbService; + @FXML private ComboBox cbCustomer; + @FXML private ComboBox cbPet; + + @FXML private ComboBox cbHour; + @FXML private ComboBox cbMinute; + + @FXML private ComboBox cbAppointmentStatus; + @FXML private DatePicker dpAppointmentDate; + + @FXML private Label lblAppointmentId; + @FXML private Label lblMode; + + // ============================ + // DATA + // ============================ + + private String mode = null; // Add | Edit + private AppointmentDTO selectedAppointment = null; + + private ObservableList statusList = + FXCollections.observableArrayList( + "Booked", "Completed", "Cancelled" + ); + + // + // MODE + // + + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Appointment"); + lblAppointmentId.setVisible(!mode.equals("Add")); + } + + // + // INITIALIZE + // + + @FXML + public void initialize() { + + new Thread(() -> { + try { + List services = DropdownApi.getInstance().getServices(); + List customers = DropdownApi.getInstance().getCustomers(); + List pets = DropdownApi.getInstance().getPets(); + + Platform.runLater(() -> { + if (services != null) { + cbService.setItems(FXCollections.observableArrayList(services)); + } + if (customers != null) { + cbCustomer.setItems(FXCollections.observableArrayList(customers)); + } + if (pets != null) { + cbPet.setItems(FXCollections.observableArrayList(pets)); + } + syncSelectedAppointment(); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.initialize", + e, + "Loading combo box data for services, customers, and pets"); + e.printStackTrace(); + }); + } + }).start(); + + cbAppointmentStatus.setItems(statusList); + + // Hours 9 AM - 5 PM + for (int i = 9; i <= 17; i++) { + cbHour.getItems().add(i); + } + + cbMinute.getItems().addAll(0, 15, 30, 45); + + // Show dropdown labels + cbService.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbService.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + + cbCustomer.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbCustomer.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + + cbPet.setCellFactory(param -> new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + cbPet.setButtonCell(new ListCell<>() { + @Override + protected void updateItem(DropdownOption option, boolean empty) { + super.updateItem(option, empty); + setText(empty || option == null ? null : option.getLabel()); + } + }); + + btnSave.setOnMouseClicked(this::buttonSaveClicked); + btnCancel.setOnMouseClicked(this::closeStage); + } + + // + // DISPLAY FOR EDIT + // + + public void displayAppointmentDetails(AppointmentDTO appt) { + + selectedAppointment = appt; + lblAppointmentId.setText("ID: " + appt.getAppointmentId()); + + try { + dpAppointmentDate.setValue( + java.time.LocalDate.parse(appt.getAppointmentDate()) + ); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.displayAppointmentDetails", + e, + "Parsing appointment date"); + } + + cbAppointmentStatus.setValue(appt.getAppointmentStatus()); + + try { + LocalTime time = LocalTime.parse(appt.getAppointmentTime()); + cbHour.setValue(time.getHour()); + cbMinute.setValue(time.getMinute()); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.displayAppointmentDetails", + e, + "Parsing appointment time"); + } + + cbService.getItems().forEach(s -> { + if (s.getId() != null && s.getId().longValue() == appt.getServiceId()) cbService.setValue(s); + }); + + cbCustomer.getItems().forEach(c -> { + if (c.getId() != null && c.getId().longValue() == appt.getCustomerId()) cbCustomer.setValue(c); + }); + + cbPet.getItems().forEach(p -> { + if (p.getId() != null && p.getId().longValue() == appt.getPetId()) cbPet.setValue(p); + }); + } + + // + // SAVE + // + + private void buttonSaveClicked(MouseEvent e) { + + if (cbService.getValue() == null || + cbCustomer.getValue() == null || + cbPet.getValue() == null || + dpAppointmentDate.getValue() == null || + cbHour.getValue() == null || + cbMinute.getValue() == null || + cbAppointmentStatus.getValue() == null) { + + showError("All fields are required"); + return; + } + + LocalTime appointmentTime = LocalTime.of(cbHour.getValue(), cbMinute.getValue()); + Long storeId = UserSession.getInstance().getStoreId(); + if (storeId == null || storeId <= 0) { + showError("Store is not set for this account"); + return; + } + + AppointmentRequest request = new AppointmentRequest(); + request.setPetIds(Collections.singletonList(cbPet.getValue().getId())); + request.setCustomerId(cbCustomer.getValue().getId()); + request.setStoreId(storeId); + request.setServiceId(cbService.getValue().getId()); + request.setAppointmentDate(dpAppointmentDate.getValue()); + request.setAppointmentTime(appointmentTime); + request.setAppointmentStatus(cbAppointmentStatus.getValue()); + + new Thread(() -> { + try { + if (mode.equals("Add")) { + AppointmentApi.getInstance().createAppointment(request); + } else { + AppointmentApi.getInstance().updateAppointment( + (long) selectedAppointment.getAppointmentId(), + request + ); + } + + Platform.runLater(() -> closeStage(e)); + + } catch (Exception ex) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "AppointmentDialogController.buttonSaveClicked", + ex, + "Saving appointment in " + mode + " mode"); + ex.printStackTrace(); + showError("Error saving appointment: " + ex.getMessage()); + }); + } + }).start(); + } + + // + // UTIL + // + + private void closeStage(MouseEvent e) { + Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow(); + stage.close(); + } + + private void showError(String msg) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(msg); + alert.showAndWait(); + } + + private void syncSelectedAppointment() { + if (selectedAppointment != null) { + displayAppointmentDetails(selectedAppointment); + } + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/InventoryDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/InventoryDialogController.java new file mode 100644 index 00000000..0b410186 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/InventoryDialogController.java @@ -0,0 +1,209 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import javafx.util.StringConverter; +import org.example.petshopdesktop.models.Inventory; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.inventory.InventoryRequest; +import org.example.petshopdesktop.api.dto.inventory.InventoryResponse; +import org.example.petshopdesktop.api.dto.product.ProductResponse; +import org.example.petshopdesktop.api.endpoints.InventoryApi; +import org.example.petshopdesktop.api.endpoints.ProductApi; +import org.example.petshopdesktop.models.Product; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; +import java.util.List; +import java.util.stream.Collectors; + +public class InventoryDialogController { + + //FXML elements + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private ComboBox cbProduct; + + @FXML + private Label lblInventoryId; + + @FXML + private Label lblMode; + + @FXML + private TextField txtQuantity; + + //Determines if the mode is add or edit + private String mode = null; + + //Loads upon .FXML boot + @FXML + void initialize() { + cbProduct.setConverter(new StringConverter() { + + //Displays product in combobox (prodID + name) + @Override + public String toString(Product product) { + return product == null ? "" : product.getProdId() + ": " + product.getProdName(); + } + + //Not needed + @Override + public Product fromString(String string) { return null; } + }); + + //Load product list from API into combobox + try { + List productResponses = ProductApi.getInstance().listProducts(null); + if (productResponses != null) { + ObservableList products = FXCollections.observableArrayList(); + for (ProductResponse pr : productResponses) { + products.add(new Product( + pr.getProdId().intValue(), + pr.getProdName(), + pr.getProdPrice().doubleValue(), + 0, + pr.getProdDesc() + )); + } + cbProduct.setItems(products); + } + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "InventoryDialogController.initialize", + e, + "Loading products for combo box"); + System.out.println("Error loading products: " + e.getMessage()); + } + + //Save button handler + btnSave.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + buttonSaveClicked(mouseEvent); + } + }); + + //Cancel button handler + btnCancel.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + closeStage(mouseEvent); + } + }); + } + + //Handles save button click event + private void buttonSaveClicked(MouseEvent mouseEvent) { + int numRow = 0; + String errorMsg = ""; + + if (cbProduct.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Product is required.\n"; + } + + //Validate inputs + errorMsg += Validator.isPresent(txtQuantity.getText(), "Quantity"); + errorMsg += Validator.isLessThanVarChars(txtQuantity.getText(), "Quantity", 11); + errorMsg += Validator.isNonNegativeInteger(txtQuantity.getText(), "Quantity"); + + //Operation only occurs if there are no errors + if (errorMsg.isEmpty()) { + try { + InventoryRequest request = new InventoryRequest(); + Product selectedProduct = cbProduct.getSelectionModel().getSelectedItem(); + request.setProdId((long) selectedProduct.getProdId()); + int quantity; + try { + quantity = Integer.parseInt(txtQuantity.getText()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid quantity format"); + } + request.setQuantity(quantity); + + if (mode.equals("Add")) { + InventoryApi.getInstance().createInventory(request); + } else { + String[] parts = lblInventoryId.getText().split(": "); + if (parts.length < 2) { + throw new IllegalStateException("Invalid inventory ID format"); + } + Long inventoryId = Long.parseLong(parts[1]); + InventoryApi.getInstance().updateInventory(inventoryId, request); + } + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Saved"); + alert.setContentText(mode + " succeeded"); + alert.showAndWait(); + closeStage(mouseEvent); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "InventoryDialogController.buttonSaveClicked", + e, + mode + " inventory"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + } + + //Displays validation errors + else { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(errorMsg); + alert.showAndWait(); + } + } + + //Close dialog view + private void closeStage(MouseEvent mouseEvent) { + Node node = (Node) mouseEvent.getSource(); + Stage stage = (Stage) node.getScene().getWindow(); + stage.close(); + } + + //Editing + //Displays fields with existing inventory data + public void displayInventoryDetails(Inventory inventory) { + if (inventory != null) { + + //Displays inventory ID + lblInventoryId.setText("ID: " + inventory.getInventoryId()); + + //Selecting matching product in combobox + for (Product product : cbProduct.getItems()) { + if (product.getProdId() == inventory.getProdId()) { + cbProduct.getSelectionModel().select(product); + break; + } + } + + txtQuantity.setText(String.valueOf(inventory.getQuantity())); + } + } + + //Sets dialog view to add/edit. Updates UI labels + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Inventory"); + lblInventoryId.setVisible(mode.equals("Edit")); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java new file mode 100644 index 00000000..cd7f2690 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/PetDialogController.java @@ -0,0 +1,201 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.pet.PetRequest; +import org.example.petshopdesktop.api.dto.pet.PetResponse; +import org.example.petshopdesktop.api.endpoints.PetApi; +import org.example.petshopdesktop.models.Pet; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; +public class PetDialogController { + + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private ComboBox cbPetStatus; + + @FXML + private Label lblMode; + + @FXML + private Label lblPetId; + + @FXML + private TextField txtPetAge; + + @FXML + private TextField txtPetBreed; + + @FXML + private TextField txtPetName; + + @FXML + private TextField txtPetPrice; + + @FXML + private TextField txtPetSpecies; + + private String mode = null; + + private ObservableList statusList = FXCollections.observableArrayList( + "Available", "Adopted" + ); + + @FXML + void initialize() { + + cbPetStatus.setItems(statusList); //set status combobox + + //Set up mouse handlers for buttons + btnSave.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + buttonSaveClicked(mouseEvent); + } + }); + + btnCancel.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + closeStage(mouseEvent); + } + }); + } + + private void buttonSaveClicked(MouseEvent mouseEvent) { + String errorMsg = ""; + + //Check validation (input required) + errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name"); + errorMsg += Validator.isPresent(txtPetAge.getText(), "Age"); + errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed"); + errorMsg += Validator.isPresent(txtPetSpecies.getText(), "Species"); + errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price"); + if (cbPetStatus.getSelectionModel().getSelectedItem() == null){ + errorMsg += "Status is required"; + } + + //Check validation (length size) + errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50); + errorMsg += Validator.isLessThanVarChars(txtPetSpecies.getText(), "Species", 50); + errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50); + errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12); + errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11); + + //Check validation (format) + errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price"); + errorMsg += Validator.isNonNegativeInteger(txtPetAge.getText(), "Age"); + + if(errorMsg.isEmpty()){ + PetRequest request = buildPetRequest(); + try { + if(mode.equals("Add")) { + PetApi.getInstance().createPet(request); + } else { + String[] parts = lblPetId.getText().split(": "); + if (parts.length < 2) { + throw new IllegalStateException("Invalid pet ID format"); + } + Long petId = Long.parseLong(parts[1]); + PetApi.getInstance().updatePet(petId, request); + } + + //tell the user operation was successful + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Saved"); + alert.setContentText(mode + " succeeded"); + alert.showAndWait(); + closeStage(mouseEvent); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "PetDialogController.buttonSaveClicked", + e, + mode + " pet record"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Operation Error"); + alert.setContentText(mode + " failed: " + e.getMessage()); + alert.showAndWait(); + } + } + else{ + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(errorMsg); + alert.showAndWait(); + } + } + + private PetRequest buildPetRequest() { + PetRequest request = new PetRequest(); + request.setPetName(txtPetName.getText()); + request.setPetSpecies(txtPetSpecies.getText()); + request.setPetBreed(txtPetBreed.getText()); + request.setPetStatus(cbPetStatus.getValue()); + try { + request.setPetPrice(new BigDecimal(txtPetPrice.getText())); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid price format"); + } + + int age; + try { + age = Integer.parseInt(txtPetAge.getText()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid age format"); + } + request.setPetAge(age); + + return request; + } + + private void closeStage(MouseEvent mouseEvent) { + Node node = (Node) mouseEvent.getSource(); + Stage stage = (Stage) node.getScene().getWindow(); + stage.close(); + } + + public void displayPetDetails(Pet pet){ + if (pet!=null){ + lblPetId.setText("ID: " + pet.getPetId()); + txtPetName.setText(pet.getPetName()); + txtPetSpecies.setText(pet.getPetSpecies()); + txtPetBreed.setText(pet.getPetBreed()); + txtPetAge.setText(pet.getPetAge() + ""); + txtPetPrice.setText(pet.getPetPrice() + ""); + + //get the right combobox selection + for (String status : cbPetStatus.getItems()) { + if(status.equals(pet.getPetStatus())){ + cbPetStatus.getSelectionModel().select(status); + } + } + + + } + } + + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Pet"); + if(mode.equals("Add")) { + lblPetId.setVisible(false); + } + else if(mode.equals("Edit")) { + lblPetId.setVisible(true); + } + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java new file mode 100644 index 00000000..25fe1da8 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductDialogController.java @@ -0,0 +1,206 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.ProductDTO; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.dto.product.ProductRequest; +import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.api.endpoints.ProductApi; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; +import java.util.List; + +public class ProductDialogController { + + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private ComboBox cbProdCategory; + + @FXML + private Label lblMode; + + @FXML + private Label lblProdId; + + @FXML + private TextField txtProdDesc; + + @FXML + private TextField txtProdName; + + @FXML + private TextField txtProdPrice; + + private String mode = null; + + /** + * Add event listeners to buttons when dialog loads + */ + @FXML + void initialize() { + //Set up mouse handlers for buttons + btnSave.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + buttonSaveClicked(mouseEvent); + } + }); + + btnCancel.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + closeStage(mouseEvent); + } + }); + + //Set up combobox for selecting category + try { + List categories = DropdownApi.getInstance().getCategories(); + if (categories != null) { + ObservableList categoriesObs = FXCollections.observableArrayList(categories); + cbProdCategory.setItems(categoriesObs); + } + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ProductDialogController.initialize", + e, + "Loading categories for combo box"); + System.out.println("Error loading categories: " + e.getMessage()); + } + + } + + /** + * Validates the inputs, then add or update the database depending + * on the mode + * @param mouseEvent click event for save button + */ + private void buttonSaveClicked(MouseEvent mouseEvent) { + int numRow = 0; //how many rows affected + String errorMsg = ""; //error message for validation + + //Check Validation (input required) + errorMsg += Validator.isPresent(txtProdName.getText(), "Product Name"); + errorMsg += Validator.isPresent(txtProdPrice.getText(), "Product Price"); + if (cbProdCategory.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Category is required \n"; + } + + //Check validation (length size) + errorMsg += Validator.isLessThanVarChars(txtProdName.getText(), "Product Name", 100); + errorMsg += Validator.isLessThanVarChars(txtProdDesc.getText(), "Description", 100); + errorMsg += Validator.isLessThanVarChars(txtProdPrice.getText(), "Product Price", 12); + + //Check Validation (format) + errorMsg += Validator.isNonNegativeDouble(txtProdPrice.getText(), "Product Price"); + + if (errorMsg.isEmpty()) { + try { + ProductRequest request = new ProductRequest(); + request.setProdName(txtProdName.getText()); + BigDecimal price; + try { + price = new BigDecimal(txtProdPrice.getText()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid price format"); + } + request.setProdPrice(price); + request.setCategoryId(cbProdCategory.getSelectionModel().getSelectedItem().getId()); + request.setProdDesc(txtProdDesc.getText()); + + if (mode.equals("Add")) { + ProductApi.getInstance().createProduct(request); + } else { + String[] parts = lblProdId.getText().split(": "); + if (parts.length < 2) { + throw new IllegalStateException("Invalid product ID format"); + } + Long productId = Long.parseLong(parts[1]); + ProductApi.getInstance().updateProduct(productId, request); + } + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Saved"); + alert.setContentText(mode + " succeeded"); + alert.showAndWait(); + closeStage(mouseEvent); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ProductDialogController.buttonSaveClicked", + e, + mode + " product"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText(e.getMessage()); + alert.showAndWait(); + } + } + else{ //Display validation errors + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(errorMsg); + alert.showAndWait(); + } + } + + /** + * Display the product data in text fields and combobox + * @param product the product entity containing data to display + */ + public void displayProductDetails(ProductDTO product){ + if (product!=null){ + lblProdId.setText("ID: " + product.getProdId()); + txtProdName.setText(product.getProdName()); + txtProdDesc.setText(product.getProdDesc()); + txtProdPrice.setText(product.getProdPrice() + ""); + + for (DropdownOption category : cbProdCategory.getItems()) { + if(category.getLabel().equals(product.getCategoryName())){ + cbProdCategory.getSelectionModel().select(category); + break; + } + } + + } + } + + /** + * Close the window + * @param mouseEvent mouse event to close + */ + private void closeStage(MouseEvent mouseEvent) { + Node node = (Node) mouseEvent.getSource(); + Stage stage = (Stage) node.getScene().getWindow(); + stage.close(); + } + + /** + * Set the mode of the dialog + * @param mode the mode to for the dialog + */ + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Product"); + if(mode.equals("Add")) { + lblProdId.setVisible(false); + } + else if(mode.equals("Edit")) { + lblProdId.setVisible(true); + } + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductSupplierDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductSupplierDialogController.java new file mode 100644 index 00000000..2348b43b --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ProductSupplierDialogController.java @@ -0,0 +1,270 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.application.Platform; +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.*; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.ProductSupplierDTO; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.common.DropdownOption; +import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierRequest; +import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierResponse; +import org.example.petshopdesktop.api.endpoints.DropdownApi; +import org.example.petshopdesktop.api.endpoints.ProductSupplierApi; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; + +public class ProductSupplierDialogController { + + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private ComboBox cbProduct; + + @FXML + private ComboBox cbSupplier; + + @FXML + private Label lblMode; + + @FXML + private Label lblProductSupplierId; + + @FXML + private TextField txtCost; + + private String mode = null; + private int selectedSupId = -1; + private int selectedProdId = -1; + + /** + * add event listeners to buttons and set up combobox + */ + @FXML + void initialize() { //Set up mouse handlers for buttons + btnSave.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + buttonSaveClicked(mouseEvent); + } + }); + + btnCancel.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + closeStage(mouseEvent); + } + }); + + cbSupplier.setButtonCell(new ListCell() { + @Override + protected void updateItem(DropdownOption item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.getLabel()); + } + } + }); + cbSupplier.setCellFactory(lv -> new ListCell() { + @Override + protected void updateItem(DropdownOption item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.getLabel()); + } + } + }); + + cbProduct.setButtonCell(new ListCell() { + @Override + protected void updateItem(DropdownOption item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.getLabel()); + } + } + }); + cbProduct.setCellFactory(lv -> new ListCell() { + @Override + protected void updateItem(DropdownOption item, boolean empty) { + super.updateItem(item, empty); + if (empty || item == null) { + setText(null); + } else { + setText(item.getLabel()); + } + } + }); + + new Thread(() -> { + try { + var suppliers = DropdownApi.getInstance().getSuppliers(); + var products = DropdownApi.getInstance().getProducts(); + + Platform.runLater(() -> { + if (suppliers != null) { + cbSupplier.setItems(FXCollections.observableArrayList(suppliers)); + } + if (products != null) { + cbProduct.setItems(FXCollections.observableArrayList(products)); + } + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "ProductSupplierDialogController.initialize", + e, + "Loading suppliers and products for combo boxes"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Initialization Error"); + alert.setContentText("Failed to load dropdown data: " + e.getMessage()); + alert.showAndWait(); + }); + } + }).start(); + + } + + /** + * Validates the inputs, then add or update the database depending + * on the mode + * @param mouseEvent click event for save button + */ + private void buttonSaveClicked(MouseEvent mouseEvent) { + String errorMsg = ""; + + errorMsg += Validator.isPresent(txtCost.getText(), "Cost"); + if (cbProduct.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Product is required \n"; + } + if (cbSupplier.getSelectionModel().getSelectedItem() == null) { + errorMsg += "Supplier is required \n"; + } + + errorMsg += Validator.isLessThanVarChars(txtCost.getText(), "Cost", 12); + errorMsg += Validator.isNonNegativeDouble(txtCost.getText(), "Cost"); + + if(errorMsg.isEmpty()){ + ProductSupplierRequest request = collectProductSupplierRequest(); + + new Thread(() -> { + try { + if (mode.equals("Add")) { + ProductSupplierApi.getInstance().createProductSupplier(request); + } else { + ProductSupplierApi.getInstance().updateProductSupplier((long) selectedProdId, (long) selectedSupId, request); + } + + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Saved"); + alert.setContentText(mode + " succeeded"); + alert.showAndWait(); + closeStage(mouseEvent); + }); + } catch (Exception e) { + Platform.runLater(() -> { + ActivityLogger.getInstance().logException( + "ProductSupplierDialogController.buttonSaveClicked", + e, + mode + " product-supplier"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText(mode + " failed: " + e.getMessage()); + alert.showAndWait(); + }); + } + }).start(); + } else { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(errorMsg); + alert.showAndWait(); + } + } + + /** + * collect the data for new/updated productSupplier + * @return productSupplier request with data + */ + private ProductSupplierRequest collectProductSupplierRequest() { + ProductSupplierRequest request = new ProductSupplierRequest(); + request.setSupplierId(cbSupplier.getSelectionModel().getSelectedItem().getId()); + request.setProductId(cbProduct.getSelectionModel().getSelectedItem().getId()); + try { + request.setCost(new BigDecimal(txtCost.getText())); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid cost format"); + } + return request; + } + + /** + * Display the productsupplier data in text fields and combobox + * @param productSupplier + */ + public void displayProductSupplierDetails(ProductSupplierDTO productSupplier){ + if(productSupplier != null){ + txtCost.setText(productSupplier.getCost() + ""); + } + + for (DropdownOption product : cbProduct.getItems()) { + if(product.getId() == productSupplier.getProdId()){ + cbProduct.getSelectionModel().select(product); + } + } + + for (DropdownOption supplier : cbSupplier.getItems()) { + if (supplier.getId() == productSupplier.getSupId()) { + cbSupplier.getSelectionModel().select(supplier); + } + } + } + + /** + * Close the window + * @param mouseEvent mouse event to close + */ + private void closeStage(MouseEvent mouseEvent) { + Node node = (Node) mouseEvent.getSource(); + Stage stage = (Stage) node.getScene().getWindow(); + stage.close(); + } + + /** + * Set the mode of the dialog + * @param mode the mode for the dialog + */ + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Product"); + lblProductSupplierId.setVisible(false); + } + + /** + * set the current supplier and productId for (needed for update since compound primary key) + * @param supId supplier id + * @param prodId product id + */ + public void setSelectedIds(int supId, int prodId){ + this.selectedSupId = supId; + this.selectedProdId = prodId; + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java new file mode 100644 index 00000000..860a9921 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/RefundDialogController.java @@ -0,0 +1,489 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.collections.FXCollections; +import javafx.collections.ObservableList; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.*; +import javafx.scene.control.cell.PropertyValueFactory; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.sale.SaleItemRequest; +import org.example.petshopdesktop.api.dto.sale.SaleItemResponse; +import org.example.petshopdesktop.api.dto.sale.SaleRequest; +import org.example.petshopdesktop.api.dto.sale.SaleResponse; +import org.example.petshopdesktop.api.endpoints.SaleApi; +import org.example.petshopdesktop.auth.UserSession; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; +import java.text.NumberFormat; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +public class RefundDialogController { + + @FXML + private TextField txtSaleId; + + @FXML + private Button btnLoadSale; + + @FXML + private Label lblSaleInfo; + + @FXML + private TableView tvOriginalItems; + + @FXML + private TableColumn colOriginalProduct; + + @FXML + private TableColumn colOriginalQuantity; + + @FXML + private TableColumn colOriginalUnitPrice; + + @FXML + private TableColumn colOriginalTotal; + + @FXML + private Button btnAddToRefund; + + @FXML + private TableView tvRefundItems; + + @FXML + private TableColumn colRefundProduct; + + @FXML + private TableColumn colRefundQuantity; + + @FXML + private TableColumn colRefundUnitPrice; + + @FXML + private TableColumn colRefundTotal; + + @FXML + private Button btnRemoveFromRefund; + + @FXML + private ComboBox cbPaymentMethod; + + @FXML + private Label lblRefundTotal; + + @FXML + private Button btnProcessRefund; + + @FXML + private Button btnCancel; + + private SaleResponse currentSale; + private final List baseOriginalItems = new ArrayList<>(); + private final ObservableList originalItems = FXCollections.observableArrayList(); + private final ObservableList refundItems = FXCollections.observableArrayList(); + private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA); + + @FXML + public void initialize() { + setupTables(); + cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card", "Debit")); + cbPaymentMethod.getSelectionModel().selectFirst(); + updateRefundTotal(); + } + + private void setupTables() { + colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); + colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal")); + tvOriginalItems.setItems(originalItems); + tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + + colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName")); + colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity")); + colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice")); + colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total")); + tvRefundItems.setItems(refundItems); + tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE); + } + + @FXML + void btnLoadSaleClicked(ActionEvent event) { + loadSale(); + } + + public void prefillSale(Long saleId) { + if (saleId == null) { + return; + } + txtSaleId.setText(String.valueOf(saleId)); + loadSale(); + } + + private void loadSale() { + String saleIdText = txtSaleId.getText().trim(); + if (saleIdText.isEmpty()) { + showError("Load Sale", "Enter a transaction ID."); + return; + } + + Long saleId; + try { + saleId = Long.parseLong(saleIdText); + } catch (NumberFormatException e) { + showError("Load Sale", "Invalid transaction ID."); + return; + } + + try { + List allSales = SaleApi.getInstance().listSales(0, 1000, null); + currentSale = SaleApi.getInstance().getSale(saleId); + if (Boolean.TRUE.equals(currentSale.getIsRefund())) { + clearLoadedSale(); + showError("Load Sale", "Select an original sale, not a refund record."); + return; + } + List previousRefunds = allSales.stream() + .filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId())) + .collect(Collectors.toList()); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"); + String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s", + currentSale.getSaleDate().format(formatter), + currentSale.getEmployeeName(), + currency.format(currentSale.getTotalAmount()), + currentSale.getPaymentMethod()); + lblSaleInfo.setText(saleInfo); + + List refundableItems = buildRefundableItems(currentSale, previousRefunds); + if (refundableItems.isEmpty()) { + showError("Load Sale", "This sale has no remaining refundable items."); + return; + } + + baseOriginalItems.clear(); + baseOriginalItems.addAll(copySaleItems(refundableItems)); + originalItems.setAll(copySaleItems(refundableItems)); + cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod()); + + refundItems.clear(); + updateOriginalItemAvailability(); + updateRefundTotal(); + + } catch (Exception e) { + ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale"); + showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale."); + } + } + + @FXML + void btnAddToRefundClicked(ActionEvent event) { + if (currentSale == null) { + showError("Add to Refund", "Load a sale first."); + return; + } + + SaleItemResponse selected = tvOriginalItems.getSelectionModel().getSelectedItem(); + if (selected == null) { + showError("Add to Refund", "Select an item from the original sale."); + return; + } + + int alreadyRefunded = refundItems.stream() + .filter(r -> r.getProdId() == selected.getProdId().intValue()) + .mapToInt(RefundItem::getQuantity) + .sum(); + + int available = selected.getQuantity() - alreadyRefunded; + if (available <= 0) { + showError("Add to Refund", "All items of this product are already in the refund list."); + return; + } + + TextInputDialog dialog = new TextInputDialog(String.valueOf(available)); + dialog.setTitle("Refund Quantity"); + dialog.setHeaderText("Product: " + selected.getProductName()); + dialog.setContentText("Enter quantity to refund (max " + available + "):"); + + Optional result = dialog.showAndWait(); + if (result.isPresent()) { + try { + int quantity = Integer.parseInt(result.get().trim()); + if (quantity <= 0) { + showError("Add to Refund", "Quantity must be at least 1."); + return; + } + if (quantity > available) { + showError("Add to Refund", "Cannot refund more than " + available + " items."); + return; + } + + addOrMergeRefundItem(selected, quantity); + updateOriginalItemAvailability(); + updateRefundTotal(); + + } catch (NumberFormatException e) { + showError("Add to Refund", "Invalid quantity."); + } + } + } + + @FXML + void btnRemoveFromRefundClicked(ActionEvent event) { + RefundItem selected = tvRefundItems.getSelectionModel().getSelectedItem(); + if (selected != null) { + refundItems.remove(selected); + updateOriginalItemAvailability(); + updateRefundTotal(); + } + } + + @FXML + void btnProcessRefundClicked(ActionEvent event) { + if (currentSale == null) { + showError("Process Refund", "Load a sale first."); + return; + } + + if (refundItems.isEmpty()) { + showError("Process Refund", "Add at least one item to refund."); + return; + } + + Long storeId = UserSession.getInstance().getStoreId(); + if (storeId == null || storeId <= 0) { + showError("Process Refund", "Store is not set for this account."); + return; + } + + String payment = cbPaymentMethod.getSelectionModel().getSelectedItem(); + if (payment == null || payment.isBlank()) { + showError("Process Refund", "Select a payment method."); + return; + } + + Alert confirm = new Alert(Alert.AlertType.CONFIRMATION); + confirm.setTitle("Confirm Refund"); + confirm.setHeaderText("Process refund for sale ID " + currentSale.getSaleId() + "?"); + confirm.setContentText("Refund amount: " + lblRefundTotal.getText()); + + Optional confirmResult = confirm.showAndWait(); + if (confirmResult.isEmpty() || confirmResult.get() != ButtonType.OK) { + return; + } + + try { + SaleRequest request = new SaleRequest(); + request.setStoreId(storeId); + request.setPaymentMethod(payment); + request.setIsRefund(true); + request.setOriginalSaleId(currentSale.getSaleId()); + + List items = new ArrayList<>(); + for (RefundItem item : refundItems) { + SaleItemRequest saleItem = new SaleItemRequest(); + saleItem.setProdId((long) item.getProdId()); + saleItem.setQuantity(-item.getQuantity()); + items.add(saleItem); + } + request.setItems(items); + + SaleResponse refundResponse = SaleApi.getInstance().createSale(request); + + Alert success = new Alert(Alert.AlertType.INFORMATION); + success.setTitle("Refund Processed"); + success.setHeaderText(null); + success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully."); + success.showAndWait(); + + closeDialog(); + + } catch (Exception e) { + ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund"); + showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund."); + } + } + + @FXML + void btnCancelClicked(ActionEvent event) { + closeDialog(); + } + + private void clearLoadedSale() { + currentSale = null; + lblSaleInfo.setText(""); + baseOriginalItems.clear(); + originalItems.clear(); + refundItems.clear(); + updateRefundTotal(); + } + + private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) { + for (int i = 0; i < refundItems.size(); i++) { + RefundItem existing = refundItems.get(i); + if (existing.getProdId() == selected.getProdId().intValue()) { + refundItems.set(i, new RefundItem( + existing.getProdId(), + existing.getProductName(), + existing.getQuantity() + quantity, + existing.getUnitPrice() + )); + return; + } + } + + refundItems.add(new RefundItem( + selected.getProdId().intValue(), + selected.getProductName(), + quantity, + selected.getUnitPrice().doubleValue() + )); + } + + private void updateOriginalItemAvailability() { + if (currentSale == null) { + baseOriginalItems.clear(); + originalItems.clear(); + return; + } + + Map pendingRefunds = new HashMap<>(); + for (RefundItem refundItem : refundItems) { + pendingRefunds.merge((long) refundItem.getProdId(), refundItem.getQuantity(), Integer::sum); + } + + List refreshedItems = new ArrayList<>(); + for (SaleItemResponse originalItem : baseOriginalItems) { + SaleItemResponse refreshedItem = copySaleItem(originalItem); + int pending = pendingRefunds.getOrDefault(refreshedItem.getProdId(), 0); + refreshedItem.setQuantity(Math.max(0, refreshedItem.getQuantity() - pending)); + if (refreshedItem.getQuantity() > 0) { + refreshedItems.add(refreshedItem); + } + } + + originalItems.setAll(refreshedItems); + tvOriginalItems.getSelectionModel().clearSelection(); + tvOriginalItems.refresh(); + tvRefundItems.refresh(); + } + + private List copySaleItems(List items) { + List copies = new ArrayList<>(); + for (SaleItemResponse item : items) { + copies.add(copySaleItem(item)); + } + return copies; + } + + private SaleItemResponse copySaleItem(SaleItemResponse source) { + SaleItemResponse copy = new SaleItemResponse(); + copy.setSaleItemId(source.getSaleItemId()); + copy.setProdId(source.getProdId()); + copy.setProductName(source.getProductName()); + copy.setQuantity(source.getQuantity()); + copy.setUnitPrice(source.getUnitPrice()); + return copy; + } + + private void updateRefundTotal() { + double total = refundItems.stream().mapToDouble(RefundItem::getTotal).sum(); + lblRefundTotal.setText(currency.format(total)); + } + + private void closeDialog() { + Stage stage = (Stage) btnCancel.getScene().getWindow(); + stage.close(); + } + + private List buildRefundableItems(SaleResponse sale, List previousRefunds) { + Map refundedByProduct = new HashMap<>(); + for (SaleResponse refund : previousRefunds) { + if (refund.getItems() == null) { + continue; + } + for (SaleItemResponse refundItem : refund.getItems()) { + if (refundItem.getProdId() == null || refundItem.getQuantity() == null) { + continue; + } + refundedByProduct.merge(refundItem.getProdId(), Math.abs(refundItem.getQuantity()), Integer::sum); + } + } + + List refundableItems = new ArrayList<>(); + if (sale.getItems() == null) { + return refundableItems; + } + + for (SaleItemResponse originalItem : sale.getItems()) { + if (originalItem.getProdId() == null || originalItem.getQuantity() == null) { + continue; + } + + int remainingQuantity = originalItem.getQuantity() - refundedByProduct.getOrDefault(originalItem.getProdId(), 0); + if (remainingQuantity <= 0) { + continue; + } + + SaleItemResponse refundableItem = new SaleItemResponse(); + refundableItem.setSaleItemId(originalItem.getSaleItemId()); + refundableItem.setProdId(originalItem.getProdId()); + refundableItem.setProductName(originalItem.getProductName()); + refundableItem.setQuantity(remainingQuantity); + refundableItem.setUnitPrice(originalItem.getUnitPrice()); + refundableItems.add(refundableItem); + } + + return refundableItems; + } + + private void showError(String title, String message) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setTitle(title); + alert.setHeaderText(null); + alert.setContentText(message); + alert.showAndWait(); + } + + public static class RefundItem { + private final int prodId; + private final String productName; + private final int quantity; + private final double unitPrice; + + public RefundItem(int prodId, String productName, int quantity, double unitPrice) { + this.prodId = prodId; + this.productName = productName; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public int getProdId() { + return prodId; + } + + public String getProductName() { + return productName; + } + + public int getQuantity() { + return quantity; + } + + public double getUnitPrice() { + return unitPrice; + } + + public double getTotal() { + return quantity * unitPrice; + } + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ServiceDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ServiceDialogController.java new file mode 100644 index 00000000..26f13b0f --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/ServiceDialogController.java @@ -0,0 +1,157 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.application.Platform; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.ComboBox; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import org.example.petshopdesktop.DTOs.ServiceDTO; +import org.example.petshopdesktop.api.dto.service.ServiceRequest; +import org.example.petshopdesktop.api.endpoints.ServiceApi; +import org.example.petshopdesktop.util.ActivityLogger; + +import java.math.BigDecimal; + + +public class ServiceDialogController { + + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private Label lblMode; + + @FXML + private Label lblServiceId; + + @FXML + private TextField txtServiceDesc; + + @FXML + private TextField txtServiceName; + + @FXML + private TextField txtServicePrice; + + @FXML + private ComboBox cbHours; + + @FXML + private ComboBox cbMinutes; + + private String mode; + private ServiceDTO selectedService; + + + + @FXML + public void initialize() { + cbHours.getItems().addAll(0, 1, 2, 3, 4); + cbMinutes.getItems().addAll(0, 15, 30, 45); + btnSave.setOnAction(e -> saveService()); + btnCancel.setOnAction(e -> close()); + } + + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Service"); + + if (mode.equals("Add")) { + lblServiceId.setVisible(false); + } else { + lblServiceId.setVisible(true); + } + } + + public void setService(ServiceDTO service) { + this.selectedService = service; + + lblServiceId.setText("ID: " + service.getServiceId()); + txtServiceName.setText(service.getServiceName()); + txtServiceDesc.setText(service.getServiceDesc()); + int totalMinutes = service.getServiceDuration(); + cbHours.setValue(totalMinutes / 60); + cbMinutes.setValue(totalMinutes % 60); + txtServicePrice.setText(String.valueOf(service.getServicePrice())); + } + + private void saveService() { + + String name = txtServiceName.getText(); + String desc = txtServiceDesc.getText(); + String priceText = txtServicePrice.getText(); + + Integer hours = cbHours.getValue(); + Integer minutes = cbMinutes.getValue(); + + // -------- VALIDATION -------- + if (name == null || name.isBlank()) { + showError("Service name is required."); + return; + } + + if (priceText == null || priceText.isBlank()) { + showError("Price is required."); + return; + } + + if (hours == null || minutes == null) { + showError("Please select duration."); + return; + } + + double price; + + try { + price = Double.parseDouble(priceText); + } catch (NumberFormatException e) { + showError("Price must be numeric."); + return; + } + + try { + int durationMinutes = (hours * 60) + minutes; + + ServiceRequest request = new ServiceRequest(); + request.setServiceName(name); + request.setServiceDesc(desc); + request.setServicePrice(BigDecimal.valueOf(price)); + request.setServiceDuration(durationMinutes); + + if (mode.equals("Add")) { + ServiceApi.getInstance().createService(request); + } else { + Long serviceId = (long) selectedService.getServiceId(); + ServiceApi.getInstance().updateService(serviceId, request); + } + + close(); + + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "ServiceDialogController.saveService", + e, + "Saving service in " + mode + " mode"); + e.printStackTrace(); + showError("Database error while saving service."); + } + } + + private void showError(String msg) { + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Invalid Input"); + alert.setContentText(msg); + alert.showAndWait(); + } + + private void close() { + Stage stage = (Stage) btnSave.getScene().getWindow(); + stage.close(); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java new file mode 100644 index 00000000..5ed6316e --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffEditDialogController.java @@ -0,0 +1,144 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; +import org.example.petshopdesktop.api.endpoints.EmployeeApi; +import org.example.petshopdesktop.models.StaffAccount; +import org.example.petshopdesktop.util.ActivityLogger; + +public class StaffEditDialogController { + + @FXML + private TextField txtFirstName; + + @FXML + private TextField txtLastName; + + @FXML + private TextField txtEmail; + + @FXML + private TextField txtPhone; + + @FXML + private TextField txtUsername; + + @FXML + private PasswordField txtPassword; + + @FXML + private PasswordField txtPasswordConfirm; + + @FXML + private Label lblError; + + @FXML + private Button btnSave; + + private StaffAccount staffAccount; + + public void setStaffAccount(StaffAccount staffAccount) { + this.staffAccount = staffAccount; + txtFirstName.setText(staffAccount.getFirstName()); + txtLastName.setText(staffAccount.getLastName()); + txtEmail.setText(staffAccount.getEmail()); + txtPhone.setText(staffAccount.getPhone()); + txtUsername.setText(staffAccount.getUsername()); + } + + @FXML + void btnSaveClicked(ActionEvent event) { + lblError.setText(""); + if (staffAccount == null) { + lblError.setText("No staff account selected."); + return; + } + + String firstName = value(txtFirstName); + String lastName = value(txtLastName); + String email = value(txtEmail); + String phone = value(txtPhone); + String username = value(txtUsername); + String password = txtPassword.getText() == null ? "" : txtPassword.getText().trim(); + String confirm = txtPasswordConfirm.getText() == null ? "" : txtPasswordConfirm.getText().trim(); + + if (firstName.isBlank() || lastName.isBlank()) { + lblError.setText("First name and last name are required."); + return; + } + if (email.isBlank()) { + lblError.setText("Email is required."); + return; + } + if (phone.isBlank()) { + lblError.setText("Phone is required."); + return; + } + String phoneError = Validator.isValidPhoneNumber(phone, "Phone"); + if (!phoneError.isEmpty()) { + lblError.setText(phoneError.trim()); + return; + } + if (username.isBlank()) { + lblError.setText("Username is required."); + return; + } + if (!password.isEmpty() && password.length() < 6) { + lblError.setText("Password must be at least 6 characters."); + return; + } + if (!password.equals(confirm)) { + lblError.setText("Passwords do not match."); + return; + } + + btnSave.setDisable(true); + + new Thread(() -> { + try { + EmployeeRequest request = new EmployeeRequest(); + request.setUsername(username); + request.setPassword(password.isEmpty() ? null : password); + request.setFirstName(firstName); + request.setLastName(lastName); + request.setEmail(email); + request.setPhone(phone); + request.setRole(staffAccount.getRole()); + request.setActive(staffAccount.isActive()); + + EmployeeApi.getInstance().updateEmployee(staffAccount.getEmployeeId(), request); + + Platform.runLater(this::close); + } catch (Exception e) { + ActivityLogger.getInstance().logException("StaffEditDialogController.btnSaveClicked", e, "Updating staff account"); + String msg = e.getMessage() == null ? "Could not update staff account." : e.getMessage(); + Platform.runLater(() -> { + lblError.setText(msg); + btnSave.setDisable(false); + }); + } + }).start(); + } + + @FXML + void btnCancelClicked(ActionEvent event) { + close(); + } + + private void close() { + Stage stage = (Stage) btnSave.getScene().getWindow(); + stage.close(); + } + + private static String value(TextField tf) { + return tf.getText() == null ? "" : tf.getText().trim(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java new file mode 100644 index 00000000..8d121dde --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/StaffRegisterDialogController.java @@ -0,0 +1,140 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.application.Platform; +import javafx.event.ActionEvent; +import javafx.fxml.FXML; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.PasswordField; +import javafx.scene.control.TextField; +import javafx.stage.Stage; +import org.example.petshopdesktop.api.dto.employee.EmployeeRequest; +import org.example.petshopdesktop.api.endpoints.EmployeeApi; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.util.ActivityLogger; + +public class StaffRegisterDialogController { + + @FXML + private TextField txtFirstName; + + @FXML + private TextField txtLastName; + + @FXML + private TextField txtEmail; + + @FXML + private TextField txtPhone; + + @FXML + private TextField txtUsername; + + @FXML + private PasswordField txtPassword; + + @FXML + private PasswordField txtPasswordConfirm; + + @FXML + private Label lblError; + + @FXML + private Button btnCreate; + + @FXML + void btnCreateClicked(ActionEvent event) { + lblError.setText(""); + + String firstName = value(txtFirstName); + String lastName = value(txtLastName); + String email = value(txtEmail); + String phone = value(txtPhone); + String username = value(txtUsername); + String password = txtPassword.getText() == null ? "" : txtPassword.getText(); + String confirm = txtPasswordConfirm.getText() == null ? "" : txtPasswordConfirm.getText(); + + if (firstName.isBlank() || lastName.isBlank()) { + lblError.setText("First name and last name are required."); + return; + } + if (email.isBlank()) { + lblError.setText("Email is required."); + return; + } + if (phone.isBlank()) { + lblError.setText("Phone is required."); + return; + } + String phoneError = Validator.isValidPhoneNumber(phone, "Phone"); + if (!phoneError.isEmpty()) { + lblError.setText(phoneError.trim()); + return; + } + if (username.isBlank()) { + lblError.setText("Username is required."); + return; + } + if (password.isBlank()) { + lblError.setText("Password is required."); + return; + } + if (!password.equals(confirm)) { + lblError.setText("Passwords do not match."); + return; + } + + btnCreate.setDisable(true); + + new Thread(() -> { + try { + EmployeeRequest request = new EmployeeRequest(); + request.setUsername(username); + request.setPassword(password); + request.setFirstName(firstName); + request.setLastName(lastName); + request.setEmail(email); + request.setPhone(phone); + request.setRole("STAFF"); + request.setActive(true); + + EmployeeApi.getInstance().createEmployee(request); + + Platform.runLater(() -> { + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setTitle("Staff Account"); + alert.setHeaderText(null); + alert.setContentText("Staff account created. You can log in now."); + alert.showAndWait(); + close(); + }); + } catch (Exception e) { + ActivityLogger.getInstance().logException("StaffRegisterDialogController.btnCreateClicked", e, "Creating staff account"); + String msg = e.getMessage() == null ? "Could not create staff account." : e.getMessage(); + Platform.runLater(() -> { + if (msg.toLowerCase().contains("duplicate") || msg.toLowerCase().contains("unique")) { + lblError.setText("Username already exists."); + } else { + lblError.setText(msg); + } + btnCreate.setDisable(false); + }); + } + }).start(); + } + + @FXML + void btnCancelClicked(ActionEvent event) { + close(); + } + + private void close() { + Stage stage = (Stage) btnCreate.getScene().getWindow(); + stage.close(); + } + + private static String value(TextField tf) { + return tf.getText() == null ? "" : tf.getText().trim(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SupplierDialogController.java b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SupplierDialogController.java new file mode 100644 index 00000000..8b61daca --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/controllers/dialogcontrollers/SupplierDialogController.java @@ -0,0 +1,189 @@ +package org.example.petshopdesktop.controllers.dialogcontrollers; + +import javafx.event.EventHandler; +import javafx.fxml.FXML; +import javafx.scene.Node; +import javafx.scene.control.Alert; +import javafx.scene.control.Button; +import javafx.scene.control.Label; +import javafx.scene.control.TextField; +import javafx.scene.input.MouseEvent; +import javafx.stage.Stage; +import org.example.petshopdesktop.Validator; +import org.example.petshopdesktop.api.dto.supplier.SupplierRequest; +import org.example.petshopdesktop.api.dto.supplier.SupplierResponse; +import org.example.petshopdesktop.api.endpoints.SupplierApi; +import org.example.petshopdesktop.models.Supplier; +import org.example.petshopdesktop.util.ActivityLogger; + +public class SupplierDialogController { + + @FXML + private Button btnCancel; + + @FXML + private Button btnSave; + + @FXML + private Label lblMode; + + @FXML + private Label lblSupId; + + @FXML + private TextField txtCompanyName; + + @FXML + private TextField txtContactFirstName; + + @FXML + private TextField txtContactLastName; + + @FXML + private TextField txtEmail; + + @FXML + private TextField txtPhone; + + private String mode = null; + + /** + * Add event listeners to buttons when dialog loads + */ + @FXML + void initialize() { + //Set up mouse handlers for buttons + btnSave.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + buttonSaveClicked(mouseEvent); + } + }); + + btnCancel.setOnMouseClicked(new EventHandler() { + @Override + public void handle(MouseEvent mouseEvent) { + closeStage(mouseEvent); + } + }); + } + + /** + * Validates the inputs, then add or update the database depending + * on the mode + * @param mouseEvent click event for save button + */ + private void buttonSaveClicked(MouseEvent mouseEvent) { + String errorMsg = ""; //error message for validation + + //Check validation (input required) + errorMsg += Validator.isPresent(txtCompanyName.getText(), "Company Name"); + errorMsg += Validator.isPresent(txtContactFirstName.getText(), "Contact First Name"); + errorMsg += Validator.isPresent(txtContactLastName.getText(), "Contact Last Name"); + errorMsg += Validator.isPresent(txtEmail.getText(), "Email Address"); + errorMsg += Validator.isPresent(txtPhone.getText(), "Phone Number"); + + //Check validation (Length size) + errorMsg += Validator.isLessThanVarChars(txtCompanyName.getText(),"Company Name", 100); + errorMsg += Validator.isLessThanVarChars(txtContactFirstName.getText(), "Contact First Name", 50); + errorMsg += Validator.isLessThanVarChars(txtContactLastName.getText(), "Contact Last Name", 50); + errorMsg += Validator.isLessThanVarChars(txtEmail.getText(), "Email Address", 100); + + //Check validation (format) + errorMsg += Validator.isValidEmail(txtEmail.getText(), "Email Address"); + errorMsg += Validator.isValidPhoneNumber(txtPhone.getText(), "Phone Number"); + + if(errorMsg.isEmpty()){ //no validation errors detected + SupplierRequest request = createSupplierRequest(); + try { + if (mode.equals("Add")) { + SupplierApi.getInstance().createSupplier(request); + } else { + String[] parts = lblSupId.getText().split(": "); + if (parts.length < 2) { + throw new IllegalStateException("Invalid supplier ID format"); + } + Long supplierId = Long.parseLong(parts[1]); + SupplierApi.getInstance().updateSupplier(supplierId, request); + } + + Alert alert = new Alert(Alert.AlertType.INFORMATION); + alert.setHeaderText("Saved"); + alert.setContentText(mode + " succeeded"); + alert.showAndWait(); + closeStage(mouseEvent); + } catch (Exception e) { + ActivityLogger.getInstance().logException( + "SupplierDialogController.buttonSaveClicked", + e, + mode.equals("Add") ? "Inserting new supplier record" : "Updating supplier record"); + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Database Operation Error"); + alert.setContentText(mode + " failed: " + e.getMessage()); + alert.showAndWait(); + } + } + else{ //Display validation errors + Alert alert = new Alert(Alert.AlertType.ERROR); + alert.setHeaderText("Input Error"); + alert.setContentText(errorMsg); + alert.showAndWait(); + } + } + + /** + * Close the window + * @param mouseEvent mouse event to close + */ + private void closeStage(MouseEvent mouseEvent) { + Node node = (Node) mouseEvent.getSource(); + Stage stage = (Stage) node.getScene().getWindow(); + stage.close(); + } + + /** + * Create a supplier request from the form inputs + * @return supplier request for API call + */ + private SupplierRequest createSupplierRequest(){ + SupplierRequest request = new SupplierRequest(); + request.setSupCompany(txtCompanyName.getText()); + request.setSupContactFirstName(txtContactFirstName.getText()); + request.setSupContactLastName(txtContactLastName.getText()); + request.setSupEmail(txtEmail.getText()); + request.setSupPhone(txtPhone.getText()); + return request; + } + + /** + * Display the supplier data in text fields + * @param supplier the supplier entity containing data to display + */ + public void displaySupplierDetails(Supplier supplier){ + if (supplier!=null){ + lblSupId.setText("ID: " + supplier.getSupId()); + txtCompanyName.setText(supplier.getSupCompany()); + txtContactFirstName.setText(supplier.getSupContactFirstName()); + txtContactLastName.setText(supplier.getSupContactLastName()); + txtEmail.setText(supplier.getSupEmail()); + txtPhone.setText(supplier.getSupPhone()); + } + } + + + /** + * Set the mode of the dialog + * @param mode the mode to for the dialog + */ + public void setMode(String mode) { + this.mode = mode; + lblMode.setText(mode + " Supplier"); + if(mode.equals("Add")) { + lblSupId.setVisible(false); + } + else if(mode.equals("Edit")) { + lblSupId.setVisible(true); + } + } + +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java b/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java new file mode 100644 index 00000000..c5c02f63 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Adoption.java @@ -0,0 +1,75 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class Adoption { + private SimpleIntegerProperty adoptionId; + private SimpleIntegerProperty petId; + private SimpleIntegerProperty customerId; + private SimpleStringProperty petName; + private SimpleStringProperty customerName; + private SimpleStringProperty adoptionDate; + private SimpleDoubleProperty adoptionFee; + private SimpleStringProperty adoptionStatus; + + public Adoption(int adoptionId, int petId, int customerId, String petName, String customerName, String adoptionDate, double adoptionFee, String adoptionStatus) { + this.adoptionId = new SimpleIntegerProperty(adoptionId); + this.petId = new SimpleIntegerProperty(petId); + this.customerId = new SimpleIntegerProperty(customerId); + this.petName = new SimpleStringProperty(petName); + this.customerName = new SimpleStringProperty(customerName); + this.adoptionDate = new SimpleStringProperty(adoptionDate); + this.adoptionFee = new SimpleDoubleProperty(adoptionFee); + this.adoptionStatus = new SimpleStringProperty(adoptionStatus); + } + + public int getAdoptionId() { return adoptionId.get(); } + + public void setAdoptionId(int adoptionId) { this.adoptionId.set(adoptionId); } + + public SimpleIntegerProperty adoptionIdProperty() { return adoptionId; } + + public int getPetId() { return petId.get(); } + + public void setPetId(int petId) { this.petId.set(petId); } + + public SimpleIntegerProperty petIdProperty() { return petId; } + + public int getCustomerId() { return customerId.get(); } + + public void setCustomerId(int customerId) { this.customerId.set(customerId); } + + public SimpleIntegerProperty customerIdProperty() { return customerId; } + + public String getPetName() { return petName.get(); } + + public void setPetName(String petName) { this.petName.set(petName); } + + public SimpleStringProperty petNameProperty() { return petName; } + + public String getCustomerName() { return customerName.get(); } + + public void setCustomerName(String customerName) { this.customerName.set(customerName); } + + public SimpleStringProperty customerNameProperty() { return customerName; } + + public String getAdoptionDate() { return adoptionDate.get(); } + + public void setAdoptionDate(String adoptionDate) { this.adoptionDate.set(adoptionDate); } + + public SimpleStringProperty adoptionDateProperty() { return adoptionDate; } + + public double getAdoptionFee() { return adoptionFee.get(); } + + public void setAdoptionFee(double adoptionFee) { this.adoptionFee.set(adoptionFee); } + + public SimpleDoubleProperty adoptionFeeProperty() { return adoptionFee; } + + public String getAdoptionStatus() { return adoptionStatus.get(); } + + public void setAdoptionStatus(String adoptionStatus) { this.adoptionStatus.set(adoptionStatus); } + + public SimpleStringProperty adoptionStatusProperty() { return adoptionStatus; } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Appointment.java b/desktop/src/main/java/org/example/petshopdesktop/models/Appointment.java new file mode 100644 index 00000000..7ab54292 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Appointment.java @@ -0,0 +1,46 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class Appointment { + + private SimpleIntegerProperty appointmentId; + private SimpleIntegerProperty serviceId; + private SimpleIntegerProperty customerId; + private SimpleStringProperty appointmentDate; + private SimpleStringProperty appointmentTime; + private SimpleStringProperty appointmentStatus; + + // Constructor + public Appointment(int appointmentId, + int serviceId, + int customerId, + String appointmentDate, + String appointmentTime, + String appointmentStatus) { + + this.appointmentId = new SimpleIntegerProperty(appointmentId); + this.serviceId = new SimpleIntegerProperty(serviceId); + this.customerId = new SimpleIntegerProperty(customerId); + this.appointmentDate = new SimpleStringProperty(appointmentDate); + this.appointmentTime = new SimpleStringProperty(appointmentTime); + this.appointmentStatus = new SimpleStringProperty(appointmentStatus); + } + + // Getters + public int getAppointmentId() { return appointmentId.get(); } + public int getServiceId() { return serviceId.get(); } + public int getCustomerId() { return customerId.get(); } + public String getAppointmentDate() { return appointmentDate.get(); } + public String getAppointmentTime() { return appointmentTime.get(); } + public String getAppointmentStatus() { return appointmentStatus.get(); } + + // Properties + public SimpleIntegerProperty appointmentIdProperty() { return appointmentId; } + public SimpleIntegerProperty serviceIdProperty() { return serviceId; } + public SimpleIntegerProperty customerIdProperty() { return customerId; } + public SimpleStringProperty appointmentDateProperty() { return appointmentDate; } + public SimpleStringProperty appointmentTimeProperty() { return appointmentTime; } + public SimpleStringProperty appointmentStatusProperty() { return appointmentStatus; } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Category.java b/desktop/src/main/java/org/example/petshopdesktop/models/Category.java new file mode 100644 index 00000000..1f28182b --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Category.java @@ -0,0 +1,59 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class Category { + private SimpleIntegerProperty categoryId; + private SimpleStringProperty categoryName; + private SimpleStringProperty categoryType; + + //Constrctor + public Category(int categoryId, String categoryName, String categoryType) { + this.categoryId = new SimpleIntegerProperty(categoryId); + this.categoryName = new SimpleStringProperty(categoryName); + this.categoryType = new SimpleStringProperty(categoryType); + } + + //getters and setters + public int getCategoryId() { + return categoryId.get(); + } + + public SimpleIntegerProperty categoryIdProperty() { + return categoryId; + } + + public void setCategoryId(int categoryId) { + this.categoryId.set(categoryId); + } + + public String getCategoryName() { + return categoryName.get(); + } + + public SimpleStringProperty categoryNameProperty() { + return categoryName; + } + + public void setCategoryName(String categoryName) { + this.categoryName.set(categoryName); + } + + public String getCategoryType() { + return categoryType.get(); + } + + public SimpleStringProperty categoryTypeProperty() { + return categoryType; + } + + public void setCategoryType(String categoryType) { + this.categoryType.set(categoryType); + } + + @Override + public String toString() { + return getCategoryName(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Customer.java b/desktop/src/main/java/org/example/petshopdesktop/models/Customer.java new file mode 100644 index 00000000..66602694 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Customer.java @@ -0,0 +1,75 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class Customer { + + private SimpleIntegerProperty customerId; + private SimpleStringProperty firstName; + private SimpleStringProperty lastName; + private SimpleStringProperty email; + private SimpleStringProperty phone; + + // Constructor + public Customer(int customerId, + String firstName, + String lastName, + String email, + String phone) { + + this.customerId = new SimpleIntegerProperty(customerId); + this.firstName = new SimpleStringProperty(firstName); + this.lastName = new SimpleStringProperty(lastName); + this.email = new SimpleStringProperty(email); + this.phone = new SimpleStringProperty(phone); + } + + // Getters + public int getCustomerId() { + return customerId.get(); + } + + public String getFirstName() { + return firstName.get(); + } + + public String getLastName() { + return lastName.get(); + } + + public String getEmail() { + return email.get(); + } + + public String getPhone() { + return phone.get(); + } + + // Properties (optional but useful later) + public SimpleIntegerProperty customerIdProperty() { + return customerId; + } + + public SimpleStringProperty firstNameProperty() { + return firstName; + } + + public SimpleStringProperty lastNameProperty() { + return lastName; + } + + public SimpleStringProperty emailProperty() { + return email; + } + + public SimpleStringProperty phoneProperty() { + return phone; + } + + // This controls how customer appears in ComboBox + @Override + public String toString() { + return getFirstName() + " " + getLastName(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Inventory.java b/desktop/src/main/java/org/example/petshopdesktop/models/Inventory.java new file mode 100644 index 00000000..53f77543 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Inventory.java @@ -0,0 +1,74 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class Inventory { + private SimpleIntegerProperty inventoryId; + private SimpleIntegerProperty prodId; + private SimpleStringProperty prodName; + private SimpleStringProperty categoryName; + private SimpleIntegerProperty storeId; + private SimpleStringProperty storeName; + private SimpleIntegerProperty quantity; + private SimpleIntegerProperty reorderLevel; + + public Inventory(int inventoryId, int prodId, String prodName, String categoryName, int storeId, String storeName, int quantity, int reorderLevel) { + this.inventoryId = new SimpleIntegerProperty(inventoryId); + this.prodId = new SimpleIntegerProperty(prodId); + this.prodName = new SimpleStringProperty(prodName); + this.categoryName = new SimpleStringProperty(categoryName); + this.storeId = new SimpleIntegerProperty(storeId); + this.storeName = new SimpleStringProperty(storeName); + this.quantity = new SimpleIntegerProperty(quantity); + this.reorderLevel = new SimpleIntegerProperty(reorderLevel); + } + + public int getInventoryId() { return inventoryId.get(); } + + public void setInventoryId(int inventoryId) { this.inventoryId.set(inventoryId); } + + public SimpleIntegerProperty inventoryIdProperty() { return inventoryId; } + + public int getProdId() { return prodId.get(); } + + public void setProdId(int prodId) { this.prodId.set(prodId); } + + public SimpleIntegerProperty prodIdProperty() { return prodId; } + + public String getProdName() { return prodName.get(); } + + public void setProdName(String prodName) { this.prodName.set(prodName); } + + public SimpleStringProperty prodNameProperty() { return prodName; } + + public String getCategoryName() { return categoryName.get(); } + + public void setCategoryName(String categoryName) { this.categoryName.set(categoryName); } + + public SimpleStringProperty categoryNameProperty() { return categoryName; } + + public int getStoreId() { return storeId.get(); } + + public void setStoreId(int storeId) { this.storeId.set(storeId); } + + public SimpleIntegerProperty storeIdProperty() { return storeId; } + + public String getStoreName() { return storeName.get(); } + + public void setStoreName(String storeName) { this.storeName.set(storeName); } + + public SimpleStringProperty storeNameProperty() { return storeName; } + + public int getQuantity() { return quantity.get(); } + + public void setQuantity(int quantity) { this.quantity.set(quantity); } + + public SimpleIntegerProperty quantityProperty() { return quantity; } + + public int getReorderLevel() { return reorderLevel.get(); } + + public void setReorderLevel(int reorderLevel) { this.reorderLevel.set(reorderLevel); } + + public SimpleIntegerProperty reorderLevelProperty() { return reorderLevel; } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java new file mode 100644 index 00000000..e1f2e3fb --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Pet.java @@ -0,0 +1,109 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +public class Pet { + private SimpleIntegerProperty petId; + private SimpleStringProperty petName; + private SimpleStringProperty petSpecies; + private SimpleStringProperty petBreed; + private SimpleIntegerProperty petAge; + private SimpleStringProperty petStatus; + private SimpleDoubleProperty petPrice; + + public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice) { + this.petId = new SimpleIntegerProperty(petId); + this.petName = new SimpleStringProperty(petName); + this.petSpecies = new SimpleStringProperty(petSpecies); + this.petBreed = new SimpleStringProperty(petBreed); + this.petAge = new SimpleIntegerProperty(petAge); + this.petStatus = new SimpleStringProperty(petStatus); + this.petPrice = new SimpleDoubleProperty(petPrice); + } + + public int getPetId() { + return petId.get(); + } + + public void setPetId(int petId) { + this.petId.set(petId); + } + + public SimpleIntegerProperty petIdProperty() { + return petId; + } + + public String getPetName() { + return petName.get(); + } + + public void setPetName(String petName) { + this.petName.set(petName); + } + + public SimpleStringProperty petNameProperty() { + return petName; + } + + public String getPetSpecies() { + return petSpecies.get(); + } + + public void setPetSpecies(String petSpecies) { + this.petSpecies.set(petSpecies); + } + + public SimpleStringProperty petSpeciesProperty() { + return petSpecies; + } + + public String getPetBreed() { + return petBreed.get(); + } + + public void setPetBreed(String petBreed) { + this.petBreed.set(petBreed); + } + + public SimpleStringProperty petBreedProperty() { + return petBreed; + } + + public int getPetAge() { + return petAge.get(); + } + + public void setPetAge(int petAge) { + this.petAge.set(petAge); + } + + public SimpleIntegerProperty petAgeProperty() { + return petAge; + } + + public String getPetStatus() { + return petStatus.get(); + } + + public void setPetStatus(String petStatus) { + this.petStatus.set(petStatus); + } + + public SimpleStringProperty petStatusProperty() { + return petStatus; + } + + public double getPetPrice() { + return petPrice.get(); + } + + public void setPetPrice(double petPrice) { + this.petPrice.set(petPrice); + } + + public SimpleDoubleProperty petPriceProperty() { + return petPrice; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Product.java b/desktop/src/main/java/org/example/petshopdesktop/models/Product.java new file mode 100644 index 00000000..bf0913e5 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Product.java @@ -0,0 +1,91 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +/** + * The class for the entity of products (contains all data relating to products) + */ +public class Product { + private SimpleIntegerProperty prodId; + private SimpleStringProperty prodName; + private SimpleDoubleProperty prodPrice; + private SimpleIntegerProperty categoryId; + private SimpleStringProperty prodDesc; + + //constructor + public Product(int prodId, String prodName, double prodPrice, int categoryId, String prodDesc) { + this.prodId = new SimpleIntegerProperty(prodId); + this.prodName = new SimpleStringProperty(prodName); + this.prodPrice = new SimpleDoubleProperty(prodPrice); + this.categoryId = new SimpleIntegerProperty(categoryId); + this.prodDesc = new SimpleStringProperty(prodDesc); + } + + //getter and setters + public int getProdId() { + return prodId.get(); + } + + public SimpleIntegerProperty prodIdProperty() { + return prodId; + } + + public void setProdId(int prodId) { + this.prodId.set(prodId); + } + + public String getProdName() { + return prodName.get(); + } + + public SimpleStringProperty prodNameProperty() { + return prodName; + } + + public void setProdName(String prodName) { + this.prodName.set(prodName); + } + + public double getProdPrice() { + return prodPrice.get(); + } + + public SimpleDoubleProperty prodPriceProperty() { + return prodPrice; + } + + public void setProdPrice(double prodPrice) { + this.prodPrice.set(prodPrice); + } + + public int getCategoryId() { + return categoryId.get(); + } + + public SimpleIntegerProperty categoryIdProperty() { + return categoryId; + } + + public void setCategoryId(int categoryId) { + this.categoryId.set(categoryId); + } + + public String getProdDesc() { + return prodDesc.get(); + } + + public SimpleStringProperty prodDescProperty() { + return prodDesc; + } + + public void setProdDesc(String prodDesc) { + this.prodDesc.set(prodDesc); + } + + @Override + public String toString() { + return getProdName(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/ProductSupplier.java b/desktop/src/main/java/org/example/petshopdesktop/models/ProductSupplier.java new file mode 100644 index 00000000..e674974d --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/ProductSupplier.java @@ -0,0 +1,54 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; + +public class ProductSupplier { + private SimpleIntegerProperty supId; + private SimpleIntegerProperty prodId; + private SimpleDoubleProperty cost; + + //constructor + public ProductSupplier(int supId, int prodId, double cost) { + this.supId = new SimpleIntegerProperty(supId); + this.prodId = new SimpleIntegerProperty(prodId); + this.cost = new SimpleDoubleProperty(cost); + } + + //getter and setters + public int getSupId() { + return supId.get(); + } + + public SimpleIntegerProperty supIdProperty() { + return supId; + } + + public void setSupId(int supId) { + this.supId.set(supId); + } + + public int getProdId() { + return prodId.get(); + } + + public SimpleIntegerProperty prodIdProperty() { + return prodId; + } + + public void setProdId(int prodId) { + this.prodId.set(prodId); + } + + public double getCost() { + return cost.get(); + } + + public SimpleDoubleProperty costProperty() { + return cost; + } + + public void setCost(double cost) { + this.cost.set(cost); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/PurchaseOrder.java b/desktop/src/main/java/org/example/petshopdesktop/models/PurchaseOrder.java new file mode 100644 index 00000000..c13afe50 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/PurchaseOrder.java @@ -0,0 +1,52 @@ +package org.example.petshopdesktop.models; + +import java.math.BigDecimal; +import java.time.LocalDate; + +public class PurchaseOrder { + + private long purchaseOrderId; + private String supplierName; + private LocalDate orderDate; + private LocalDate expectedDeliveryDate; + private String status; + private BigDecimal totalAmount; + + public PurchaseOrder(long purchaseOrderId, + String supplierName, + LocalDate orderDate, + LocalDate expectedDeliveryDate, + String status, + BigDecimal totalAmount) { + this.purchaseOrderId = purchaseOrderId; + this.supplierName = supplierName; + this.orderDate = orderDate; + this.expectedDeliveryDate = expectedDeliveryDate; + this.status = status; + this.totalAmount = totalAmount; + } + + public long getPurchaseOrderId() { + return purchaseOrderId; + } + + public String getSupplierName() { + return supplierName; + } + + public LocalDate getOrderDate() { + return orderDate; + } + + public LocalDate getExpectedDeliveryDate() { + return expectedDeliveryDate; + } + + public String getStatus() { + return status; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/SaleCartItem.java b/desktop/src/main/java/org/example/petshopdesktop/models/SaleCartItem.java new file mode 100644 index 00000000..25c4da36 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/SaleCartItem.java @@ -0,0 +1,39 @@ +package org.example.petshopdesktop.models; + +public class SaleCartItem { + private final int prodId; + private final String prodName; + private int quantity; + private final double unitPrice; + + public SaleCartItem(int prodId, String prodName, int quantity, double unitPrice) { + this.prodId = prodId; + this.prodName = prodName; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + public int getProdId() { + return prodId; + } + + public String getProdName() { + return prodName; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public double getUnitPrice() { + return unitPrice; + } + + public double getTotal() { + return unitPrice * quantity; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java new file mode 100644 index 00000000..8ed6e291 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/SaleDetail.java @@ -0,0 +1,82 @@ +package org.example.petshopdesktop.models; + +import javafx.collections.ObservableList; +import java.time.LocalDateTime; + +public class SaleDetail { + private final int saleId; + private final LocalDateTime saleDate; + private final double totalAmount; + private final String paymentMethod; + private final String employeeName; + private final ObservableList items; + + public SaleDetail(int saleId, LocalDateTime saleDate, double totalAmount, String paymentMethod, String employeeName, ObservableList items) { + this.saleId = saleId; + this.saleDate = saleDate; + this.totalAmount = totalAmount; + this.paymentMethod = paymentMethod; + this.employeeName = employeeName; + this.items = items; + } + + public int getSaleId() { + return saleId; + } + + public LocalDateTime getSaleDate() { + return saleDate; + } + + public double getTotalAmount() { + return totalAmount; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public String getEmployeeName() { + return employeeName; + } + + public ObservableList getItems() { + return items; + } + + public static class SaleDetailItem { + private final int prodId; + private final String productName; + private final int quantity; + private final double unitPrice; + private final double total; + + public SaleDetailItem(int prodId, String productName, int quantity, double unitPrice, double total) { + this.prodId = prodId; + this.productName = productName; + this.quantity = quantity; + this.unitPrice = unitPrice; + this.total = total; + } + + public int getProdId() { + return prodId; + } + + public String getProductName() { + return productName; + } + + public int getQuantity() { + return quantity; + } + + public double getUnitPrice() { + return unitPrice; + } + + public double getTotal() { + return total; + } + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java b/desktop/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java new file mode 100644 index 00000000..a8a3d134 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/SaleLineItem.java @@ -0,0 +1,61 @@ +package org.example.petshopdesktop.models; + +public class SaleLineItem { + private final int saleId; + private final String saleDate; + private final String employeeName; + private final String itemName; + private final int quantity; + private final double unitPrice; + private final double total; + private final String paymentMethod; + private final boolean isRefund; + + public SaleLineItem(int saleId, String saleDate, String employeeName, String itemName, int quantity, double unitPrice, double total, String paymentMethod, boolean isRefund) { + this.saleId = saleId; + this.saleDate = saleDate; + this.employeeName = employeeName; + this.itemName = itemName; + this.quantity = quantity; + this.unitPrice = unitPrice; + this.total = total; + this.paymentMethod = paymentMethod; + this.isRefund = isRefund; + } + + public int getSaleId() { + return saleId; + } + + public String getSaleDate() { + return saleDate; + } + + public String getEmployeeName() { + return employeeName; + } + + public String getItemName() { + return itemName; + } + + public int getQuantity() { + return quantity; + } + + public double getUnitPrice() { + return unitPrice; + } + + public double getTotal() { + return total; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public boolean isRefund() { + return isRefund; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Service.java b/desktop/src/main/java/org/example/petshopdesktop/models/Service.java new file mode 100644 index 00000000..b259fc21 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Service.java @@ -0,0 +1,98 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleDoubleProperty; +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +/** + * The class for the entity of services (contains all data relating to services) + */ +public class Service { + + private SimpleIntegerProperty serviceId; + private SimpleStringProperty serviceName; + private SimpleStringProperty serviceDesc; + private SimpleIntegerProperty serviceDuration; + private SimpleDoubleProperty servicePrice; + + // constructor + public Service(int serviceId, + String serviceName, + String serviceDesc, + int serviceDuration, + double servicePrice) { + + this.serviceId = new SimpleIntegerProperty(serviceId); + this.serviceName = new SimpleStringProperty(serviceName); + this.serviceDesc = new SimpleStringProperty(serviceDesc); + this.serviceDuration = new SimpleIntegerProperty(serviceDuration); + this.servicePrice = new SimpleDoubleProperty(servicePrice); + } + + // getters & setters + + public int getServiceId() { + return serviceId.get(); + } + + public SimpleIntegerProperty serviceIdProperty() { + return serviceId; + } + + public void setServiceId(int serviceId) { + this.serviceId.set(serviceId); + } + + public String getServiceName() { + return serviceName.get(); + } + + public SimpleStringProperty serviceNameProperty() { + return serviceName; + } + + public void setServiceName(String serviceName) { + this.serviceName.set(serviceName); + } + + public String getServiceDesc() { + return serviceDesc.get(); + } + + public SimpleStringProperty serviceDescProperty() { + return serviceDesc; + } + + public void setServiceDesc(String serviceDesc) { + this.serviceDesc.set(serviceDesc); + } + + public int getServiceDuration() { + return serviceDuration.get(); + } + + public SimpleIntegerProperty serviceDurationProperty() { + return serviceDuration; + } + + public void setServiceDuration(int serviceDuration) { + this.serviceDuration.set(serviceDuration); + } + + public double getServicePrice() { + return servicePrice.get(); + } + + public SimpleDoubleProperty servicePriceProperty() { + return servicePrice; + } + + public void setServicePrice(double servicePrice) { + this.servicePrice.set(servicePrice); + } + + @Override + public String toString() { + return getServiceName(); + } +} \ No newline at end of file diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/StaffAccount.java b/desktop/src/main/java/org/example/petshopdesktop/models/StaffAccount.java new file mode 100644 index 00000000..59923338 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/StaffAccount.java @@ -0,0 +1,79 @@ +package org.example.petshopdesktop.models; + +import java.sql.Timestamp; + +public class StaffAccount { + private final long userId; + private final long employeeId; + private final String username; + private final String firstName; + private final String lastName; + private final String email; + private final String phone; + private final String role; + private final boolean active; + private final Timestamp createdAt; + + public StaffAccount(long userId, long employeeId, String username, String firstName, String lastName, String email, String phone, String role, boolean active, Timestamp createdAt) { + this.userId = userId; + this.employeeId = employeeId; + this.username = username; + this.firstName = firstName; + this.lastName = lastName; + this.email = email; + this.phone = phone; + this.role = role; + this.active = active; + this.createdAt = createdAt; + } + + public long getUserId() { + return userId; + } + + public long getEmployeeId() { + return employeeId; + } + + public String getUsername() { + return username; + } + + public String getFirstName() { + return firstName; + } + + public String getLastName() { + return lastName; + } + + public String getFullName() { + String fn = firstName == null ? "" : firstName.trim(); + String ln = lastName == null ? "" : lastName.trim(); + return (fn + " " + ln).trim(); + } + + public String getEmail() { + return email; + } + + public String getPhone() { + return phone; + } + + public String getRole() { + return role; + } + + public boolean isActive() { + return active; + } + + public Timestamp getCreatedAt() { + return createdAt; + } + + public String getStatus() { + return active ? "Active" : "Inactive"; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/Supplier.java b/desktop/src/main/java/org/example/petshopdesktop/models/Supplier.java new file mode 100644 index 00000000..1120670a --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/Supplier.java @@ -0,0 +1,114 @@ +package org.example.petshopdesktop.models; + +import javafx.beans.property.SimpleIntegerProperty; +import javafx.beans.property.SimpleStringProperty; + +/** + * The class for the entity of supplier (contains all data relating to suppliers) + */ +public class Supplier { + SimpleIntegerProperty supId; + SimpleStringProperty supCompany; + SimpleStringProperty supContactFirstName; + SimpleStringProperty supContactLastName; + SimpleStringProperty supEmail; + SimpleStringProperty supPhone; + + //constructor + public Supplier(int supId, String supCompany, String supContactFirstName, String supContactLastName, String supEmail, String supPhone) { + this.supId = new SimpleIntegerProperty(supId); + this.supCompany = new SimpleStringProperty(supCompany); + this.supContactFirstName = new SimpleStringProperty(supContactFirstName); + this.supContactLastName = new SimpleStringProperty(supContactLastName); + this.supEmail = new SimpleStringProperty(supEmail); + this.supPhone = new SimpleStringProperty(supPhone); + } + + //getter and setter + public int getSupId() { + return supId.get(); + } + + public SimpleIntegerProperty supIdProperty() { + return supId; + } + + public void setSupId(int supId) { + this.supId.set(supId); + } + + public String getSupCompany() { + return supCompany.get(); + } + + public SimpleStringProperty supCompanyProperty() { + return supCompany; + } + + public void setSupCompany(String supCompany) { + this.supCompany.set(supCompany); + } + + public String getSupContactFirstName() { + return supContactFirstName.get(); + } + + public SimpleStringProperty supContactFirstNameProperty() { + return supContactFirstName; + } + + public void setSupContactFirstName(String supContactFirstName) { + this.supContactFirstName.set(supContactFirstName); + } + + public String getSupContactLastName() { + return supContactLastName.get(); + } + + public SimpleStringProperty supContactLastNameProperty() { + return supContactLastName; + } + + public void setSupContactLastName(String supContactLastName) { + this.supContactLastName.set(supContactLastName); + } + + public String getSupEmail() { + return supEmail.get(); + } + + public SimpleStringProperty supEmailProperty() { + return supEmail; + } + + public void setSupEmail(String supEmail) { + this.supEmail.set(supEmail); + } + + public String getSupPhone() { + return supPhone.get(); + } + + public SimpleStringProperty supPhoneProperty() { + return supPhone; + } + + public void setSupPhone(String supPhone) { + this.supPhone.set(supPhone); + } + + //custom methods + + /** + * Get the full name of supplier contact to display in a single cell + * @return full name of supplier contact + */ + public String getSupFullName() { + return getSupContactFirstName() + " " + getSupContactLastName(); + } + + @Override + public String toString() { + return getSupCompany(); + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/User.java b/desktop/src/main/java/org/example/petshopdesktop/models/User.java new file mode 100644 index 00000000..7d2199e9 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/User.java @@ -0,0 +1,52 @@ +package org.example.petshopdesktop.models; + +import org.example.petshopdesktop.auth.Role; + +public class User { + private final int userId; + private final int employeeId; + private final String username; + private final String employeeFirstName; + private final String employeeLastName; + private final Role role; + + public User(int userId, int employeeId, String username, String employeeFirstName, String employeeLastName, Role role) { + this.userId = userId; + this.employeeId = employeeId; + this.username = username; + this.employeeFirstName = employeeFirstName; + this.employeeLastName = employeeLastName; + this.role = role; + } + + public int getUserId() { + return userId; + } + + public int getEmployeeId() { + return employeeId; + } + + public String getUsername() { + return username; + } + + public String getEmployeeFirstName() { + return employeeFirstName; + } + + public String getEmployeeLastName() { + return employeeLastName; + } + + public String getEmployeeFullName() { + String fn = employeeFirstName == null ? "" : employeeFirstName.trim(); + String ln = employeeLastName == null ? "" : employeeLastName.trim(); + String full = (fn + " " + ln).trim(); + return full.isBlank() ? username : full; + } + + public Role getRole() { + return role; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/analytics/DailySalesData.java b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/DailySalesData.java new file mode 100644 index 00000000..1f41b8f2 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/DailySalesData.java @@ -0,0 +1,21 @@ +package org.example.petshopdesktop.models.analytics; + +import java.time.LocalDate; + +public class DailySalesData { + private final LocalDate date; + private final double revenue; + + public DailySalesData(LocalDate date, double revenue) { + this.date = date; + this.revenue = revenue; + } + + public LocalDate getDate() { + return date; + } + + public double getRevenue() { + return revenue; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/analytics/EmployeeSalesData.java b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/EmployeeSalesData.java new file mode 100644 index 00000000..a954d86b --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/EmployeeSalesData.java @@ -0,0 +1,31 @@ +package org.example.petshopdesktop.models.analytics; + +public class EmployeeSalesData { + private final String employeeName; + private final int transactionCount; + private final double totalRevenue; + private final int totalItemsSold; + + public EmployeeSalesData(String employeeName, int transactionCount, double totalRevenue, int totalItemsSold) { + this.employeeName = employeeName; + this.transactionCount = transactionCount; + this.totalRevenue = totalRevenue; + this.totalItemsSold = totalItemsSold; + } + + public String getEmployeeName() { + return employeeName; + } + + public int getTransactionCount() { + return transactionCount; + } + + public double getTotalRevenue() { + return totalRevenue; + } + + public int getTotalItemsSold() { + return totalItemsSold; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/analytics/PaymentMethodData.java b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/PaymentMethodData.java new file mode 100644 index 00000000..de45c727 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/PaymentMethodData.java @@ -0,0 +1,25 @@ +package org.example.petshopdesktop.models.analytics; + +public class PaymentMethodData { + private final String paymentMethod; + private final int transactionCount; + private final double totalRevenue; + + public PaymentMethodData(String paymentMethod, int transactionCount, double totalRevenue) { + this.paymentMethod = paymentMethod; + this.transactionCount = transactionCount; + this.totalRevenue = totalRevenue; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public int getTransactionCount() { + return transactionCount; + } + + public double getTotalRevenue() { + return totalRevenue; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/analytics/ProductSalesData.java b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/ProductSalesData.java new file mode 100644 index 00000000..3723eee6 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/ProductSalesData.java @@ -0,0 +1,25 @@ +package org.example.petshopdesktop.models.analytics; + +public class ProductSalesData { + private final String productName; + private final int totalQuantity; + private final double totalRevenue; + + public ProductSalesData(String productName, int totalQuantity, double totalRevenue) { + this.productName = productName; + this.totalQuantity = totalQuantity; + this.totalRevenue = totalRevenue; + } + + public String getProductName() { + return productName; + } + + public int getTotalQuantity() { + return totalQuantity; + } + + public double getTotalRevenue() { + return totalRevenue; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/models/analytics/SalesSummary.java b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/SalesSummary.java new file mode 100644 index 00000000..65e9f9c1 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/models/analytics/SalesSummary.java @@ -0,0 +1,31 @@ +package org.example.petshopdesktop.models.analytics; + +public class SalesSummary { + private final int totalTransactions; + private final double totalRevenue; + private final double avgTransactionValue; + private final int totalItemsSold; + + public SalesSummary(int totalTransactions, double totalRevenue, double avgTransactionValue, int totalItemsSold) { + this.totalTransactions = totalTransactions; + this.totalRevenue = totalRevenue; + this.avgTransactionValue = avgTransactionValue; + this.totalItemsSold = totalItemsSold; + } + + public int getTotalTransactions() { + return totalTransactions; + } + + public double getTotalRevenue() { + return totalRevenue; + } + + public double getAvgTransactionValue() { + return avgTransactionValue; + } + + public int getTotalItemsSold() { + return totalItemsSold; + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/ui/SvgWebViewFactory.java b/desktop/src/main/java/org/example/petshopdesktop/ui/SvgWebViewFactory.java new file mode 100644 index 00000000..6243e901 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/ui/SvgWebViewFactory.java @@ -0,0 +1,50 @@ +package org.example.petshopdesktop.ui; + +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.scene.web.WebView; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; + +public final class SvgWebViewFactory { + private SvgWebViewFactory() { + } + + public static StackPane build(String resourcePath, double size) { + WebView webView = new WebView(); + webView.setContextMenuEnabled(false); + webView.setPrefSize(size, size); + webView.setMinSize(size, size); + webView.setMaxSize(size, size); + webView.setZoom(1.0); + webView.setPageFill(Color.TRANSPARENT); + webView.setStyle("-fx-background-color: transparent;"); + + String svg = loadSvg(resourcePath) + .replaceFirst(""; + webView.getEngine().loadContent(html, "text/html"); + + StackPane container = new StackPane(webView); + container.setPrefSize(size, size); + container.setMinSize(size, size); + container.setMaxSize(size, size); + return container; + } + + private static String loadSvg(String resourcePath) { + try (InputStream stream = SvgWebViewFactory.class.getResourceAsStream(resourcePath)) { + if (stream == null) { + throw new IllegalArgumentException("Resource not found: " + resourcePath); + } + return new String(stream.readAllBytes(), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new IllegalStateException("Unable to read SVG: " + resourcePath, e); + } + } +} diff --git a/desktop/src/main/java/org/example/petshopdesktop/util/ActivityLogger.java b/desktop/src/main/java/org/example/petshopdesktop/util/ActivityLogger.java new file mode 100644 index 00000000..6a104a39 --- /dev/null +++ b/desktop/src/main/java/org/example/petshopdesktop/util/ActivityLogger.java @@ -0,0 +1,90 @@ +package org.example.petshopdesktop.util; + +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.StandardOpenOption; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; + +public final class ActivityLogger { + + private static final ActivityLogger INSTANCE = new ActivityLogger(); + + private static final String LOG_FILE_NAME = "log.txt"; + private static final DateTimeFormatter TS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + + private final Path logFilePath; + + private ActivityLogger() { + this.logFilePath = resolveProjectRoot().resolve(LOG_FILE_NAME); + } + + public static ActivityLogger getInstance() { + return INSTANCE; + } + + public void logInsert(String tableName, String recordId, String details) { + write("INSERT", String.format("DB_INSERT | Table: %s | ID: %s | Details: %s", tableName, recordId, details)); + } + + public void logUpdate(String tableName, String recordId, String details) { + write("UPDATE", String.format("DB_UPDATE | Table: %s | ID: %s | Details: %s", tableName, recordId, details)); + } + + public void logDelete(String tableName, String recordId, String details) { + write("DELETE", String.format("DB_DELETE | Table: %s | ID: %s | Details: %s", tableName, recordId, details)); + } + + public void logException(String location, Exception exception, String context) { + write("ERROR", String.format( + "EXCEPTION | Location: %s | Type: %s | Message: %s | Context: %s", + location, + exception.getClass().getSimpleName(), + String.valueOf(exception.getMessage()), + context + )); + } + + public void logInfo(String category, String message) { + write(category, message); + } + + public Path getLogFilePath() { + return logFilePath; + } + + private static Path resolveProjectRoot() { + Path start = Paths.get(System.getProperty("user.dir")).toAbsolutePath(); + Path current = start; + + for (int i = 0; i < 6 && current != null; i++) { + if (Files.exists(current.resolve("pom.xml")) + || Files.exists(current.resolve("mvnw")) + || Files.exists(current.resolve("connectionpetstore.properties.example"))) { + return current; + } + current = current.getParent(); + } + + return start; + } + + private synchronized void write(String level, String message) { + String timestamp = LocalDateTime.now().format(TS); + String logEntry = String.format("[%s] [%s] %s%n", timestamp, level, message); + + try { + Files.writeString( + logFilePath, + logEntry, + StandardCharsets.UTF_8, + StandardOpenOption.CREATE, + StandardOpenOption.APPEND + ); + } catch (Exception e) { + System.err.println("Failed to write log entry: " + e.getMessage()); + } + } +} diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml new file mode 100644 index 00000000..c3a14b62 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/adoption-dialog-view.fxml @@ -0,0 +1,148 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml new file mode 100644 index 00000000..99cf0833 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/appointment-dialog-view.fxml @@ -0,0 +1,221 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/inventory-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/inventory-dialog-view.fxml new file mode 100644 index 00000000..acb21d94 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/inventory-dialog-view.fxml @@ -0,0 +1,122 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml new file mode 100644 index 00000000..d4c97ddb --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/pet-dialog-view.fxml @@ -0,0 +1,175 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/product-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/product-dialog-view.fxml new file mode 100644 index 00000000..2164ec66 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/product-dialog-view.fxml @@ -0,0 +1,149 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/product-supplier-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/product-supplier-dialog-view.fxml new file mode 100644 index 00000000..ec29e4b6 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/product-supplier-dialog-view.fxml @@ -0,0 +1,136 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml new file mode 100644 index 00000000..b349a1d9 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml @@ -0,0 +1,192 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/service-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/service-dialog-view.fxml new file mode 100644 index 00000000..6cf098fd --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/service-dialog-view.fxml @@ -0,0 +1,181 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml new file mode 100644 index 00000000..354ea20d --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/images/.gitkeep b/desktop/src/main/resources/org/example/petshopdesktop/images/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg new file mode 100644 index 00000000..fbc2f2f7 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text-light.svg b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text-light.svg new file mode 100644 index 00000000..b9cfb55f --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text-light.svg @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text.png b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text.png new file mode 100644 index 00000000..1f7075f0 Binary files /dev/null and b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text.png differ diff --git a/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg new file mode 100644 index 00000000..7d976db9 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge-text.svg @@ -0,0 +1,13 @@ + + + + + + + + + \ No newline at end of file diff --git a/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge.png b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge.png new file mode 100644 index 00000000..7708c92e Binary files /dev/null and b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge.png differ diff --git a/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge.svg b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge.svg new file mode 100644 index 00000000..34800b7c --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/images/leons-pet-store-badge.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/login-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/login-view.fxml new file mode 100644 index 00000000..1fe902ed --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/login-view.fxml @@ -0,0 +1,92 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml new file mode 100644 index 00000000..4e1cee6f --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/main-layout-view.fxml @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml new file mode 100644 index 00000000..9443ef14 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/adoption-view.fxml @@ -0,0 +1,79 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml new file mode 100644 index 00000000..dec7a883 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/analytics-view.fxml @@ -0,0 +1,223 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml new file mode 100644 index 00000000..f698c5c1 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/appointment-view.fxml @@ -0,0 +1,80 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/desktop/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml new file mode 100644 index 00000000..e425ec67 --- /dev/null +++ b/desktop/src/main/resources/org/example/petshopdesktop/modelviews/chat-view.fxml @@ -0,0 +1,78 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + + + + + +