Add 'desktop/' from commit 'bc9a54b29a8a8c8272609082438f36b92de7f4d1'

git-subtree-dir: desktop
git-subtree-mainline: 737028e06d5207fc69425cf4431b7d047cf3110f
git-subtree-split: bc9a54b29a
This commit is contained in:
2026-03-18 19:23:20 -06:00
161 changed files with 16703 additions and 0 deletions

50
desktop/.gitignore vendored Normal file
View File

@@ -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/

BIN
desktop/.mvn/wrapper/maven-wrapper.jar vendored Normal file

Binary file not shown.

View File

@@ -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

48
desktop/README.md Normal file
View File

@@ -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.

View File

@@ -0,0 +1,3 @@
url=jdbc:mysql://127.0.0.1:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC
user=petapp
password=petapppass

316
desktop/mvnw vendored Executable file
View File

@@ -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 "$@"

188
desktop/mvnw.cmd vendored Normal file
View File

@@ -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%

101
desktop/pom.xml Normal file
View File

@@ -0,0 +1,101 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>org.example</groupId>
<artifactId>PetShopDesktop</artifactId>
<version>1.0-SNAPSHOT</version>
<name>PetShopDesktop</name>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<javafx.version>25.0.1</javafx.version>
<junit.version>5.12.1</junit.version>
</properties>
<dependencies>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-controls</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-fxml</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.openjfx</groupId>
<artifactId>javafx-web</artifactId>
<version>${javafx.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-engine</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.18.2</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.18.2</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<release>25</release>
</configuration>
</plugin>
<plugin>
<groupId>org.openjfx</groupId>
<artifactId>javafx-maven-plugin</artifactId>
<version>0.0.8</version>
<executions>
<execution>
<!-- Default configuration for running with: mvn clean javafx:run -->
<id>default-cli</id>
<configuration>
<mainClass>org.example.petshopdesktop/org.example.petshopdesktop.PetShopApplication</mainClass>
<launcher>app</launcher>
<jlinkZipName>app</jlinkZipName>
<jlinkImageName>app</jlinkImageName>
<noManPages>true</noManPages>
<stripDebug>true</stripDebug>
<noHeaderFiles>true</noHeaderFiles>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@@ -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;
}

View File

@@ -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(); }
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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(); }
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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> T get(String path, Class<T> 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<String> 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<String> 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> T post(String path, Object requestBody, Class<T> 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<String> response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
return handleResponse(response, responseClass);
}
public <T> T postMultipart(String path, String partName, Path filePath, Class<T> 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<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
return handleResponse(response, responseClass);
}
public <T> T put(String path, Object requestBody, Class<T> 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<String> 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<String> 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<String> 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> T handleResponse(HttpResponse<String> response, Class<T> 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<String> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<String, String> 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<ConversationResponse> conversationListener;
private Consumer<MessageResponse> messageListener;
private Consumer<String> 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<ConversationResponse> conversationListener) {
this.conversationListener = conversationListener;
}
public void setMessageListener(Consumer<MessageResponse> messageListener) {
this.messageListener = messageListener;
}
public void setStatusListener(Consumer<String> 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<String, String> 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");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<TopProduct> topProducts;
private List<DailySales> 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<TopProduct> getTopProducts() {
return topProducts;
}
public void setTopProducts(List<TopProduct> topProducts) {
this.topProducts = topProducts;
}
public List<DailySales> getDailySales() {
return dailySales;
}
public void setDailySales(List<DailySales> 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;
}
}
}

View File

@@ -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;
}
}

View File

@@ -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<Long> petIds;
private Long customerId;
private Long storeId;
private Long serviceId;
private LocalDate appointmentDate;
private LocalTime appointmentTime;
private String appointmentStatus;
public AppointmentRequest() {
}
public List<Long> getPetIds() {
return petIds;
}
public void setPetIds(List<Long> 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;
}
}

View File

@@ -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<String> petNames;
private java.util.List<Long> 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<String> getPetNames() {
return petNames;
}
public void setPetNames(java.util.List<String> petNames) {
this.petNames = petNames;
}
public java.util.List<Long> getPetIds() {
return petIds;
}
public void setPetIds(java.util.List<Long> 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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,22 @@
package org.example.petshopdesktop.api.dto.common;
import java.util.List;
public class BulkDeleteRequest {
private List<Long> ids;
public BulkDeleteRequest() {
}
public BulkDeleteRequest(List<Long> ids) {
this.ids = ids;
}
public List<Long> getIds() {
return ids;
}
public void setIds(List<Long> ids) {
this.ids = ids;
}
}

View File

@@ -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;
}
}

View File

@@ -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<T> {
private List<T> content;
@JsonProperty("number")
private int pageNumber;
@JsonProperty("size")
private int pageSize;
private long totalElements;
private int totalPages;
private boolean last;
public PageResponse() {
}
public List<T> getContent() {
return content;
}
public void setContent(List<T> 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;
}
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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));
}
}

View File

@@ -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<SaleItemRequest> 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<SaleItemRequest> getItems() {
return items;
}
public void setItems(List<SaleItemRequest> 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;
}
}

View File

@@ -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<SaleItemResponse> 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<SaleItemResponse> getItems() {
return items;
}
public void setItems(List<SaleItemResponse> items) {
this.items = items;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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<AdoptionResponse> 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<AdoptionResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<AdoptionResponse>>() {}
);
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<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/adoptions", new BulkDeleteRequest(ids));
}
}

View File

@@ -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);
}
}

View File

@@ -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<AppointmentResponse> 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<AppointmentResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<AppointmentResponse>>() {}
);
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<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/appointments", new BulkDeleteRequest(ids));
}
}

View File

@@ -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");
}
}

View File

@@ -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<ConversationResponse> listConversations() throws Exception {
String response = apiClient.getRawResponse("/api/v1/chat/conversations");
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<ConversationResponse>>() {});
}
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<MessageResponse> listMessages(Long conversationId) throws Exception {
String response = apiClient.getRawResponse("/api/v1/chat/conversations/" + conversationId + "/messages");
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<MessageResponse>>() {});
}
public MessageResponse sendMessage(Long conversationId, MessageRequest request) throws Exception {
return apiClient.post("/api/v1/chat/conversations/" + conversationId + "/messages", request, MessageResponse.class);
}
}

View File

@@ -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<DropdownOption> 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<List<DropdownOption>>() {});
}
public List<DropdownOption> 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<List<DropdownOption>>() {});
}
public List<DropdownOption> 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<List<DropdownOption>>() {});
}
public List<DropdownOption> 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<List<DropdownOption>>() {});
}
public List<DropdownOption> 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<List<DropdownOption>>() {});
}
public List<DropdownOption> 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<List<DropdownOption>>() {});
}
public List<DropdownOption> 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<List<DropdownOption>>() {});
}
}

View File

@@ -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<EmployeeResponse> 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<EmployeeResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<EmployeeResponse>>() {}
);
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);
}
}

View File

@@ -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<InventoryResponse> 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<InventoryResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<InventoryResponse>>() {}
);
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);
}
}

View File

@@ -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<PetResponse> 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<PetResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<PetResponse>>() {}
);
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<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/pets", new BulkDeleteRequest(ids));
}
}

View File

@@ -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<ProductResponse> 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<ProductResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<ProductResponse>>() {}
);
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<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/products", new BulkDeleteRequest(ids));
}
}

View File

@@ -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<ProductSupplierResponse> 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<ProductSupplierResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<ProductSupplierResponse>>() {}
);
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);
}
}

View File

@@ -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<PurchaseOrderResponse> 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<PurchaseOrderResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<PurchaseOrderResponse>>() {}
);
if (pageResponse == null) {
throw new IllegalStateException("Null response from purchase-orders endpoint");
}
return pageResponse.getContent();
}
}

View File

@@ -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<SaleResponse> 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<SaleResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<SaleResponse>>() {}
);
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);
}
}

View File

@@ -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<ServiceResponse> 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<ServiceResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<ServiceResponse>>() {}
);
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<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/services", new BulkDeleteRequest(ids));
}
}

View File

@@ -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<SupplierResponse> 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<SupplierResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<SupplierResponse>>() {}
);
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<Long> ids) throws Exception {
apiClient.deleteWithBody("/api/v1/suppliers", new BulkDeleteRequest(ids));
}
}

View File

@@ -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<UserResponse> 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<UserResponse> pageResponse = apiClient.getObjectMapper().readValue(
response,
new TypeReference<PageResponse<UserResponse>>() {}
);
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);
}
}

View File

@@ -0,0 +1,6 @@
package org.example.petshopdesktop.auth;
public enum Role {
ADMIN,
STAFF
}

View File

@@ -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);
}
}

View File

@@ -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<Adoption, Integer> colAdoptionId;
@FXML
private TableColumn<Adoption, String> colPetId;
@FXML
private TableColumn<Adoption, String> colCustomerName;
@FXML
private TableColumn<Adoption, String> colAdoptionDate;
@FXML
private TableColumn<Adoption, Double> colAdoptionFee;
@FXML
private TableColumn<Adoption, String> colAdoptionStatus;
@FXML
private TableView<Adoption> tvAdoptions;
@FXML
private TextField txtSearch;
private ObservableList<Adoption> 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<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> 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<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(filter);
List<Adoption> 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<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(null);
List<Adoption> 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()
);
}
}

View File

@@ -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<Number, Number> chartSalesOverTime;
@FXML
private NumberAxis axisSalesDate;
@FXML
private BarChart<Number, String> chartTopRevenue;
@FXML
private BarChart<Number, String> chartTopQuantity;
@FXML
private PieChart chartPaymentMethods;
@FXML
private BarChart<String, Number> 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<String> 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<String> 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<SaleResponse> 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> dailySales = dashboard.getDailySales() != null ? dashboard.getDailySales() : new ArrayList<>();
XYChart.Series<Number, Number> 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<TopProduct> topProducts = dashboard.getTopProducts() != null ? dashboard.getTopProducts() : new ArrayList<>();
XYChart.Series<Number, String> 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<TopProduct> topProducts = dashboard.getTopProducts() != null ? dashboard.getTopProducts() : new ArrayList<>();
XYChart.Series<Number, String> 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<SaleResponse> sales) throws Exception {
Map<String, Long> 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<Map.Entry<String, Long>> paymentEntries = paymentMethodCount.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.toList();
for (Map.Entry<String, Long> 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<SaleResponse> sales) throws Exception {
Map<String, Double> 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<String, Number> series = new XYChart.Series<>();
series.setName("Revenue");
List<Map.Entry<String, Double>> employeeEntries = employeeRevenue.entrySet().stream()
.sorted(Map.Entry.comparingByKey())
.toList();
for (Map.Entry<String, Double> 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<Number, Number> chart, String color) {
Platform.runLater(() -> {
for (XYChart.Series<Number, Number> series : chart.getData()) {
if (series.getNode() != null) {
series.getNode().setStyle("-fx-stroke: " + color + ";");
}
for (XYChart.Data<Number, Number> data : series.getData()) {
if (data.getNode() != null) {
data.getNode().setStyle("-fx-background-color: white, " + color + ";");
}
}
}
});
}
private <X, Y> void applyBarChartColor(XYChart<X, Y> chart, String color) {
Platform.runLater(() -> {
for (XYChart.Series<X, Y> series : chart.getData()) {
for (XYChart.Data<X, Y> 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();
}
}

View File

@@ -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<AppointmentDTO> tvAppointments;
@FXML private TableColumn<AppointmentDTO,Integer> colAppointmentId;
@FXML private TableColumn<AppointmentDTO,String> colPetName;
@FXML private TableColumn<AppointmentDTO,String> colServiceName;
@FXML private TableColumn<AppointmentDTO,String> colAppointmentDate;
@FXML private TableColumn<AppointmentDTO,String> colAppointmentTime;
@FXML private TableColumn<AppointmentDTO,String> colCustomerName;
@FXML private TableColumn<AppointmentDTO,String> colAppointmentStatus;
@FXML private Button btnAdd;
@FXML private Button btnEdit;
@FXML private Button btnDelete;
@FXML private TextField txtSearch;
private final ObservableList<AppointmentDTO> appointments = FXCollections.observableArrayList();
private FilteredList<AppointmentDTO> 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<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(null);
List<AppointmentDTO> 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<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(query);
List<AppointmentDTO> 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<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> 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()
);
}
}

View File

@@ -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<ConversationResponse> 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<ConversationResponse> conversations = FXCollections.observableArrayList();
private final Map<Long, String> 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<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
Map<Long, String> 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<ConversationResponse> 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<MessageResponse> 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<MessageResponse> 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<ConversationResponse> 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));
}
}

