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