View File

@@ -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<Inventory, Integer> colInventoryId;
@FXML
private TableColumn<Inventory, Integer> colProductId;
@FXML
private TableColumn<Inventory, String> colProductName;
@FXML
private TableColumn<Inventory, Integer> colQuantity;
@FXML
private TableView<Inventory> tvInventory;
@FXML
private TextField txtSearch;
private ObservableList<Inventory> 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<ButtonType> 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<InventoryResponse> inventories = InventoryApi.getInstance().listInventory(filter);
List<Inventory> 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<InventoryResponse> inventories = InventoryApi.getInstance().listInventory(null);
List<Inventory> 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
);
}
}

View File

@@ -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();
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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<Pet, Integer> colPetAge;
@FXML
private TableColumn<Pet, String> colPetBreed;
@FXML
private TableColumn<Pet, Integer> colPetId;
@FXML
private TableColumn<Pet, String> colPetName;
@FXML
private TableColumn<Pet, Double> colPetPrice;
@FXML
private TableColumn<Pet, String> colPetSpecies;
@FXML
private TableColumn<Pet, String> colPetStatus;
@FXML
private TableView<Pet> 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<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> 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<Pet> 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<Pet,Integer>("petId"));
colPetName.setCellValueFactory(new PropertyValueFactory<Pet,String>("petName"));
colPetSpecies.setCellValueFactory(new PropertyValueFactory<Pet,String>("petSpecies"));
colPetBreed.setCellValueFactory(new PropertyValueFactory<Pet,String>("petBreed"));
colPetAge.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petAge"));
colPetStatus.setCellValueFactory(new PropertyValueFactory<Pet,String>("petStatus"));
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("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<PetResponse> pets = PetApi.getInstance().listPets(filter);
List<Pet> 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<PetResponse> pets = PetApi.getInstance().listPets(null);
List<Pet> 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()
);
}
}

View File

@@ -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<ProductDTO, String> colProductCategory;
@FXML
private TableColumn<ProductDTO, String> colProductDesc;
@FXML
private TableColumn<ProductDTO, Integer> colProductId;
@FXML
private TableColumn<ProductDTO, String> colProductName;
@FXML
private TableColumn<ProductDTO, Double> colProductPrice;
@FXML
private TableView<ProductDTO> tvProducts;
@FXML
private TextField txtSearch;
//data declaration
private ObservableList<ProductDTO> 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<ProductDTO,Integer>("prodId"));
colProductName.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodName"));
colProductPrice.setCellValueFactory(new PropertyValueFactory<ProductDTO,Double>("prodPrice"));
colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName"));
colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("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<ProductResponse> products = ProductApi.getInstance().listProducts(null);
List<ProductDTO> 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<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> 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<ProductResponse> products = ProductApi.getInstance().listProducts(filter);
List<ProductDTO> 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()
);
}
}

View File

@@ -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<ProductSupplierDTO, Double> colCost;
@FXML
private TableColumn<ProductSupplierDTO, Integer> colProductId;
@FXML
private TableColumn<ProductSupplierDTO, String> colProductName;
@FXML
private TableColumn<ProductSupplierDTO, Integer> colSupplierId;
@FXML
private TableColumn<ProductSupplierDTO, String> colSupplierName;
@FXML
private TableView<ProductSupplierDTO> tvProductSuppliers;
@FXML
private TextField txtSearch;
//data declaration
private ObservableList<ProductSupplierDTO> 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<ProductSupplierDTO,Integer>("prodId"));
colProductName.setCellValueFactory(new PropertyValueFactory<ProductSupplierDTO,String>("prodName"));
colSupplierId.setCellValueFactory(new PropertyValueFactory<ProductSupplierDTO,Integer>("supId"));
colSupplierName.setCellValueFactory(new PropertyValueFactory<ProductSupplierDTO,String>("supCompany"));
colCost.setCellValueFactory(new PropertyValueFactory<ProductSupplierDTO,Double>("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<ProductSupplierResponse> productSuppliers = ProductSupplierApi.getInstance().listProductSuppliers(null);
List<ProductSupplierDTO> 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<ProductSupplierResponse> productSuppliers = ProductSupplierApi.getInstance().listProductSuppliers(filter);
List<ProductSupplierDTO> 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<ButtonType> 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()
);
}
}

View File

@@ -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<PurchaseOrderDTO> tvPurchaseOrders;
@FXML private TableColumn<PurchaseOrderDTO,Long> colOrderId;
@FXML private TableColumn<PurchaseOrderDTO,String> colSupplier;
@FXML private TableColumn<PurchaseOrderDTO,String> colOrderDate;
@FXML private TableColumn<PurchaseOrderDTO,String> colStatus;
private final ObservableList<PurchaseOrderDTO> purchaseOrders = FXCollections.observableArrayList();
private FilteredList<PurchaseOrderDTO> filtered;
@FXML
public void initialize() {
colOrderId.setCellValueFactory(
new PropertyValueFactory<>("purchaseOrderId"));
colSupplier.setCellValueFactory(
new PropertyValueFactory<>("supplierName"));
colOrderDate.setCellValueFactory(
new PropertyValueFactory<>("orderDate"));
colStatus.setCellValueFactory(
new PropertyValueFactory<>("status"));
filtered = new FilteredList<>(purchaseOrders, p -> true);
tvPurchaseOrders.setItems(filtered);
if (txtSearch != null) {
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
}
loadPurchaseOrders();
}
private void loadPurchaseOrders() {
new Thread(() -> {
try {
List<PurchaseOrderResponse> responses = PurchaseOrderApi.getInstance().listPurchaseOrders(null);
List<PurchaseOrderDTO> dtos = responses.stream()
.map(this::mapToPurchaseOrderDTO)
.collect(Collectors.toList());
Platform.runLater(() -> {
purchaseOrders.setAll(dtos);
tvPurchaseOrders.setItems(filtered);
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"PurchaseOrderController.loadPurchaseOrders",
e,
"Loading purchase orders for table display");
new Alert(Alert.AlertType.ERROR,
"Unable to load purchase orders").showAndWait();
});
}
}).start();
}
private void applyFilter(String text) {
if (filtered == null) {
return;
}
String q = text == null ? "" : text.trim().toLowerCase();
if (q.isEmpty()) {
filtered.setPredicate(p -> true);
return;
}
filtered.setPredicate(p ->
String.valueOf(p.getPurchaseOrderId()).contains(q)
|| safe(p.getSupplierName()).contains(q)
|| safe(p.getOrderDate()).contains(q)
|| safe(p.getStatus()).contains(q)
);
}
private static String safe(String v) {
return v == null ? "" : v.toLowerCase();
}
@FXML
void btnRefresh() {
loadPurchaseOrders();
}
private PurchaseOrderDTO mapToPurchaseOrderDTO(PurchaseOrderResponse response) {
return new PurchaseOrderDTO(
response.getPurchaseOrderId(),
response.getSupplierName(),
response.getOrderDate() != null ? response.getOrderDate().toString() : "",
response.getOrderStatus()
);
}
}

View File

@@ -0,0 +1,440 @@
package org.example.petshopdesktop.controllers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.Spinner;
import javafx.scene.control.SpinnerValueFactory;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.example.petshopdesktop.auth.UserSession;
import javafx.concurrent.Task;
import org.example.petshopdesktop.api.endpoints.ProductApi;
import org.example.petshopdesktop.api.endpoints.SaleApi;
import org.example.petshopdesktop.api.dto.product.ProductResponse;
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
import org.example.petshopdesktop.api.dto.sale.SaleRequest;
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
import org.example.petshopdesktop.models.Product;
import org.example.petshopdesktop.models.SaleCartItem;
import org.example.petshopdesktop.models.SaleLineItem;
import org.example.petshopdesktop.util.ActivityLogger;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
public class SaleController {
@FXML
private Button btnRefresh;
@FXML
private Button btnRefund;
@FXML
private Label lblModeNote;
@FXML
private VBox vbCreateSale;
@FXML
private ComboBox<Product> cbProduct;
@FXML
private Spinner<Integer> spQuantity;
@FXML
private Button btnAddToCart;
@FXML
private Button btnRemoveSelected;
@FXML
private TableView<SaleCartItem> tvCart;
@FXML
private TableColumn<SaleCartItem, String> colCartProduct;
@FXML
private TableColumn<SaleCartItem, Integer> colCartQty;
@FXML
private TableColumn<SaleCartItem, Double> colCartUnitPrice;
@FXML
private TableColumn<SaleCartItem, Double> colCartTotal;
@FXML
private ComboBox<String> cbPaymentMethod;
@FXML
private Label lblCartTotal;
@FXML
private Button btnClearCart;
@FXML
private Button btnSaveSale;
@FXML
private TableColumn<SaleLineItem, Integer> colSaleId;
@FXML
private TableColumn<SaleLineItem, String> colSaleDate;
@FXML
private TableColumn<SaleLineItem, String> colEmployeeName;
@FXML
private TableColumn<SaleLineItem, String> colServiceProduct;
@FXML
private TableColumn<SaleLineItem, Integer> colSaleQuantity;
@FXML
private TableColumn<SaleLineItem, Double> colSaleUnitPrice;
@FXML
private TableColumn<SaleLineItem, Double> colSaleTotal;
@FXML
private TableColumn<SaleLineItem, String> colSalePaymentType;
@FXML
private TableView<SaleLineItem> tvSales;
@FXML
private TextField txtSearch;
private final ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList();
private FilteredList<SaleLineItem> filteredSales;
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
@FXML
public void initialize() {
setupTables();
setupCreateSale();
applyRoleMode();
refreshSales();
}
private void setupTables() {
tvCart.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tvCart.setFixedCellSize(34);
colCartProduct.setCellValueFactory(new PropertyValueFactory<>("prodName"));
colCartQty.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colCartUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colCartTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
tvCart.setItems(cartItems);
tvCart.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
tvSales.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
tvSales.setFixedCellSize(34);
colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId"));
colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate"));
colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName"));
colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName"));
colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colSaleTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
colSalePaymentType.setCellValueFactory(new PropertyValueFactory<>("paymentMethod"));
filteredSales = new FilteredList<>(saleItems, s -> true);
tvSales.setItems(filteredSales);
txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal));
}
private void setupCreateSale() {
spQuantity.setValueFactory(new SpinnerValueFactory.IntegerSpinnerValueFactory(1, 999, 1));
spQuantity.setEditable(true);
cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card"));
cbPaymentMethod.getSelectionModel().selectFirst();
updateCartTotal();
try {
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
ObservableList<Product> products = FXCollections.observableArrayList();
for (ProductResponse pr : productResponses) {
products.add(new Product(
pr.getProdId().intValue(),
pr.getProdName(),
pr.getProdPrice().doubleValue(),
0,
pr.getProdDesc()
));
}
cbProduct.setItems(products);
} catch (Exception e) {
ActivityLogger.getInstance().logException("SaleController.setupCreateSale", e, "Loading products");
}
}
private void applyRoleMode() {
boolean isAdmin = UserSession.getInstance().isAdmin();
vbCreateSale.setVisible(!isAdmin);
vbCreateSale.setManaged(!isAdmin);
lblModeNote.setText(isAdmin ? "(View only)" : "(Staff can create sales)");
}
private void refreshSales() {
refreshSales(false);
}
private void refreshSales(boolean showErrorDialog) {
Task<List<SaleLineItem>> task = new Task<List<SaleLineItem>>() {
@Override
protected List<SaleLineItem> call() throws Exception {
List<SaleResponse> sales = SaleApi.getInstance().listSales(0, 1000, null);
List<SaleLineItem> lineItems = new ArrayList<>();
for (SaleResponse sale : sales) {
String saleDate = sale.getSaleDate() != null
? sale.getSaleDate().format(DATE_FORMATTER)
: "";
if (sale.getItems() != null && !sale.getItems().isEmpty()) {
for (SaleItemResponse item : sale.getItems()) {
double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0;
double lineTotal = unitPrice * item.getQuantity();
lineItems.add(new SaleLineItem(
sale.getSaleId().intValue(),
saleDate,
sale.getEmployeeName(),
item.getProductName(),
item.getQuantity(),
unitPrice,
lineTotal,
sale.getPaymentMethod(),
sale.getIsRefund() != null && sale.getIsRefund()
));
}
}
}
return lineItems;
}
};
task.setOnSucceeded(event -> {
saleItems.setAll(task.getValue());
});
task.setOnFailed(event -> {
Throwable e = task.getException();
ActivityLogger.getInstance().logException("SaleController.refreshSales", (Exception) e, "Loading sales");
if (showErrorDialog) {
showError("Sales", "Could not load sales: " + e.getMessage());
}
});
new Thread(task).start();
}
@FXML
void btnRefresh(ActionEvent event) {
refreshSales(true);
}
@FXML
void btnAddToCart(ActionEvent event) {
Product product = cbProduct.getSelectionModel().getSelectedItem();
if (product == null) {
showError("Create Sale", "Select a product.");
return;
}
int requestedQty;
try {
requestedQty = spQuantity.getValue();
} catch (Exception e) {
showError("Create Sale", "Enter a valid quantity.");
return;
}
if (requestedQty <= 0) {
showError("Create Sale", "Quantity must be at least 1.");
return;
}
for (SaleCartItem item : cartItems) {
if (item.getProdId() == product.getProdId()) {
item.setQuantity(item.getQuantity() + requestedQty);
tvCart.refresh();
updateCartTotal();
return;
}
}
cartItems.add(new SaleCartItem(product.getProdId(), product.getProdName(), requestedQty, product.getProdPrice()));
updateCartTotal();
}
@FXML
void btnRemoveSelected(ActionEvent event) {
SaleCartItem selected = tvCart.getSelectionModel().getSelectedItem();
if (selected != null) {
cartItems.remove(selected);
updateCartTotal();
}
}
@FXML
void btnClearCart(ActionEvent event) {
cartItems.clear();
updateCartTotal();
}
@FXML
void btnSaveSale(ActionEvent event) {
if (UserSession.getInstance().isAdmin()) {
showError("Create Sale", "This action is restricted to staff.");
return;
}
Long storeId = UserSession.getInstance().getStoreId();
if (storeId == null || storeId <= 0) {
showError("Create Sale", "Store is not set for this account.");
return;
}
if (cartItems.isEmpty()) {
showError("Create Sale", "Add at least one item.");
return;
}
String payment = cbPaymentMethod.getSelectionModel().getSelectedItem();
if (payment == null || payment.isBlank()) {
showError("Create Sale", "Select a payment method.");
return;
}
try {
SaleRequest request = new SaleRequest();
request.setStoreId(storeId);
request.setPaymentMethod(payment);
List<SaleItemRequest> itemRequests = new ArrayList<>();
for (SaleCartItem cartItem : cartItems) {
SaleItemRequest itemRequest = new SaleItemRequest();
itemRequest.setProdId((long) cartItem.getProdId());
itemRequest.setQuantity(cartItem.getQuantity());
itemRequests.add(itemRequest);
}
request.setItems(itemRequests);
SaleResponse response = SaleApi.getInstance().createSale(request);
showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created.");
cartItems.clear();
updateCartTotal();
refreshSales(true);
} catch (Exception e) {
ActivityLogger.getInstance().logException("SaleController.btnSaveSale", e, "Creating sale");
String errorMsg = e.getMessage();
if (errorMsg != null && errorMsg.contains("Insufficient inventory")) {
showError("Create Sale", "Insufficient stock for one or more items.");
} else {
showError("Create Sale", errorMsg != null ? errorMsg : "Could not save the sale.");
}
}
}
@FXML
void btnRefund(ActionEvent event) {
openRefundDialog();
}
private void openRefundDialog() {
try {
SaleLineItem selectedSale = tvSales.getSelectionModel().getSelectedItem();
if (selectedSale != null && selectedSale.isRefund()) {
showError("Refund", "Select an original sale, not an existing refund.");
return;
}
FXMLLoader loader = new FXMLLoader(getClass().getResource(
"/org/example/petshopdesktop/dialogviews/refund-dialog-view.fxml"));
Stage dialog = new Stage();
dialog.initOwner(btnRefund.getScene().getWindow());
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle("Process Refund");
dialog.setScene(new Scene(loader.load()));
if (selectedSale != null) {
loader.<org.example.petshopdesktop.controllers.dialogcontrollers.RefundDialogController>getController()
.prefillSale((long) selectedSale.getSaleId());
}
dialog.setResizable(false);
dialog.showAndWait();
refreshSales(true);
} catch (Exception e) {
ActivityLogger.getInstance().logException("SaleController.openRefundDialog", e, "Opening refund dialog");
}
}
private void updateCartTotal() {
double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum();
lblCartTotal.setText(currency.format(total));
}
private void applySalesFilter(String filter) {
String f = filter == null ? "" : filter.trim().toLowerCase();
if (f.isEmpty()) {
filteredSales.setPredicate(s -> true);
return;
}
filteredSales.setPredicate(s ->
String.valueOf(s.getSaleId()).contains(f)
|| safe(s.getSaleDate()).contains(f)
|| safe(s.getEmployeeName()).contains(f)
|| safe(s.getItemName()).contains(f)
|| safe(s.getPaymentMethod()).contains(f)
);
}
private static String safe(String v) {
return v == null ? "" : v.toLowerCase();
}
private void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
private void showInfo(String title, String message) {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
}

View File

@@ -0,0 +1,233 @@
package org.example.petshopdesktop.controllers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
import org.example.petshopdesktop.DTOs.ServiceDTO;
import org.example.petshopdesktop.api.dto.service.ServiceResponse;
import org.example.petshopdesktop.api.endpoints.ServiceApi;
import org.example.petshopdesktop.controllers.dialogcontrollers.ServiceDialogController;
import org.example.petshopdesktop.util.ActivityLogger;
import javafx.stage.Modality;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
public class ServiceController {
@FXML private Button btnAdd;
@FXML private Button btnDelete;
@FXML private Button btnEdit;
@FXML private TableColumn<ServiceDTO, Integer> colServiceId;
@FXML private TableColumn<ServiceDTO, String> colServiceName;
@FXML private TableColumn<ServiceDTO, String> colServiceDesc;
@FXML private TableColumn<ServiceDTO, Integer> colServiceDuration;
@FXML private TableColumn<ServiceDTO, Double> colServicePrice;
@FXML private TableView<ServiceDTO> tvServices;
@FXML private TextField txtSearch;
private ObservableList<ServiceDTO> data = FXCollections.observableArrayList();
private String mode = null;
@FXML
public void initialize() {
btnEdit.setDisable(true);
btnDelete.setDisable(true);
tvServices.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
colServiceId.setCellValueFactory(new PropertyValueFactory<>("serviceId"));
colServiceName.setCellValueFactory(new PropertyValueFactory<>("serviceName"));
colServiceDesc.setCellValueFactory(new PropertyValueFactory<>("serviceDesc"));
colServiceDuration.setCellValueFactory(new PropertyValueFactory<>("serviceDuration"));
colServicePrice.setCellValueFactory(new PropertyValueFactory<>("servicePrice"));
displayServices();
tvServices.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
btnEdit.setDisable(false);
btnDelete.setDisable(false);
}
);
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
displayFilteredServices(newValue);
});
tvServices.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
if (tvServices.getSelectionModel().getSelectedItem() != null) {
btnDeleteClicked(null);
}
}
});
}
private void displayServices() {
new Thread(() -> {
try {
List<ServiceResponse> services = ServiceApi.getInstance().listServices(null);
List<ServiceDTO> serviceDTOs = services.stream()
.map(this::mapToServiceDTO)
.collect(Collectors.toList());
Platform.runLater(() -> {
data.setAll(serviceDTOs);
tvServices.setItems(data);
});
} catch (Exception e) {
Platform.runLater(() -> {
System.out.println("Error while fetching table data: " + e.getMessage());
ActivityLogger.getInstance().logException(
"ServiceController.displayServices",
e,
"Fetching service data for table display");
});
}
}).start();
}
private void displayFilteredServices(String filter) {
if (txtSearch.getText() == null || txtSearch.getText().isEmpty()) {
displayServices();
} else {
new Thread(() -> {
try {
List<ServiceResponse> services = ServiceApi.getInstance().listServices(filter);
List<ServiceDTO> serviceDTOs = services.stream()
.map(this::mapToServiceDTO)
.collect(Collectors.toList());
Platform.runLater(() -> {
data.setAll(serviceDTOs);
tvServices.setItems(data);
});
} catch (Exception e) {
Platform.runLater(() -> {
System.out.println("Error while fetching table data: " + e.getMessage());
ActivityLogger.getInstance().logException(
"ServiceController.displayFilteredServices",
e,
String.format("Filtering services with keyword: %s", filter));
});
}
}).start();
}
}
@FXML
void btnAddClicked(ActionEvent event) {
mode = "Add";
openDialog(null, mode);
}
@FXML
void btnEditClicked(ActionEvent event) {
ServiceDTO selected = tvServices.getSelectionModel().getSelectedItem();
if (selected != null) {
mode = "Edit";
openDialog(selected, mode);
}
}
@FXML
void btnDeleteClicked(ActionEvent event) {
var selectedServices = tvServices.getSelectionModel().getSelectedItems();
if (selectedServices.isEmpty()) return;
Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Please confirm delete");
String message = selectedServices.size() == 1
? "Are you sure you want to delete this service?"
: "Are you sure you want to delete " + selectedServices.size() + " services?";
question.setContentText(message);
question.getDialogPane().lookupButton(ButtonType.OK).requestFocus();
Optional<ButtonType> result = question.showAndWait();
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> ids = selectedServices.stream()
.map(s -> (long) s.getServiceId())
.collect(Collectors.toList());
try {
ServiceApi.getInstance().deleteServices(ids);
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Database Operation Confirmed");
alert.setContentText("Successfully deleted " + ids.size() + " service(s)");
alert.showAndWait();
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"ServiceController.btnDeleteClicked",
e,
"Deleting services");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Delete Operation Failed");
alert.setContentText(e.getMessage());
alert.showAndWait();
}
displayServices();
btnDelete.setDisable(true);
btnEdit.setDisable(true);
txtSearch.setText("");
}
}
private void openDialog(ServiceDTO service, String mode) {
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/service-dialog-view.fxml"));
Scene scene = null;
try {
scene = new Scene(fxmlLoader.load());
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"ServiceController.openDialog",
e,
String.format("Loading service dialog view in %s mode", mode));
throw new RuntimeException(e);
}
ServiceDialogController dialogController = fxmlLoader.getController();
dialogController.setMode(mode);
if (mode.equals("Edit")) {
dialogController.setService(service);
}
Stage dialogStage = new Stage();
dialogStage.initModality(Modality.APPLICATION_MODAL);
if (mode.equals("Add")) {
dialogStage.setTitle("Add Service");
} else {
dialogStage.setTitle("Edit Service");
}
dialogStage.setScene(scene);
dialogStage.showAndWait();
displayServices();
btnDelete.setDisable(true);
btnEdit.setDisable(true);
txtSearch.setText("");
}
private ServiceDTO mapToServiceDTO(ServiceResponse response) {
return new ServiceDTO(
response.getServiceId().intValue(),
response.getServiceName(),
response.getServiceDesc(),
response.getServiceDuration() != null ? response.getServiceDuration() : 0,
response.getServicePrice().doubleValue()
);
}
}

View File

@@ -0,0 +1,231 @@
package org.example.petshopdesktop.controllers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.FilteredList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.employee.EmployeeResponse;
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.models.StaffAccount;
import org.example.petshopdesktop.util.ActivityLogger;
import java.sql.Timestamp;
import java.time.ZoneId;
import java.util.List;
import java.util.stream.Collectors;
public class StaffAccountsController {
@FXML
private TableView<StaffAccount> tvStaff;
@FXML
private TableColumn<StaffAccount, String> colUsername;
@FXML
private TableColumn<StaffAccount, String> colName;
@FXML
private TableColumn<StaffAccount, String> colEmail;
@FXML
private TableColumn<StaffAccount, String> colPhone;
@FXML
private TableColumn<StaffAccount, String> colStatus;
@FXML
private TableColumn<StaffAccount, java.sql.Timestamp> colCreated;
@FXML
private TextField txtSearch;
@FXML
private Label lblError;
@FXML
private Button btnCreateAccount;
@FXML
private Button btnEditAccount;
private final ObservableList<StaffAccount> staffAccounts = FXCollections.observableArrayList();
private FilteredList<StaffAccount> filtered;
@FXML
public void initialize() {
colUsername.setCellValueFactory(new PropertyValueFactory<>("username"));
colName.setCellValueFactory(new PropertyValueFactory<>("fullName"));
colEmail.setCellValueFactory(new PropertyValueFactory<>("email"));
colPhone.setCellValueFactory(new PropertyValueFactory<>("phone"));
colStatus.setCellValueFactory(new PropertyValueFactory<>("status"));
colCreated.setCellValueFactory(new PropertyValueFactory<>("createdAt"));
filtered = new FilteredList<>(staffAccounts, a -> true);
tvStaff.setItems(filtered);
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
tvStaff.getSelectionModel().selectedItemProperty().addListener((obs, oldValue, newValue) -> {
if (btnEditAccount != null) {
btnEditAccount.setDisable(newValue == null);
}
});
if (!UserSession.getInstance().isAdmin()) {
lblError.setText("Access restricted.");
tvStaff.setDisable(true);
btnCreateAccount.setDisable(true);
if (btnEditAccount != null) {
btnEditAccount.setDisable(true);
}
return;
}
if (btnEditAccount != null) {
btnEditAccount.setDisable(true);
}
refresh();
}
@FXML
void btnRefreshClicked(ActionEvent event) {
refresh();
}
@FXML
void btnCreateAccountClicked(ActionEvent event) {
lblError.setText("");
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-register-dialog-view.fxml"));
Stage dialog = new Stage();
dialog.initOwner(tvStaff.getScene().getWindow());
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle("Create Staff Account");
dialog.setScene(new Scene(loader.load()));
dialog.setResizable(false);
dialog.showAndWait();
refresh();
} catch (Exception e) {
ActivityLogger.getInstance().logException("StaffAccountsController.btnCreateAccountClicked", e, "Opening staff register dialog");
lblError.setText("Could not open staff account creation.");
}
}
@FXML
void btnEditAccountClicked(ActionEvent event) {
lblError.setText("");
StaffAccount selected = tvStaff.getSelectionModel().getSelectedItem();
if (selected == null) {
lblError.setText("Select a staff account to edit.");
return;
}
try {
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml"));
Stage dialog = new Stage();
dialog.initOwner(tvStaff.getScene().getWindow());
dialog.initModality(Modality.APPLICATION_MODAL);
dialog.setTitle("Edit Staff Account");
dialog.setScene(new Scene(loader.load()));
dialog.setResizable(false);
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
controller.setStaffAccount(selected);
dialog.showAndWait();
refresh();
} catch (Exception e) {
ActivityLogger.getInstance().logException("StaffAccountsController.btnEditAccountClicked", e, "Opening staff edit dialog");
lblError.setText("Could not open staff account editor.");
}
}
private void refresh() {
lblError.setText("");
tvStaff.setDisable(true);
new Thread(() -> {
try {
List<EmployeeResponse> employees = EmployeeApi.getInstance().listEmployees(null);
List<StaffAccount> accounts = employees.stream()
.map(this::mapToStaffAccount)
.collect(Collectors.toList());
Platform.runLater(() -> {
staffAccounts.setAll(accounts);
tvStaff.setDisable(false);
});
} catch (Exception e) {
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts");
Platform.runLater(() -> {
String message = e.getMessage();
lblError.setText(message == null || message.isBlank()
? "Could not load staff accounts."
: "Could not load staff accounts: " + message);
tvStaff.setDisable(false);
});
}
}).start();
}
private StaffAccount mapToStaffAccount(EmployeeResponse employee) {
long userId = employee.getUserId() != null ? employee.getUserId() : 0L;
long employeeId = employee.getEmployeeId() != null ? employee.getEmployeeId() : 0L;
String username = employee.getUsername();
String fullName = employee.getFullName() != null ? employee.getFullName() : "";
String[] names = splitFullName(fullName);
String firstName = names[0];
String lastName = names[1];
String email = employee.getEmail() != null ? employee.getEmail() : "";
String phone = employee.getPhone() != null ? employee.getPhone() : "";
String role = employee.getRole() != null ? employee.getRole() : "STAFF";
boolean active = employee.getActive() != null ? employee.getActive() : false;
Timestamp createdAt = employee.getCreatedAt() != null
? Timestamp.from(employee.getCreatedAt().atZone(ZoneId.systemDefault()).toInstant())
: null;
return new StaffAccount(userId, employeeId, username, firstName, lastName, email, phone, role, active, createdAt);
}
private String[] splitFullName(String fullName) {
if (fullName == null || fullName.trim().isEmpty()) {
return new String[]{"", ""};
}
String[] parts = fullName.trim().split("\\s+", 2);
String firstName = parts.length > 0 ? parts[0] : "";
String lastName = parts.length > 1 ? parts[1] : "";
return new String[]{firstName, lastName};
}
private void applyFilter(String text) {
String q = text == null ? "" : text.trim().toLowerCase();
if (q.isEmpty()) {
filtered.setPredicate(a -> true);
return;
}
filtered.setPredicate(a ->
safe(a.getUsername()).contains(q)
|| safe(a.getFullName()).contains(q)
|| safe(a.getEmail()).contains(q)
|| safe(a.getPhone()).contains(q)
|| safe(a.getStatus()).contains(q)
);
}
private static String safe(String v) {
return v == null ? "" : v.toLowerCase();
}
}

View File

@@ -0,0 +1,303 @@
package org.example.petshopdesktop.controllers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Modality;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.supplier.SupplierResponse;
import org.example.petshopdesktop.api.endpoints.SupplierApi;
import org.example.petshopdesktop.controllers.dialogcontrollers.SupplierDialogController;
import org.example.petshopdesktop.models.Supplier;
import org.example.petshopdesktop.util.ActivityLogger;
import java.io.IOException;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
/**
* The controller for any operations in the supplier view
*/
public class SupplierController {
@FXML
private Button btnAdd;
@FXML
private Button btnDelete;
@FXML
private Button btnEdit;
@FXML
private TableColumn<Supplier, String> colContactPerson;
@FXML
private TableColumn<Supplier, String> colSupplierEmail;
@FXML
private TableColumn<Supplier, Integer> colSupplierId;
@FXML
private TableColumn<Supplier, String> colSupplierName;
@FXML
private TableColumn<Supplier, String> colSupplierPhone;
@FXML
private TableView<Supplier> tvSuppliers;
@FXML
private TextField txtSearch;
private ObservableList<Supplier> data = FXCollections.observableArrayList();
private String mode = null;
/**
* Set up the table view for suppliers and display it when starting up
*/
@FXML
void initialize(){
//Disable buttons until a row is selected
btnEdit.setDisable(true);
btnDelete.setDisable(true);
//Enable multiple selection
tvSuppliers.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
//set columns for table view
colSupplierId.setCellValueFactory(new PropertyValueFactory<Supplier, Integer>("supId"));
colSupplierName.setCellValueFactory(new PropertyValueFactory<Supplier, String>("supCompany"));
colContactPerson.setCellValueFactory(new PropertyValueFactory<Supplier, String>("supFullName"));
colSupplierEmail.setCellValueFactory(new PropertyValueFactory<Supplier, String>("supEmail"));
colSupplierPhone.setCellValueFactory(new PropertyValueFactory<Supplier, String>("supPhone"));
displaySupplier();
//EventListener to Enable buttons when a row is selected
tvSuppliers.getSelectionModel().selectedItemProperty().addListener(
(observable, oldValue, newValue) -> {
btnEdit.setDisable(false);
btnDelete.setDisable(false);
}
);
//EventListener to search when text is changed on searchbar
txtSearch.textProperty().addListener((observable, oldValue, newValue) -> {
displayFilteredSupplier(newValue);
});
//EventListener for DELETE key
tvSuppliers.setOnKeyPressed(event -> {
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
if (tvSuppliers.getSelectionModel().getSelectedItem() != null) {
btnDeleteClicked(null);
}
}
});
}
/**
* Display the suppliers to table view
*/
private void displaySupplier(){
new Thread(() -> {
try {
List<SupplierResponse> suppliers = SupplierApi.getInstance().listSuppliers(null);
List<Supplier> supplierList = suppliers.stream()
.map(this::mapToSupplier)
.collect(Collectors.toList());
Platform.runLater(() -> {
data.setAll(supplierList);
tvSuppliers.setItems(data);
});
} catch (Exception e) {
Platform.runLater(() -> {
System.out.println("Error while fetching table data: " + e.getMessage());
ActivityLogger.getInstance().logException(
"SupplierController.displaySupplier",
e,
"Fetching supplier data for table display");
});
}
}).start();
}
/**
* Filter the table given the string from the searchbar
* @param filter word to filter table
*/
private void displayFilteredSupplier(String filter){
if (txtSearch.getText() == null || txtSearch.getText().isEmpty()){
displaySupplier();
} else {
new Thread(() -> {
try {
List<SupplierResponse> suppliers = SupplierApi.getInstance().listSuppliers(filter);
List<Supplier> supplierList = suppliers.stream()
.map(this::mapToSupplier)
.collect(Collectors.toList());
Platform.runLater(() -> {
data.setAll(supplierList);
tvSuppliers.setItems(data);
});
} catch (Exception e) {
Platform.runLater(() -> {
System.out.println("Error while fetching table data: " + e.getMessage());
ActivityLogger.getInstance().logException(
"SupplierController.displayFilteredSupplier",
e,
"Filtering suppliers with filter: " + filter);
});
}
}).start();
}
}
/**
* open a new dialog for adding a supplier
* @param event click event for button
*/
@FXML
void btnAddClicked(ActionEvent event) {
mode = "Add";
openDialog(null,mode);
}
/**
* Delete selected supplier(s) when delete is clicked
* @param event click event for button
*/
@FXML
void btnDeleteClicked(ActionEvent event) {
//get selected suppliers
var selectedSuppliers = tvSuppliers.getSelectionModel().getSelectedItems();
if (selectedSuppliers.isEmpty()) return;
//ask user to confirm
Alert question = new Alert(Alert.AlertType.CONFIRMATION);
question.setHeaderText("Please confirm delete");
String message = selectedSuppliers.size() == 1
? "Are you sure you want to delete this supplier?"
: "Are you sure you want to delete " + selectedSuppliers.size() + " suppliers?";
question.setContentText(message);
question.getDialogPane().lookupButton(ButtonType.OK).requestFocus();
Optional<ButtonType> result = question.showAndWait();
//if confirmed, start deletion
if (result.isPresent() && result.get() == ButtonType.OK) {
List<Long> ids = selectedSuppliers.stream()
.map(s -> (long) s.getSupId())
.collect(Collectors.toList());
try {
SupplierApi.getInstance().deleteSuppliers(ids);
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Database Operation Confirmed");
alert.setContentText("Successfully deleted " + ids.size() + " supplier(s)");
alert.showAndWait();
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"SupplierController.btnDeleteClicked",
e,
"Deleting suppliers");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Delete Operation Failed");
alert.setContentText(e.getMessage());
alert.showAndWait();
}
//refresh display and reset inputs
displaySupplier();
btnDelete.setDisable(true);
btnEdit.setDisable(true);
txtSearch.setText("");
}
}
/**
* Open a new dialog for editing a supplier
* @param event click event for button
*/
@FXML
void btnEditClicked(ActionEvent event) {
//set selected supplier
Supplier selectedSupplier = tvSuppliers.getSelectionModel().getSelectedItem();
if (selectedSupplier != null) {
mode = "Edit";
openDialog(selectedSupplier, mode);
}
}
/**
* Function to open the new Dialog for edit or adding
* depending on the mode given
* @param supplier the supplier entity for editing, null if adding
* @param mode the mode the dialog should be in
*/
private void openDialog(Supplier supplier, String mode){
//Get new view
FXMLLoader fxmlLoader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/supplier-dialog-view.fxml"));
Scene scene = null;
try{
scene = new Scene(fxmlLoader.load());
} catch (IOException e) {
ActivityLogger.getInstance().logException(
"SupplierController.openDialog",
e,
"Loading supplier dialog in " + mode + " mode");
throw new RuntimeException(e);
}
SupplierDialogController dialogController = fxmlLoader.getController(); //controller associated with this view
dialogController.setMode(mode);
//Open the dialog depending on the mode
if(mode.equals("Edit")){
//Make it display suppliers details in dialog
dialogController.displaySupplierDetails(supplier);
}
Stage dialogStage = new Stage();
dialogStage.initModality(Modality.APPLICATION_MODAL); //make it modal
if(mode.equals("Add")){
dialogStage.setTitle("Add Supplier");
}
else {
dialogStage.setTitle("Edit Supplier");
}
dialogStage.setScene(scene);
dialogStage.showAndWait();
//When dialog closes update table view and disable edit and delete buttons, and reset search bar
displaySupplier();
btnDelete.setDisable(true);
btnEdit.setDisable(true);
txtSearch.setText("");
}
private Supplier mapToSupplier(SupplierResponse response) {
String contactPerson = response.getSupContactFirstName() + " " + response.getSupContactLastName() != null ? response.getSupContactFirstName() + " " + response.getSupContactLastName() : "";
String[] nameParts = contactPerson.split(" ", 2);
String firstName = nameParts.length > 0 ? nameParts[0] : "";
String lastName = nameParts.length > 1 ? nameParts[1] : "";
return new Supplier(
response.getSupId().intValue(),
response.getSupCompany(),
firstName,
lastName,
response.getSupEmail(),
response.getSupPhone()
);
}
}

View File

@@ -0,0 +1,232 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.DatePicker;
import javafx.scene.control.Label;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest;
import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.endpoints.AdoptionApi;
import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.models.Adoption;
import org.example.petshopdesktop.util.ActivityLogger;
import java.time.LocalDate;
import java.util.List;
public class AdoptionDialogController {
//FXML elements
@FXML
private Button btnCancel;
@FXML
private Button btnSave;
@FXML
private ComboBox<String> cbAdoptionStatus;
@FXML
private ComboBox<DropdownOption> cbCustomer;
@FXML
private ComboBox<DropdownOption> cbPet;
@FXML
private DatePicker dpAdoptionDate;
@FXML
private Label lblAdoptionId;
@FXML
private Label lblMode;
//Stores if the dialog view is in add/edit mode
private String mode = null;
//Adoption statuses
private ObservableList<String> statusList = FXCollections.observableArrayList(
"Pending", "Completed", "Cancelled"
);
@FXML
void initialize() {
cbAdoptionStatus.setItems(statusList);
new Thread(() -> {
try {
List<DropdownOption> pets = DropdownApi.getInstance().getPets();
Platform.runLater(() -> {
if (pets != null) {
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
cbPet.setItems(petsObs);
}
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AdoptionDialogController.initialize",
e,
"Loading pets for combo box");
System.out.println("Error loading pets: " + e.getMessage());
});
}
}).start();
new Thread(() -> {
try {
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
Platform.runLater(() -> {
if (customers != null) {
ObservableList<DropdownOption> customersObs = FXCollections.observableArrayList(customers);
cbCustomer.setItems(customersObs);
}
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AdoptionDialogController.initialize",
e,
"Loading customers for combo box");
System.out.println("Error loading customers: " + e.getMessage());
});
}
}).start();
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
buttonSaveClicked(mouseEvent);
}
});
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
closeStage(mouseEvent);
}
});
}
private void buttonSaveClicked(MouseEvent mouseEvent) {
String errorMsg = "";
if (cbPet.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Pet is required.\n";
}
if (cbCustomer.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Customer is required.\n";
}
if (dpAdoptionDate.getValue() == null) {
errorMsg += "Adoption Date is required.\n";
}
if (cbAdoptionStatus.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Status is required.\n";
}
if (errorMsg.isEmpty()) {
try {
AdoptionRequest request = new AdoptionRequest();
request.setPetId(cbPet.getSelectionModel().getSelectedItem().getId());
request.setCustomerId(cbCustomer.getSelectionModel().getSelectedItem().getId());
request.setAdoptionDate(dpAdoptionDate.getValue());
request.setAdoptionStatus(cbAdoptionStatus.getValue());
if (mode.equals("Add")) {
AdoptionApi.getInstance().createAdoption(request);
} else {
String[] parts = lblAdoptionId.getText().split(": ");
if (parts.length < 2) {
throw new IllegalStateException("Invalid adoption ID format");
}
Long adoptionId = Long.parseLong(parts[1]);
AdoptionApi.getInstance().updateAdoption(adoptionId, request);
}
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Saved");
alert.setContentText(mode + " succeeded");
alert.showAndWait();
closeStage(mouseEvent);
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"AdoptionDialogController.buttonSaveClicked",
e,
mode + " adoption");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Database Operation Error");
alert.setContentText(e.getMessage());
alert.showAndWait();
}
} else {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(errorMsg);
alert.showAndWait();
}
}
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
}
public void displayAdoptionDetails(Adoption adoption) {
if (adoption != null) {
lblAdoptionId.setText("ID: " + adoption.getAdoptionId());
for (DropdownOption pet : cbPet.getItems()) {
if (pet.getLabel().equals(adoption.getPetName())) {
cbPet.getSelectionModel().select(pet);
break;
}
}
for (DropdownOption customer : cbCustomer.getItems()) {
if (customer.getLabel().equals(adoption.getCustomerName())) {
cbCustomer.getSelectionModel().select(customer);
break;
}
}
if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) {
try {
dpAdoptionDate.setValue(LocalDate.parse(adoption.getAdoptionDate()));
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"AdoptionDialogController.displayAdoptionDetails",
e,
"Parsing adoption date");
}
}
for (String status : cbAdoptionStatus.getItems()) {
if (status.equals(adoption.getAdoptionStatus())) {
cbAdoptionStatus.getSelectionModel().select(status);
break;
}
}
}
}
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Adoption");
lblAdoptionId.setVisible(mode.equals("Edit"));
}
}

View File

@@ -0,0 +1,291 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import javafx.scene.control.ListCell;
import org.example.petshopdesktop.DTOs.AppointmentDTO;
import org.example.petshopdesktop.api.dto.appointment.AppointmentRequest;
import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.endpoints.AppointmentApi;
import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.util.ActivityLogger;
import java.time.LocalTime;
import java.util.Collections;
import java.util.List;
public class AppointmentDialogController {
// ============================
// FXML
// ============================
@FXML private Button btnCancel;
@FXML private Button btnSave;
@FXML private ComboBox<DropdownOption> cbService;
@FXML private ComboBox<DropdownOption> cbCustomer;
@FXML private ComboBox<DropdownOption> cbPet;
@FXML private ComboBox<Integer> cbHour;
@FXML private ComboBox<Integer> cbMinute;
@FXML private ComboBox<String> cbAppointmentStatus;
@FXML private DatePicker dpAppointmentDate;
@FXML private Label lblAppointmentId;
@FXML private Label lblMode;
// ============================
// DATA
// ============================
private String mode = null; // Add | Edit
private AppointmentDTO selectedAppointment = null;
private ObservableList<String> statusList =
FXCollections.observableArrayList(
"Booked", "Completed", "Cancelled"
);
//
// MODE
//
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Appointment");
lblAppointmentId.setVisible(!mode.equals("Add"));
}
//
// INITIALIZE
//
@FXML
public void initialize() {
new Thread(() -> {
try {
List<DropdownOption> services = DropdownApi.getInstance().getServices();
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
List<DropdownOption> pets = DropdownApi.getInstance().getPets();
Platform.runLater(() -> {
if (services != null) {
cbService.setItems(FXCollections.observableArrayList(services));
}
if (customers != null) {
cbCustomer.setItems(FXCollections.observableArrayList(customers));
}
if (pets != null) {
cbPet.setItems(FXCollections.observableArrayList(pets));
}
syncSelectedAppointment();
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.initialize",
e,
"Loading combo box data for services, customers, and pets");
e.printStackTrace();
});
}
}).start();
cbAppointmentStatus.setItems(statusList);
// Hours 9 AM - 5 PM
for (int i = 9; i <= 17; i++) {
cbHour.getItems().add(i);
}
cbMinute.getItems().addAll(0, 15, 30, 45);
// Show dropdown labels
cbService.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbService.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbCustomer.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbCustomer.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbPet.setCellFactory(param -> new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
cbPet.setButtonCell(new ListCell<>() {
@Override
protected void updateItem(DropdownOption option, boolean empty) {
super.updateItem(option, empty);
setText(empty || option == null ? null : option.getLabel());
}
});
btnSave.setOnMouseClicked(this::buttonSaveClicked);
btnCancel.setOnMouseClicked(this::closeStage);
}
//
// DISPLAY FOR EDIT
//
public void displayAppointmentDetails(AppointmentDTO appt) {
selectedAppointment = appt;
lblAppointmentId.setText("ID: " + appt.getAppointmentId());
try {
dpAppointmentDate.setValue(
java.time.LocalDate.parse(appt.getAppointmentDate())
);
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.displayAppointmentDetails",
e,
"Parsing appointment date");
}
cbAppointmentStatus.setValue(appt.getAppointmentStatus());
try {
LocalTime time = LocalTime.parse(appt.getAppointmentTime());
cbHour.setValue(time.getHour());
cbMinute.setValue(time.getMinute());
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.displayAppointmentDetails",
e,
"Parsing appointment time");
}
cbService.getItems().forEach(s -> {
if (s.getId() != null && s.getId().longValue() == appt.getServiceId()) cbService.setValue(s);
});
cbCustomer.getItems().forEach(c -> {
if (c.getId() != null && c.getId().longValue() == appt.getCustomerId()) cbCustomer.setValue(c);
});
cbPet.getItems().forEach(p -> {
if (p.getId() != null && p.getId().longValue() == appt.getPetId()) cbPet.setValue(p);
});
}
//
// SAVE
//
private void buttonSaveClicked(MouseEvent e) {
if (cbService.getValue() == null ||
cbCustomer.getValue() == null ||
cbPet.getValue() == null ||
dpAppointmentDate.getValue() == null ||
cbHour.getValue() == null ||
cbMinute.getValue() == null ||
cbAppointmentStatus.getValue() == null) {
showError("All fields are required");
return;
}
LocalTime appointmentTime = LocalTime.of(cbHour.getValue(), cbMinute.getValue());
Long storeId = UserSession.getInstance().getStoreId();
if (storeId == null || storeId <= 0) {
showError("Store is not set for this account");
return;
}
AppointmentRequest request = new AppointmentRequest();
request.setPetIds(Collections.singletonList(cbPet.getValue().getId()));
request.setCustomerId(cbCustomer.getValue().getId());
request.setStoreId(storeId);
request.setServiceId(cbService.getValue().getId());
request.setAppointmentDate(dpAppointmentDate.getValue());
request.setAppointmentTime(appointmentTime);
request.setAppointmentStatus(cbAppointmentStatus.getValue());
new Thread(() -> {
try {
if (mode.equals("Add")) {
AppointmentApi.getInstance().createAppointment(request);
} else {
AppointmentApi.getInstance().updateAppointment(
(long) selectedAppointment.getAppointmentId(),
request
);
}
Platform.runLater(() -> closeStage(e));
} catch (Exception ex) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"AppointmentDialogController.buttonSaveClicked",
ex,
"Saving appointment in " + mode + " mode");
ex.printStackTrace();
showError("Error saving appointment: " + ex.getMessage());
});
}
}).start();
}
//
// UTIL
//
private void closeStage(MouseEvent e) {
Stage stage = (Stage) ((Node) e.getSource()).getScene().getWindow();
stage.close();
}
private void showError(String msg) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(msg);
alert.showAndWait();
}
private void syncSelectedAppointment() {
if (selectedAppointment != null) {
displayAppointmentDetails(selectedAppointment);
}
}
}

View File

@@ -0,0 +1,209 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import org.example.petshopdesktop.models.Inventory;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.api.dto.inventory.InventoryRequest;
import org.example.petshopdesktop.api.dto.inventory.InventoryResponse;
import org.example.petshopdesktop.api.dto.product.ProductResponse;
import org.example.petshopdesktop.api.endpoints.InventoryApi;
import org.example.petshopdesktop.api.endpoints.ProductApi;
import org.example.petshopdesktop.models.Product;
import org.example.petshopdesktop.util.ActivityLogger;
import java.math.BigDecimal;
import java.util.List;
import java.util.stream.Collectors;
public class InventoryDialogController {
//FXML elements
@FXML
private Button btnCancel;
@FXML
private Button btnSave;
@FXML
private ComboBox<Product> cbProduct;
@FXML
private Label lblInventoryId;
@FXML
private Label lblMode;
@FXML
private TextField txtQuantity;
//Determines if the mode is add or edit
private String mode = null;
//Loads upon .FXML boot
@FXML
void initialize() {
cbProduct.setConverter(new StringConverter<Product>() {
//Displays product in combobox (prodID + name)
@Override
public String toString(Product product) {
return product == null ? "" : product.getProdId() + ": " + product.getProdName();
}
//Not needed
@Override
public Product fromString(String string) { return null; }
});
//Load product list from API into combobox
try {
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
if (productResponses != null) {
ObservableList<Product> products = FXCollections.observableArrayList();
for (ProductResponse pr : productResponses) {
products.add(new Product(
pr.getProdId().intValue(),
pr.getProdName(),
pr.getProdPrice().doubleValue(),
0,
pr.getProdDesc()
));
}
cbProduct.setItems(products);
}
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"InventoryDialogController.initialize",
e,
"Loading products for combo box");
System.out.println("Error loading products: " + e.getMessage());
}
//Save button handler
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
buttonSaveClicked(mouseEvent);
}
});
//Cancel button handler
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
closeStage(mouseEvent);
}
});
}
//Handles save button click event
private void buttonSaveClicked(MouseEvent mouseEvent) {
int numRow = 0;
String errorMsg = "";
if (cbProduct.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Product is required.\n";
}
//Validate inputs
errorMsg += Validator.isPresent(txtQuantity.getText(), "Quantity");
errorMsg += Validator.isLessThanVarChars(txtQuantity.getText(), "Quantity", 11);
errorMsg += Validator.isNonNegativeInteger(txtQuantity.getText(), "Quantity");
//Operation only occurs if there are no errors
if (errorMsg.isEmpty()) {
try {
InventoryRequest request = new InventoryRequest();
Product selectedProduct = cbProduct.getSelectionModel().getSelectedItem();
request.setProdId((long) selectedProduct.getProdId());
int quantity;
try {
quantity = Integer.parseInt(txtQuantity.getText());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid quantity format");
}
request.setQuantity(quantity);
if (mode.equals("Add")) {
InventoryApi.getInstance().createInventory(request);
} else {
String[] parts = lblInventoryId.getText().split(": ");
if (parts.length < 2) {
throw new IllegalStateException("Invalid inventory ID format");
}
Long inventoryId = Long.parseLong(parts[1]);
InventoryApi.getInstance().updateInventory(inventoryId, request);
}
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Saved");
alert.setContentText(mode + " succeeded");
alert.showAndWait();
closeStage(mouseEvent);
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"InventoryDialogController.buttonSaveClicked",
e,
mode + " inventory");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Database Operation Error");
alert.setContentText(e.getMessage());
alert.showAndWait();
}
}
//Displays validation errors
else {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(errorMsg);
alert.showAndWait();
}
}
//Close dialog view
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
}
//Editing
//Displays fields with existing inventory data
public void displayInventoryDetails(Inventory inventory) {
if (inventory != null) {
//Displays inventory ID
lblInventoryId.setText("ID: " + inventory.getInventoryId());
//Selecting matching product in combobox
for (Product product : cbProduct.getItems()) {
if (product.getProdId() == inventory.getProdId()) {
cbProduct.getSelectionModel().select(product);
break;
}
}
txtQuantity.setText(String.valueOf(inventory.getQuantity()));
}
}
//Sets dialog view to add/edit. Updates UI labels
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Inventory");
lblInventoryId.setVisible(mode.equals("Edit"));
}
}

View File

@@ -0,0 +1,201 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.api.dto.pet.PetRequest;
import org.example.petshopdesktop.api.dto.pet.PetResponse;
import org.example.petshopdesktop.api.endpoints.PetApi;
import org.example.petshopdesktop.models.Pet;
import org.example.petshopdesktop.util.ActivityLogger;
import java.math.BigDecimal;
public class PetDialogController {
@FXML
private Button btnCancel;
@FXML
private Button btnSave;
@FXML
private ComboBox<String> cbPetStatus;
@FXML
private Label lblMode;
@FXML
private Label lblPetId;
@FXML
private TextField txtPetAge;
@FXML
private TextField txtPetBreed;
@FXML
private TextField txtPetName;
@FXML
private TextField txtPetPrice;
@FXML
private TextField txtPetSpecies;
private String mode = null;
private ObservableList<String> statusList = FXCollections.observableArrayList(
"Available", "Adopted"
);
@FXML
void initialize() {
cbPetStatus.setItems(statusList); //set status combobox
//Set up mouse handlers for buttons
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
buttonSaveClicked(mouseEvent);
}
});
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
closeStage(mouseEvent);
}
});
}
private void buttonSaveClicked(MouseEvent mouseEvent) {
String errorMsg = "";
//Check validation (input required)
errorMsg += Validator.isPresent(txtPetName.getText(), "Pet Name");
errorMsg += Validator.isPresent(txtPetAge.getText(), "Age");
errorMsg += Validator.isPresent(txtPetBreed.getText(), "Breed");
errorMsg += Validator.isPresent(txtPetSpecies.getText(), "Species");
errorMsg += Validator.isPresent(txtPetPrice.getText(), "Price");
if (cbPetStatus.getSelectionModel().getSelectedItem() == null){
errorMsg += "Status is required";
}
//Check validation (length size)
errorMsg += Validator.isLessThanVarChars(txtPetName.getText(), "Pet Name", 50);
errorMsg += Validator.isLessThanVarChars(txtPetSpecies.getText(), "Species", 50);
errorMsg += Validator.isLessThanVarChars(txtPetBreed.getText(), "Breed", 50);
errorMsg += Validator.isLessThanVarChars(txtPetPrice.getText(), "Price", 12);
errorMsg += Validator.isLessThanVarChars(txtPetAge.getText(), "Age", 11);
//Check validation (format)
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
errorMsg += Validator.isNonNegativeInteger(txtPetAge.getText(), "Age");
if(errorMsg.isEmpty()){
PetRequest request = buildPetRequest();
try {
if(mode.equals("Add")) {
PetApi.getInstance().createPet(request);
} else {
String[] parts = lblPetId.getText().split(": ");
if (parts.length < 2) {
throw new IllegalStateException("Invalid pet ID format");
}
Long petId = Long.parseLong(parts[1]);
PetApi.getInstance().updatePet(petId, request);
}
//tell the user operation was successful
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Saved");
alert.setContentText(mode + " succeeded");
alert.showAndWait();
closeStage(mouseEvent);
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"PetDialogController.buttonSaveClicked",
e,
mode + " pet record");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Operation Error");
alert.setContentText(mode + " failed: " + e.getMessage());
alert.showAndWait();
}
}
else{
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(errorMsg);
alert.showAndWait();
}
}
private PetRequest buildPetRequest() {
PetRequest request = new PetRequest();
request.setPetName(txtPetName.getText());
request.setPetSpecies(txtPetSpecies.getText());
request.setPetBreed(txtPetBreed.getText());
request.setPetStatus(cbPetStatus.getValue());
try {
request.setPetPrice(new BigDecimal(txtPetPrice.getText()));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid price format");
}
int age;
try {
age = Integer.parseInt(txtPetAge.getText());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid age format");
}
request.setPetAge(age);
return request;
}
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
}
public void displayPetDetails(Pet pet){
if (pet!=null){
lblPetId.setText("ID: " + pet.getPetId());
txtPetName.setText(pet.getPetName());
txtPetSpecies.setText(pet.getPetSpecies());
txtPetBreed.setText(pet.getPetBreed());
txtPetAge.setText(pet.getPetAge() + "");
txtPetPrice.setText(pet.getPetPrice() + "");
//get the right combobox selection
for (String status : cbPetStatus.getItems()) {
if(status.equals(pet.getPetStatus())){
cbPetStatus.getSelectionModel().select(status);
}
}
}
}
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Pet");
if(mode.equals("Add")) {
lblPetId.setVisible(false);
}
else if(mode.equals("Edit")) {
lblPetId.setVisible(true);
}
}
}

View File

@@ -0,0 +1,206 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.example.petshopdesktop.DTOs.ProductDTO;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.dto.product.ProductRequest;
import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.api.endpoints.ProductApi;
import org.example.petshopdesktop.util.ActivityLogger;
import java.math.BigDecimal;
import java.util.List;
public class ProductDialogController {
@FXML
private Button btnCancel;
@FXML
private Button btnSave;
@FXML
private ComboBox<DropdownOption> cbProdCategory;
@FXML
private Label lblMode;
@FXML
private Label lblProdId;
@FXML
private TextField txtProdDesc;
@FXML
private TextField txtProdName;
@FXML
private TextField txtProdPrice;
private String mode = null;
/**
* Add event listeners to buttons when dialog loads
*/
@FXML
void initialize() {
//Set up mouse handlers for buttons
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
buttonSaveClicked(mouseEvent);
}
});
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
closeStage(mouseEvent);
}
});
//Set up combobox for selecting category
try {
List<DropdownOption> categories = DropdownApi.getInstance().getCategories();
if (categories != null) {
ObservableList<DropdownOption> categoriesObs = FXCollections.observableArrayList(categories);
cbProdCategory.setItems(categoriesObs);
}
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"ProductDialogController.initialize",
e,
"Loading categories for combo box");
System.out.println("Error loading categories: " + e.getMessage());
}
}
/**
* Validates the inputs, then add or update the database depending
* on the mode
* @param mouseEvent click event for save button
*/
private void buttonSaveClicked(MouseEvent mouseEvent) {
int numRow = 0; //how many rows affected
String errorMsg = ""; //error message for validation
//Check Validation (input required)
errorMsg += Validator.isPresent(txtProdName.getText(), "Product Name");
errorMsg += Validator.isPresent(txtProdPrice.getText(), "Product Price");
if (cbProdCategory.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Category is required \n";
}
//Check validation (length size)
errorMsg += Validator.isLessThanVarChars(txtProdName.getText(), "Product Name", 100);
errorMsg += Validator.isLessThanVarChars(txtProdDesc.getText(), "Description", 100);
errorMsg += Validator.isLessThanVarChars(txtProdPrice.getText(), "Product Price", 12);
//Check Validation (format)
errorMsg += Validator.isNonNegativeDouble(txtProdPrice.getText(), "Product Price");
if (errorMsg.isEmpty()) {
try {
ProductRequest request = new ProductRequest();
request.setProdName(txtProdName.getText());
BigDecimal price;
try {
price = new BigDecimal(txtProdPrice.getText());
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid price format");
}
request.setProdPrice(price);
request.setCategoryId(cbProdCategory.getSelectionModel().getSelectedItem().getId());
request.setProdDesc(txtProdDesc.getText());
if (mode.equals("Add")) {
ProductApi.getInstance().createProduct(request);
} else {
String[] parts = lblProdId.getText().split(": ");
if (parts.length < 2) {
throw new IllegalStateException("Invalid product ID format");
}
Long productId = Long.parseLong(parts[1]);
ProductApi.getInstance().updateProduct(productId, request);
}
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Saved");
alert.setContentText(mode + " succeeded");
alert.showAndWait();
closeStage(mouseEvent);
} catch (Exception e) {
ActivityLogger.getInstance().logException(
"ProductDialogController.buttonSaveClicked",
e,
mode + " product");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Database Operation Error");
alert.setContentText(e.getMessage());
alert.showAndWait();
}
}
else{ //Display validation errors
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(errorMsg);
alert.showAndWait();
}
}
/**
* Display the product data in text fields and combobox
* @param product the product entity containing data to display
*/
public void displayProductDetails(ProductDTO product){
if (product!=null){
lblProdId.setText("ID: " + product.getProdId());
txtProdName.setText(product.getProdName());
txtProdDesc.setText(product.getProdDesc());
txtProdPrice.setText(product.getProdPrice() + "");
for (DropdownOption category : cbProdCategory.getItems()) {
if(category.getLabel().equals(product.getCategoryName())){
cbProdCategory.getSelectionModel().select(category);
break;
}
}
}
}
/**
* Close the window
* @param mouseEvent mouse event to close
*/
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
}
/**
* Set the mode of the dialog
* @param mode the mode to for the dialog
*/
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Product");
if(mode.equals("Add")) {
lblProdId.setVisible(false);
}
else if(mode.equals("Edit")) {
lblProdId.setVisible(true);
}
}
}

View File

@@ -0,0 +1,270 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.application.Platform;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.fxml.FXML;
import javafx.scene.Node;
import javafx.scene.control.*;
import javafx.scene.input.MouseEvent;
import javafx.stage.Stage;
import org.example.petshopdesktop.DTOs.ProductSupplierDTO;
import org.example.petshopdesktop.Validator;
import org.example.petshopdesktop.api.dto.common.DropdownOption;
import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierRequest;
import org.example.petshopdesktop.api.dto.productsupplier.ProductSupplierResponse;
import org.example.petshopdesktop.api.endpoints.DropdownApi;
import org.example.petshopdesktop.api.endpoints.ProductSupplierApi;
import org.example.petshopdesktop.util.ActivityLogger;
import java.math.BigDecimal;
public class ProductSupplierDialogController {
@FXML
private Button btnCancel;
@FXML
private Button btnSave;
@FXML
private ComboBox<DropdownOption> cbProduct;
@FXML
private ComboBox<DropdownOption> cbSupplier;
@FXML
private Label lblMode;
@FXML
private Label lblProductSupplierId;
@FXML
private TextField txtCost;
private String mode = null;
private int selectedSupId = -1;
private int selectedProdId = -1;
/**
* add event listeners to buttons and set up combobox
*/
@FXML
void initialize() { //Set up mouse handlers for buttons
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
buttonSaveClicked(mouseEvent);
}
});
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent mouseEvent) {
closeStage(mouseEvent);
}
});
cbSupplier.setButtonCell(new ListCell<DropdownOption>() {
@Override
protected void updateItem(DropdownOption item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getLabel());
}
}
});
cbSupplier.setCellFactory(lv -> new ListCell<DropdownOption>() {
@Override
protected void updateItem(DropdownOption item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getLabel());
}
}
});
cbProduct.setButtonCell(new ListCell<DropdownOption>() {
@Override
protected void updateItem(DropdownOption item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getLabel());
}
}
});
cbProduct.setCellFactory(lv -> new ListCell<DropdownOption>() {
@Override
protected void updateItem(DropdownOption item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
setText(null);
} else {
setText(item.getLabel());
}
}
});
new Thread(() -> {
try {
var suppliers = DropdownApi.getInstance().getSuppliers();
var products = DropdownApi.getInstance().getProducts();
Platform.runLater(() -> {
if (suppliers != null) {
cbSupplier.setItems(FXCollections.observableArrayList(suppliers));
}
if (products != null) {
cbProduct.setItems(FXCollections.observableArrayList(products));
}
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"ProductSupplierDialogController.initialize",
e,
"Loading suppliers and products for combo boxes");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Initialization Error");
alert.setContentText("Failed to load dropdown data: " + e.getMessage());
alert.showAndWait();
});
}
}).start();
}
/**
* Validates the inputs, then add or update the database depending
* on the mode
* @param mouseEvent click event for save button
*/
private void buttonSaveClicked(MouseEvent mouseEvent) {
String errorMsg = "";
errorMsg += Validator.isPresent(txtCost.getText(), "Cost");
if (cbProduct.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Product is required \n";
}
if (cbSupplier.getSelectionModel().getSelectedItem() == null) {
errorMsg += "Supplier is required \n";
}
errorMsg += Validator.isLessThanVarChars(txtCost.getText(), "Cost", 12);
errorMsg += Validator.isNonNegativeDouble(txtCost.getText(), "Cost");
if(errorMsg.isEmpty()){
ProductSupplierRequest request = collectProductSupplierRequest();
new Thread(() -> {
try {
if (mode.equals("Add")) {
ProductSupplierApi.getInstance().createProductSupplier(request);
} else {
ProductSupplierApi.getInstance().updateProductSupplier((long) selectedProdId, (long) selectedSupId, request);
}
Platform.runLater(() -> {
Alert alert = new Alert(Alert.AlertType.INFORMATION);
alert.setHeaderText("Saved");
alert.setContentText(mode + " succeeded");
alert.showAndWait();
closeStage(mouseEvent);
});
} catch (Exception e) {
Platform.runLater(() -> {
ActivityLogger.getInstance().logException(
"ProductSupplierDialogController.buttonSaveClicked",
e,
mode + " product-supplier");
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Database Operation Error");
alert.setContentText(mode + " failed: " + e.getMessage());
alert.showAndWait();
});
}
}).start();
} else {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setHeaderText("Input Error");
alert.setContentText(errorMsg);
alert.showAndWait();
}
}
/**
* collect the data for new/updated productSupplier
* @return productSupplier request with data
*/
private ProductSupplierRequest collectProductSupplierRequest() {
ProductSupplierRequest request = new ProductSupplierRequest();
request.setSupplierId(cbSupplier.getSelectionModel().getSelectedItem().getId());
request.setProductId(cbProduct.getSelectionModel().getSelectedItem().getId());
try {
request.setCost(new BigDecimal(txtCost.getText()));
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Invalid cost format");
}
return request;
}
/**
* Display the productsupplier data in text fields and combobox
* @param productSupplier
*/
public void displayProductSupplierDetails(ProductSupplierDTO productSupplier){
if(productSupplier != null){
txtCost.setText(productSupplier.getCost() + "");
}
for (DropdownOption product : cbProduct.getItems()) {
if(product.getId() == productSupplier.getProdId()){
cbProduct.getSelectionModel().select(product);
}
}
for (DropdownOption supplier : cbSupplier.getItems()) {
if (supplier.getId() == productSupplier.getSupId()) {
cbSupplier.getSelectionModel().select(supplier);
}
}
}
/**
* Close the window
* @param mouseEvent mouse event to close
*/
private void closeStage(MouseEvent mouseEvent) {
Node node = (Node) mouseEvent.getSource();
Stage stage = (Stage) node.getScene().getWindow();
stage.close();
}
/**
* Set the mode of the dialog
* @param mode the mode for the dialog
*/
public void setMode(String mode) {
this.mode = mode;
lblMode.setText(mode + " Product");
lblProductSupplierId.setVisible(false);
}
/**
* set the current supplier and productId for (needed for update since compound primary key)
* @param supId supplier id
* @param prodId product id
*/
public void setSelectedIds(int supId, int prodId){
this.selectedSupId = supId;
this.selectedProdId = prodId;
}
}

View File

@@ -0,0 +1,489 @@
package org.example.petshopdesktop.controllers.dialogcontrollers;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.control.*;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.stage.Stage;
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
import org.example.petshopdesktop.api.dto.sale.SaleRequest;
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
import org.example.petshopdesktop.api.endpoints.SaleApi;
import org.example.petshopdesktop.auth.UserSession;
import org.example.petshopdesktop.util.ActivityLogger;
import java.math.BigDecimal;
import java.text.NumberFormat;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Collectors;
public class RefundDialogController {
@FXML
private TextField txtSaleId;
@FXML
private Button btnLoadSale;
@FXML
private Label lblSaleInfo;
@FXML
private TableView<SaleItemResponse> tvOriginalItems;
@FXML
private TableColumn<SaleItemResponse, String> colOriginalProduct;
@FXML
private TableColumn<SaleItemResponse, Integer> colOriginalQuantity;
@FXML
private TableColumn<SaleItemResponse, BigDecimal> colOriginalUnitPrice;
@FXML
private TableColumn<SaleItemResponse, BigDecimal> colOriginalTotal;
@FXML
private Button btnAddToRefund;
@FXML
private TableView<RefundItem> tvRefundItems;
@FXML
private TableColumn<RefundItem, String> colRefundProduct;
@FXML
private TableColumn<RefundItem, Integer> colRefundQuantity;
@FXML
private TableColumn<RefundItem, Double> colRefundUnitPrice;
@FXML
private TableColumn<RefundItem, Double> colRefundTotal;
@FXML
private Button btnRemoveFromRefund;
@FXML
private ComboBox<String> cbPaymentMethod;
@FXML
private Label lblRefundTotal;
@FXML
private Button btnProcessRefund;
@FXML
private Button btnCancel;
private SaleResponse currentSale;
private final List<SaleItemResponse> baseOriginalItems = new ArrayList<>();
private final ObservableList<SaleItemResponse> originalItems = FXCollections.observableArrayList();
private final ObservableList<RefundItem> refundItems = FXCollections.observableArrayList();
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
@FXML
public void initialize() {
setupTables();
cbPaymentMethod.setItems(FXCollections.observableArrayList("Cash", "Card", "Debit"));
cbPaymentMethod.getSelectionModel().selectFirst();
updateRefundTotal();
}
private void setupTables() {
colOriginalProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
colOriginalQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colOriginalUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colOriginalTotal.setCellValueFactory(new PropertyValueFactory<>("lineTotal"));
tvOriginalItems.setItems(originalItems);
tvOriginalItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
colRefundProduct.setCellValueFactory(new PropertyValueFactory<>("productName"));
colRefundQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
colRefundUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
colRefundTotal.setCellValueFactory(new PropertyValueFactory<>("total"));
tvRefundItems.setItems(refundItems);
tvRefundItems.getSelectionModel().setSelectionMode(SelectionMode.SINGLE);
}
@FXML
void btnLoadSaleClicked(ActionEvent event) {
loadSale();
}
public void prefillSale(Long saleId) {
if (saleId == null) {
return;
}
txtSaleId.setText(String.valueOf(saleId));
loadSale();
}
private void loadSale() {
String saleIdText = txtSaleId.getText().trim();
if (saleIdText.isEmpty()) {
showError("Load Sale", "Enter a transaction ID.");
return;
}
Long saleId;
try {
saleId = Long.parseLong(saleIdText);
} catch (NumberFormatException e) {
showError("Load Sale", "Invalid transaction ID.");
return;
}
try {
List<SaleResponse> allSales = SaleApi.getInstance().listSales(0, 1000, null);
currentSale = SaleApi.getInstance().getSale(saleId);
if (Boolean.TRUE.equals(currentSale.getIsRefund())) {
clearLoadedSale();
showError("Load Sale", "Select an original sale, not a refund record.");
return;
}
List<SaleResponse> previousRefunds = allSales.stream()
.filter(s -> Boolean.TRUE.equals(s.getIsRefund()) && saleId.equals(s.getOriginalSaleId()))
.collect(Collectors.toList());
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
String saleInfo = String.format("Sale Date: %s | Employee: %s | Original Total: %s | Payment: %s",
currentSale.getSaleDate().format(formatter),
currentSale.getEmployeeName(),
currency.format(currentSale.getTotalAmount()),
currentSale.getPaymentMethod());
lblSaleInfo.setText(saleInfo);
List<SaleItemResponse> refundableItems = buildRefundableItems(currentSale, previousRefunds);
if (refundableItems.isEmpty()) {
showError("Load Sale", "This sale has no remaining refundable items.");
return;
}
baseOriginalItems.clear();
baseOriginalItems.addAll(copySaleItems(refundableItems));
originalItems.setAll(copySaleItems(refundableItems));
cbPaymentMethod.getSelectionModel().select(currentSale.getPaymentMethod());
refundItems.clear();
updateOriginalItemAvailability();
updateRefundTotal();
} catch (Exception e) {
ActivityLogger.getInstance().logException("RefundDialogController.btnLoadSaleClicked", e, "Loading sale");
showError("Load Sale", e.getMessage() != null ? e.getMessage() : "Could not load sale.");
}
}
@FXML
void btnAddToRefundClicked(ActionEvent event) {
if (currentSale == null) {
showError("Add to Refund", "Load a sale first.");
return;
}
SaleItemResponse selected = tvOriginalItems.getSelectionModel().getSelectedItem();
if (selected == null) {
showError("Add to Refund", "Select an item from the original sale.");
return;
}
int alreadyRefunded = refundItems.stream()
.filter(r -> r.getProdId() == selected.getProdId().intValue())
.mapToInt(RefundItem::getQuantity)
.sum();
int available = selected.getQuantity() - alreadyRefunded;
if (available <= 0) {
showError("Add to Refund", "All items of this product are already in the refund list.");
return;
}
TextInputDialog dialog = new TextInputDialog(String.valueOf(available));
dialog.setTitle("Refund Quantity");
dialog.setHeaderText("Product: " + selected.getProductName());
dialog.setContentText("Enter quantity to refund (max " + available + "):");
Optional<String> result = dialog.showAndWait();
if (result.isPresent()) {
try {
int quantity = Integer.parseInt(result.get().trim());
if (quantity <= 0) {
showError("Add to Refund", "Quantity must be at least 1.");
return;
}
if (quantity > available) {
showError("Add to Refund", "Cannot refund more than " + available + " items.");
return;
}
addOrMergeRefundItem(selected, quantity);
updateOriginalItemAvailability();
updateRefundTotal();
} catch (NumberFormatException e) {
showError("Add to Refund", "Invalid quantity.");
}
}
}
@FXML
void btnRemoveFromRefundClicked(ActionEvent event) {
RefundItem selected = tvRefundItems.getSelectionModel().getSelectedItem();
if (selected != null) {
refundItems.remove(selected);
updateOriginalItemAvailability();
updateRefundTotal();
}
}
@FXML
void btnProcessRefundClicked(ActionEvent event) {
if (currentSale == null) {
showError("Process Refund", "Load a sale first.");
return;
}
if (refundItems.isEmpty()) {
showError("Process Refund", "Add at least one item to refund.");
return;
}
Long storeId = UserSession.getInstance().getStoreId();
if (storeId == null || storeId <= 0) {
showError("Process Refund", "Store is not set for this account.");
return;
}
String payment = cbPaymentMethod.getSelectionModel().getSelectedItem();
if (payment == null || payment.isBlank()) {
showError("Process Refund", "Select a payment method.");
return;
}
Alert confirm = new Alert(Alert.AlertType.CONFIRMATION);
confirm.setTitle("Confirm Refund");
confirm.setHeaderText("Process refund for sale ID " + currentSale.getSaleId() + "?");
confirm.setContentText("Refund amount: " + lblRefundTotal.getText());
Optional<ButtonType> confirmResult = confirm.showAndWait();
if (confirmResult.isEmpty() || confirmResult.get() != ButtonType.OK) {
return;
}
try {
SaleRequest request = new SaleRequest();
request.setStoreId(storeId);
request.setPaymentMethod(payment);
request.setIsRefund(true);
request.setOriginalSaleId(currentSale.getSaleId());
List<SaleItemRequest> items = new ArrayList<>();
for (RefundItem item : refundItems) {
SaleItemRequest saleItem = new SaleItemRequest();
saleItem.setProdId((long) item.getProdId());
saleItem.setQuantity(-item.getQuantity());
items.add(saleItem);
}
request.setItems(items);
SaleResponse refundResponse = SaleApi.getInstance().createSale(request);
Alert success = new Alert(Alert.AlertType.INFORMATION);
success.setTitle("Refund Processed");
success.setHeaderText(null);
success.setContentText("Refund ID " + refundResponse.getSaleId() + " was created successfully.");
success.showAndWait();
closeDialog();
} catch (Exception e) {
ActivityLogger.getInstance().logException("RefundDialogController.btnProcessRefundClicked", e, "Processing refund");
showError("Process Refund", e.getMessage() != null ? e.getMessage() : "Could not process refund.");
}
}
@FXML
void btnCancelClicked(ActionEvent event) {
closeDialog();
}
private void clearLoadedSale() {
currentSale = null;
lblSaleInfo.setText("");
baseOriginalItems.clear();
originalItems.clear();
refundItems.clear();
updateRefundTotal();
}
private void addOrMergeRefundItem(SaleItemResponse selected, int quantity) {
for (int i = 0; i < refundItems.size(); i++) {
RefundItem existing = refundItems.get(i);
if (existing.getProdId() == selected.getProdId().intValue()) {
refundItems.set(i, new RefundItem(
existing.getProdId(),
existing.getProductName(),
existing.getQuantity() + quantity,
existing.getUnitPrice()
));
return;
}
}
refundItems.add(new RefundItem(
selected.getProdId().intValue(),
selected.getProductName(),
quantity,
selected.getUnitPrice().doubleValue()
));
}
private void updateOriginalItemAvailability() {
if (currentSale == null) {
baseOriginalItems.clear();
originalItems.clear();
return;
}
Map<Long, Integer> pendingRefunds = new HashMap<>();
for (RefundItem refundItem : refundItems) {
pendingRefunds.merge((long) refundItem.getProdId(), refundItem.getQuantity(), Integer::sum);
}
List<SaleItemResponse> refreshedItems = new ArrayList<>();
for (SaleItemResponse originalItem : baseOriginalItems) {
SaleItemResponse refreshedItem = copySaleItem(originalItem);
int pending = pendingRefunds.getOrDefault(refreshedItem.getProdId(), 0);
refreshedItem.setQuantity(Math.max(0, refreshedItem.getQuantity() - pending));
if (refreshedItem.getQuantity() > 0) {
refreshedItems.add(refreshedItem);
}
}
originalItems.setAll(refreshedItems);
tvOriginalItems.getSelectionModel().clearSelection();
tvOriginalItems.refresh();
tvRefundItems.refresh();
}
private List<SaleItemResponse> copySaleItems(List<SaleItemResponse> items) {
List<SaleItemResponse> copies = new ArrayList<>();
for (SaleItemResponse item : items) {
copies.add(copySaleItem(item));
}
return copies;
}
private SaleItemResponse copySaleItem(SaleItemResponse source) {
SaleItemResponse copy = new SaleItemResponse();
copy.setSaleItemId(source.getSaleItemId());
copy.setProdId(source.getProdId());
copy.setProductName(source.getProductName());
copy.setQuantity(source.getQuantity());
copy.setUnitPrice(source.getUnitPrice());
return copy;
}
private void updateRefundTotal() {
double total = refundItems.stream().mapToDouble(RefundItem::getTotal).sum();
lblRefundTotal.setText(currency.format(total));
}
private void closeDialog() {
Stage stage = (Stage) btnCancel.getScene().getWindow();
stage.close();
}
private List<SaleItemResponse> buildRefundableItems(SaleResponse sale, List<SaleResponse> previousRefunds) {
Map<Long, Integer> refundedByProduct = new HashMap<>();
for (SaleResponse refund : previousRefunds) {
if (refund.getItems() == null) {
continue;
}
for (SaleItemResponse refundItem : refund.getItems()) {
if (refundItem.getProdId() == null || refundItem.getQuantity() == null) {
continue;
}
refundedByProduct.merge(refundItem.getProdId(), Math.abs(refundItem.getQuantity()), Integer::sum);
}
}
List<SaleItemResponse> refundableItems = new ArrayList<>();
if (sale.getItems() == null) {
return refundableItems;
}
for (SaleItemResponse originalItem : sale.getItems()) {
if (originalItem.getProdId() == null || originalItem.getQuantity() == null) {
continue;
}
int remainingQuantity = originalItem.getQuantity() - refundedByProduct.getOrDefault(originalItem.getProdId(), 0);
if (remainingQuantity <= 0) {
continue;
}
SaleItemResponse refundableItem = new SaleItemResponse();
refundableItem.setSaleItemId(originalItem.getSaleItemId());
refundableItem.setProdId(originalItem.getProdId());
refundableItem.setProductName(originalItem.getProductName());
refundableItem.setQuantity(remainingQuantity);
refundableItem.setUnitPrice(originalItem.getUnitPrice());
refundableItems.add(refundableItem);
}
return refundableItems;
}
private void showError(String title, String message) {
Alert alert = new Alert(Alert.AlertType.ERROR);
alert.setTitle(title);
alert.setHeaderText(null);
alert.setContentText(message);
alert.showAndWait();
}
public static class RefundItem {
private final int prodId;
private final String productName;
private final int quantity;
private final double unitPrice;
public RefundItem(int prodId, String productName, int quantity, double unitPrice) {
this.prodId = prodId;
this.productName = productName;
this.quantity = quantity;
this.unitPrice = unitPrice;
}
public int getProdId() {
return prodId;
}
public String getProductName() {
return productName;
}
public int getQuantity() {
return quantity;
}
public double getUnitPrice() {
return unitPrice;
}
public double getTotal() {
return quantity * unitPrice;
}
}
}

Some files were not shown because too many files have changed in this diff Show More