Merge main into nomorebreaking
3
android/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
*.iml
|
*.iml
|
||||||
|
nohup.out
|
||||||
.gradle
|
.gradle
|
||||||
/local.properties
|
/local.properties
|
||||||
/.idea/*
|
/.idea/*
|
||||||
@@ -16,6 +17,8 @@
|
|||||||
/app/src/androidTest/
|
/app/src/androidTest/
|
||||||
/app/src/test/
|
/app/src/test/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
/.project
|
||||||
|
/.settings/
|
||||||
/build
|
/build
|
||||||
/captures
|
/captures
|
||||||
.externalNativeBuild
|
.externalNativeBuild
|
||||||
|
|||||||
4
android/app/.gitignore
vendored
@@ -1,3 +1,7 @@
|
|||||||
/build
|
/build
|
||||||
|
/nohup.out
|
||||||
|
/.classpath
|
||||||
|
/.project
|
||||||
|
/.settings/
|
||||||
/src/test/
|
/src/test/
|
||||||
/src/androidTest/
|
/src/androidTest/
|
||||||
|
|||||||
@@ -56,6 +56,9 @@ dependencies {
|
|||||||
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
|
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
|
||||||
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
|
implementation("io.reactivex.rxjava2:rxandroid:2.1.1")
|
||||||
|
|
||||||
|
implementation("com.github.bumptech.glide:glide:4.16.0")
|
||||||
|
annotationProcessor("com.github.bumptech.glide:compiler:4.16.0")
|
||||||
|
|
||||||
testImplementation(libs.junit)
|
testImplementation(libs.junit)
|
||||||
androidTestImplementation(libs.ext.junit)
|
androidTestImplementation(libs.ext.junit)
|
||||||
androidTestImplementation(libs.espresso.core)
|
androidTestImplementation(libs.espresso.core)
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
<uses-feature
|
<uses-feature
|
||||||
android:name="android.hardware.camera"
|
android:name="android.hardware.camera"
|
||||||
@@ -24,6 +25,11 @@
|
|||||||
android:roundIcon="@mipmap/ic_launcher_round"
|
android:roundIcon="@mipmap/ic_launcher_round"
|
||||||
android:supportsRtl="true"
|
android:supportsRtl="true"
|
||||||
android:theme="@style/Theme.PetStoreMobile">
|
android:theme="@style/Theme.PetStoreMobile">
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".services.ChatNotificationService"
|
||||||
|
android:exported="false" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".activities.HomeActivity"
|
android:name=".activities.HomeActivity"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
|
|||||||
BIN
android/app/src/main/ic_launcher-playstore.png
Normal file
|
After Width: | Height: | Size: 109 KiB |
@@ -7,7 +7,5 @@ public class PetStoreApplication extends Application {
|
|||||||
@Override
|
@Override
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
// Clear login data on app so when the application closes, the user is logged out and have to re-login
|
|
||||||
TokenManager.getInstance(this).clearLoginData();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,40 @@
|
|||||||
package com.example.petstoremobile.activities;
|
package com.example.petstoremobile.activities;
|
||||||
|
|
||||||
|
import android.Manifest;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
|
||||||
import androidx.activity.EdgeToEdge;
|
import androidx.activity.EdgeToEdge;
|
||||||
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.graphics.Insets;
|
import androidx.core.graphics.Insets;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowCompat;
|
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
import androidx.core.view.WindowInsetsControllerCompat;
|
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.fragments.ChatFragment;
|
import com.example.petstoremobile.fragments.ChatFragment;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.ProfileFragment;
|
import com.example.petstoremobile.fragments.ProfileFragment;
|
||||||
|
import com.example.petstoremobile.services.ChatNotificationService;
|
||||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||||
|
|
||||||
public class HomeActivity extends AppCompatActivity {
|
public class HomeActivity extends AppCompatActivity {
|
||||||
|
private BottomNavigationView bottomNav;
|
||||||
|
|
||||||
|
// Launcher to ask for notification permission
|
||||||
|
private final ActivityResultLauncher<String> requestPermissionLauncher =
|
||||||
|
registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
|
||||||
|
if (!isGranted) {
|
||||||
|
Log.w("HomeActivity", "Notification permission denied");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
@@ -34,17 +49,15 @@ public class HomeActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
|
|
||||||
//get the bottom navbar from the layout
|
//get the bottom navbar from the layout
|
||||||
BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
|
bottomNav = findViewById(R.id.bottom_navigation);
|
||||||
|
|
||||||
// Load ListFragment by default only if this is a fresh start
|
//load the list fragment by default if it's a fresh start
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
loadFragment(new ListFragment());
|
handleIntent(getIntent());
|
||||||
bottomNav.setSelectedItemId(R.id.nav_list);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//when an item in the bar is selected, load the corresponding fragment
|
//when an item in the bottom bar is selected, load the corresponding fragment
|
||||||
bottomNav.setOnItemSelectedListener(item -> {
|
bottomNav.setOnItemSelectedListener(item -> {
|
||||||
|
|
||||||
if (item.getItemId() == R.id.nav_list) {
|
if (item.getItemId() == R.id.nav_list) {
|
||||||
loadFragment(new ListFragment());
|
loadFragment(new ListFragment());
|
||||||
return true;
|
return true;
|
||||||
@@ -57,9 +70,55 @@ public class HomeActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Start the notification service and request for notification permission
|
||||||
|
startNotificationService();
|
||||||
|
requestNotificationPermission();
|
||||||
}
|
}
|
||||||
|
|
||||||
//helper function to load a fragment
|
// Handle new intents when the activity is already running,
|
||||||
|
// like clicking a notification while the app is in use
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
handleIntent(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to process intents for navigation.
|
||||||
|
// like clicking a notification or just launching the app from a fresh start
|
||||||
|
private void handleIntent(Intent intent) {
|
||||||
|
if (intent != null && "chat".equals(intent.getStringExtra("navigate_to"))) {
|
||||||
|
ChatFragment chatFragment = new ChatFragment();
|
||||||
|
if (intent.hasExtra("conversation_id")) {
|
||||||
|
Bundle args = new Bundle();
|
||||||
|
args.putLong("conversation_id", intent.getLongExtra("conversation_id", -1));
|
||||||
|
chatFragment.setArguments(args);
|
||||||
|
}
|
||||||
|
loadFragment(chatFragment);
|
||||||
|
bottomNav.setSelectedItemId(R.id.nav_chat);
|
||||||
|
} else {
|
||||||
|
loadFragment(new ListFragment());
|
||||||
|
bottomNav.setSelectedItemId(R.id.nav_list);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to start the notification service in the background
|
||||||
|
// to receive notifications when a new conversation is created
|
||||||
|
private void startNotificationService() {
|
||||||
|
Intent serviceIntent = new Intent(this, ChatNotificationService.class);
|
||||||
|
startService(serviceIntent);
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to request for notification permission
|
||||||
|
private void requestNotificationPermission() {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||||
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||||
|
requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to load a fragment
|
||||||
private void loadFragment(Fragment fragment) {
|
private void loadFragment(Fragment fragment) {
|
||||||
getSupportFragmentManager()
|
getSupportFragmentManager()
|
||||||
.beginTransaction()
|
.beginTransaction()
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.example.petstoremobile.api.auth.AuthApi;
|
|||||||
import com.example.petstoremobile.api.auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
import com.example.petstoremobile.api.RetrofitClient;
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
import com.example.petstoremobile.dtos.AuthDTO;
|
import com.example.petstoremobile.dtos.AuthDTO;
|
||||||
|
import com.example.petstoremobile.dtos.UserDTO;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -38,11 +39,17 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
// Check if user is already logged in
|
// Check if user is already logged in
|
||||||
if (TokenManager.getInstance(this).isLoggedIn()) {
|
TokenManager tokenManager = TokenManager.getInstance(this);
|
||||||
Intent intent = new Intent(this, HomeActivity.class);
|
if (tokenManager.isLoggedIn()) {
|
||||||
startActivity(intent);
|
if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) {
|
||||||
finish();
|
// If a customer somehow remained logged in, clear them out
|
||||||
return;
|
tokenManager.clearLoginData();
|
||||||
|
} else {
|
||||||
|
Intent intent = new Intent(this, HomeActivity.class);
|
||||||
|
startActivity(intent);
|
||||||
|
finish();
|
||||||
|
return;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
EdgeToEdge.enable(this);
|
EdgeToEdge.enable(this);
|
||||||
@@ -82,19 +89,28 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<AuthDTO.LoginResponse> call, Response<AuthDTO.LoginResponse> response) {
|
public void onResponse(Call<AuthDTO.LoginResponse> call, Response<AuthDTO.LoginResponse> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
String role = response.body().getRole();
|
||||||
|
|
||||||
|
// Check if the user is a CUSTOMER and deny login if so
|
||||||
|
if ("CUSTOMER".equalsIgnoreCase(role)) {
|
||||||
|
Toast.makeText(MainActivity.this, "Access denied: Customers are not allowed to log in.", Toast.LENGTH_LONG).show();
|
||||||
|
tvLoginStatus.setText("Customers are not allowed to log in");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
//save login data in shared preferences
|
//save login data in shared preferences
|
||||||
TokenManager.getInstance(MainActivity.this).saveLoginData(
|
TokenManager.getInstance(MainActivity.this).saveLoginData(
|
||||||
response.body().getToken(),
|
response.body().getToken(),
|
||||||
response.body().getUsername(),
|
response.body().getUsername(),
|
||||||
response.body().getRole()
|
role
|
||||||
);
|
);
|
||||||
|
|
||||||
//fetch user id from api then login to home activity
|
//fetch user id from api then login to home activity
|
||||||
RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser()
|
RetrofitClient.getAuthApi(MainActivity.this).getMe()
|
||||||
.enqueue(new Callback<AuthDTO.UserResponse>() {
|
.enqueue(new Callback<UserDTO>() {
|
||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<AuthDTO.UserResponse> call,
|
public void onResponse(Call<UserDTO> call,
|
||||||
Response<AuthDTO.UserResponse> response) {
|
Response<UserDTO> response) {
|
||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
TokenManager.getInstance(MainActivity.this)
|
TokenManager.getInstance(MainActivity.this)
|
||||||
.saveUserId(response.body().getId());
|
.saveUserId(response.body().getId());
|
||||||
@@ -106,7 +122,7 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<AuthDTO.UserResponse> call,
|
public void onFailure(Call<UserDTO> call,
|
||||||
Throwable t) {
|
Throwable t) {
|
||||||
Log.e("MainActivity", "Failed to fetch userId", t);
|
Log.e("MainActivity", "Failed to fetch userId", t);
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,23 @@ package com.example.petstoremobile.api;
|
|||||||
import com.example.petstoremobile.dtos.PageResponse;
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
|
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
import retrofit2.http.DELETE;
|
import retrofit2.http.DELETE;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.Multipart;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
import retrofit2.http.PUT;
|
import retrofit2.http.PUT;
|
||||||
|
import retrofit2.http.Part;
|
||||||
import retrofit2.http.Path;
|
import retrofit2.http.Path;
|
||||||
import retrofit2.http.Query;
|
import retrofit2.http.Query;
|
||||||
|
|
||||||
//api calls to CRUD pets
|
//api calls to CRUD pets
|
||||||
public interface PetApi {
|
public interface PetApi {
|
||||||
|
// endpoint for downloading the pet's image file
|
||||||
|
String PET_IMAGE_PATH = "api/v1/pets/%d/image";
|
||||||
|
|
||||||
// Get all pets
|
// Get all pets
|
||||||
@GET("api/v1/pets")
|
@GET("api/v1/pets")
|
||||||
Call<PageResponse<PetDTO>> getAllPets(
|
Call<PageResponse<PetDTO>> getAllPets(
|
||||||
@@ -37,4 +43,9 @@ public interface PetApi {
|
|||||||
@DELETE("api/v1/pets/{id}")
|
@DELETE("api/v1/pets/{id}")
|
||||||
Call<Void> deletePet(@Path("id") Long id);
|
Call<Void> deletePet(@Path("id") Long id);
|
||||||
|
|
||||||
|
// Upload pet image
|
||||||
|
@Multipart
|
||||||
|
@POST("api/v1/pets/{id}/image")
|
||||||
|
Call<Void> uploadPetImage(@Path("id") Long id, @Part MultipartBody.Part image);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.petstoremobile.api;
|
package com.example.petstoremobile.api;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.os.Build;
|
||||||
|
|
||||||
import com.example.petstoremobile.api.auth.AuthApi;
|
import com.example.petstoremobile.api.auth.AuthApi;
|
||||||
import com.example.petstoremobile.api.auth.AuthInterceptor;
|
import com.example.petstoremobile.api.auth.AuthInterceptor;
|
||||||
@@ -12,9 +13,23 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
|||||||
|
|
||||||
//Retrofit client Used for API calls
|
//Retrofit client Used for API calls
|
||||||
public class RetrofitClient {
|
public class RetrofitClient {
|
||||||
//base URL
|
public static final String BASE_URL = getBaseUrl();
|
||||||
public static final String BASE_URL = "http://10.0.2.2:8080"; //for emulator testing
|
|
||||||
// public static final String BASE_URL = "http://10.0.0.200:8080/"; //for hardware testing
|
// Helper function to determine BASE_URL based on whether we are testing on an emulator or a real device
|
||||||
|
private static String getBaseUrl() {
|
||||||
|
if (Build.FINGERPRINT.contains("generic")
|
||||||
|
|| Build.FINGERPRINT.contains("unknown")
|
||||||
|
|| Build.MODEL.contains("google_sdk")
|
||||||
|
|| Build.MODEL.contains("Emulator")
|
||||||
|
|| Build.MODEL.contains("Android SDK built for x86")
|
||||||
|
|| Build.MANUFACTURER.contains("Genymotion")
|
||||||
|
|| (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic"))
|
||||||
|
|| "google_sdk".equals(Build.PRODUCT)) {
|
||||||
|
return "http://10.0.2.2:8080/"; //emulator testing
|
||||||
|
} else {
|
||||||
|
return "http://10.0.0.200:8080/"; //Hardware testing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static Retrofit retrofit = null;
|
private static Retrofit retrofit = null;
|
||||||
|
|
||||||
@@ -95,7 +110,6 @@ public class RetrofitClient {
|
|||||||
return getClient(context).create(MessageApi.class);
|
return getClient(context).create(MessageApi.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static StoreApi getStoreApi(Context context) {
|
public static StoreApi getStoreApi(Context context) {
|
||||||
return getClient(context).create(StoreApi.class);
|
return getClient(context).create(StoreApi.class);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,17 +1,40 @@
|
|||||||
package com.example.petstoremobile.api.auth;
|
package com.example.petstoremobile.api.auth;
|
||||||
|
|
||||||
import com.example.petstoremobile.dtos.AuthDTO;
|
import com.example.petstoremobile.dtos.AuthDTO;
|
||||||
|
import com.example.petstoremobile.dtos.UserDTO;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.http.Body;
|
import retrofit2.http.Body;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.Multipart;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
|
import retrofit2.http.PUT;
|
||||||
|
import retrofit2.http.Part;
|
||||||
|
|
||||||
//Api for logging in and getting current user
|
//Api for logging in and getting current user
|
||||||
public interface AuthApi {
|
public interface AuthApi {
|
||||||
|
|
||||||
|
// endpoint for downloading the current user's avatar file
|
||||||
|
String AVATAR_FILE_PATH = "api/v1/auth/me/avatar/file";
|
||||||
|
|
||||||
|
//login endpoint
|
||||||
@POST("api/v1/auth/login")
|
@POST("api/v1/auth/login")
|
||||||
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
||||||
|
|
||||||
|
//get current user endpoint
|
||||||
@GET("api/v1/auth/me")
|
@GET("api/v1/auth/me")
|
||||||
Call<AuthDTO.UserResponse> getCurrentUser();
|
Call<UserDTO> getMe();
|
||||||
|
|
||||||
|
//update current user endpoint
|
||||||
|
@PUT("api/v1/auth/me")
|
||||||
|
Call<UserDTO> updateMe(@Body Map<String, String> updates);
|
||||||
|
|
||||||
|
//upload avatar endpoint
|
||||||
|
@Multipart
|
||||||
|
@POST("api/v1/auth/me/avatar")
|
||||||
|
Call<UserDTO> uploadAvatar(@Part MultipartBody.Part avatar);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package com.example.petstoremobile.dtos;
|
||||||
|
|
||||||
|
//Used to get messages of any errors from the backend
|
||||||
|
|
||||||
|
public class ErrorResponse {
|
||||||
|
private String message;
|
||||||
|
|
||||||
|
public String getMessage() {
|
||||||
|
return message;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMessage(String message) {
|
||||||
|
this.message = message;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package com.example.petstoremobile.dtos;
|
||||||
|
|
||||||
|
public class UserDTO {
|
||||||
|
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;
|
||||||
|
|
||||||
|
// Getters
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFullName() {
|
||||||
|
return fullName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhone() {
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAvatarUrl() {
|
||||||
|
return avatarUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getRole() {
|
||||||
|
return role;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getStoreId() {
|
||||||
|
return storeId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getStoreName() {
|
||||||
|
return storeName;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,7 @@ import com.example.petstoremobile.dtos.PageResponse;
|
|||||||
import com.example.petstoremobile.dtos.SendMessageRequest;
|
import com.example.petstoremobile.dtos.SendMessageRequest;
|
||||||
import com.example.petstoremobile.models.Chat;
|
import com.example.petstoremobile.models.Chat;
|
||||||
import com.example.petstoremobile.models.Message;
|
import com.example.petstoremobile.models.Message;
|
||||||
|
import com.example.petstoremobile.services.ChatNotificationService;
|
||||||
import com.example.petstoremobile.websocket.StompChatManager;
|
import com.example.petstoremobile.websocket.StompChatManager;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -40,6 +41,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
private RecyclerView rvChatList, rvMessages;
|
private RecyclerView rvChatList, rvMessages;
|
||||||
private EditText etMessage;
|
private EditText etMessage;
|
||||||
private Button btnSend;
|
private Button btnSend;
|
||||||
|
private TextView tvChatTitle;
|
||||||
|
|
||||||
// Adapters
|
// Adapters
|
||||||
private ChatAdapter chatAdapter;
|
private ChatAdapter chatAdapter;
|
||||||
@@ -75,6 +77,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
rvMessages = view.findViewById(R.id.rvMessages);
|
rvMessages = view.findViewById(R.id.rvMessages);
|
||||||
etMessage = view.findViewById(R.id.etMessage);
|
etMessage = view.findViewById(R.id.etMessage);
|
||||||
btnSend = view.findViewById(R.id.btnSend);
|
btnSend = view.findViewById(R.id.btnSend);
|
||||||
|
tvChatTitle = view.findViewById(R.id.tvChatTitle);
|
||||||
|
|
||||||
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
|
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
|
||||||
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
|
hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START));
|
||||||
@@ -121,6 +124,10 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
Log.e(TAG, "No token found");
|
Log.e(TAG, "No token found");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (getArguments() != null && getArguments().containsKey("conversation_id")) {
|
||||||
|
activeConversationId = getArguments().getLong("conversation_id");
|
||||||
|
}
|
||||||
|
|
||||||
loadCustomers();
|
loadCustomers();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,7 +172,21 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
chatList.addAll(loaded);
|
chatList.addAll(loaded);
|
||||||
chatAdapter.notifyDataSetChanged();
|
chatAdapter.notifyDataSetChanged();
|
||||||
if (activeConversationId == null) {
|
|
||||||
|
if (activeConversationId != null) {
|
||||||
|
setConversationActive(true);
|
||||||
|
// Update title to customer name of active conversation
|
||||||
|
for (Chat chat : chatList) {
|
||||||
|
if (chat.getChatId().equals(String.valueOf(activeConversationId))) {
|
||||||
|
tvChatTitle.setText(chat.getCustomerName());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (stompChatManager != null) {
|
||||||
|
stompChatManager.subscribeToConversation(activeConversationId);
|
||||||
|
}
|
||||||
|
loadMessageHistory(activeConversationId);
|
||||||
|
} else {
|
||||||
messageList.clear();
|
messageList.clear();
|
||||||
messageAdapter.notifyDataSetChanged();
|
messageAdapter.notifyDataSetChanged();
|
||||||
setConversationActive(false);
|
setConversationActive(false);
|
||||||
@@ -186,6 +207,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
public void onChatClick(Chat chat) {
|
public void onChatClick(Chat chat) {
|
||||||
activeConversationId = Long.parseLong(chat.getChatId());
|
activeConversationId = Long.parseLong(chat.getChatId());
|
||||||
setConversationActive(true);
|
setConversationActive(true);
|
||||||
|
tvChatTitle.setText(chat.getCustomerName());
|
||||||
drawerLayout.closeDrawer(GravityCompat.START);
|
drawerLayout.closeDrawer(GravityCompat.START);
|
||||||
|
|
||||||
if (stompChatManager != null) {
|
if (stompChatManager != null) {
|
||||||
@@ -305,6 +327,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
|
|
||||||
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
||||||
setConversationActive(true);
|
setConversationActive(true);
|
||||||
|
tvChatTitle.setText(name);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,6 +409,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
etMessage.setEnabled(active);
|
etMessage.setEnabled(active);
|
||||||
if (!active) {
|
if (!active) {
|
||||||
activeConversationId = null;
|
activeConversationId = null;
|
||||||
|
ChatNotificationService.activeConversationIdInUi = null;
|
||||||
|
if (tvChatTitle != null) tvChatTitle.setText("Customer Chat");
|
||||||
if (stompChatManager != null) {
|
if (stompChatManager != null) {
|
||||||
stompChatManager.clearConversationSubscription();
|
stompChatManager.clearConversationSubscription();
|
||||||
}
|
}
|
||||||
@@ -395,6 +420,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
etMessage.setHint("Select a chat to start messaging");
|
etMessage.setHint("Select a chat to start messaging");
|
||||||
} else {
|
} else {
|
||||||
etMessage.setHint("Type a message...");
|
etMessage.setHint("Type a message...");
|
||||||
|
ChatNotificationService.activeConversationIdInUi = activeConversationId;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,6 +428,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
|
ChatNotificationService.activeConversationIdInUi = null;
|
||||||
if (stompChatManager != null) stompChatManager.disconnect();
|
if (stompChatManager != null) stompChatManager.disconnect();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import android.widget.LinearLayout;
|
|||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
|
|
||||||
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
||||||
@@ -56,6 +57,13 @@ public class ListFragment extends Fragment {
|
|||||||
drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView);
|
drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView);
|
||||||
|
|
||||||
|
|
||||||
|
// Check user role and restrict access for STAFF
|
||||||
|
String role = TokenManager.getInstance(requireContext()).getRole();
|
||||||
|
if ("STAFF".equalsIgnoreCase(role)) {
|
||||||
|
drawerSuppliers.setVisibility(View.GONE);
|
||||||
|
drawerInventory.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
//needed to disable touches on the innerContainer while the drawer is open
|
//needed to disable touches on the innerContainer while the drawer is open
|
||||||
touchBlocker = view.findViewById(R.id.touchBlocker);
|
touchBlocker = view.findViewById(R.id.touchBlocker);
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
|
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
import android.text.InputType;
|
import android.text.InputType;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
@@ -23,20 +24,43 @@ import android.widget.Button;
|
|||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
|
import com.bumptech.glide.load.model.GlideUrl;
|
||||||
|
import com.bumptech.glide.load.model.LazyHeaders;
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.activities.MainActivity;
|
import com.example.petstoremobile.activities.MainActivity;
|
||||||
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
|
import com.example.petstoremobile.api.auth.AuthApi;
|
||||||
import com.example.petstoremobile.api.auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
|
import com.example.petstoremobile.dtos.ErrorResponse;
|
||||||
|
import com.example.petstoremobile.dtos.UserDTO;
|
||||||
|
import com.example.petstoremobile.services.ChatNotificationService;
|
||||||
|
import com.example.petstoremobile.utils.InputValidator;
|
||||||
|
import com.google.gson.Gson;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class ProfileFragment extends Fragment {
|
public class ProfileFragment extends Fragment {
|
||||||
|
|
||||||
//initialize the view/controls
|
//initialize the view/controls
|
||||||
private ImageView imgProfile;
|
private ImageView imgProfile;
|
||||||
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
||||||
private Button btnChangePhoto, btnEditEmail, btnEditPhone, btnLogout;
|
|
||||||
private Uri photoUri;
|
private Uri photoUri;
|
||||||
|
private UserDTO currentUser;
|
||||||
|
|
||||||
//Initialize the launchers for camera and gallery
|
//Initialize the launchers for camera and gallery
|
||||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||||
@@ -58,8 +82,7 @@ public class ProfileFragment extends Fragment {
|
|||||||
&& result.getData() != null) {
|
&& result.getData() != null) {
|
||||||
//get the selected image and set the image to the profile
|
//get the selected image and set the image to the profile
|
||||||
Uri selectedImage = result.getData().getData();
|
Uri selectedImage = result.getData().getData();
|
||||||
imgProfile.setImageURI(selectedImage);
|
uploadAvatar(selectedImage);
|
||||||
//TODO: SAVE CHANGED PHOTO TO DATABASE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -71,10 +94,7 @@ public class ProfileFragment extends Fragment {
|
|||||||
success -> {
|
success -> {
|
||||||
//if a photo is taken set the image profile to it otherwise do nothing
|
//if a photo is taken set the image profile to it otherwise do nothing
|
||||||
if (success) {
|
if (success) {
|
||||||
//Clear the old image and set the new one
|
uploadAvatar(photoUri);
|
||||||
imgProfile.setImageURI(null);
|
|
||||||
imgProfile.setImageURI(photoUri);
|
|
||||||
//TODO: SAVE CHANGED PHOTO TO DATABASE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -107,7 +127,6 @@ public class ProfileFragment extends Fragment {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
//TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
@@ -119,10 +138,13 @@ public class ProfileFragment extends Fragment {
|
|||||||
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
|
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
|
||||||
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
|
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
|
||||||
tvProfileRole = view.findViewById(R.id.tvProfileRole);
|
tvProfileRole = view.findViewById(R.id.tvProfileRole);
|
||||||
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
Button btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
||||||
btnEditEmail = view.findViewById(R.id.btnEditEmail);
|
Button btnEditEmail = view.findViewById(R.id.btnEditEmail);
|
||||||
btnEditPhone = view.findViewById(R.id.btnEditPhone);
|
Button btnEditPhone = view.findViewById(R.id.btnEditPhone);
|
||||||
btnLogout = view.findViewById(R.id.btnLogout);
|
Button btnLogout = view.findViewById(R.id.btnLogout);
|
||||||
|
|
||||||
|
//Load Profile Data from backend
|
||||||
|
loadProfileData();
|
||||||
|
|
||||||
//Set up listeners for the buttons
|
//Set up listeners for the buttons
|
||||||
//Change photo button
|
//Change photo button
|
||||||
@@ -150,7 +172,6 @@ public class ProfileFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.show();
|
.show();
|
||||||
//TODO: UPDATE PHOTO IN DATABASE
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//Edit email button
|
//Edit email button
|
||||||
@@ -170,19 +191,10 @@ public class ProfileFragment extends Fragment {
|
|||||||
.setTitle("Edit Email")
|
.setTitle("Edit Email")
|
||||||
.setView(input)
|
.setView(input)
|
||||||
.setPositiveButton("Save", (dialog, which) -> {
|
.setPositiveButton("Save", (dialog, which) -> {
|
||||||
String newEmail = input.getText().toString();
|
if (InputValidator.isValidEmail(input)) {
|
||||||
//if the new value is a valid email then set the email to the new value
|
updateProfileField("email", input.getText().toString());
|
||||||
if (android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches()) {
|
} else {
|
||||||
tvProfileEmail.setText(newEmail);
|
Toast.makeText(requireContext(), "Email is invalid", Toast.LENGTH_SHORT).show();
|
||||||
//TODO: UPDATE THE EMAIL IN DATABASE
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//tell the user to email is invalid
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle("Error")
|
|
||||||
.setMessage("Email is invalid")
|
|
||||||
.setPositiveButton("OK", null)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton("Cancel", null)
|
.setNegativeButton("Cancel", null)
|
||||||
@@ -210,19 +222,10 @@ public class ProfileFragment extends Fragment {
|
|||||||
.setTitle("Edit Phone Number")
|
.setTitle("Edit Phone Number")
|
||||||
.setView(input)
|
.setView(input)
|
||||||
.setPositiveButton("Save", (dialog, which) -> {
|
.setPositiveButton("Save", (dialog, which) -> {
|
||||||
String newPhone = input.getText().toString();
|
if (InputValidator.isValidPhone(input)) {
|
||||||
//if the new value is format: (XXX) XXX-XXXX then set the phone to the new value
|
updateProfileField("phone", input.getText().toString());
|
||||||
if (newPhone.matches("\\(\\d{3}\\) \\d{3}-\\d{4}")) { //TODO MAKE VALIDATION CLASS INSTEAD FOR THIS
|
} else {
|
||||||
tvProfilePhone.setText(newPhone);
|
Toast.makeText(requireContext(), "Phone number is invalid", Toast.LENGTH_SHORT).show();
|
||||||
//TODO: UPDATE PHONE IN DATABASE
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
//tell the user to email cannot be empty
|
|
||||||
new AlertDialog.Builder(requireContext())
|
|
||||||
.setTitle("Error")
|
|
||||||
.setMessage("Phone number is invalid. Format: (XXX) XXX-XXXX")
|
|
||||||
.setPositiveButton("OK", null)
|
|
||||||
.show();
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.setNegativeButton("Cancel", null)
|
.setNegativeButton("Cancel", null)
|
||||||
@@ -231,6 +234,10 @@ public class ProfileFragment extends Fragment {
|
|||||||
|
|
||||||
//Logout button
|
//Logout button
|
||||||
btnLogout.setOnClickListener(v -> {
|
btnLogout.setOnClickListener(v -> {
|
||||||
|
// Stop notification service before logging out so notifications stop
|
||||||
|
Intent serviceIntent = new Intent(requireContext(), ChatNotificationService.class);
|
||||||
|
requireContext().stopService(serviceIntent);
|
||||||
|
|
||||||
TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login
|
TokenManager.getInstance(requireContext()).clearLoginData(); // clear the token for next login
|
||||||
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
|
//get the intent to the main activity and clear the back stack so the back button won't allow the user to go back to the previous screen
|
||||||
Intent intent = new Intent(getActivity(), MainActivity.class);
|
Intent intent = new Intent(getActivity(), MainActivity.class);
|
||||||
@@ -252,4 +259,152 @@ public class ProfileFragment extends Fragment {
|
|||||||
//launch the camera to capture the photo and save the photo to photoUri
|
//launch the camera to capture the photo and save the photo to photoUri
|
||||||
cameraLauncher.launch(photoUri);
|
cameraLauncher.launch(photoUri);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Helper function to call the backend to get profile data and load it to the view
|
||||||
|
private void loadProfileData() {
|
||||||
|
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
|
||||||
|
|
||||||
|
authApi.getMe().enqueue(new Callback<UserDTO>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||||
|
//if the response is successful and the body is not null then set the user to the view
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
currentUser = response.body();
|
||||||
|
|
||||||
|
//set the user data to the view
|
||||||
|
tvProfileName.setText(currentUser.getFullName());
|
||||||
|
tvProfileEmail.setText(currentUser.getEmail());
|
||||||
|
tvProfilePhone.setText(currentUser.getPhone());
|
||||||
|
tvProfileRole.setText(currentUser.getRole());
|
||||||
|
|
||||||
|
// get the avatar endpoint to load profile image and the token for authorization
|
||||||
|
String avatarUrl = RetrofitClient.BASE_URL + AuthApi.AVATAR_FILE_PATH;
|
||||||
|
String token = TokenManager.getInstance(requireContext()).getToken();
|
||||||
|
|
||||||
|
if (token != null) {
|
||||||
|
// Create GlideUrl with token to fetch the image
|
||||||
|
GlideUrl glideUrl = new GlideUrl(avatarUrl, new LazyHeaders.Builder()
|
||||||
|
.addHeader("Authorization", "Bearer " + token)
|
||||||
|
.build());
|
||||||
|
|
||||||
|
// Load image using Glide
|
||||||
|
Glide.with(ProfileFragment.this)
|
||||||
|
.load(glideUrl)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.placeholder(R.drawable.placeholder)
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(imgProfile);
|
||||||
|
} else {
|
||||||
|
// load placeholder image if token is null
|
||||||
|
Glide.with(ProfileFragment.this)
|
||||||
|
.load(R.drawable.placeholder)
|
||||||
|
.into(imgProfile);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
Log.e("onResponse: ", response.message());
|
||||||
|
Toast.makeText(getContext(), "Failed to load profile: ", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<UserDTO> call, Throwable t) {
|
||||||
|
Log.e("PROFILE", "onFailure: " + t.getMessage());
|
||||||
|
Toast.makeText(getContext(), "Network error: could not load profile", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to call the backend to upload a profile image
|
||||||
|
private void uploadAvatar(Uri uri) {
|
||||||
|
try {
|
||||||
|
File file = getFileFromUri(uri);
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
|
// Create RequestBody for file upload
|
||||||
|
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
|
||||||
|
MultipartBody.Part body = MultipartBody.Part.createFormData("avatar", file.getName(), requestFile);
|
||||||
|
|
||||||
|
//Call the backend to upload the avatar
|
||||||
|
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
|
||||||
|
authApi.uploadAvatar(body).enqueue(new Callback<UserDTO>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
currentUser = response.body();
|
||||||
|
Toast.makeText(requireContext(), "Avatar updated successfully", Toast.LENGTH_SHORT).show();
|
||||||
|
// Reload image after successful upload
|
||||||
|
loadProfileData();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "Failed to upload avatar", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<UserDTO> call, Throwable t) {
|
||||||
|
Log.e("UPLOAD_AVATAR", "Failure: " + t.getMessage());
|
||||||
|
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UPLOAD_AVATAR", "Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a temporary File object from a Uri for uploading the avatar
|
||||||
|
private File getFileFromUri(Uri uri) {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
|
||||||
|
File tempFile = new File(requireContext().getCacheDir(), "upload_avatar.jpg");
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(tempFile);
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int length;
|
||||||
|
while ((length = inputStream.read(buffer)) > 0) {
|
||||||
|
outputStream.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
outputStream.close();
|
||||||
|
inputStream.close();
|
||||||
|
return tempFile;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("FILE_UTILS", "Error creating temp file", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//Helper function to update a profile field in the backend
|
||||||
|
private void updateProfileField(String fieldName, String value) {
|
||||||
|
AuthApi authApi = RetrofitClient.getAuthApi(requireContext());
|
||||||
|
Map<String, String> updates = new HashMap<>();
|
||||||
|
updates.put(fieldName, value);
|
||||||
|
|
||||||
|
authApi.updateMe(updates).enqueue(new Callback<UserDTO>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<UserDTO> call, Response<UserDTO> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
currentUser = response.body();
|
||||||
|
// Update the view with the new data from backend
|
||||||
|
tvProfileEmail.setText(currentUser.getEmail());
|
||||||
|
tvProfilePhone.setText(currentUser.getPhone());
|
||||||
|
Toast.makeText(requireContext(), "Profile updated successfully", Toast.LENGTH_SHORT).show();
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
String errorJson = response.errorBody().string();
|
||||||
|
ErrorResponse errorResponse = new Gson().fromJson(errorJson, ErrorResponse.class);
|
||||||
|
String errorMessage = errorResponse.getMessage();
|
||||||
|
Toast.makeText(requireContext(), errorMessage, Toast.LENGTH_LONG).show();
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UPDATE_PROFILE", "Error parsing error body", e);
|
||||||
|
Toast.makeText(requireContext(), "Failed to update profile", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<UserDTO> call, Throwable t) {
|
||||||
|
Log.e("UPDATE_PROFILE", "Failure: " + t.getMessage());
|
||||||
|
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -13,8 +13,11 @@ import android.util.Log;
|
|||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.AdapterView;
|
||||||
|
import android.widget.ArrayAdapter;
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
import android.widget.Spinner;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
@@ -43,6 +46,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
private PetApi api;
|
private PetApi api;
|
||||||
private SwipeRefreshLayout swipeRefreshLayout;
|
private SwipeRefreshLayout swipeRefreshLayout;
|
||||||
private EditText etSearch;
|
private EditText etSearch;
|
||||||
|
private Spinner spinnerStatus;
|
||||||
|
|
||||||
//load pet view
|
//load pet view
|
||||||
@Override
|
@Override
|
||||||
@@ -57,6 +61,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
|
|
||||||
setupRecyclerView(view);
|
setupRecyclerView(view);
|
||||||
setupSearch(view);
|
setupSearch(view);
|
||||||
|
setupStatusFilter(view);
|
||||||
setupSwipeRefresh(view);
|
setupSwipeRefresh(view);
|
||||||
loadPetData();
|
loadPetData();
|
||||||
|
|
||||||
@@ -82,24 +87,48 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
etSearch.addTextChangedListener(new TextWatcher() {
|
etSearch.addTextChangedListener(new TextWatcher() {
|
||||||
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||||
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||||
filterPets(s.toString());
|
filterPets();
|
||||||
}
|
}
|
||||||
@Override public void afterTextChanged(Editable s) {}
|
@Override public void afterTextChanged(Editable s) {}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void filterPets(String query) {
|
//Setup the status filter spinner
|
||||||
|
private void setupStatusFilter(View view) {
|
||||||
|
spinnerStatus = view.findViewById(R.id.spinnerStatus);
|
||||||
|
String[] statuses = {"All Statuses", "Available", "Adopted"};
|
||||||
|
ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_spinner_item, statuses);
|
||||||
|
adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||||
|
spinnerStatus.setAdapter(adapter);
|
||||||
|
|
||||||
|
spinnerStatus.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
|
@Override
|
||||||
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
|
filterPets();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onNothingSelected(AdapterView<?> parent) {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to filter pets based on search and status filter
|
||||||
|
private void filterPets() {
|
||||||
|
String query = etSearch.getText().toString().toLowerCase();
|
||||||
|
String selectedStatus = spinnerStatus.getSelectedItem().toString();
|
||||||
|
|
||||||
filteredList.clear();
|
filteredList.clear();
|
||||||
if (query.isEmpty()) {
|
for (PetDTO p : petList) {
|
||||||
filteredList.addAll(petList);
|
boolean matchesSearch = query.isEmpty() ||
|
||||||
} else {
|
p.getPetName().toLowerCase().contains(query) ||
|
||||||
String lower = query.toLowerCase();
|
p.getPetSpecies().toLowerCase().contains(query) ||
|
||||||
for (PetDTO p : petList) {
|
p.getPetBreed().toLowerCase().contains(query);
|
||||||
if (p.getPetName().toLowerCase().contains(lower)
|
|
||||||
|| p.getPetSpecies().toLowerCase().contains(lower)
|
boolean matchesStatus = selectedStatus.equals("All Statuses") ||
|
||||||
|| p.getPetBreed().toLowerCase().contains(lower)) {
|
p.getPetStatus().equalsIgnoreCase(selectedStatus);
|
||||||
filteredList.add(p);
|
|
||||||
}
|
if (matchesSearch && matchesStatus) {
|
||||||
|
filteredList.add(p);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
adapter.notifyDataSetChanged();
|
adapter.notifyDataSetChanged();
|
||||||
@@ -173,7 +202,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
if (response.isSuccessful() && response.body() != null) {
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
petList.clear();
|
petList.clear();
|
||||||
petList.addAll(response.body().getContent());
|
petList.addAll(response.body().getContent());
|
||||||
filterPets(etSearch.getText().toString());
|
filterPets();
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Log.e("onResponse: ", response.message());
|
Log.e("onResponse: ", response.message());
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ import com.example.petstoremobile.api.RetrofitClient;
|
|||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
||||||
|
import com.example.petstoremobile.utils.ActivityLogger;
|
||||||
|
import com.example.petstoremobile.utils.InputValidator;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -65,26 +67,27 @@ public class PetDetailFragment extends Fragment {
|
|||||||
|
|
||||||
//Method to Update or Add a pet
|
//Method to Update or Add a pet
|
||||||
private void savePet() {
|
private void savePet() {
|
||||||
|
// Validates all fields using InputValidator
|
||||||
|
if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return;
|
||||||
|
if (!InputValidator.isNotEmpty(etPetSpecies, "Species")) return;
|
||||||
|
if (!InputValidator.isNotEmpty(etPetBreed, "Breed")) return;
|
||||||
|
if (!InputValidator.isPositiveInteger(etPetAge, "Age")) return;
|
||||||
|
if (!InputValidator.isPositiveDecimal(etPetPrice, "Price")) return;
|
||||||
|
|
||||||
//get all the values from the fields
|
//get all the values from the fields
|
||||||
String name = etPetName.getText().toString().trim();
|
String name = etPetName.getText().toString().trim();
|
||||||
String species = etPetSpecies.getText().toString().trim();
|
String species = etPetSpecies.getText().toString().trim();
|
||||||
String breed = etPetBreed.getText().toString().trim();
|
String breed = etPetBreed.getText().toString().trim();
|
||||||
String ageStr = etPetAge.getText().toString().trim();
|
int age = Integer.parseInt(etPetAge.getText().toString().trim());
|
||||||
String priceStr = etPetPrice.getText().toString().trim();
|
String priceStr = etPetPrice.getText().toString().trim();
|
||||||
String status = spinnerPetStatus.getSelectedItem().toString();
|
String status = spinnerPetStatus.getSelectedItem().toString();
|
||||||
|
|
||||||
//check if all the fields are filled
|
|
||||||
if (name.isEmpty() || species.isEmpty() || breed.isEmpty() || ageStr.isEmpty() || priceStr.isEmpty()) {
|
|
||||||
Toast.makeText(getContext(), "Please fill in all fields", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//create a pet object to send to the API
|
//create a pet object to send to the API
|
||||||
PetDTO petDTO = new PetDTO();
|
PetDTO petDTO = new PetDTO();
|
||||||
petDTO.setPetName(name);
|
petDTO.setPetName(name);
|
||||||
petDTO.setPetSpecies(species);
|
petDTO.setPetSpecies(species);
|
||||||
petDTO.setPetBreed(breed);
|
petDTO.setPetBreed(breed);
|
||||||
petDTO.setPetAge(Integer.parseInt(ageStr));
|
petDTO.setPetAge(age);
|
||||||
petDTO.setPetPrice(priceStr);
|
petDTO.setPetPrice(priceStr);
|
||||||
petDTO.setPetStatus(status);
|
petDTO.setPetStatus(status);
|
||||||
|
|
||||||
@@ -98,6 +101,7 @@ public class PetDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId);
|
||||||
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -107,6 +111,7 @@ public class PetDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<PetDTO> call, Throwable t) {
|
public void onFailure(Call<PetDTO> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "PetDetailFragment.updatePet", new Exception(t));
|
||||||
Log.e("PetDetailFragment", "Error updating pet", t);
|
Log.e("PetDetailFragment", "Error updating pet", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
@@ -117,6 +122,7 @@ public class PetDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.log(requireContext(), "Added new Pet: " + name);
|
||||||
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -126,6 +132,7 @@ public class PetDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<PetDTO> call, Throwable t) {
|
public void onFailure(Call<PetDTO> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "PetDetailFragment.createPet", new Exception(t));
|
||||||
Log.e("PetDetailFragment", "Error adding pet", t);
|
Log.e("PetDetailFragment", "Error adding pet", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
@@ -146,6 +153,7 @@ public class PetDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId);
|
||||||
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -155,6 +163,7 @@ public class PetDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<Void> call, Throwable t) {
|
public void onFailure(Call<Void> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t));
|
||||||
Log.e("PetDetailFragment", "Error deleting pet", t);
|
Log.e("PetDetailFragment", "Error deleting pet", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import com.example.petstoremobile.api.ServiceApi;
|
|||||||
import com.example.petstoremobile.dtos.ServiceDTO;
|
import com.example.petstoremobile.dtos.ServiceDTO;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
||||||
|
import com.example.petstoremobile.utils.ActivityLogger;
|
||||||
|
import com.example.petstoremobile.utils.InputValidator;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -58,24 +60,24 @@ public class ServiceDetailFragment extends Fragment {
|
|||||||
|
|
||||||
//Method to Update or Add a service
|
//Method to Update or Add a service
|
||||||
private void saveService() {
|
private void saveService() {
|
||||||
|
// Validates all fields using InputValidator
|
||||||
|
if (!InputValidator.isNotEmpty(etServiceName, "Service Name")) return;
|
||||||
|
if (!InputValidator.isNotEmpty(etServiceDesc, "Description")) return;
|
||||||
|
if (!InputValidator.isPositiveInteger(etServiceDuration, "Duration")) return;
|
||||||
|
if (!InputValidator.isPositiveDecimal(etServicePrice, "Price")) return;
|
||||||
|
|
||||||
//get all the values from the fields
|
//get all the values from the fields
|
||||||
String name = etServiceName.getText().toString().trim();
|
String name = etServiceName.getText().toString().trim();
|
||||||
String desc = etServiceDesc.getText().toString().trim();
|
String desc = etServiceDesc.getText().toString().trim();
|
||||||
String durationStr = etServiceDuration.getText().toString().trim();
|
int duration = Integer.parseInt(etServiceDuration.getText().toString().trim());
|
||||||
String priceStr = etServicePrice.getText().toString().trim();
|
double price = Double.parseDouble(etServicePrice.getText().toString().trim());
|
||||||
|
|
||||||
//check if all the fields are filled (desc is optional)
|
|
||||||
if (name.isEmpty() || durationStr.isEmpty() || priceStr.isEmpty()) {
|
|
||||||
Toast.makeText(getContext(), "Please fill in all fields", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//create a service object to send to the API
|
//create a service object to send to the API
|
||||||
ServiceDTO serviceDTO = new ServiceDTO();
|
ServiceDTO serviceDTO = new ServiceDTO();
|
||||||
serviceDTO.setServiceName(name);
|
serviceDTO.setServiceName(name);
|
||||||
serviceDTO.setServiceDesc(desc);
|
serviceDTO.setServiceDesc(desc);
|
||||||
serviceDTO.setServiceDuration(Integer.parseInt(durationStr));
|
serviceDTO.setServiceDuration(duration);
|
||||||
serviceDTO.setServicePrice(Double.parseDouble(priceStr));
|
serviceDTO.setServicePrice(price);
|
||||||
|
|
||||||
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
|
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
|
||||||
|
|
||||||
@@ -87,6 +89,7 @@ public class ServiceDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId);
|
||||||
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -96,6 +99,7 @@ public class ServiceDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.updateService", new Exception(t));
|
||||||
Log.e("ServiceDetailFragment", "Error updating service", t);
|
Log.e("ServiceDetailFragment", "Error updating service", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
@@ -106,6 +110,7 @@ public class ServiceDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.log(requireContext(), "Added new Service: " + name);
|
||||||
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -115,6 +120,7 @@ public class ServiceDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.createService", new Exception(t));
|
||||||
Log.e("ServiceDetailFragment", "Error adding service", t);
|
Log.e("ServiceDetailFragment", "Error adding service", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
@@ -134,6 +140,7 @@ public class ServiceDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId);
|
||||||
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -143,6 +150,7 @@ public class ServiceDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<Void> call, Throwable t) {
|
public void onFailure(Call<Void> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t));
|
||||||
Log.e("ServiceDetailFragment", "Error deleting service", t);
|
Log.e("ServiceDetailFragment", "Error deleting service", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,6 +20,8 @@ import com.example.petstoremobile.api.SupplierApi;
|
|||||||
import com.example.petstoremobile.dtos.SupplierDTO;
|
import com.example.petstoremobile.dtos.SupplierDTO;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
||||||
|
import com.example.petstoremobile.utils.ActivityLogger;
|
||||||
|
import com.example.petstoremobile.utils.InputValidator;
|
||||||
|
|
||||||
import retrofit2.Call;
|
import retrofit2.Call;
|
||||||
import retrofit2.Callback;
|
import retrofit2.Callback;
|
||||||
@@ -58,6 +60,13 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
|
|
||||||
//Method to Update or Add a supplier
|
//Method to Update or Add a supplier
|
||||||
private void saveSupplier() {
|
private void saveSupplier() {
|
||||||
|
// Validates all fields using InputValidator
|
||||||
|
if (!InputValidator.isNotEmpty(etSupCompany, "Company Name")) return;
|
||||||
|
if (!InputValidator.isNotEmpty(etSupContactFirstName, "First Name")) return;
|
||||||
|
if (!InputValidator.isNotEmpty(etSupContactLastName, "Last Name")) return;
|
||||||
|
if (!InputValidator.isValidEmail(etSupEmail)) return;
|
||||||
|
if (!InputValidator.isValidPhone(etSupPhone)) return;
|
||||||
|
|
||||||
//get all the values from the fields
|
//get all the values from the fields
|
||||||
String company = etSupCompany.getText().toString().trim();
|
String company = etSupCompany.getText().toString().trim();
|
||||||
String firstName = etSupContactFirstName.getText().toString().trim();
|
String firstName = etSupContactFirstName.getText().toString().trim();
|
||||||
@@ -65,12 +74,6 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
String email = etSupEmail.getText().toString().trim();
|
String email = etSupEmail.getText().toString().trim();
|
||||||
String phone = etSupPhone.getText().toString().trim();
|
String phone = etSupPhone.getText().toString().trim();
|
||||||
|
|
||||||
//check if all the fields are filled
|
|
||||||
if (company.isEmpty() || firstName.isEmpty() || lastName.isEmpty() || email.isEmpty() || phone.isEmpty()) {
|
|
||||||
Toast.makeText(getContext(), "Please fill in all fields", Toast.LENGTH_SHORT).show();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
//create a supplier object to send to the API
|
//create a supplier object to send to the API
|
||||||
SupplierDTO supplierDTO = new SupplierDTO();
|
SupplierDTO supplierDTO = new SupplierDTO();
|
||||||
supplierDTO.setSupCompany(company);
|
supplierDTO.setSupCompany(company);
|
||||||
@@ -89,6 +92,7 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId);
|
||||||
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -98,6 +102,7 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.updateSupplier", new Exception(t));
|
||||||
Log.e("SupplierDetailFragment", "Error updating supplier", t);
|
Log.e("SupplierDetailFragment", "Error updating supplier", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
@@ -108,6 +113,7 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.log(requireContext(), "Added new Supplier: " + company);
|
||||||
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -117,6 +123,7 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.createSupplier", new Exception(t));
|
||||||
Log.e("SupplierDetailFragment", "Error adding supplier", t);
|
Log.e("SupplierDetailFragment", "Error adding supplier", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
@@ -136,6 +143,7 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
@Override
|
@Override
|
||||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
|
ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId);
|
||||||
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||||
navigateBack();
|
navigateBack();
|
||||||
} else {
|
} else {
|
||||||
@@ -145,6 +153,7 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onFailure(Call<Void> call, Throwable t) {
|
public void onFailure(Call<Void> call, Throwable t) {
|
||||||
|
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t));
|
||||||
Log.e("SupplierDetailFragment", "Error deleting supplier", t);
|
Log.e("SupplierDetailFragment", "Error deleting supplier", t);
|
||||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
@@ -197,6 +206,11 @@ public class SupplierDetailFragment extends Fragment {
|
|||||||
etSupContactLastName = view.findViewById(R.id.etSupContactLastName);
|
etSupContactLastName = view.findViewById(R.id.etSupContactLastName);
|
||||||
etSupEmail = view.findViewById(R.id.etSupEmail);
|
etSupEmail = view.findViewById(R.id.etSupEmail);
|
||||||
etSupPhone = view.findViewById(R.id.etSupPhone);
|
etSupPhone = view.findViewById(R.id.etSupPhone);
|
||||||
|
|
||||||
|
// Add phone number formatting (CA) and limit length to 14 characters
|
||||||
|
etSupPhone.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA"));
|
||||||
|
etSupPhone.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)});
|
||||||
|
|
||||||
btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier);
|
btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier);
|
||||||
btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier);
|
btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier);
|
||||||
btnBack = view.findViewById(R.id.btnBack);
|
btnBack = view.findViewById(R.id.btnBack);
|
||||||
|
|||||||
@@ -16,26 +16,42 @@ import androidx.core.content.FileProvider;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
|
|
||||||
import android.provider.MediaStore;
|
import android.provider.MediaStore;
|
||||||
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Button;
|
import android.widget.Button;
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import com.bumptech.glide.Glide;
|
||||||
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
|
import com.example.petstoremobile.api.PetApi;
|
||||||
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
import com.example.petstoremobile.fragments.ListFragment;
|
import com.example.petstoremobile.fragments.ListFragment;
|
||||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
|
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
|
import java.io.FileOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
|
import okhttp3.MediaType;
|
||||||
|
import okhttp3.MultipartBody;
|
||||||
|
import okhttp3.RequestBody;
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
import retrofit2.Response;
|
||||||
|
|
||||||
public class PetProfileFragment extends Fragment {
|
public class PetProfileFragment extends Fragment {
|
||||||
|
|
||||||
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
|
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
|
||||||
private Button btnBack, btnEditPet, btnChangePhoto;
|
private Button btnBack, btnEditPet, btnChangePhoto;
|
||||||
private ImageView imgPet;
|
private ImageView imgPet;
|
||||||
private Uri photoUri;
|
private Uri photoUri;
|
||||||
|
private int petId;
|
||||||
|
|
||||||
// launchers for camera and gallery
|
// launchers for camera and gallery
|
||||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||||
@@ -53,8 +69,7 @@ public class PetProfileFragment extends Fragment {
|
|||||||
result -> {
|
result -> {
|
||||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||||
Uri selectedImage = result.getData().getData();
|
Uri selectedImage = result.getData().getData();
|
||||||
imgPet.setImageURI(selectedImage);
|
uploadPetImage(selectedImage);
|
||||||
// TODO: SAVE CHANGED PHOTO TO DATABASE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -64,9 +79,7 @@ public class PetProfileFragment extends Fragment {
|
|||||||
new ActivityResultContracts.TakePicture(),
|
new ActivityResultContracts.TakePicture(),
|
||||||
success -> {
|
success -> {
|
||||||
if (success) {
|
if (success) {
|
||||||
imgPet.setImageURI(null);
|
uploadPetImage(photoUri);
|
||||||
imgPet.setImageURI(photoUri);
|
|
||||||
// TODO: SAVE CHANGED PHOTO TO DATABASE
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
@@ -112,11 +125,15 @@ public class PetProfileFragment extends Fragment {
|
|||||||
|
|
||||||
// Set pet details to display
|
// Set pet details to display
|
||||||
if (getArguments() != null) {
|
if (getArguments() != null) {
|
||||||
|
petId = getArguments().getInt("petId");
|
||||||
tvPetName.setText(getArguments().getString("petName"));
|
tvPetName.setText(getArguments().getString("petName"));
|
||||||
tvPetSpecies.setText(getArguments().getString("petSpecies"));
|
tvPetSpecies.setText(getArguments().getString("petSpecies"));
|
||||||
tvPetBreed.setText(getArguments().getString("petBreed"));
|
tvPetBreed.setText(getArguments().getString("petBreed"));
|
||||||
tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge")));
|
tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge")));
|
||||||
tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice")));
|
tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice")));
|
||||||
|
|
||||||
|
// Load pet image from backend
|
||||||
|
loadPetImage(petId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//set button click listeners
|
//set button click listeners
|
||||||
@@ -169,6 +186,74 @@ public class PetProfileFragment extends Fragment {
|
|||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Helper function to load pet image from backend
|
||||||
|
private void loadPetImage(int petId) {
|
||||||
|
String imageUrl = RetrofitClient.BASE_URL + String.format(Locale.US, PetApi.PET_IMAGE_PATH, petId);
|
||||||
|
|
||||||
|
Glide.with(this)
|
||||||
|
.load(imageUrl)
|
||||||
|
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||||
|
.skipMemoryCache(true)
|
||||||
|
.placeholder(R.drawable.placeholder)
|
||||||
|
.error(R.drawable.placeholder)
|
||||||
|
.into(imgPet);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to upload pet image to backend
|
||||||
|
private void uploadPetImage(Uri uri) {
|
||||||
|
try {
|
||||||
|
File file = getFileFromUri(uri);
|
||||||
|
if (file == null) return;
|
||||||
|
|
||||||
|
// Create RequestBody for file upload
|
||||||
|
RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri)));
|
||||||
|
MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile);
|
||||||
|
|
||||||
|
// Call the backend to upload the image
|
||||||
|
PetApi petApi = RetrofitClient.getPetApi(requireContext());
|
||||||
|
petApi.uploadPetImage((long) petId, body).enqueue(new Callback<Void>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||||
|
if (response.isSuccessful()) {
|
||||||
|
Toast.makeText(requireContext(), "Pet photo updated successfully", Toast.LENGTH_SHORT).show();
|
||||||
|
// Reload image after successful upload
|
||||||
|
loadPetImage(petId);
|
||||||
|
} else {
|
||||||
|
Toast.makeText(requireContext(), "Failed to upload pet photo", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(Call<Void> call, Throwable t) {
|
||||||
|
Log.e("UPLOAD_PET_IMAGE", "Failure: " + t.getMessage());
|
||||||
|
Toast.makeText(requireContext(), "Network error", Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("UPLOAD_PET_IMAGE", "Error: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to create a temporary File object from a Uri for uploading
|
||||||
|
private File getFileFromUri(Uri uri) {
|
||||||
|
try {
|
||||||
|
InputStream inputStream = requireContext().getContentResolver().openInputStream(uri);
|
||||||
|
File tempFile = new File(requireContext().getCacheDir(), "upload_pet_image.jpg");
|
||||||
|
FileOutputStream outputStream = new FileOutputStream(tempFile);
|
||||||
|
byte[] buffer = new byte[1024];
|
||||||
|
int length;
|
||||||
|
while ((length = inputStream.read(buffer)) > 0) {
|
||||||
|
outputStream.write(buffer, 0, length);
|
||||||
|
}
|
||||||
|
outputStream.close();
|
||||||
|
inputStream.close();
|
||||||
|
return tempFile;
|
||||||
|
} catch (Exception e) {
|
||||||
|
Log.e("FILE_UTILS", "Error creating temp file", e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void launchCamera() {
|
private void launchCamera() {
|
||||||
File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg");
|
File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg");
|
||||||
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
|
photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile);
|
||||||
|
|||||||
@@ -0,0 +1,226 @@
|
|||||||
|
package com.example.petstoremobile.services;
|
||||||
|
|
||||||
|
import android.app.Service;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.IBinder;
|
||||||
|
import android.util.Log;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import com.example.petstoremobile.api.ChatApi;
|
||||||
|
import com.example.petstoremobile.api.CustomerApi;
|
||||||
|
import com.example.petstoremobile.api.RetrofitClient;
|
||||||
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
|
import com.example.petstoremobile.dtos.ConversationDTO;
|
||||||
|
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||||
|
import com.example.petstoremobile.dtos.MessageDTO;
|
||||||
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
|
import com.example.petstoremobile.utils.NotificationHelper;
|
||||||
|
import com.example.petstoremobile.websocket.StompChatManager;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
import retrofit2.Response;
|
||||||
|
|
||||||
|
// Service to receive notifications when a new conversation is created
|
||||||
|
public class ChatNotificationService extends Service {
|
||||||
|
private static final String TAG = "ChatNotificationService";
|
||||||
|
|
||||||
|
public static Long activeConversationIdInUi = null;
|
||||||
|
|
||||||
|
private StompChatManager stompChatManager;
|
||||||
|
private final Set<Long> knownConversationIds = new HashSet<>();
|
||||||
|
private final Map<Long, Long> conversationToCustomerId = new HashMap<>();
|
||||||
|
private final Map<Long, String> customerIdToName = new HashMap<>();
|
||||||
|
private Long currentUserId;
|
||||||
|
|
||||||
|
//When the service starts, connect to the websocket
|
||||||
|
@Override
|
||||||
|
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||||
|
Log.d(TAG, "Service started");
|
||||||
|
connectWebSocket();
|
||||||
|
return START_STICKY;
|
||||||
|
}
|
||||||
|
|
||||||
|
// helper function to connect to the websocket
|
||||||
|
private void connectWebSocket() {
|
||||||
|
//get the token and role from the shared preferences
|
||||||
|
TokenManager tm = TokenManager.getInstance(this);
|
||||||
|
String token = tm.getToken();
|
||||||
|
String role = tm.getRole();
|
||||||
|
currentUserId = tm.getUserId();
|
||||||
|
|
||||||
|
if (token != null && stompChatManager == null) {
|
||||||
|
//load customers to have names associated with customer ids
|
||||||
|
CustomerApi customerApi = RetrofitClient.getCustomerApi(this);
|
||||||
|
customerApi.getAllCustomers(0, 1000).enqueue(new Callback<PageResponse<CustomerDTO>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<PageResponse<CustomerDTO>> call, @NonNull Response<PageResponse<CustomerDTO>> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
for (CustomerDTO customer : response.body().getContent()) {
|
||||||
|
customerIdToName.put(customer.getCustomerId(), customer.getFullName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
loadConversationsAndStartStomp(token, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<PageResponse<CustomerDTO>> call, @NonNull Throwable t) {
|
||||||
|
Log.e(TAG, "Failed to load customers", t);
|
||||||
|
loadConversationsAndStartStomp(token, role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadConversationsAndStartStomp(String token, String role) {
|
||||||
|
// Fetch existing conversations
|
||||||
|
ChatApi chatApi = RetrofitClient.getChatApi(this);
|
||||||
|
chatApi.getAllConversations().enqueue(new Callback<List<ConversationDTO>>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<List<ConversationDTO>> call, @NonNull Response<List<ConversationDTO>> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
for (ConversationDTO conversation : response.body()) {
|
||||||
|
if (conversation.getId() != null) {
|
||||||
|
knownConversationIds.add(conversation.getId());
|
||||||
|
conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId());
|
||||||
|
// subscribe to existing conversations to get message notifications
|
||||||
|
if (stompChatManager != null) {
|
||||||
|
stompChatManager.subscribeToConversation(conversation.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Loaded " + knownConversationIds.size() + " existing conversations");
|
||||||
|
}
|
||||||
|
startStomp(token, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<List<ConversationDTO>> call, @NonNull Throwable t) {
|
||||||
|
Log.e(TAG, "Failed to load existing conversations", t);
|
||||||
|
//tries to connect if loading fails
|
||||||
|
startStomp(token, role);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void startStomp(String token, String role) {
|
||||||
|
if (stompChatManager != null) return;
|
||||||
|
|
||||||
|
stompChatManager = new StompChatManager(token, role);
|
||||||
|
|
||||||
|
// Listen for messages in existing conversations
|
||||||
|
stompChatManager.setMessageListener(message -> {
|
||||||
|
if (message != null && !message.getSenderId().equals(currentUserId)) {
|
||||||
|
// Check if this conversation is already active in the view
|
||||||
|
//if it is then don't make a notification for this chat
|
||||||
|
if (activeConversationIdInUi != null && activeConversationIdInUi.equals(message.getConversationId())) {
|
||||||
|
Log.d(TAG, "Disable notification for active conversation: " + message.getConversationId());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String title = "New Message";
|
||||||
|
Long customerId = conversationToCustomerId.get(message.getConversationId());
|
||||||
|
if (customerId != null && customerIdToName.containsKey(customerId)) {
|
||||||
|
//append the customer name to the title of the notification
|
||||||
|
title = "New message from " + customerIdToName.get(customerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationHelper.showNotification(
|
||||||
|
getApplicationContext(),
|
||||||
|
title,
|
||||||
|
message.getContent(),
|
||||||
|
message.getConversationId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
//When a conversation gets created, show a notification
|
||||||
|
stompChatManager.setConversationListener(conversation -> {
|
||||||
|
//check if the conversation exists
|
||||||
|
if (conversation != null && conversation.getId() != null) {
|
||||||
|
//check if the conversation is new
|
||||||
|
if (!knownConversationIds.contains(conversation.getId())) {
|
||||||
|
//add the conversation to the set of known conversations
|
||||||
|
knownConversationIds.add(conversation.getId());
|
||||||
|
conversationToCustomerId.put(conversation.getId(), conversation.getCustomerId());
|
||||||
|
|
||||||
|
// Subscribe to the new conversation's messages
|
||||||
|
stompChatManager.subscribeToConversation(conversation.getId());
|
||||||
|
|
||||||
|
String title = "New Support Request";
|
||||||
|
if (customerIdToName.containsKey(conversation.getCustomerId())) {
|
||||||
|
//append the customer name to the title of the notification
|
||||||
|
title = "New Support Request from " + customerIdToName.get(conversation.getCustomerId());
|
||||||
|
} else {
|
||||||
|
// Try to fetch customer name for the new request
|
||||||
|
fetchCustomerName(conversation.getCustomerId());
|
||||||
|
}
|
||||||
|
|
||||||
|
//Display a notification
|
||||||
|
NotificationHelper.showNotification(
|
||||||
|
getApplicationContext(),
|
||||||
|
title,
|
||||||
|
"A customer is requesting assistance",
|
||||||
|
conversation.getId()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Subscribe to existing conversations if they were already loaded
|
||||||
|
for (Long id : knownConversationIds) {
|
||||||
|
stompChatManager.subscribeToConversation(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
stompChatManager.setConnectionListener(new StompChatManager.ConnectionListener() {
|
||||||
|
@Override
|
||||||
|
public void onSocketOpened() {
|
||||||
|
Log.d(TAG, "WebSocket connected in service");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSocketClosed() { Log.d(TAG, "WebSocket closed in service"); }
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSocketError() { Log.e(TAG, "WebSocket error in service"); }
|
||||||
|
});
|
||||||
|
stompChatManager.connect();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function to fetch customer name for a conversation
|
||||||
|
private void fetchCustomerName(Long customerId) {
|
||||||
|
CustomerApi customerApi = RetrofitClient.getCustomerApi(this);
|
||||||
|
customerApi.getCustomerById(customerId).enqueue(new Callback<CustomerDTO>() {
|
||||||
|
@Override
|
||||||
|
public void onResponse(@NonNull Call<CustomerDTO> call, @NonNull Response<CustomerDTO> response) {
|
||||||
|
if (response.isSuccessful() && response.body() != null) {
|
||||||
|
customerIdToName.put(customerId, response.body().getFullName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onFailure(@NonNull Call<CustomerDTO> call, @NonNull Throwable t) {
|
||||||
|
Log.e(TAG, "Failed to fetch customer name", t);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
//When the service is destroyed, disconnect from the websocket
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
if (stompChatManager != null) {
|
||||||
|
stompChatManager.disconnect();
|
||||||
|
}
|
||||||
|
super.onDestroy();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
@Override
|
||||||
|
public IBinder onBind(Intent intent) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,10 +61,11 @@ public class InputValidator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Checks if the phone number is valid (digits, spaces, dashes, brackets allowed)
|
// Checks if the phone number is valid
|
||||||
public static boolean isValidPhone(EditText field) {
|
public static boolean isValidPhone(EditText field) {
|
||||||
String phone = field.getText().toString().trim();
|
String phone = field.getText().toString().trim();
|
||||||
if (phone.isEmpty() || !phone.matches("[0-9\\-\\s\\(\\)\\+]+")) {
|
// Android built in phone validation pattern
|
||||||
|
if (phone.isEmpty() || !android.util.Patterns.PHONE.matcher(phone).matches()) {
|
||||||
field.setError("Enter a valid phone number");
|
field.setError("Enter a valid phone number");
|
||||||
field.requestFocus();
|
field.requestFocus();
|
||||||
return false;
|
return false;
|
||||||
@@ -94,4 +95,3 @@ public class InputValidator {
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package com.example.petstoremobile.utils;
|
||||||
|
|
||||||
|
import android.app.NotificationChannel;
|
||||||
|
import android.app.NotificationManager;
|
||||||
|
import android.app.PendingIntent;
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Build;
|
||||||
|
import androidx.core.app.NotificationCompat;
|
||||||
|
import com.example.petstoremobile.R;
|
||||||
|
import com.example.petstoremobile.activities.HomeActivity;
|
||||||
|
|
||||||
|
// Helper class to show notifications when called
|
||||||
|
public class NotificationHelper {
|
||||||
|
private static final String CHANNEL_ID = "chat_notifications";
|
||||||
|
private static final String CHANNEL_NAME = "Chat Notifications";
|
||||||
|
private static final String CHANNEL_DESC = "Notifications for new conversations";
|
||||||
|
private static final int NOTIFICATION_ID = 1;
|
||||||
|
|
||||||
|
// a function to show a notification
|
||||||
|
public static void showNotification(Context context, String title, String message, Long conversationId) {
|
||||||
|
NotificationManager notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
|
|
||||||
|
//check if the device is running on Oreo or higher so we can set up a notification channel
|
||||||
|
// for these devices
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
// Create a notification channel
|
||||||
|
NotificationChannel channel = new NotificationChannel(CHANNEL_ID, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
|
||||||
|
channel.setDescription(CHANNEL_DESC);
|
||||||
|
notificationManager.createNotificationChannel(channel);
|
||||||
|
}
|
||||||
|
|
||||||
|
//make the notification navigate the chat if it is clicked
|
||||||
|
Intent intent = new Intent(context, HomeActivity.class);
|
||||||
|
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TASK);
|
||||||
|
intent.putExtra("navigate_to", "chat");
|
||||||
|
if (conversationId != null) {
|
||||||
|
intent.putExtra("conversation_id", conversationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingIntent pendingIntent = PendingIntent.getActivity(context, 0, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
|
||||||
|
//build the notification for display
|
||||||
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, CHANNEL_ID)
|
||||||
|
.setSmallIcon(R.mipmap.ic_launcher)
|
||||||
|
.setContentTitle(title)
|
||||||
|
.setContentText(message)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setDefaults(NotificationCompat.DEFAULT_ALL)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setContentIntent(pendingIntent);
|
||||||
|
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, builder.build());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,12 +28,15 @@
|
|||||||
android:contentDescription="Open menu"/>
|
android:contentDescription="Open menu"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
android:id="@+id/tvChatTitle"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Customer Chat"
|
android:text="Customer Chat"
|
||||||
android:textColor="@color/white"
|
android:textColor="@color/white"
|
||||||
android:textSize="20sp"
|
android:textSize="20sp"
|
||||||
android:textStyle="bold"/>
|
android:textStyle="bold"
|
||||||
|
android:paddingStart="8dp"
|
||||||
|
android:paddingEnd="8dp"/>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -38,18 +38,34 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<EditText
|
<LinearLayout
|
||||||
android:id="@+id/etSearchPet"
|
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_margin="8dp"
|
android:orientation="horizontal"
|
||||||
android:hint="Search by name, species or breed..."
|
android:padding="8dp"
|
||||||
android:inputType="text"
|
android:gravity="center_vertical">
|
||||||
android:drawableStart="@android:drawable/ic_menu_search"
|
|
||||||
android:drawablePadding="8dp"
|
<EditText
|
||||||
android:background="@android:color/white"
|
android:id="@+id/etSearchPet"
|
||||||
android:padding="12dp"
|
android:layout_width="0dp"
|
||||||
android:textColor="@color/text_dark"/>
|
android:layout_height="48dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:hint="Search..."
|
||||||
|
android:inputType="text"
|
||||||
|
android:drawableStart="@android:drawable/ic_menu_search"
|
||||||
|
android:drawablePadding="8dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:padding="12dp"
|
||||||
|
android:textColor="@color/text_dark"/>
|
||||||
|
|
||||||
|
<Spinner
|
||||||
|
android:id="@+id/spinnerStatus"
|
||||||
|
android:layout_width="140dp"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_marginStart="8dp"
|
||||||
|
android:background="@android:color/white"
|
||||||
|
android:padding="10dp"/>
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
android:id="@+id/swipeRefreshPet"
|
android:id="@+id/swipeRefreshPet"
|
||||||
|
|||||||
@@ -83,7 +83,7 @@
|
|||||||
android:id="@+id/tvProfileEmail"
|
android:id="@+id/tvProfileEmail"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="example@email.com"
|
android:text="No email loaded"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
@@ -129,7 +129,7 @@
|
|||||||
android:id="@+id/tvProfilePhone"
|
android:id="@+id/tvProfilePhone"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="(123) 123-1234"
|
android:text="No phone loaded"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textSize="16sp" />
|
android:textSize="16sp" />
|
||||||
|
|
||||||
@@ -174,7 +174,7 @@
|
|||||||
android:id="@+id/tvProfileRole"
|
android:id="@+id/tvProfileRole"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:text="Manager"
|
android:text="No role loaded"
|
||||||
android:textSize="16sp"
|
android:textSize="16sp"
|
||||||
android:textColor="@color/accent_coral"/>
|
android:textColor="@color/accent_coral"/>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<background android:drawable="@drawable/ic_launcher_background" />
|
<background android:drawable="@color/ic_launcher_background"/>
|
||||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||||
</adaptive-icon>
|
</adaptive-icon>
|
||||||
|
Before Width: | Height: | Size: 1.4 KiB After Width: | Height: | Size: 2.6 KiB |
BIN
android/app/src/main/res/mipmap-hdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 4.5 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 4.6 KiB |
|
Before Width: | Height: | Size: 982 B After Width: | Height: | Size: 1.6 KiB |
BIN
android/app/src/main/res/mipmap-mdpi/ic_launcher_foreground.webp
Normal file
|
After Width: | Height: | Size: 2.5 KiB |
|
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 2.8 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 3.7 KiB |
|
After Width: | Height: | Size: 7.0 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 6.6 KiB |
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 6.3 KiB |
|
After Width: | Height: | Size: 14 KiB |
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 11 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 9.8 KiB |
|
After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 7.6 KiB After Width: | Height: | Size: 16 KiB |
@@ -0,0 +1,4 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<resources>
|
||||||
|
<color name="ic_launcher_background">#FDE0E0</color>
|
||||||
|
</resources>
|
||||||
1
backend/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
target/
|
target/
|
||||||
|
nohup.out
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
!**/src/main/**/target/
|
!**/src/main/**/target/
|
||||||
!**/src/test/**/target/
|
!**/src/test/**/target/
|
||||||
|
|||||||
@@ -90,6 +90,10 @@
|
|||||||
"key": "avatarFile",
|
"key": "avatarFile",
|
||||||
"value": "postman/avatar.png"
|
"value": "postman/avatar.png"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"key": "avatarUrl",
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"key": "bulkPetId",
|
"key": "bulkPetId",
|
||||||
"value": ""
|
"value": ""
|
||||||
@@ -117,6 +121,10 @@
|
|||||||
{
|
{
|
||||||
"key": "bulkInventoryId",
|
"key": "bulkInventoryId",
|
||||||
"value": ""
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"key": "adoptedPetId",
|
||||||
|
"value": "4"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"item": [
|
"item": [
|
||||||
@@ -212,6 +220,7 @@
|
|||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});",
|
"});",
|
||||||
"var jsonData = pm.response.json();",
|
"var jsonData = pm.response.json();",
|
||||||
|
"if (jsonData.id !== undefined) pm.collectionVariables.set('userId', jsonData.id);",
|
||||||
"if (jsonData.token) pm.collectionVariables.set('customerToken', jsonData.token);"
|
"if (jsonData.token) pm.collectionVariables.set('customerToken', jsonData.token);"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -307,7 +316,9 @@
|
|||||||
"exec": [
|
"exec": [
|
||||||
"pm.test('Status code is 200', function () {",
|
"pm.test('Status code is 200', function () {",
|
||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});"
|
"});",
|
||||||
|
"var jsonData = pm.response.json();",
|
||||||
|
"if (jsonData.id !== undefined) pm.collectionVariables.set('userId', jsonData.id);"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -381,7 +392,8 @@
|
|||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});",
|
"});",
|
||||||
"var jsonData = pm.response.json();",
|
"var jsonData = pm.response.json();",
|
||||||
"pm.expect(jsonData.avatarUrl).to.be.a('string');"
|
"pm.expect(jsonData.avatarUrl).to.be.a('string');",
|
||||||
|
"pm.collectionVariables.set('avatarUrl', jsonData.avatarUrl);"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -414,7 +426,68 @@
|
|||||||
" pm.response.to.have.status(200);",
|
" pm.response.to.have.status(200);",
|
||||||
"});",
|
"});",
|
||||||
"var jsonData = pm.response.json();",
|
"var jsonData = pm.response.json();",
|
||||||
"pm.expect(jsonData.avatarUrl).to.be.a('string');"
|
"pm.expect(jsonData.avatarUrl).to.be.a('string');",
|
||||||
|
"pm.collectionVariables.set('avatarUrl', jsonData.avatarUrl);"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get My Avatar File",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}{{avatarUrl}}",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{customerToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"pm.test('Avatar response is an image', function () {",
|
||||||
|
" pm.expect(pm.response.headers.get('Content-Type')).to.include('image/');",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get User Avatar File As Staff",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}/api/v1/users/{{userId}}/avatar/file",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"pm.test('Avatar response is an image', function () {",
|
||||||
|
" pm.expect(pm.response.headers.get('Content-Type')).to.include('image/');",
|
||||||
|
"});"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -662,6 +735,95 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "Upload Pet Image",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "{{baseUrl}}/api/v1/pets/{{petId}}/image",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "formdata",
|
||||||
|
"formdata": [
|
||||||
|
{
|
||||||
|
"key": "image",
|
||||||
|
"type": "file",
|
||||||
|
"src": "{{avatarFile}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"var jsonData = pm.response.json();",
|
||||||
|
"pm.expect(jsonData.imageUrl).to.be.a('string');"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Pet Image Public",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}/api/v1/pets/{{petId}}/image"
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"pm.test('Pet image response is an image', function () {",
|
||||||
|
" pm.expect(pm.response.headers.get('Content-Type')).to.include('image/');",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Pet Image",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"url": "{{baseUrl}}/api/v1/pets/{{petId}}/image",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "Delete Pet",
|
"name": "Delete Pet",
|
||||||
"request": {
|
"request": {
|
||||||
@@ -769,6 +931,120 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Upload Adopted Pet Image",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "{{baseUrl}}/api/v1/pets/{{adoptedPetId}}/image",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "formdata",
|
||||||
|
"formdata": [
|
||||||
|
{
|
||||||
|
"key": "image",
|
||||||
|
"type": "file",
|
||||||
|
"src": "{{avatarFile}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Adopted Pet Image Public",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}/api/v1/pets/{{adoptedPetId}}/image"
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 403', function () {",
|
||||||
|
" pm.response.to.have.status(403);",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Adopted Pet Image As Staff",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}/api/v1/pets/{{adoptedPetId}}/image",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"pm.test('Pet image response is an image', function () {",
|
||||||
|
" pm.expect(pm.response.headers.get('Content-Type')).to.include('image/');",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Adopted Pet Image",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"url": "{{baseUrl}}/api/v1/pets/{{adoptedPetId}}/image",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -1015,6 +1291,95 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Upload Product Image",
|
||||||
|
"request": {
|
||||||
|
"method": "POST",
|
||||||
|
"url": "{{baseUrl}}/api/v1/products/{{productId}}/image",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{staffToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"body": {
|
||||||
|
"mode": "formdata",
|
||||||
|
"formdata": [
|
||||||
|
{
|
||||||
|
"key": "image",
|
||||||
|
"type": "file",
|
||||||
|
"src": "{{avatarFile}}"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"var jsonData = pm.response.json();",
|
||||||
|
"pm.expect(jsonData.imageUrl).to.be.a('string');"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Get Product Image",
|
||||||
|
"request": {
|
||||||
|
"method": "GET",
|
||||||
|
"url": "{{baseUrl}}/api/v1/products/{{productId}}/image"
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});",
|
||||||
|
"pm.test('Product image response is an image', function () {",
|
||||||
|
" pm.expect(pm.response.headers.get('Content-Type')).to.include('image/');",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Delete Product Image",
|
||||||
|
"request": {
|
||||||
|
"method": "DELETE",
|
||||||
|
"url": "{{baseUrl}}/api/v1/products/{{productId}}/image",
|
||||||
|
"header": [
|
||||||
|
{
|
||||||
|
"key": "Authorization",
|
||||||
|
"value": "Bearer {{adminToken}}",
|
||||||
|
"type": "text"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"event": [
|
||||||
|
{
|
||||||
|
"listen": "test",
|
||||||
|
"script": {
|
||||||
|
"type": "text/javascript",
|
||||||
|
"exec": [
|
||||||
|
"pm.test('Status code is 200', function () {",
|
||||||
|
" pm.response.to.have.status(200);",
|
||||||
|
"});"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,6 +10,9 @@ import java.util.Arrays;
|
|||||||
|
|
||||||
public class FlywayContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
public class FlywayContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
|
||||||
|
|
||||||
|
private static final int MAX_RETRIES = 15;
|
||||||
|
private static final long RETRY_DELAY_MILLIS = 1000L;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void initialize(ConfigurableApplicationContext applicationContext) {
|
public void initialize(ConfigurableApplicationContext applicationContext) {
|
||||||
ConfigurableEnvironment environment = applicationContext.getEnvironment();
|
ConfigurableEnvironment environment = applicationContext.getEnvironment();
|
||||||
@@ -29,12 +32,33 @@ public class FlywayContextInitializer implements ApplicationContextInitializer<C
|
|||||||
.filter(location -> !location.isEmpty())
|
.filter(location -> !location.isEmpty())
|
||||||
.toArray(String[]::new);
|
.toArray(String[]::new);
|
||||||
|
|
||||||
Flyway.configure()
|
RuntimeException lastFailure = null;
|
||||||
.dataSource(url, username, password)
|
for (int attempt = 1; attempt <= MAX_RETRIES; attempt++) {
|
||||||
.locations(locations)
|
try {
|
||||||
.baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false))
|
Flyway.configure()
|
||||||
.baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1")))
|
.dataSource(url, username, password)
|
||||||
.load()
|
.locations(locations)
|
||||||
.migrate();
|
.baselineOnMigrate(environment.getProperty("spring.flyway.baseline-on-migrate", Boolean.class, false))
|
||||||
|
.baselineVersion(MigrationVersion.fromVersion(environment.getProperty("spring.flyway.baseline-version", "1")))
|
||||||
|
.load()
|
||||||
|
.migrate();
|
||||||
|
return;
|
||||||
|
} catch (RuntimeException ex) {
|
||||||
|
lastFailure = ex;
|
||||||
|
if (attempt == MAX_RETRIES) {
|
||||||
|
throw ex;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
Thread.sleep(RETRY_DELAY_MILLIS);
|
||||||
|
} catch (InterruptedException interruptedException) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
throw new IllegalStateException("Interrupted while waiting for database startup", interruptedException);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lastFailure != null) {
|
||||||
|
throw lastFailure;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import com.petshop.backend.repository.EmployeeRepository;
|
|||||||
import com.petshop.backend.repository.EmployeeStoreRepository;
|
import com.petshop.backend.repository.EmployeeStoreRepository;
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
import com.petshop.backend.security.JwtUtil;
|
import com.petshop.backend.security.JwtUtil;
|
||||||
|
import com.petshop.backend.service.AvatarStorageService;
|
||||||
import com.petshop.backend.service.UserBusinessLinkageService;
|
import com.petshop.backend.service.UserBusinessLinkageService;
|
||||||
import com.petshop.backend.util.AuthenticationHelper;
|
import com.petshop.backend.util.AuthenticationHelper;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.authentication.AuthenticationManager;
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
import org.springframework.security.authentication.BadCredentialsException;
|
import org.springframework.security.authentication.BadCredentialsException;
|
||||||
@@ -28,15 +31,9 @@ import org.springframework.security.crypto.password.PasswordEncoder;
|
|||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
import org.springframework.web.multipart.MultipartFile;
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.nio.file.Paths;
|
|
||||||
import java.nio.file.StandardCopyOption;
|
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.UUID;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/v1/auth")
|
@RequestMapping("/api/v1/auth")
|
||||||
@@ -49,8 +46,9 @@ public class AuthController {
|
|||||||
private final UserBusinessLinkageService userBusinessLinkageService;
|
private final UserBusinessLinkageService userBusinessLinkageService;
|
||||||
private final EmployeeRepository employeeRepository;
|
private final EmployeeRepository employeeRepository;
|
||||||
private final EmployeeStoreRepository employeeStoreRepository;
|
private final EmployeeStoreRepository employeeStoreRepository;
|
||||||
|
private final AvatarStorageService avatarStorageService;
|
||||||
|
|
||||||
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository) {
|
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, UserBusinessLinkageService userBusinessLinkageService, EmployeeRepository employeeRepository, EmployeeStoreRepository employeeStoreRepository, AvatarStorageService avatarStorageService) {
|
||||||
this.authenticationManager = authenticationManager;
|
this.authenticationManager = authenticationManager;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.jwtUtil = jwtUtil;
|
this.jwtUtil = jwtUtil;
|
||||||
@@ -58,6 +56,7 @@ public class AuthController {
|
|||||||
this.userBusinessLinkageService = userBusinessLinkageService;
|
this.userBusinessLinkageService = userBusinessLinkageService;
|
||||||
this.employeeRepository = employeeRepository;
|
this.employeeRepository = employeeRepository;
|
||||||
this.employeeStoreRepository = employeeStoreRepository;
|
this.employeeStoreRepository = employeeStoreRepository;
|
||||||
|
this.avatarStorageService = avatarStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@@ -155,7 +154,7 @@ public class AuthController {
|
|||||||
user.getEmail(),
|
user.getEmail(),
|
||||||
user.getFullName(),
|
user.getFullName(),
|
||||||
user.getPhone(),
|
user.getPhone(),
|
||||||
user.getAvatarUrl(),
|
avatarStorageService.toOwnerAvatarUrl(user),
|
||||||
user.getRole().name(),
|
user.getRole().name(),
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
||||||
@@ -224,7 +223,7 @@ public class AuthController {
|
|||||||
updatedUser.getEmail(),
|
updatedUser.getEmail(),
|
||||||
updatedUser.getFullName(),
|
updatedUser.getFullName(),
|
||||||
updatedUser.getPhone(),
|
updatedUser.getPhone(),
|
||||||
updatedUser.getAvatarUrl(),
|
avatarStorageService.toOwnerAvatarUrl(updatedUser),
|
||||||
updatedUser.getRole().name(),
|
updatedUser.getRole().name(),
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
employeeStore != null ? employeeStore.getStore().getStoreId() : null,
|
||||||
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
employeeStore != null ? employeeStore.getStore().getStoreName() : null
|
||||||
@@ -273,26 +272,12 @@ public class AuthController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
String uploadDir = "uploads/avatars";
|
avatarStorageService.deleteAvatar(user);
|
||||||
File directory = new File(uploadDir);
|
String avatarPath = avatarStorageService.storeAvatar(file);
|
||||||
if (!directory.exists()) {
|
user.setAvatarUrl(avatarPath);
|
||||||
directory.mkdirs();
|
|
||||||
}
|
|
||||||
|
|
||||||
String originalFilename = file.getOriginalFilename();
|
|
||||||
String extension = originalFilename != null && originalFilename.contains(".")
|
|
||||||
? originalFilename.substring(originalFilename.lastIndexOf("."))
|
|
||||||
: ".jpg";
|
|
||||||
String filename = UUID.randomUUID().toString() + extension;
|
|
||||||
Path filePath = Paths.get(uploadDir, filename);
|
|
||||||
|
|
||||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
|
||||||
|
|
||||||
String avatarUrl = "/uploads/avatars/" + filename;
|
|
||||||
user.setAvatarUrl(avatarUrl);
|
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
|
|
||||||
return ResponseEntity.ok(new AvatarUploadResponse(avatarUrl, "Avatar uploaded successfully"));
|
return ResponseEntity.ok(new AvatarUploadResponse(avatarStorageService.toOwnerAvatarUrl(user), "Avatar uploaded successfully"));
|
||||||
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
Map<String, String> error = new HashMap<>();
|
Map<String, String> error = new HashMap<>();
|
||||||
@@ -305,25 +290,41 @@ public class AuthController {
|
|||||||
public ResponseEntity<?> getAvatar() {
|
public ResponseEntity<?> getAvatar() {
|
||||||
User user = getAuthenticatedUser();
|
User user = getAuthenticatedUser();
|
||||||
|
|
||||||
if (user.getAvatarUrl() == null || user.getAvatarUrl().isEmpty()) {
|
if (!avatarStorageService.hasAvatar(user)) {
|
||||||
Map<String, String> error = new HashMap<>();
|
Map<String, String> error = new HashMap<>();
|
||||||
error.put("message", "No avatar uploaded");
|
error.put("message", "No avatar uploaded");
|
||||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
return ResponseEntity.status(HttpStatus.NOT_FOUND).body(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
Map<String, String> response = new HashMap<>();
|
Map<String, String> response = new HashMap<>();
|
||||||
response.put("avatarUrl", user.getAvatarUrl());
|
response.put("avatarUrl", avatarStorageService.toOwnerAvatarUrl(user));
|
||||||
return ResponseEntity.ok(response);
|
return ResponseEntity.ok(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/me/avatar/file")
|
||||||
|
public ResponseEntity<Resource> getAvatarFile() {
|
||||||
|
User user = getAuthenticatedUser();
|
||||||
|
|
||||||
|
if (!avatarStorageService.hasAvatar(user)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Resource resource = avatarStorageService.loadAvatarResource(user);
|
||||||
|
MediaType mediaType = avatarStorageService.resolveMediaType(user);
|
||||||
|
return ResponseEntity.ok().contentType(mediaType).body(resource);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@DeleteMapping("/me/avatar")
|
@DeleteMapping("/me/avatar")
|
||||||
public ResponseEntity<?> deleteAvatar() {
|
public ResponseEntity<?> deleteAvatar() {
|
||||||
User user = getAuthenticatedUser();
|
User user = getAuthenticatedUser();
|
||||||
|
|
||||||
if (user.getAvatarUrl() != null && !user.getAvatarUrl().isEmpty()) {
|
if (avatarStorageService.hasAvatar(user)) {
|
||||||
try {
|
try {
|
||||||
Path filePath = Paths.get("." + user.getAvatarUrl());
|
avatarStorageService.deleteAvatar(user);
|
||||||
Files.deleteIfExists(filePath);
|
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
}
|
}
|
||||||
user.setAvatarUrl(null);
|
user.setAvatarUrl(null);
|
||||||
|
|||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package com.petshop.backend.controller;
|
||||||
|
|
||||||
|
import com.petshop.backend.dto.pet.PetResponse;
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.petshop.backend.security.AppPrincipal;
|
||||||
|
import com.petshop.backend.service.PetService;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.security.core.Authentication;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/pets")
|
||||||
|
public class PetImageController {
|
||||||
|
|
||||||
|
private final PetService petService;
|
||||||
|
|
||||||
|
public PetImageController(PetService petService) {
|
||||||
|
this.petService = petService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/image")
|
||||||
|
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||||
|
public ResponseEntity<?> uploadPetImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) {
|
||||||
|
try {
|
||||||
|
PetResponse response = petService.uploadPetImage(id, image);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return badRequest(ex.getMessage());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
return badRequest("Failed to upload pet image: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/image")
|
||||||
|
public ResponseEntity<Resource> getPetImage(@PathVariable Long id) {
|
||||||
|
try {
|
||||||
|
PetService.ImagePayload payload = petService.loadPetImage(id, currentUserId(), currentUserRole());
|
||||||
|
return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource());
|
||||||
|
} catch (PetService.ForbiddenImageAccessException ex) {
|
||||||
|
return ResponseEntity.status(HttpStatus.FORBIDDEN).build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}/image")
|
||||||
|
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||||
|
public ResponseEntity<PetResponse> deletePetImage(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(petService.deletePetImage(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, String>> badRequest(String message) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("message", message);
|
||||||
|
return ResponseEntity.badRequest().body(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long currentUserId() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
if (principal instanceof AppPrincipal appPrincipal) {
|
||||||
|
return appPrincipal.getUserId();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private User.Role currentUserRole() {
|
||||||
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
|
if (authentication == null || !authentication.isAuthenticated()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
Object principal = authentication.getPrincipal();
|
||||||
|
if (principal instanceof AppPrincipal appPrincipal) {
|
||||||
|
return appPrincipal.getRole();
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package com.petshop.backend.controller;
|
||||||
|
|
||||||
|
import com.petshop.backend.dto.product.ProductResponse;
|
||||||
|
import com.petshop.backend.service.ProductService;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/products")
|
||||||
|
public class ProductImageController {
|
||||||
|
|
||||||
|
private final ProductService productService;
|
||||||
|
|
||||||
|
public ProductImageController(ProductService productService) {
|
||||||
|
this.productService = productService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{id}/image")
|
||||||
|
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||||
|
public ResponseEntity<?> uploadProductImage(@PathVariable Long id, @RequestParam("image") MultipartFile image) {
|
||||||
|
try {
|
||||||
|
ProductResponse response = productService.uploadProductImage(id, image);
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return badRequest(ex.getMessage());
|
||||||
|
} catch (IOException ex) {
|
||||||
|
return badRequest("Failed to upload product image: " + ex.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{id}/image")
|
||||||
|
public ResponseEntity<Resource> getProductImage(@PathVariable Long id) {
|
||||||
|
ProductService.ImagePayload payload = productService.loadProductImage(id);
|
||||||
|
return ResponseEntity.ok().contentType(payload.mediaType()).body(payload.resource());
|
||||||
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{id}/image")
|
||||||
|
@PreAuthorize("hasRole('ADMIN')")
|
||||||
|
public ResponseEntity<ProductResponse> deleteProductImage(@PathVariable Long id) {
|
||||||
|
return ResponseEntity.ok(productService.deleteProductImage(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<Map<String, String>> badRequest(String message) {
|
||||||
|
Map<String, String> error = new HashMap<>();
|
||||||
|
error.put("message", message);
|
||||||
|
return ResponseEntity.badRequest().body(error);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package com.petshop.backend.controller;
|
||||||
|
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.petshop.backend.repository.UserRepository;
|
||||||
|
import com.petshop.backend.service.AvatarStorageService;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
import org.springframework.web.bind.annotation.PathVariable;
|
||||||
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/v1/users")
|
||||||
|
public class UserAvatarController {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final AvatarStorageService avatarStorageService;
|
||||||
|
|
||||||
|
public UserAvatarController(UserRepository userRepository, AvatarStorageService avatarStorageService) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.avatarStorageService = avatarStorageService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/{userId}/avatar/file")
|
||||||
|
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||||
|
public ResponseEntity<Resource> getUserAvatarFile(@PathVariable Long userId) {
|
||||||
|
User user = userRepository.findById(userId).orElse(null);
|
||||||
|
if (user == null || !avatarStorageService.hasAvatar(user)) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
Resource resource = avatarStorageService.loadAvatarResource(user);
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.contentType(avatarStorageService.resolveMediaType(user))
|
||||||
|
.body(resource);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return ResponseEntity.notFound().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,13 +12,14 @@ public class PetResponse {
|
|||||||
private Integer petAge;
|
private Integer petAge;
|
||||||
private String petStatus;
|
private String petStatus;
|
||||||
private BigDecimal petPrice;
|
private BigDecimal petPrice;
|
||||||
|
private String imageUrl;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
public PetResponse() {
|
public PetResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
public PetResponse(Long petId, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||||
this.petId = petId;
|
this.petId = petId;
|
||||||
this.petName = petName;
|
this.petName = petName;
|
||||||
this.petSpecies = petSpecies;
|
this.petSpecies = petSpecies;
|
||||||
@@ -26,6 +27,7 @@ public class PetResponse {
|
|||||||
this.petAge = petAge;
|
this.petAge = petAge;
|
||||||
this.petStatus = petStatus;
|
this.petStatus = petStatus;
|
||||||
this.petPrice = petPrice;
|
this.petPrice = petPrice;
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -86,6 +88,14 @@ public class PetResponse {
|
|||||||
this.petPrice = petPrice;
|
this.petPrice = petPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -107,12 +117,12 @@ public class PetResponse {
|
|||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
PetResponse that = (PetResponse) o;
|
PetResponse that = (PetResponse) o;
|
||||||
return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
|
return Objects.equals(petId, that.petId) && Objects.equals(petName, that.petName) && Objects.equals(petSpecies, that.petSpecies) && Objects.equals(petBreed, that.petBreed) && Objects.equals(petAge, that.petAge) && Objects.equals(petStatus, that.petStatus) && Objects.equals(petPrice, that.petPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, createdAt, updatedAt);
|
return Objects.hash(petId, petName, petSpecies, petBreed, petAge, petStatus, petPrice, imageUrl, createdAt, updatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -125,6 +135,7 @@ public class PetResponse {
|
|||||||
", petAge=" + petAge +
|
", petAge=" + petAge +
|
||||||
", petStatus='" + petStatus + '\'' +
|
", petStatus='" + petStatus + '\'' +
|
||||||
", petPrice=" + petPrice +
|
", petPrice=" + petPrice +
|
||||||
|
", imageUrl='" + imageUrl + '\'' +
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -11,19 +11,21 @@ public class ProductResponse {
|
|||||||
private String categoryName;
|
private String categoryName;
|
||||||
private String prodDesc;
|
private String prodDesc;
|
||||||
private BigDecimal prodPrice;
|
private BigDecimal prodPrice;
|
||||||
|
private String imageUrl;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
public ProductResponse() {
|
public ProductResponse() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public ProductResponse(Long prodId, String prodName, Long categoryId, String categoryName, String prodDesc, BigDecimal prodPrice, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
public ProductResponse(Long prodId, String prodName, Long categoryId, String categoryName, String prodDesc, BigDecimal prodPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||||
this.prodId = prodId;
|
this.prodId = prodId;
|
||||||
this.prodName = prodName;
|
this.prodName = prodName;
|
||||||
this.categoryId = categoryId;
|
this.categoryId = categoryId;
|
||||||
this.categoryName = categoryName;
|
this.categoryName = categoryName;
|
||||||
this.prodDesc = prodDesc;
|
this.prodDesc = prodDesc;
|
||||||
this.prodPrice = prodPrice;
|
this.prodPrice = prodPrice;
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -76,6 +78,14 @@ public class ProductResponse {
|
|||||||
this.prodPrice = prodPrice;
|
this.prodPrice = prodPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -97,12 +107,12 @@ public class ProductResponse {
|
|||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
ProductResponse that = (ProductResponse) o;
|
ProductResponse that = (ProductResponse) o;
|
||||||
return Objects.equals(prodId, that.prodId) && Objects.equals(prodName, that.prodName) && Objects.equals(categoryId, that.categoryId) && Objects.equals(categoryName, that.categoryName) && Objects.equals(prodDesc, that.prodDesc) && Objects.equals(prodPrice, that.prodPrice) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
|
return Objects.equals(prodId, that.prodId) && Objects.equals(prodName, that.prodName) && Objects.equals(categoryId, that.categoryId) && Objects.equals(categoryName, that.categoryName) && Objects.equals(prodDesc, that.prodDesc) && Objects.equals(prodPrice, that.prodPrice) && Objects.equals(imageUrl, that.imageUrl) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(prodId, prodName, categoryId, categoryName, prodDesc, prodPrice, createdAt, updatedAt);
|
return Objects.hash(prodId, prodName, categoryId, categoryName, prodDesc, prodPrice, imageUrl, createdAt, updatedAt);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -114,6 +124,7 @@ public class ProductResponse {
|
|||||||
", categoryName='" + categoryName + '\'' +
|
", categoryName='" + categoryName + '\'' +
|
||||||
", prodDesc='" + prodDesc + '\'' +
|
", prodDesc='" + prodDesc + '\'' +
|
||||||
", prodPrice=" + prodPrice +
|
", prodPrice=" + prodPrice +
|
||||||
|
", imageUrl='" + imageUrl + '\'' +
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ public class Pet {
|
|||||||
@Column(nullable = false, precision = 10, scale = 2)
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
private BigDecimal petPrice;
|
private BigDecimal petPrice;
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -46,7 +49,7 @@ public class Pet {
|
|||||||
public Pet() {
|
public Pet() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Pet(Long id, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
public Pet(Long id, String petName, String petSpecies, String petBreed, Integer petAge, String petStatus, BigDecimal petPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
this.petName = petName;
|
this.petName = petName;
|
||||||
this.petSpecies = petSpecies;
|
this.petSpecies = petSpecies;
|
||||||
@@ -54,6 +57,7 @@ public class Pet {
|
|||||||
this.petAge = petAge;
|
this.petAge = petAge;
|
||||||
this.petStatus = petStatus;
|
this.petStatus = petStatus;
|
||||||
this.petPrice = petPrice;
|
this.petPrice = petPrice;
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -114,6 +118,14 @@ public class Pet {
|
|||||||
this.petPrice = petPrice;
|
this.petPrice = petPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -153,6 +165,7 @@ public class Pet {
|
|||||||
", petAge=" + petAge +
|
", petAge=" + petAge +
|
||||||
", petStatus='" + petStatus + '\'' +
|
", petStatus='" + petStatus + '\'' +
|
||||||
", petPrice=" + petPrice +
|
", petPrice=" + petPrice +
|
||||||
|
", imageUrl='" + imageUrl + '\'' +
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -29,6 +29,9 @@ public class Product {
|
|||||||
@Column(nullable = false, precision = 10, scale = 2)
|
@Column(nullable = false, precision = 10, scale = 2)
|
||||||
private BigDecimal prodPrice;
|
private BigDecimal prodPrice;
|
||||||
|
|
||||||
|
@Column(length = 255)
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -40,12 +43,13 @@ public class Product {
|
|||||||
public Product() {
|
public Product() {
|
||||||
}
|
}
|
||||||
|
|
||||||
public Product(Long prodId, String prodName, Category category, String prodDesc, BigDecimal prodPrice, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
public Product(Long prodId, String prodName, Category category, String prodDesc, BigDecimal prodPrice, String imageUrl, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||||
this.prodId = prodId;
|
this.prodId = prodId;
|
||||||
this.prodName = prodName;
|
this.prodName = prodName;
|
||||||
this.category = category;
|
this.category = category;
|
||||||
this.prodDesc = prodDesc;
|
this.prodDesc = prodDesc;
|
||||||
this.prodPrice = prodPrice;
|
this.prodPrice = prodPrice;
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -90,6 +94,14 @@ public class Product {
|
|||||||
this.prodPrice = prodPrice;
|
this.prodPrice = prodPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -127,6 +139,7 @@ public class Product {
|
|||||||
", category=" + category +
|
", category=" + category +
|
||||||
", prodDesc='" + prodDesc + '\'' +
|
", prodDesc='" + prodDesc + '\'' +
|
||||||
", prodPrice=" + prodPrice +
|
", prodPrice=" + prodPrice +
|
||||||
|
", imageUrl='" + imageUrl + '\'' +
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import org.springframework.data.jpa.repository.Query;
|
|||||||
import org.springframework.data.repository.query.Param;
|
import org.springframework.data.repository.query.Param;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
@Repository
|
@Repository
|
||||||
public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
|
public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
|
||||||
|
|
||||||
@@ -24,4 +26,6 @@ public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
|
|||||||
"LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
"LOWER(a.customer.lastName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||||
"LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%')))")
|
"LOWER(a.pet.petName) LIKE LOWER(CONCAT('%', :q, '%')))")
|
||||||
Page<Adoption> searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable);
|
Page<Adoption> searchAdoptionsByCustomer(@Param("customerId") Long customerId, @Param("q") String query, Pageable pageable);
|
||||||
|
|
||||||
|
Optional<Adoption> findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(Long petId, String adoptionStatus);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
|
import org.springframework.core.io.PathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.MediaTypeFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class AvatarStorageService {
|
||||||
|
|
||||||
|
private static final String STORED_PREFIX = "/uploads/avatars/";
|
||||||
|
private static final String OWNER_ENDPOINT = "/api/v1/auth/me/avatar/file";
|
||||||
|
|
||||||
|
private final Path avatarDirectory = Paths.get("uploads", "avatars").toAbsolutePath().normalize();
|
||||||
|
|
||||||
|
public String storeAvatar(MultipartFile file) throws IOException {
|
||||||
|
Files.createDirectories(avatarDirectory);
|
||||||
|
|
||||||
|
String originalFilename = file.getOriginalFilename();
|
||||||
|
String extension = resolveExtension(originalFilename);
|
||||||
|
String filename = UUID.randomUUID() + extension;
|
||||||
|
Path filePath = avatarDirectory.resolve(filename).normalize();
|
||||||
|
|
||||||
|
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
return STORED_PREFIX + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource loadAvatarResource(User user) {
|
||||||
|
Path filePath = resolveStoredAvatarPath(user.getAvatarUrl());
|
||||||
|
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||||
|
throw new IllegalArgumentException("Avatar file was not found");
|
||||||
|
}
|
||||||
|
return new PathResource(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteAvatar(User user) throws IOException {
|
||||||
|
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toOwnerAvatarUrl(User user) {
|
||||||
|
return hasAvatar(user) ? OWNER_ENDPOINT : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String toStoredAvatarUrl(String avatarFilenamePath) {
|
||||||
|
return avatarFilenamePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasAvatar(User user) {
|
||||||
|
return user.getAvatarUrl() != null && !user.getAvatarUrl().isBlank();
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaType resolveMediaType(User user) {
|
||||||
|
try {
|
||||||
|
return MediaTypeFactory.getMediaType(loadAvatarResource(user)).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
} catch (IllegalArgumentException ex) {
|
||||||
|
return MediaType.APPLICATION_OCTET_STREAM;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveStoredAvatarPath(String storedAvatarUrl) {
|
||||||
|
if (storedAvatarUrl == null || storedAvatarUrl.isBlank() || !storedAvatarUrl.startsWith(STORED_PREFIX)) {
|
||||||
|
throw new IllegalArgumentException("Avatar file was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
String filename = storedAvatarUrl.substring(STORED_PREFIX.length());
|
||||||
|
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
|
||||||
|
throw new IllegalArgumentException("Avatar file was not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
Path resolved = avatarDirectory.resolve(filename).normalize();
|
||||||
|
if (!resolved.startsWith(avatarDirectory)) {
|
||||||
|
throw new IllegalArgumentException("Avatar file was not found");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveExtension(String originalFilename) {
|
||||||
|
if (originalFilename == null) {
|
||||||
|
return ".jpg";
|
||||||
|
}
|
||||||
|
int extensionIndex = originalFilename.lastIndexOf('.');
|
||||||
|
if (extensionIndex < 0 || extensionIndex == originalFilename.length() - 1) {
|
||||||
|
return ".jpg";
|
||||||
|
}
|
||||||
|
String extension = originalFilename.substring(extensionIndex).toLowerCase(Locale.ROOT);
|
||||||
|
return switch (extension) {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif" -> extension;
|
||||||
|
default -> ".jpg";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
|
import org.springframework.core.io.PathResource;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.MediaTypeFactory;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.nio.file.Files;
|
||||||
|
import java.nio.file.Path;
|
||||||
|
import java.nio.file.Paths;
|
||||||
|
import java.nio.file.StandardCopyOption;
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.UUID;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class CatalogImageStorageService {
|
||||||
|
|
||||||
|
private static final String PET_PREFIX = "/uploads/pets/";
|
||||||
|
private static final String PRODUCT_PREFIX = "/uploads/products/";
|
||||||
|
|
||||||
|
public String storePetImage(MultipartFile file) throws IOException {
|
||||||
|
return storeImage(file, Paths.get("uploads", "pets").toAbsolutePath().normalize(), PET_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public String storeProductImage(MultipartFile file) throws IOException {
|
||||||
|
return storeImage(file, Paths.get("uploads", "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource loadPetImage(String storedPath) {
|
||||||
|
return new PathResource(resolveStoredPath(storedPath, Paths.get("uploads", "pets").toAbsolutePath().normalize(), PET_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
public Resource loadProductImage(String storedPath) {
|
||||||
|
return new PathResource(resolveStoredPath(storedPath, Paths.get("uploads", "products").toAbsolutePath().normalize(), PRODUCT_PREFIX));
|
||||||
|
}
|
||||||
|
|
||||||
|
public MediaType resolveMediaType(Resource resource) {
|
||||||
|
return MediaTypeFactory.getMediaType(resource).orElse(MediaType.APPLICATION_OCTET_STREAM);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePetImage(String storedPath) throws IOException {
|
||||||
|
deleteImage(storedPath, Paths.get("uploads", "pets").toAbsolutePath().normalize(), PET_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteProductImage(String storedPath) throws IOException {
|
||||||
|
deleteImage(storedPath, Paths.get("uploads", "products").toAbsolutePath().normalize(), PRODUCT_PREFIX);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String storeImage(MultipartFile file, Path directory, String prefix) throws IOException {
|
||||||
|
Files.createDirectories(directory);
|
||||||
|
String extension = resolveExtension(file.getOriginalFilename());
|
||||||
|
String filename = UUID.randomUUID() + extension;
|
||||||
|
Path filePath = directory.resolve(filename).normalize();
|
||||||
|
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||||
|
return prefix + filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteImage(String storedPath, Path directory, String prefix) throws IOException {
|
||||||
|
if (storedPath == null || storedPath.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Files.deleteIfExists(resolveStoredPath(storedPath, directory, prefix));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path resolveStoredPath(String storedPath, Path directory, String prefix) {
|
||||||
|
if (storedPath == null || storedPath.isBlank() || !storedPath.startsWith(prefix)) {
|
||||||
|
throw new IllegalArgumentException("Image file was not found");
|
||||||
|
}
|
||||||
|
String filename = storedPath.substring(prefix.length());
|
||||||
|
if (filename.isBlank() || filename.contains("/") || filename.contains("\\") || filename.contains("..")) {
|
||||||
|
throw new IllegalArgumentException("Image file was not found");
|
||||||
|
}
|
||||||
|
Path resolved = directory.resolve(filename).normalize();
|
||||||
|
if (!resolved.startsWith(directory)) {
|
||||||
|
throw new IllegalArgumentException("Image file was not found");
|
||||||
|
}
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resolveExtension(String originalFilename) {
|
||||||
|
if (originalFilename == null) {
|
||||||
|
return ".jpg";
|
||||||
|
}
|
||||||
|
int extensionIndex = originalFilename.lastIndexOf('.');
|
||||||
|
if (extensionIndex < 0 || extensionIndex == originalFilename.length() - 1) {
|
||||||
|
return ".jpg";
|
||||||
|
}
|
||||||
|
String extension = originalFilename.substring(extensionIndex).toLowerCase(Locale.ROOT);
|
||||||
|
return switch (extension) {
|
||||||
|
case ".jpg", ".jpeg", ".png", ".gif" -> extension;
|
||||||
|
default -> ".jpg";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,21 +3,34 @@ package com.petshop.backend.service;
|
|||||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||||
import com.petshop.backend.dto.pet.PetRequest;
|
import com.petshop.backend.dto.pet.PetRequest;
|
||||||
import com.petshop.backend.dto.pet.PetResponse;
|
import com.petshop.backend.dto.pet.PetResponse;
|
||||||
|
import com.petshop.backend.entity.Adoption;
|
||||||
import com.petshop.backend.entity.Pet;
|
import com.petshop.backend.entity.Pet;
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
|
import com.petshop.backend.repository.AdoptionRepository;
|
||||||
import com.petshop.backend.repository.PetRepository;
|
import com.petshop.backend.repository.PetRepository;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class PetService {
|
public class PetService {
|
||||||
|
|
||||||
private final PetRepository petRepository;
|
private final PetRepository petRepository;
|
||||||
|
private final AdoptionRepository adoptionRepository;
|
||||||
|
private final CatalogImageStorageService catalogImageStorageService;
|
||||||
|
|
||||||
public PetService(PetRepository petRepository) {
|
public PetService(PetRepository petRepository, AdoptionRepository adoptionRepository, CatalogImageStorageService catalogImageStorageService) {
|
||||||
this.petRepository = petRepository;
|
this.petRepository = petRepository;
|
||||||
|
this.adoptionRepository = adoptionRepository;
|
||||||
|
this.catalogImageStorageService = catalogImageStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<PetResponse> getAllPets(String query, Pageable pageable) {
|
public Page<PetResponse> getAllPets(String query, Pageable pageable) {
|
||||||
@@ -68,17 +81,107 @@ public class PetService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deletePet(Long id) {
|
public void deletePet(Long id) {
|
||||||
if (!petRepository.existsById(id)) {
|
Pet pet = petRepository.findById(id)
|
||||||
throw new ResourceNotFoundException("Pet not found with id: " + id);
|
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
|
||||||
}
|
deleteStoredImageIfPresent(pet.getImageUrl());
|
||||||
petRepository.deleteById(id);
|
petRepository.delete(pet);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void bulkDeletePets(BulkDeleteRequest request) {
|
public void bulkDeletePets(BulkDeleteRequest request) {
|
||||||
|
petRepository.findAllById(request.getIds()).forEach(pet -> deleteStoredImageIfPresent(pet.getImageUrl()));
|
||||||
petRepository.deleteAllById(request.getIds());
|
petRepository.deleteAllById(request.getIds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PetResponse uploadPetImage(Long id, MultipartFile file) throws IOException {
|
||||||
|
validateImageFile(file);
|
||||||
|
Pet pet = findPet(id);
|
||||||
|
deleteStoredImageIfPresent(pet.getImageUrl());
|
||||||
|
pet.setImageUrl(catalogImageStorageService.storePetImage(file));
|
||||||
|
return mapToResponse(petRepository.save(pet));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public PetResponse deletePetImage(Long id) {
|
||||||
|
Pet pet = findPet(id);
|
||||||
|
deleteStoredImageIfPresent(pet.getImageUrl());
|
||||||
|
pet.setImageUrl(null);
|
||||||
|
return mapToResponse(petRepository.save(pet));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImagePayload loadPetImage(Long id, Long requesterUserId, User.Role requesterRole) {
|
||||||
|
Pet pet = findPet(id);
|
||||||
|
if (pet.getImageUrl() == null || pet.getImageUrl().isBlank()) {
|
||||||
|
throw new ResourceNotFoundException("Pet image not found for id: " + id);
|
||||||
|
}
|
||||||
|
if (!canViewPetImage(pet, requesterUserId, requesterRole)) {
|
||||||
|
throw new ForbiddenImageAccessException();
|
||||||
|
}
|
||||||
|
Resource resource = catalogImageStorageService.loadPetImage(pet.getImageUrl());
|
||||||
|
MediaType mediaType = catalogImageStorageService.resolveMediaType(resource);
|
||||||
|
return new ImagePayload(resource, mediaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isPubliclyVisible(Pet pet) {
|
||||||
|
return "available".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean canViewPetImage(Pet pet, Long requesterUserId, User.Role requesterRole) {
|
||||||
|
if (isPubliclyVisible(pet)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (requesterRole == User.Role.STAFF || requesterRole == User.Role.ADMIN) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (requesterUserId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!"adopted".equalsIgnoreCase(normalizeStatus(pet.getPetStatus()))) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return adoptionRepository.findFirstByPet_IdAndAdoptionStatusOrderByAdoptionDateDesc(pet.getPetId(), "Completed")
|
||||||
|
.map(Adoption::getCustomer)
|
||||||
|
.map(customer -> requesterUserId.equals(customer.getUserId()))
|
||||||
|
.orElse(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Pet findPet(Long id) {
|
||||||
|
return petRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Pet not found with id: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateImageFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Please select an image to upload");
|
||||||
|
}
|
||||||
|
if (file.getSize() > 5 * 1024 * 1024) {
|
||||||
|
throw new IllegalArgumentException("Image file size must be less than 5MB");
|
||||||
|
}
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
if (contentType == null) {
|
||||||
|
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
|
||||||
|
}
|
||||||
|
String normalized = contentType.toLowerCase(Locale.ROOT);
|
||||||
|
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) {
|
||||||
|
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteStoredImageIfPresent(String storedImagePath) {
|
||||||
|
if (storedImagePath == null || storedImagePath.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
catalogImageStorageService.deletePetImage(storedImagePath);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeStatus(String status) {
|
||||||
|
return status == null ? "" : status.trim();
|
||||||
|
}
|
||||||
|
|
||||||
private PetResponse mapToResponse(Pet pet) {
|
private PetResponse mapToResponse(Pet pet) {
|
||||||
return new PetResponse(
|
return new PetResponse(
|
||||||
pet.getPetId(),
|
pet.getPetId(),
|
||||||
@@ -88,8 +191,15 @@ public class PetService {
|
|||||||
pet.getPetAge(),
|
pet.getPetAge(),
|
||||||
pet.getPetStatus(),
|
pet.getPetStatus(),
|
||||||
pet.getPetPrice(),
|
pet.getPetPrice(),
|
||||||
|
pet.getImageUrl() != null && !pet.getImageUrl().isBlank() ? "/api/v1/pets/" + pet.getPetId() + "/image" : null,
|
||||||
pet.getCreatedAt(),
|
pet.getCreatedAt(),
|
||||||
pet.getUpdatedAt()
|
pet.getUpdatedAt()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record ImagePayload(Resource resource, MediaType mediaType) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class ForbiddenImageAccessException extends RuntimeException {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,20 +8,28 @@ import com.petshop.backend.entity.Product;
|
|||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
import com.petshop.backend.repository.CategoryRepository;
|
import com.petshop.backend.repository.CategoryRepository;
|
||||||
import com.petshop.backend.repository.ProductRepository;
|
import com.petshop.backend.repository.ProductRepository;
|
||||||
|
import org.springframework.core.io.Resource;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class ProductService {
|
public class ProductService {
|
||||||
|
|
||||||
private final ProductRepository productRepository;
|
private final ProductRepository productRepository;
|
||||||
private final CategoryRepository categoryRepository;
|
private final CategoryRepository categoryRepository;
|
||||||
|
private final CatalogImageStorageService catalogImageStorageService;
|
||||||
|
|
||||||
public ProductService(ProductRepository productRepository, CategoryRepository categoryRepository) {
|
public ProductService(ProductRepository productRepository, CategoryRepository categoryRepository, CatalogImageStorageService catalogImageStorageService) {
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
this.categoryRepository = categoryRepository;
|
this.categoryRepository = categoryRepository;
|
||||||
|
this.catalogImageStorageService = catalogImageStorageService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<ProductResponse> getAllProducts(String query, Pageable pageable) {
|
public Page<ProductResponse> getAllProducts(String query, Pageable pageable) {
|
||||||
@@ -74,17 +82,76 @@ public class ProductService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteProduct(Long id) {
|
public void deleteProduct(Long id) {
|
||||||
if (!productRepository.existsById(id)) {
|
Product product = findProduct(id);
|
||||||
throw new ResourceNotFoundException("Product not found with id: " + id);
|
deleteStoredImageIfPresent(product.getImageUrl());
|
||||||
}
|
productRepository.delete(product);
|
||||||
productRepository.deleteById(id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void bulkDeleteProducts(BulkDeleteRequest request) {
|
public void bulkDeleteProducts(BulkDeleteRequest request) {
|
||||||
|
productRepository.findAllById(request.getIds()).forEach(product -> deleteStoredImageIfPresent(product.getImageUrl()));
|
||||||
productRepository.deleteAllById(request.getIds());
|
productRepository.deleteAllById(request.getIds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ProductResponse uploadProductImage(Long id, MultipartFile file) throws IOException {
|
||||||
|
validateImageFile(file);
|
||||||
|
Product product = findProduct(id);
|
||||||
|
deleteStoredImageIfPresent(product.getImageUrl());
|
||||||
|
product.setImageUrl(catalogImageStorageService.storeProductImage(file));
|
||||||
|
return mapToResponse(productRepository.save(product));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public ProductResponse deleteProductImage(Long id) {
|
||||||
|
Product product = findProduct(id);
|
||||||
|
deleteStoredImageIfPresent(product.getImageUrl());
|
||||||
|
product.setImageUrl(null);
|
||||||
|
return mapToResponse(productRepository.save(product));
|
||||||
|
}
|
||||||
|
|
||||||
|
public ImagePayload loadProductImage(Long id) {
|
||||||
|
Product product = findProduct(id);
|
||||||
|
if (product.getImageUrl() == null || product.getImageUrl().isBlank()) {
|
||||||
|
throw new ResourceNotFoundException("Product image not found with id: " + id);
|
||||||
|
}
|
||||||
|
Resource resource = catalogImageStorageService.loadProductImage(product.getImageUrl());
|
||||||
|
MediaType mediaType = catalogImageStorageService.resolveMediaType(resource);
|
||||||
|
return new ImagePayload(resource, mediaType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Product findProduct(Long id) {
|
||||||
|
return productRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void validateImageFile(MultipartFile file) {
|
||||||
|
if (file == null || file.isEmpty()) {
|
||||||
|
throw new IllegalArgumentException("Please select an image to upload");
|
||||||
|
}
|
||||||
|
if (file.getSize() > 5 * 1024 * 1024) {
|
||||||
|
throw new IllegalArgumentException("Image file size must be less than 5MB");
|
||||||
|
}
|
||||||
|
String contentType = file.getContentType();
|
||||||
|
if (contentType == null) {
|
||||||
|
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
|
||||||
|
}
|
||||||
|
String normalized = contentType.toLowerCase(Locale.ROOT);
|
||||||
|
if (!normalized.equals("image/jpeg") && !normalized.equals("image/png") && !normalized.equals("image/gif")) {
|
||||||
|
throw new IllegalArgumentException("Only JPG, PNG, and GIF images are allowed");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deleteStoredImageIfPresent(String storedImagePath) {
|
||||||
|
if (storedImagePath == null || storedImagePath.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
catalogImageStorageService.deleteProductImage(storedImagePath);
|
||||||
|
} catch (IOException ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private ProductResponse mapToResponse(Product product) {
|
private ProductResponse mapToResponse(Product product) {
|
||||||
return new ProductResponse(
|
return new ProductResponse(
|
||||||
product.getProdId(),
|
product.getProdId(),
|
||||||
@@ -93,8 +160,12 @@ public class ProductService {
|
|||||||
product.getCategory().getCategoryName(),
|
product.getCategory().getCategoryName(),
|
||||||
product.getProdDesc(),
|
product.getProdDesc(),
|
||||||
product.getProdPrice(),
|
product.getProdPrice(),
|
||||||
|
product.getImageUrl() != null && !product.getImageUrl().isBlank() ? "/api/v1/products/" + product.getProdId() + "/image" : null,
|
||||||
product.getCreatedAt(),
|
product.getCreatedAt(),
|
||||||
product.getUpdatedAt()
|
product.getUpdatedAt()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public record ImagePayload(Resource resource, MediaType mediaType) {
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE pet
|
||||||
|
ADD COLUMN imageUrl VARCHAR(255) NULL;
|
||||||
|
|
||||||
|
ALTER TABLE product
|
||||||
|
ADD COLUMN imageUrl VARCHAR(255) NULL;
|
||||||
1
desktop/.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
target/
|
target/
|
||||||
|
nohup.out
|
||||||
!.mvn/wrapper/maven-wrapper.jar
|
!.mvn/wrapper/maven-wrapper.jar
|
||||||
!**/src/main/**/target/
|
!**/src/main/**/target/
|
||||||
!**/src/test/**/target/
|
!**/src/test/**/target/
|
||||||
|
|||||||
@@ -76,6 +76,14 @@
|
|||||||
<release>25</release>
|
<release>25</release>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.2.5</version>
|
||||||
|
<configuration>
|
||||||
|
<argLine>--add-opens org.example.petshopdesktop/org.example.petshopdesktop=ALL-UNNAMED</argLine>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
<plugin>
|
<plugin>
|
||||||
<groupId>org.openjfx</groupId>
|
<groupId>org.openjfx</groupId>
|
||||||
<artifactId>javafx-maven-plugin</artifactId>
|
<artifactId>javafx-maven-plugin</artifactId>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ module org.example.petshopdesktop {
|
|||||||
requires javafx.controls;
|
requires javafx.controls;
|
||||||
requires javafx.fxml;
|
requires javafx.fxml;
|
||||||
requires javafx.web;
|
requires javafx.web;
|
||||||
|
requires java.desktop;
|
||||||
requires java.sql;
|
requires java.sql;
|
||||||
requires java.net.http;
|
requires java.net.http;
|
||||||
requires com.fasterxml.jackson.databind;
|
requires com.fasterxml.jackson.databind;
|
||||||
|
|||||||
@@ -15,15 +15,17 @@ public class ProductDTO {
|
|||||||
private SimpleIntegerProperty categoryId; //used for edit and delete
|
private SimpleIntegerProperty categoryId; //used for edit and delete
|
||||||
private SimpleStringProperty categoryName;
|
private SimpleStringProperty categoryName;
|
||||||
private SimpleStringProperty prodDesc;
|
private SimpleStringProperty prodDesc;
|
||||||
|
private SimpleStringProperty imageUrl;
|
||||||
|
|
||||||
//constructor
|
//constructor
|
||||||
public ProductDTO(int prodId, String prodName, double prodPrice, int categoryId, String categoryName, String prodDesc) {
|
public ProductDTO(int prodId, String prodName, double prodPrice, int categoryId, String categoryName, String prodDesc, String imageUrl) {
|
||||||
this.prodId = new SimpleIntegerProperty(prodId);
|
this.prodId = new SimpleIntegerProperty(prodId);
|
||||||
this.prodName = new SimpleStringProperty(prodName);
|
this.prodName = new SimpleStringProperty(prodName);
|
||||||
this.prodPrice = new SimpleDoubleProperty(prodPrice);
|
this.prodPrice = new SimpleDoubleProperty(prodPrice);
|
||||||
this.categoryId = new SimpleIntegerProperty(categoryId);
|
this.categoryId = new SimpleIntegerProperty(categoryId);
|
||||||
this.categoryName = new SimpleStringProperty(categoryName);
|
this.categoryName = new SimpleStringProperty(categoryName);
|
||||||
this.prodDesc = new SimpleStringProperty(prodDesc);
|
this.prodDesc = new SimpleStringProperty(prodDesc);
|
||||||
|
this.imageUrl = new SimpleStringProperty(imageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
//getter and setters
|
//getter and setters
|
||||||
@@ -99,6 +101,18 @@ public class ProductDTO {
|
|||||||
this.categoryId.set(categoryId);
|
this.categoryId.set(categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleStringProperty imageUrlProperty() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl.set(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Converts DTO into product for editing and deleting
|
* Converts DTO into product for editing and deleting
|
||||||
* @return
|
* @return
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class Validator {
|
|||||||
if (value == null || value.isBlank()){
|
if (value == null || value.isBlank()){
|
||||||
msg += name + " is required. \n";
|
msg += name + " is required. \n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,8 +25,13 @@ public class Validator {
|
|||||||
*/
|
*/
|
||||||
public static String isNonNegativeDouble(String value, String name){
|
public static String isNonNegativeDouble(String value, String name){
|
||||||
String msg ="";
|
String msg ="";
|
||||||
|
if (value == null) {
|
||||||
|
msg += name + " must be a number.\n";
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
double result;
|
double result;
|
||||||
try{
|
try {
|
||||||
result = Double.parseDouble(value);
|
result = Double.parseDouble(value);
|
||||||
if (result < 0){
|
if (result < 0){
|
||||||
msg += name + " must be greater than or equal 0. \n";
|
msg += name + " must be greater than or equal 0. \n";
|
||||||
@@ -34,6 +40,34 @@ public class Validator {
|
|||||||
catch (NumberFormatException e){
|
catch (NumberFormatException e){
|
||||||
msg += name + " must be a number.\n";
|
msg += name + " must be a number.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the input is a positive double
|
||||||
|
* @param value input of string
|
||||||
|
* @param name name of input
|
||||||
|
* @return error msg if input is not a number or not positive, otherwise empty
|
||||||
|
*/
|
||||||
|
public static String isPositiveDouble(String value, String name){
|
||||||
|
String msg ="";
|
||||||
|
if (value == null) {
|
||||||
|
msg += name + " must be a number.\n";
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
double result;
|
||||||
|
try {
|
||||||
|
result = Double.parseDouble(value);
|
||||||
|
if (result <= 0){
|
||||||
|
msg += name + " must be greater than 0. \n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e){
|
||||||
|
msg += name + " must be a number.\n";
|
||||||
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -47,8 +81,13 @@ public class Validator {
|
|||||||
*/
|
*/
|
||||||
public static String isDoubleInRange(String value, String name, double minValue, double maxValue){
|
public static String isDoubleInRange(String value, String name, double minValue, double maxValue){
|
||||||
String msg ="";
|
String msg ="";
|
||||||
|
if (value == null) {
|
||||||
|
msg += name + " must be a number.\n";
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
double result;
|
double result;
|
||||||
try{
|
try {
|
||||||
result = Double.parseDouble(value);
|
result = Double.parseDouble(value);
|
||||||
if (result < minValue || result > maxValue){
|
if (result < minValue || result > maxValue){
|
||||||
msg += name + " must be between " + minValue + " and " + maxValue + "\n";
|
msg += name + " must be between " + minValue + " and " + maxValue + "\n";
|
||||||
@@ -57,6 +96,7 @@ public class Validator {
|
|||||||
catch (NumberFormatException e){
|
catch (NumberFormatException e){
|
||||||
msg += name + " must be a number.\n";
|
msg += name + " must be a number.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +109,7 @@ public class Validator {
|
|||||||
public static String isNonNegativeInteger(String value, String name){
|
public static String isNonNegativeInteger(String value, String name){
|
||||||
String msg ="";
|
String msg ="";
|
||||||
int result;
|
int result;
|
||||||
try{
|
try {
|
||||||
result = Integer.parseInt(value);
|
result = Integer.parseInt(value);
|
||||||
if (result < 0){
|
if (result < 0){
|
||||||
msg += name + " must be greater than or equal 0. \n";
|
msg += name + " must be greater than or equal 0. \n";
|
||||||
@@ -78,6 +118,29 @@ public class Validator {
|
|||||||
catch (NumberFormatException e){
|
catch (NumberFormatException e){
|
||||||
msg += name + " must be a whole number.\n";
|
msg += name + " must be a whole number.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the input is a positive integer
|
||||||
|
* @param value input of string
|
||||||
|
* @param name name of input
|
||||||
|
* @return error msg if input is not a number or not positive, otherwise empty
|
||||||
|
*/
|
||||||
|
public static String isPositiveInteger(String value, String name){
|
||||||
|
String msg ="";
|
||||||
|
int result;
|
||||||
|
try {
|
||||||
|
result = Integer.parseInt(value);
|
||||||
|
if (result <= 0){
|
||||||
|
msg += name + " must be greater than 0. \n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (NumberFormatException e){
|
||||||
|
msg += name + " must be a whole number.\n";
|
||||||
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,9 +153,10 @@ public class Validator {
|
|||||||
*/
|
*/
|
||||||
public static String isLessThanVarChars(String value, String name, int length){
|
public static String isLessThanVarChars(String value, String name, int length){
|
||||||
String msg ="";
|
String msg ="";
|
||||||
if (value.length() > length){
|
if (value == null || value.length() > length){
|
||||||
msg += name + " must be less than " + length + " characters. \n";
|
msg += name + " must be less than " + length + " characters. \n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,11 +168,17 @@ public class Validator {
|
|||||||
*/
|
*/
|
||||||
public static String isValidEmail(String value, String name){
|
public static String isValidEmail(String value, String name){
|
||||||
String msg = "";
|
String msg = "";
|
||||||
|
if (value == null) {
|
||||||
|
msg += name + " is not in a valid format. \n";
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
|
String regex = "^[A-Za-z0-9+_.-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}$";
|
||||||
|
|
||||||
if (!value.matches(regex)){
|
if (!value.matches(regex)){
|
||||||
msg += name + " is not in a valid format. \n";
|
msg += name + " is not in a valid format. \n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,11 +190,17 @@ public class Validator {
|
|||||||
*/
|
*/
|
||||||
public static String isValidPhoneNumber(String value, String name){
|
public static String isValidPhoneNumber(String value, String name){
|
||||||
String msg = "";
|
String msg = "";
|
||||||
|
if (value == null) {
|
||||||
|
msg += name + " must be in format XXX-XXX-XXXX. \n";
|
||||||
|
|
||||||
|
return msg;
|
||||||
|
}
|
||||||
String regex = "^\\d{3}-\\d{3}-\\d{4}$";
|
String regex = "^\\d{3}-\\d{3}-\\d{4}$";
|
||||||
|
|
||||||
if (!value.matches(regex)){
|
if (!value.matches(regex)){
|
||||||
msg += name + " must be in format XXX-XXX-XXXX. \n";
|
msg += name + " must be in format XXX-XXX-XXXX. \n";
|
||||||
}
|
}
|
||||||
|
|
||||||
return msg;
|
return msg;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,31 @@ public class ApiClient {
|
|||||||
return handleResponse(response, responseClass);
|
return handleResponse(response, responseClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getBytes(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<byte[]> response = httpClient.send(request, HttpResponse.BodyHandlers.ofByteArray());
|
||||||
|
|
||||||
|
int statusCode = response.statusCode();
|
||||||
|
if (statusCode == 200 || statusCode == 201) {
|
||||||
|
return response.body();
|
||||||
|
} 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 if (statusCode == 404) {
|
||||||
|
throw new RuntimeException("File not found.");
|
||||||
|
} else {
|
||||||
|
throw new RuntimeException("Request failed with status " + statusCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public String getRawResponse(String path) throws Exception {
|
public String getRawResponse(String path) throws Exception {
|
||||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(baseUrl + path))
|
.uri(URI.create(baseUrl + path))
|
||||||
@@ -199,15 +224,21 @@ public class ApiClient {
|
|||||||
try {
|
try {
|
||||||
if (response.body() != null && !response.body().isEmpty()) {
|
if (response.body() != null && !response.body().isEmpty()) {
|
||||||
var errorNode = objectMapper.readTree(response.body());
|
var errorNode = objectMapper.readTree(response.body());
|
||||||
if (errorNode.has("message")) {
|
|
||||||
return errorNode.get("message").asText();
|
|
||||||
}
|
|
||||||
if (errorNode.has("errors")) {
|
if (errorNode.has("errors")) {
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
errorNode.get("errors").fields().forEachRemaining(entry -> {
|
errorNode.get("errors").fields().forEachRemaining(entry -> {
|
||||||
sb.append(entry.getValue().asText()).append("\n");
|
String errorText = entry.getValue().asText();
|
||||||
|
if (errorText != null && !errorText.isBlank()) {
|
||||||
|
sb.append(errorText).append("\n");
|
||||||
|
}
|
||||||
});
|
});
|
||||||
return sb.toString().trim();
|
if (sb.length() > 0) {
|
||||||
|
String message = errorNode.has("message") ? errorNode.get("message").asText() : null;
|
||||||
|
return (message != null && !message.isBlank() ? message + "\n" : "") + sb.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (errorNode.has("message")) {
|
||||||
|
return errorNode.get("message").asText();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ public class PetResponse {
|
|||||||
private Integer petAge;
|
private Integer petAge;
|
||||||
private String petStatus;
|
private String petStatus;
|
||||||
private BigDecimal petPrice;
|
private BigDecimal petPrice;
|
||||||
|
private String imageUrl;
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
@@ -73,6 +74,14 @@ public class PetResponse {
|
|||||||
this.petPrice = petPrice;
|
this.petPrice = petPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ public class ProductResponse {
|
|||||||
private String categoryName;
|
private String categoryName;
|
||||||
private BigDecimal prodPrice;
|
private BigDecimal prodPrice;
|
||||||
private String prodDesc;
|
private String prodDesc;
|
||||||
|
private String imageUrl;
|
||||||
|
|
||||||
public ProductResponse() {
|
public ProductResponse() {
|
||||||
}
|
}
|
||||||
@@ -51,4 +52,12 @@ public class ProductResponse {
|
|||||||
public void setProdDesc(String prodDesc) {
|
public void setProdDesc(String prodDesc) {
|
||||||
this.prodDesc = prodDesc;
|
this.prodDesc = prodDesc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl = imageUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,10 @@ public class AuthApi {
|
|||||||
return apiClient.postMultipart("/api/v1/auth/me/avatar", "avatar", filePath, AvatarUploadResponse.class);
|
return apiClient.postMultipart("/api/v1/auth/me/avatar", "avatar", filePath, AvatarUploadResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public byte[] getMyAvatarFile() throws Exception {
|
||||||
|
return apiClient.getBytes("/api/v1/auth/me/avatar/file");
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteAvatar() throws Exception {
|
public void deleteAvatar() throws Exception {
|
||||||
apiClient.delete("/api/v1/auth/me/avatar");
|
apiClient.delete("/api/v1/auth/me/avatar");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.example.petshopdesktop.api.dto.pet.PetResponse;
|
|||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class PetApi {
|
public class PetApi {
|
||||||
@@ -47,6 +48,18 @@ public class PetApi {
|
|||||||
return apiClient.put("/api/v1/pets/" + id, request, PetResponse.class);
|
return apiClient.put("/api/v1/pets/" + id, request, PetResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PetResponse uploadPetImage(Long id, Path imagePath) throws Exception {
|
||||||
|
return apiClient.postMultipart("/api/v1/pets/" + id + "/image", "image", imagePath, PetResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deletePetImage(Long id) throws Exception {
|
||||||
|
apiClient.delete("/api/v1/pets/" + id + "/image");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getPetImage(Long id) throws Exception {
|
||||||
|
return apiClient.getBytes("/api/v1/pets/" + id + "/image");
|
||||||
|
}
|
||||||
|
|
||||||
public void deletePets(List<Long> ids) throws Exception {
|
public void deletePets(List<Long> ids) throws Exception {
|
||||||
apiClient.deleteWithBody("/api/v1/pets", new BulkDeleteRequest(ids));
|
apiClient.deleteWithBody("/api/v1/pets", new BulkDeleteRequest(ids));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
|||||||
|
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.nio.file.Path;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ProductApi {
|
public class ProductApi {
|
||||||
@@ -47,6 +48,18 @@ public class ProductApi {
|
|||||||
return apiClient.put("/api/v1/products/" + id, request, ProductResponse.class);
|
return apiClient.put("/api/v1/products/" + id, request, ProductResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ProductResponse uploadProductImage(Long id, Path imagePath) throws Exception {
|
||||||
|
return apiClient.postMultipart("/api/v1/products/" + id + "/image", "image", imagePath, ProductResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void deleteProductImage(Long id) throws Exception {
|
||||||
|
apiClient.delete("/api/v1/products/" + id + "/image");
|
||||||
|
}
|
||||||
|
|
||||||
|
public byte[] getProductImage(Long id) throws Exception {
|
||||||
|
return apiClient.getBytes("/api/v1/products/" + id + "/image");
|
||||||
|
}
|
||||||
|
|
||||||
public void deleteProducts(List<Long> ids) throws Exception {
|
public void deleteProducts(List<Long> ids) throws Exception {
|
||||||
apiClient.deleteWithBody("/api/v1/products", new BulkDeleteRequest(ids));
|
apiClient.deleteWithBody("/api/v1/products", new BulkDeleteRequest(ids));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,18 @@ import javafx.scene.layout.StackPane;
|
|||||||
import javafx.scene.paint.Color;
|
import javafx.scene.paint.Color;
|
||||||
import javafx.scene.paint.ImagePattern;
|
import javafx.scene.paint.ImagePattern;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.stage.FileChooser;
|
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.ApiConfig;
|
|
||||||
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
||||||
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
||||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.AuthApi;
|
import org.example.petshopdesktop.api.endpoints.AuthApi;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
|
import org.example.petshopdesktop.util.FilePickerSupport;
|
||||||
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
|
||||||
public class MainLayoutController {
|
public class MainLayoutController {
|
||||||
|
|
||||||
private static final String NAV_BASE_STYLE = "-fx-background-color: transparent; " +
|
private static final String NAV_BASE_STYLE = "-fx-background-color: transparent; " +
|
||||||
@@ -205,12 +206,7 @@ public class MainLayoutController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnChangeAvatarClicked(ActionEvent event) {
|
void btnChangeAvatarClicked(ActionEvent event) {
|
||||||
FileChooser chooser = new FileChooser();
|
java.io.File file = FilePickerSupport.pickImageFile(btnChangeAvatar.getScene().getWindow());
|
||||||
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) {
|
if (file == null) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -218,8 +214,7 @@ public class MainLayoutController {
|
|||||||
try {
|
try {
|
||||||
AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath());
|
AvatarUploadResponse response = AuthApi.getInstance().uploadAvatar(file.toPath());
|
||||||
UserSession.getInstance().setAvatarUrl(response.getAvatarUrl());
|
UserSession.getInstance().setAvatarUrl(response.getAvatarUrl());
|
||||||
renderAvatar(UserSession.getInstance().getEmployeeName(), response.getAvatarUrl());
|
refreshProfileHeader();
|
||||||
btnRemoveAvatar.setDisable(response.getAvatarUrl() == null || response.getAvatarUrl().isBlank());
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar");
|
ActivityLogger.getInstance().logException("MainLayoutController.btnChangeAvatarClicked", e, "Uploading avatar");
|
||||||
showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture.");
|
showAvatarError(e.getMessage() != null ? e.getMessage() : "Could not upload profile picture.");
|
||||||
@@ -263,7 +258,7 @@ public class MainLayoutController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg", 94));
|
logoContainer.getChildren().setAll(SvgWebViewFactory.build("/org/example/petshopdesktop/images/leons-pet-store-badge-light.svg", 94));
|
||||||
renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl());
|
renderAvatar(UserSession.getInstance().getEmployeeName(), null);
|
||||||
btnRemoveAvatar.setDisable(UserSession.getInstance().getAvatarUrl() == null || UserSession.getInstance().getAvatarUrl().isBlank());
|
btnRemoveAvatar.setDisable(UserSession.getInstance().getAvatarUrl() == null || UserSession.getInstance().getAvatarUrl().isBlank());
|
||||||
refreshProfileHeader();
|
refreshProfileHeader();
|
||||||
applyRBAC();
|
applyRBAC();
|
||||||
@@ -285,20 +280,35 @@ public class MainLayoutController {
|
|||||||
String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank()
|
String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank()
|
||||||
? UserSession.getInstance().getUsername()
|
? UserSession.getInstance().getUsername()
|
||||||
: userInfo.getFullName();
|
: userInfo.getFullName();
|
||||||
|
Image avatarImage = loadAvatarImage(userInfo.getAvatarUrl());
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
UserSession.getInstance().setEmployeeName(displayName);
|
UserSession.getInstance().setEmployeeName(displayName);
|
||||||
UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl());
|
UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl());
|
||||||
lblUsername.setText(displayName);
|
lblUsername.setText(displayName);
|
||||||
renderAvatar(displayName, userInfo.getAvatarUrl());
|
renderAvatar(displayName, avatarImage);
|
||||||
btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank());
|
btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank());
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), UserSession.getInstance().getAvatarUrl()));
|
Platform.runLater(() -> renderAvatar(UserSession.getInstance().getEmployeeName(), null));
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void renderAvatar(String displayName, String avatarUrl) {
|
private Image loadAvatarImage(String avatarUrl) {
|
||||||
|
if (avatarUrl == null || avatarUrl.isBlank()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
byte[] imageBytes = AuthApi.getInstance().getMyAvatarFile();
|
||||||
|
Image image = new Image(new ByteArrayInputStream(imageBytes), 52, 52, true, true);
|
||||||
|
return image.isError() ? null : image;
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderAvatar(String displayName, Image avatarImage) {
|
||||||
Circle border = new Circle(29);
|
Circle border = new Circle(29);
|
||||||
border.setFill(Color.web("#dbe4ee"));
|
border.setFill(Color.web("#dbe4ee"));
|
||||||
|
|
||||||
@@ -306,21 +316,9 @@ public class MainLayoutController {
|
|||||||
Label initials = new Label(initials(displayName));
|
Label initials = new Label(initials(displayName));
|
||||||
initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;");
|
initials.setStyle("-fx-text-fill: white; -fx-font-weight: bold; -fx-font-size: 16px;");
|
||||||
|
|
||||||
if (avatarUrl != null && !avatarUrl.isBlank()) {
|
if (avatarImage != null) {
|
||||||
try {
|
circle.setFill(new ImagePattern(avatarImage));
|
||||||
String resolvedUrl = avatarUrl.startsWith("http") ? avatarUrl : ApiConfig.getInstance().getBaseUrl() + avatarUrl;
|
initials.setVisible(false);
|
||||||
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 {
|
} else {
|
||||||
circle.setFill(Color.web("#4ECDC4"));
|
circle.setFill(Color.web("#4ECDC4"));
|
||||||
initials.setVisible(true);
|
initials.setVisible(true);
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import javafx.collections.ObservableList;
|
|||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.dto.pet.PetResponse;
|
import org.example.petshopdesktop.api.dto.pet.PetResponse;
|
||||||
@@ -16,6 +19,7 @@ import org.example.petshopdesktop.api.endpoints.PetApi;
|
|||||||
import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController;
|
import org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController;
|
||||||
import org.example.petshopdesktop.models.Pet;
|
import org.example.petshopdesktop.models.Pet;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -42,6 +46,9 @@ public class PetController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TableColumn<Pet, Integer> colPetId;
|
private TableColumn<Pet, Integer> colPetId;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableColumn<Pet, String> colPetImage;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TableColumn<Pet, String> colPetName;
|
private TableColumn<Pet, String> colPetName;
|
||||||
|
|
||||||
@@ -134,12 +141,14 @@ public class PetController {
|
|||||||
tvPets.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
tvPets.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||||
|
|
||||||
colPetId.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petId"));
|
colPetId.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petId"));
|
||||||
|
colPetImage.setCellValueFactory(new PropertyValueFactory<Pet, String>("imageUrl"));
|
||||||
colPetName.setCellValueFactory(new PropertyValueFactory<Pet,String>("petName"));
|
colPetName.setCellValueFactory(new PropertyValueFactory<Pet,String>("petName"));
|
||||||
colPetSpecies.setCellValueFactory(new PropertyValueFactory<Pet,String>("petSpecies"));
|
colPetSpecies.setCellValueFactory(new PropertyValueFactory<Pet,String>("petSpecies"));
|
||||||
colPetBreed.setCellValueFactory(new PropertyValueFactory<Pet,String>("petBreed"));
|
colPetBreed.setCellValueFactory(new PropertyValueFactory<Pet,String>("petBreed"));
|
||||||
colPetAge.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petAge"));
|
colPetAge.setCellValueFactory(new PropertyValueFactory<Pet,Integer>("petAge"));
|
||||||
colPetStatus.setCellValueFactory(new PropertyValueFactory<Pet,String>("petStatus"));
|
colPetStatus.setCellValueFactory(new PropertyValueFactory<Pet,String>("petStatus"));
|
||||||
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
|
colPetPrice.setCellValueFactory(new PropertyValueFactory<Pet,Double>("petPrice"));
|
||||||
|
configureImageColumn(colPetImage);
|
||||||
|
|
||||||
displayPets();
|
displayPets();
|
||||||
|
|
||||||
@@ -262,8 +271,30 @@ public class PetController {
|
|||||||
response.getPetBreed(),
|
response.getPetBreed(),
|
||||||
response.getPetAge() != null ? response.getPetAge() : 0,
|
response.getPetAge() != null ? response.getPetAge() : 0,
|
||||||
response.getPetStatus(),
|
response.getPetStatus(),
|
||||||
response.getPetPrice().doubleValue()
|
response.getPetPrice().doubleValue(),
|
||||||
|
response.getImageUrl()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void configureImageColumn(TableColumn<Pet, String> column) {
|
||||||
|
column.setCellFactory(col -> new TableCell<>() {
|
||||||
|
private final ImageView imageView = new ImageView();
|
||||||
|
private final StackPane container = new StackPane(imageView);
|
||||||
|
{
|
||||||
|
container.setAlignment(Pos.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null || item.isBlank()) {
|
||||||
|
setGraphic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DesktopImageSupport.loadImageInto(imageView, item, 48, 48);
|
||||||
|
setGraphic(container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,12 @@ import javafx.collections.ObservableList;
|
|||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.DTOs.ProductDTO;
|
import org.example.petshopdesktop.DTOs.ProductDTO;
|
||||||
@@ -16,6 +19,7 @@ import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
|||||||
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
||||||
import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController;
|
import org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
@@ -46,6 +50,9 @@ public class ProductController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TableColumn<ProductDTO, Integer> colProductId;
|
private TableColumn<ProductDTO, Integer> colProductId;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableColumn<ProductDTO, String> colProductImage;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TableColumn<ProductDTO, String> colProductName;
|
private TableColumn<ProductDTO, String> colProductName;
|
||||||
|
|
||||||
@@ -74,10 +81,12 @@ public class ProductController {
|
|||||||
tvProducts.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
tvProducts.getSelectionModel().setSelectionMode(javafx.scene.control.SelectionMode.MULTIPLE);
|
||||||
//set up table columns
|
//set up table columns
|
||||||
colProductId.setCellValueFactory(new PropertyValueFactory<ProductDTO,Integer>("prodId"));
|
colProductId.setCellValueFactory(new PropertyValueFactory<ProductDTO,Integer>("prodId"));
|
||||||
|
colProductImage.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("imageUrl"));
|
||||||
colProductName.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodName"));
|
colProductName.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodName"));
|
||||||
colProductPrice.setCellValueFactory(new PropertyValueFactory<ProductDTO,Double>("prodPrice"));
|
colProductPrice.setCellValueFactory(new PropertyValueFactory<ProductDTO,Double>("prodPrice"));
|
||||||
colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName"));
|
colProductCategory.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("categoryName"));
|
||||||
colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodDesc"));
|
colProductDesc.setCellValueFactory(new PropertyValueFactory<ProductDTO,String>("prodDesc"));
|
||||||
|
configureImageColumn(colProductImage);
|
||||||
|
|
||||||
displayProduct();
|
displayProduct();
|
||||||
|
|
||||||
@@ -292,8 +301,30 @@ public class ProductController {
|
|||||||
response.getProdPrice().doubleValue(),
|
response.getProdPrice().doubleValue(),
|
||||||
0,
|
0,
|
||||||
response.getCategoryName(),
|
response.getCategoryName(),
|
||||||
response.getProdDesc()
|
response.getProdDesc(),
|
||||||
|
response.getImageUrl()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void configureImageColumn(TableColumn<ProductDTO, String> column) {
|
||||||
|
column.setCellFactory(col -> new TableCell<>() {
|
||||||
|
private final ImageView imageView = new ImageView();
|
||||||
|
private final StackPane container = new StackPane(imageView);
|
||||||
|
{
|
||||||
|
container.setAlignment(Pos.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null || item.isBlank()) {
|
||||||
|
setGraphic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DesktopImageSupport.loadImageInto(imageView, item, 48, 48);
|
||||||
|
setGraphic(container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import javafx.event.EventHandler;
|
|||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.Validator;
|
import org.example.petshopdesktop.Validator;
|
||||||
@@ -14,8 +15,12 @@ import org.example.petshopdesktop.api.dto.pet.PetResponse;
|
|||||||
import org.example.petshopdesktop.api.endpoints.PetApi;
|
import org.example.petshopdesktop.api.endpoints.PetApi;
|
||||||
import org.example.petshopdesktop.models.Pet;
|
import org.example.petshopdesktop.models.Pet;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||||
|
import org.example.petshopdesktop.util.FilePickerSupport;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
|
|
||||||
public class PetDialogController {
|
public class PetDialogController {
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
@@ -24,6 +29,12 @@ public class PetDialogController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Button btnSave;
|
private Button btnSave;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnChangeImage;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnRemoveImage;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ComboBox<String> cbPetStatus;
|
private ComboBox<String> cbPetStatus;
|
||||||
|
|
||||||
@@ -33,6 +44,12 @@ public class PetDialogController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Label lblPetId;
|
private Label lblPetId;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblImageStatus;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ImageView imgPetPreview;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TextField txtPetAge;
|
private TextField txtPetAge;
|
||||||
|
|
||||||
@@ -49,6 +66,9 @@ public class PetDialogController {
|
|||||||
private TextField txtPetSpecies;
|
private TextField txtPetSpecies;
|
||||||
|
|
||||||
private String mode = null;
|
private String mode = null;
|
||||||
|
private File selectedImageFile;
|
||||||
|
private String currentImageUrl;
|
||||||
|
private boolean removeImageRequested;
|
||||||
|
|
||||||
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
||||||
"Available", "Adopted"
|
"Available", "Adopted"
|
||||||
@@ -73,6 +93,10 @@ public class PetDialogController {
|
|||||||
closeStage(mouseEvent);
|
closeStage(mouseEvent);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
|
||||||
|
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
|
||||||
|
refreshImagePreview();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void buttonSaveClicked(MouseEvent mouseEvent) {
|
private void buttonSaveClicked(MouseEvent mouseEvent) {
|
||||||
@@ -97,13 +121,14 @@ public class PetDialogController {
|
|||||||
|
|
||||||
//Check validation (format)
|
//Check validation (format)
|
||||||
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
|
errorMsg += Validator.isNonNegativeDouble(txtPetPrice.getText(), "Price");
|
||||||
errorMsg += Validator.isNonNegativeInteger(txtPetAge.getText(), "Age");
|
errorMsg += Validator.isPositiveInteger(txtPetAge.getText(), "Age");
|
||||||
|
|
||||||
if(errorMsg.isEmpty()){
|
if(errorMsg.isEmpty()){
|
||||||
PetRequest request = buildPetRequest();
|
PetRequest request = buildPetRequest();
|
||||||
try {
|
try {
|
||||||
if(mode.equals("Add")) {
|
if(mode.equals("Add")) {
|
||||||
PetApi.getInstance().createPet(request);
|
PetResponse response = PetApi.getInstance().createPet(request);
|
||||||
|
applyImageChanges(response.getPetId());
|
||||||
} else {
|
} else {
|
||||||
String[] parts = lblPetId.getText().split(": ");
|
String[] parts = lblPetId.getText().split(": ");
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
@@ -111,6 +136,7 @@ public class PetDialogController {
|
|||||||
}
|
}
|
||||||
Long petId = Long.parseLong(parts[1]);
|
Long petId = Long.parseLong(parts[1]);
|
||||||
PetApi.getInstance().updatePet(petId, request);
|
PetApi.getInstance().updatePet(petId, request);
|
||||||
|
applyImageChanges(petId);
|
||||||
}
|
}
|
||||||
|
|
||||||
//tell the user operation was successful
|
//tell the user operation was successful
|
||||||
@@ -175,6 +201,10 @@ public class PetDialogController {
|
|||||||
txtPetBreed.setText(pet.getPetBreed());
|
txtPetBreed.setText(pet.getPetBreed());
|
||||||
txtPetAge.setText(pet.getPetAge() + "");
|
txtPetAge.setText(pet.getPetAge() + "");
|
||||||
txtPetPrice.setText(pet.getPetPrice() + "");
|
txtPetPrice.setText(pet.getPetPrice() + "");
|
||||||
|
currentImageUrl = pet.getImageUrl();
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = false;
|
||||||
|
refreshImagePreview();
|
||||||
|
|
||||||
//get the right combobox selection
|
//get the right combobox selection
|
||||||
for (String status : cbPetStatus.getItems()) {
|
for (String status : cbPetStatus.getItems()) {
|
||||||
@@ -192,10 +222,76 @@ public class PetDialogController {
|
|||||||
lblMode.setText(mode + " Pet");
|
lblMode.setText(mode + " Pet");
|
||||||
if(mode.equals("Add")) {
|
if(mode.equals("Add")) {
|
||||||
lblPetId.setVisible(false);
|
lblPetId.setVisible(false);
|
||||||
|
currentImageUrl = null;
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = false;
|
||||||
|
refreshImagePreview();
|
||||||
}
|
}
|
||||||
else if(mode.equals("Edit")) {
|
else if(mode.equals("Edit")) {
|
||||||
lblPetId.setVisible(true);
|
lblPetId.setVisible(true);
|
||||||
|
refreshImagePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleChangeImage() {
|
||||||
|
File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow());
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedImageFile = file;
|
||||||
|
removeImageRequested = false;
|
||||||
|
lblImageStatus.setText("Selected: " + file.getName());
|
||||||
|
DesktopImageSupport.loadImageInto(imgPetPreview, file.toURI().toString(), 120, 120);
|
||||||
|
btnRemoveImage.setDisable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRemoveImage() {
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = true;
|
||||||
|
currentImageUrl = null;
|
||||||
|
refreshImagePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyImageChanges(Long petId) throws Exception {
|
||||||
|
String previousImageUrl = currentImageUrl;
|
||||||
|
if (removeImageRequested) {
|
||||||
|
try {
|
||||||
|
PetApi.getInstance().deletePetImage(petId);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedImageFile != null) {
|
||||||
|
PetApi.getInstance().uploadPetImage(petId, selectedImageFile.toPath());
|
||||||
|
currentImageUrl = "/api/v1/pets/" + petId + "/image";
|
||||||
|
} else if (removeImageRequested) {
|
||||||
|
currentImageUrl = null;
|
||||||
|
}
|
||||||
|
DesktopImageSupport.evict(previousImageUrl);
|
||||||
|
DesktopImageSupport.evict(currentImageUrl);
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = false;
|
||||||
|
refreshImagePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshImagePreview() {
|
||||||
|
if (imgPetPreview == null || lblImageStatus == null || btnRemoveImage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
imgPetPreview.setImage(null);
|
||||||
|
if (selectedImageFile != null) {
|
||||||
|
lblImageStatus.setText("Selected: " + selectedImageFile.getName());
|
||||||
|
DesktopImageSupport.loadImageInto(imgPetPreview, selectedImageFile.toURI().toString(), 120, 120);
|
||||||
|
btnRemoveImage.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentImageUrl != null && !currentImageUrl.isBlank()) {
|
||||||
|
lblImageStatus.setText("Current image loaded");
|
||||||
|
DesktopImageSupport.loadImageInto(imgPetPreview, currentImageUrl, 120, 120);
|
||||||
|
btnRemoveImage.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lblImageStatus.setText("No image selected");
|
||||||
|
btnRemoveImage.setDisable(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,21 @@ import javafx.event.EventHandler;
|
|||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.DTOs.ProductDTO;
|
import org.example.petshopdesktop.DTOs.ProductDTO;
|
||||||
import org.example.petshopdesktop.Validator;
|
import org.example.petshopdesktop.Validator;
|
||||||
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.dto.product.ProductRequest;
|
import org.example.petshopdesktop.api.dto.product.ProductRequest;
|
||||||
|
import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||||
|
import org.example.petshopdesktop.util.FilePickerSupport;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -27,6 +32,12 @@ public class ProductDialogController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Button btnSave;
|
private Button btnSave;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnChangeImage;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnRemoveImage;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private ComboBox<DropdownOption> cbProdCategory;
|
private ComboBox<DropdownOption> cbProdCategory;
|
||||||
|
|
||||||
@@ -36,6 +47,12 @@ public class ProductDialogController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Label lblProdId;
|
private Label lblProdId;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblImageStatus;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ImageView imgProductPreview;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TextField txtProdDesc;
|
private TextField txtProdDesc;
|
||||||
|
|
||||||
@@ -46,6 +63,9 @@ public class ProductDialogController {
|
|||||||
private TextField txtProdPrice;
|
private TextField txtProdPrice;
|
||||||
|
|
||||||
private String mode = null;
|
private String mode = null;
|
||||||
|
private File selectedImageFile;
|
||||||
|
private String currentImageUrl;
|
||||||
|
private boolean removeImageRequested;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add event listeners to buttons when dialog loads
|
* Add event listeners to buttons when dialog loads
|
||||||
@@ -82,6 +102,10 @@ public class ProductDialogController {
|
|||||||
System.out.println("Error loading categories: " + e.getMessage());
|
System.out.println("Error loading categories: " + e.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
btnChangeImage.setOnMouseClicked(mouseEvent -> handleChangeImage());
|
||||||
|
btnRemoveImage.setOnMouseClicked(mouseEvent -> handleRemoveImage());
|
||||||
|
refreshImagePreview();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -106,7 +130,7 @@ public class ProductDialogController {
|
|||||||
errorMsg += Validator.isLessThanVarChars(txtProdPrice.getText(), "Product Price", 12);
|
errorMsg += Validator.isLessThanVarChars(txtProdPrice.getText(), "Product Price", 12);
|
||||||
|
|
||||||
//Check Validation (format)
|
//Check Validation (format)
|
||||||
errorMsg += Validator.isNonNegativeDouble(txtProdPrice.getText(), "Product Price");
|
errorMsg += Validator.isPositiveDouble(txtProdPrice.getText(), "Product Price");
|
||||||
|
|
||||||
if (errorMsg.isEmpty()) {
|
if (errorMsg.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
@@ -123,7 +147,8 @@ public class ProductDialogController {
|
|||||||
request.setProdDesc(txtProdDesc.getText());
|
request.setProdDesc(txtProdDesc.getText());
|
||||||
|
|
||||||
if (mode.equals("Add")) {
|
if (mode.equals("Add")) {
|
||||||
ProductApi.getInstance().createProduct(request);
|
ProductResponse response = ProductApi.getInstance().createProduct(request);
|
||||||
|
applyImageChanges(response.getProdId());
|
||||||
} else {
|
} else {
|
||||||
String[] parts = lblProdId.getText().split(": ");
|
String[] parts = lblProdId.getText().split(": ");
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) {
|
||||||
@@ -131,6 +156,7 @@ public class ProductDialogController {
|
|||||||
}
|
}
|
||||||
Long productId = Long.parseLong(parts[1]);
|
Long productId = Long.parseLong(parts[1]);
|
||||||
ProductApi.getInstance().updateProduct(productId, request);
|
ProductApi.getInstance().updateProduct(productId, request);
|
||||||
|
applyImageChanges(productId);
|
||||||
}
|
}
|
||||||
|
|
||||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||||
@@ -167,6 +193,10 @@ public class ProductDialogController {
|
|||||||
txtProdName.setText(product.getProdName());
|
txtProdName.setText(product.getProdName());
|
||||||
txtProdDesc.setText(product.getProdDesc());
|
txtProdDesc.setText(product.getProdDesc());
|
||||||
txtProdPrice.setText(product.getProdPrice() + "");
|
txtProdPrice.setText(product.getProdPrice() + "");
|
||||||
|
currentImageUrl = product.getImageUrl();
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = false;
|
||||||
|
refreshImagePreview();
|
||||||
|
|
||||||
for (DropdownOption category : cbProdCategory.getItems()) {
|
for (DropdownOption category : cbProdCategory.getItems()) {
|
||||||
if(category.getLabel().equals(product.getCategoryName())){
|
if(category.getLabel().equals(product.getCategoryName())){
|
||||||
@@ -197,10 +227,76 @@ public class ProductDialogController {
|
|||||||
lblMode.setText(mode + " Product");
|
lblMode.setText(mode + " Product");
|
||||||
if(mode.equals("Add")) {
|
if(mode.equals("Add")) {
|
||||||
lblProdId.setVisible(false);
|
lblProdId.setVisible(false);
|
||||||
|
currentImageUrl = null;
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = false;
|
||||||
|
refreshImagePreview();
|
||||||
}
|
}
|
||||||
else if(mode.equals("Edit")) {
|
else if(mode.equals("Edit")) {
|
||||||
lblProdId.setVisible(true);
|
lblProdId.setVisible(true);
|
||||||
|
refreshImagePreview();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void handleChangeImage() {
|
||||||
|
File file = FilePickerSupport.pickImageFile(btnSave.getScene().getWindow());
|
||||||
|
if (file == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
selectedImageFile = file;
|
||||||
|
removeImageRequested = false;
|
||||||
|
lblImageStatus.setText("Selected: " + file.getName());
|
||||||
|
DesktopImageSupport.loadImageInto(imgProductPreview, file.toURI().toString(), 120, 120);
|
||||||
|
btnRemoveImage.setDisable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void handleRemoveImage() {
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = true;
|
||||||
|
currentImageUrl = null;
|
||||||
|
refreshImagePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyImageChanges(Long productId) throws Exception {
|
||||||
|
String previousImageUrl = currentImageUrl;
|
||||||
|
if (removeImageRequested) {
|
||||||
|
try {
|
||||||
|
ProductApi.getInstance().deleteProductImage(productId);
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (selectedImageFile != null) {
|
||||||
|
ProductApi.getInstance().uploadProductImage(productId, selectedImageFile.toPath());
|
||||||
|
currentImageUrl = "/api/v1/products/" + productId + "/image";
|
||||||
|
} else if (removeImageRequested) {
|
||||||
|
currentImageUrl = null;
|
||||||
|
}
|
||||||
|
DesktopImageSupport.evict(previousImageUrl);
|
||||||
|
DesktopImageSupport.evict(currentImageUrl);
|
||||||
|
selectedImageFile = null;
|
||||||
|
removeImageRequested = false;
|
||||||
|
refreshImagePreview();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void refreshImagePreview() {
|
||||||
|
if (imgProductPreview == null || lblImageStatus == null || btnRemoveImage == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
imgProductPreview.setImage(null);
|
||||||
|
if (selectedImageFile != null) {
|
||||||
|
lblImageStatus.setText("Selected: " + selectedImageFile.getName());
|
||||||
|
DesktopImageSupport.loadImageInto(imgProductPreview, selectedImageFile.toURI().toString(), 120, 120);
|
||||||
|
btnRemoveImage.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentImageUrl != null && !currentImageUrl.isBlank()) {
|
||||||
|
lblImageStatus.setText("Current image loaded");
|
||||||
|
DesktopImageSupport.loadImageInto(imgProductPreview, currentImageUrl, 120, 120);
|
||||||
|
btnRemoveImage.setDisable(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
lblImageStatus.setText("No image selected");
|
||||||
|
btnRemoveImage.setDisable(true);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,8 +12,9 @@ public class Pet {
|
|||||||
private SimpleIntegerProperty petAge;
|
private SimpleIntegerProperty petAge;
|
||||||
private SimpleStringProperty petStatus;
|
private SimpleStringProperty petStatus;
|
||||||
private SimpleDoubleProperty petPrice;
|
private SimpleDoubleProperty petPrice;
|
||||||
|
private SimpleStringProperty imageUrl;
|
||||||
|
|
||||||
public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice) {
|
public Pet(int petId, String petName, String petSpecies, String petBreed, int petAge, String petStatus, double petPrice, String imageUrl) {
|
||||||
this.petId = new SimpleIntegerProperty(petId);
|
this.petId = new SimpleIntegerProperty(petId);
|
||||||
this.petName = new SimpleStringProperty(petName);
|
this.petName = new SimpleStringProperty(petName);
|
||||||
this.petSpecies = new SimpleStringProperty(petSpecies);
|
this.petSpecies = new SimpleStringProperty(petSpecies);
|
||||||
@@ -21,6 +22,7 @@ public class Pet {
|
|||||||
this.petAge = new SimpleIntegerProperty(petAge);
|
this.petAge = new SimpleIntegerProperty(petAge);
|
||||||
this.petStatus = new SimpleStringProperty(petStatus);
|
this.petStatus = new SimpleStringProperty(petStatus);
|
||||||
this.petPrice = new SimpleDoubleProperty(petPrice);
|
this.petPrice = new SimpleDoubleProperty(petPrice);
|
||||||
|
this.imageUrl = new SimpleStringProperty(imageUrl);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getPetId() {
|
public int getPetId() {
|
||||||
@@ -106,4 +108,16 @@ public class Pet {
|
|||||||
public SimpleDoubleProperty petPriceProperty() {
|
public SimpleDoubleProperty petPriceProperty() {
|
||||||
return petPrice;
|
return petPrice;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getImageUrl() {
|
||||||
|
return imageUrl.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setImageUrl(String imageUrl) {
|
||||||
|
this.imageUrl.set(imageUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
public SimpleStringProperty imageUrlProperty() {
|
||||||
|
return imageUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package org.example.petshopdesktop.util;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import org.example.petshopdesktop.api.ApiClient;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.ConcurrentHashMap;
|
||||||
|
|
||||||
|
public final class DesktopImageSupport {
|
||||||
|
|
||||||
|
private static final Map<String, Image> IMAGE_CACHE = new ConcurrentHashMap<>();
|
||||||
|
|
||||||
|
private DesktopImageSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void loadImageInto(ImageView imageView, String imageUrl, double width, double height) {
|
||||||
|
imageView.setFitWidth(width);
|
||||||
|
imageView.setFitHeight(height);
|
||||||
|
imageView.setPreserveRatio(true);
|
||||||
|
imageView.setSmooth(true);
|
||||||
|
imageView.setImage(null);
|
||||||
|
|
||||||
|
if (imageUrl == null || imageUrl.isBlank()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (imageUrl.startsWith("file:")) {
|
||||||
|
Image image = new Image(imageUrl, 0, 0, true, true);
|
||||||
|
if (!image.isError()) {
|
||||||
|
imageView.setImage(image);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Image cached = IMAGE_CACHE.get(imageUrl);
|
||||||
|
if (cached != null) {
|
||||||
|
imageView.setImage(cached);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
byte[] bytes = ApiClient.getInstance().getBytes(imageUrl);
|
||||||
|
Image image = new Image(new ByteArrayInputStream(bytes));
|
||||||
|
if (!image.isError()) {
|
||||||
|
IMAGE_CACHE.put(imageUrl, image);
|
||||||
|
Platform.runLater(() -> imageView.setImage(image));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
}, "desktop-image-loader").start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void evict(String imageUrl) {
|
||||||
|
if (imageUrl != null && !imageUrl.isBlank()) {
|
||||||
|
IMAGE_CACHE.remove(imageUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package org.example.petshopdesktop.util;
|
||||||
|
|
||||||
|
import javafx.stage.FileChooser;
|
||||||
|
import javafx.stage.Window;
|
||||||
|
|
||||||
|
import javax.swing.JFileChooser;
|
||||||
|
import javax.swing.UIManager;
|
||||||
|
import javax.swing.filechooser.FileNameExtensionFilter;
|
||||||
|
import java.awt.Component;
|
||||||
|
import java.awt.GraphicsEnvironment;
|
||||||
|
import java.io.File;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
import java.util.concurrent.atomic.AtomicReference;
|
||||||
|
|
||||||
|
public final class FilePickerSupport {
|
||||||
|
|
||||||
|
private FilePickerSupport() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public static File pickImageFile(Window ownerWindow) {
|
||||||
|
if (shouldUseAwtPicker()) {
|
||||||
|
return pickImageFileWithSwing();
|
||||||
|
}
|
||||||
|
return pickImageFileWithJavaFx(ownerWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static boolean shouldUseAwtPicker() {
|
||||||
|
if (GraphicsEnvironment.isHeadless()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
String sessionType = System.getenv("XDG_SESSION_TYPE");
|
||||||
|
String waylandDisplay = System.getenv("WAYLAND_DISPLAY");
|
||||||
|
return "wayland".equalsIgnoreCase(sessionType) || (waylandDisplay != null && !waylandDisplay.isBlank());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File pickImageFileWithJavaFx(Window ownerWindow) {
|
||||||
|
FileChooser chooser = new FileChooser();
|
||||||
|
chooser.setTitle("Choose Profile Picture");
|
||||||
|
chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Image Files", "*.png", "*.jpg", "*.jpeg", "*.gif"));
|
||||||
|
return chooser.showOpenDialog(ownerWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static File pickImageFileWithSwing() {
|
||||||
|
AtomicReference<File> selectedFile = new AtomicReference<>();
|
||||||
|
Runnable dialogTask = () -> {
|
||||||
|
try {
|
||||||
|
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
}
|
||||||
|
|
||||||
|
JFileChooser chooser = new JFileChooser();
|
||||||
|
chooser.setDialogTitle("Choose Profile Picture");
|
||||||
|
chooser.setAcceptAllFileFilterUsed(false);
|
||||||
|
chooser.setFileFilter(new FileNameExtensionFilter("Image Files", "png", "jpg", "jpeg", "gif"));
|
||||||
|
|
||||||
|
int result = chooser.showOpenDialog((Component) null);
|
||||||
|
if (result == JFileChooser.APPROVE_OPTION) {
|
||||||
|
selectedFile.set(chooser.getSelectedFile());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (java.awt.EventQueue.isDispatchThread()) {
|
||||||
|
dialogTask.run();
|
||||||
|
} else {
|
||||||
|
java.awt.EventQueue.invokeAndWait(dialogTask);
|
||||||
|
}
|
||||||
|
} catch (InterruptedException ex) {
|
||||||
|
Thread.currentThread().interrupt();
|
||||||
|
return null;
|
||||||
|
} catch (InvocationTargetException ex) {
|
||||||
|
throw new IllegalStateException("Failed to open Swing file picker", ex.getCause());
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedFile.get();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,15 +5,15 @@
|
|||||||
<?import javafx.scene.control.ComboBox?>
|
<?import javafx.scene.control.ComboBox?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.TextField?>
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
<?import javafx.scene.layout.ColumnConstraints?>
|
<?import javafx.scene.layout.ColumnConstraints?>
|
||||||
<?import javafx.scene.layout.GridPane?>
|
<?import javafx.scene.layout.GridPane?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.Region?>
|
<?import javafx.scene.layout.Region?>
|
||||||
<?import javafx.scene.layout.RowConstraints?>
|
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Font?>
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="523.0" prefWidth="790.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController">
|
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="560.0" prefWidth="790.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.PetDialogController">
|
||||||
<children>
|
<children>
|
||||||
<HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;">
|
<HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;">
|
||||||
<children>
|
<children>
|
||||||
@@ -62,18 +62,13 @@
|
|||||||
<Insets left="15.0" right="15.0" />
|
<Insets left="15.0" right="15.0" />
|
||||||
</padding>
|
</padding>
|
||||||
</HBox>
|
</HBox>
|
||||||
<VBox prefHeight="370.0" prefWidth="750.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #5580b5; -fx-border-radius: 14;">
|
<VBox prefHeight="405.0" prefWidth="750.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #5580b5; -fx-border-radius: 14;">
|
||||||
<children>
|
<children>
|
||||||
<GridPane hgap="25.0" VBox.vgrow="ALWAYS">
|
<GridPane hgap="25.0" vgap="10.0">
|
||||||
<columnConstraints>
|
<columnConstraints>
|
||||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
<rowConstraints>
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
|
||||||
</rowConstraints>
|
|
||||||
<children>
|
<children>
|
||||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0">
|
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0">
|
||||||
<children>
|
<children>
|
||||||
@@ -163,6 +158,22 @@
|
|||||||
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
||||||
</VBox.margin>
|
</VBox.margin>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="15.0">
|
||||||
|
<children>
|
||||||
|
<ImageView fx:id="imgPetPreview" fitHeight="120.0" fitWidth="120.0" pickOnBounds="true" preserveRatio="true" />
|
||||||
|
<VBox spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Label fx:id="lblImageStatus" text="No image selected" textFill="#2c3e50" />
|
||||||
|
<HBox spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Button fx:id="btnChangeImage" mnemonicParsing="false" text="Change Image" />
|
||||||
|
<Button fx:id="btnRemoveImage" mnemonicParsing="false" text="Remove Image" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
</children>
|
</children>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
||||||
|
|||||||
@@ -5,15 +5,15 @@
|
|||||||
<?import javafx.scene.control.ComboBox?>
|
<?import javafx.scene.control.ComboBox?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.TextField?>
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
<?import javafx.scene.layout.ColumnConstraints?>
|
<?import javafx.scene.layout.ColumnConstraints?>
|
||||||
<?import javafx.scene.layout.GridPane?>
|
<?import javafx.scene.layout.GridPane?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.Region?>
|
<?import javafx.scene.layout.Region?>
|
||||||
<?import javafx.scene.layout.RowConstraints?>
|
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Font?>
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="523.0" prefWidth="790.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController">
|
<VBox minHeight="-Infinity" minWidth="-Infinity" prefHeight="560.0" prefWidth="790.0" spacing="20.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/17" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.dialogcontrollers.ProductDialogController">
|
||||||
<children>
|
<children>
|
||||||
<HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;">
|
<HBox alignment="CENTER_LEFT" prefHeight="79.0" prefWidth="727.0" spacing="20.0" style="-fx-background-color: #2C3E50; -fx-background-radius: 14;">
|
||||||
<children>
|
<children>
|
||||||
@@ -62,19 +62,14 @@
|
|||||||
<Insets left="15.0" right="15.0" />
|
<Insets left="15.0" right="15.0" />
|
||||||
</padding>
|
</padding>
|
||||||
</HBox>
|
</HBox>
|
||||||
<VBox prefHeight="370.0" prefWidth="750.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #5580b5; -fx-border-radius: 14;">
|
<VBox prefHeight="405.0" prefWidth="750.0" style="-fx-background-color: white; -fx-background-radius: 14; -fx-border-width: 2; -fx-border-color: #5580b5; -fx-border-radius: 14;">
|
||||||
<children>
|
<children>
|
||||||
<GridPane hgap="25.0" VBox.vgrow="ALWAYS">
|
<GridPane hgap="25.0" vgap="10.0">
|
||||||
<columnConstraints>
|
<columnConstraints>
|
||||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||||
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
<ColumnConstraints hgrow="SOMETIMES" minWidth="10.0" prefWidth="100.0" />
|
||||||
</columnConstraints>
|
</columnConstraints>
|
||||||
<rowConstraints>
|
<children>
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
|
||||||
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
|
|
||||||
</rowConstraints>
|
|
||||||
<children>
|
|
||||||
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0">
|
<VBox prefHeight="200.0" prefWidth="100.0" spacing="8.0">
|
||||||
<children>
|
<children>
|
||||||
<Label text="Product Name:" textFill="#2c3e50">
|
<Label text="Product Name:" textFill="#2c3e50">
|
||||||
@@ -136,8 +131,24 @@
|
|||||||
<VBox.margin>
|
<VBox.margin>
|
||||||
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
||||||
</VBox.margin>
|
</VBox.margin>
|
||||||
</GridPane>
|
</GridPane>
|
||||||
</children>
|
<HBox alignment="CENTER_LEFT" spacing="15.0">
|
||||||
|
<children>
|
||||||
|
<ImageView fx:id="imgProductPreview" fitHeight="120.0" fitWidth="120.0" pickOnBounds="true" preserveRatio="true" />
|
||||||
|
<VBox spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Label fx:id="lblImageStatus" text="No image selected" textFill="#2c3e50" />
|
||||||
|
<HBox spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Button fx:id="btnChangeImage" mnemonicParsing="false" text="Change Image" />
|
||||||
|
<Button fx:id="btnRemoveImage" mnemonicParsing="false" text="Remove Image" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
|
</VBox>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
</children>
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
<Insets bottom="15.0" left="15.0" right="15.0" top="15.0" />
|
||||||
</padding>
|
</padding>
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<?import javafx.scene.control.TableColumn?>
|
<?import javafx.scene.control.TableColumn?>
|
||||||
<?import javafx.scene.control.TableView?>
|
<?import javafx.scene.control.TableView?>
|
||||||
<?import javafx.scene.control.TextField?>
|
<?import javafx.scene.control.TextField?>
|
||||||
|
<?import javafx.scene.image.ImageView?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
<?import javafx.scene.layout.Region?>
|
<?import javafx.scene.layout.Region?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
@@ -67,14 +68,15 @@
|
|||||||
</HBox>
|
</HBox>
|
||||||
<TableView fx:id="tvPets" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
<TableView fx:id="tvPets" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="colPetId" prefWidth="60.0" text="ID" />
|
<TableColumn fx:id="colPetId" prefWidth="55.0" text="ID" />
|
||||||
<TableColumn fx:id="colPetName" prefWidth="113.14285278320312" text="Name" />
|
<TableColumn fx:id="colPetImage" prefWidth="80.0" text="Image" />
|
||||||
<TableColumn fx:id="colPetSpecies" prefWidth="110.28570556640625" text="Species" />
|
<TableColumn fx:id="colPetName" prefWidth="110.0" text="Name" />
|
||||||
<TableColumn fx:id="colPetBreed" prefWidth="174.85711669921875" text="Breed" />
|
<TableColumn fx:id="colPetSpecies" prefWidth="105.0" text="Species" />
|
||||||
<TableColumn fx:id="colPetAge" prefWidth="72.0" text="Age" />
|
<TableColumn fx:id="colPetBreed" prefWidth="145.0" text="Breed" />
|
||||||
<TableColumn fx:id="colPetStatus" prefWidth="133.142822265625" text="Status" />
|
<TableColumn fx:id="colPetAge" prefWidth="60.0" text="Age" />
|
||||||
<TableColumn fx:id="colPetPrice" prefWidth="89.142822265625" text="Price" />
|
<TableColumn fx:id="colPetStatus" prefWidth="110.0" text="Status" />
|
||||||
</columns>
|
<TableColumn fx:id="colPetPrice" prefWidth="80.0" text="Price" />
|
||||||
|
</columns>
|
||||||
</TableView>
|
</TableView>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
@@ -67,12 +67,13 @@
|
|||||||
</HBox>
|
</HBox>
|
||||||
<TableView fx:id="tvProducts" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
<TableView fx:id="tvProducts" prefHeight="362.0" prefWidth="752.0" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="colProductId" prefWidth="60.0" text="ID" />
|
<TableColumn fx:id="colProductId" prefWidth="55.0" text="ID" />
|
||||||
<TableColumn fx:id="colProductName" prefWidth="170.85714721679688" text="Name" />
|
<TableColumn fx:id="colProductImage" prefWidth="80.0" text="Image" />
|
||||||
<TableColumn fx:id="colProductCategory" prefWidth="195.4285888671875" text="Category" />
|
<TableColumn fx:id="colProductName" prefWidth="150.0" text="Name" />
|
||||||
<TableColumn fx:id="colProductDesc" prefWidth="210.28570556640625" text="Description" />
|
<TableColumn fx:id="colProductCategory" prefWidth="160.0" text="Category" />
|
||||||
<TableColumn fx:id="colProductPrice" prefWidth="115.4285888671875" text="Price" />
|
<TableColumn fx:id="colProductDesc" prefWidth="195.0" text="Description" />
|
||||||
</columns>
|
<TableColumn fx:id="colProductPrice" prefWidth="110.0" text="Price" />
|
||||||
|
</columns>
|
||||||
</TableView>
|
</TableView>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
//Validator JUnits tests
|
||||||
|
|
||||||
|
package org.example.petshopdesktop;
|
||||||
|
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
class ValidatorTest {
|
||||||
|
|
||||||
|
//isPresent
|
||||||
|
@Test
|
||||||
|
void isPresent_nullValue_returnsError() {
|
||||||
|
String result = Validator.isPresent(null, "Name");
|
||||||
|
assertFalse(result.isEmpty(), "null value should produce an error message");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isPresent_blankValue_returnsError() {
|
||||||
|
assertFalse(Validator.isPresent(" ", "Name").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isPresent_emptyString_returnsError() {
|
||||||
|
assertFalse(Validator.isPresent("", "Name").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isPresent_validValue_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isPresent("Leon", "Name").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
//isNonNegativeDouble
|
||||||
|
@Test
|
||||||
|
void isNonNegativeDouble_positiveValue_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isNonNegativeDouble("5.5", "Price").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeDouble_zero_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isNonNegativeDouble("0", "Price").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeDouble_negativeValue_returnsError() {
|
||||||
|
assertFalse(Validator.isNonNegativeDouble("-1.0", "Price").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeDouble_nonNumeric_returnsError() {
|
||||||
|
assertFalse(Validator.isNonNegativeDouble("abc", "Price").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeDouble_nullInput_returnsError() {
|
||||||
|
assertFalse(Validator.isNonNegativeDouble(null, "Price").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
//isDoubleInRange
|
||||||
|
@Test
|
||||||
|
void isDoubleInRange_withinRange_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isDoubleInRange("5.0", "Discount", 0.0, 10.0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isDoubleInRange_atMinBoundary_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isDoubleInRange("0.0", "Discount", 0.0, 10.0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isDoubleInRange_atMaxBoundary_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isDoubleInRange("10.0", "Discount", 0.0, 10.0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isDoubleInRange_belowMin_returnsError() {
|
||||||
|
assertFalse(Validator.isDoubleInRange("-1.0", "Discount", 0.0, 10.0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isDoubleInRange_aboveMax_returnsError() {
|
||||||
|
assertFalse(Validator.isDoubleInRange("11.0", "Discount", 0.0, 10.0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isDoubleInRange_nonNumeric_returnsError() {
|
||||||
|
assertFalse(Validator.isDoubleInRange("abc", "Discount", 0.0, 10.0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isDoubleInRange_nullInput_returnsError() {
|
||||||
|
assertFalse(Validator.isDoubleInRange(null, "Discount", 0.0, 10.0).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
//isNonNegativeInteger
|
||||||
|
@Test
|
||||||
|
void isNonNegativeInteger_positiveValue_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isNonNegativeInteger("10", "Quantity").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeInteger_zero_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isNonNegativeInteger("0", "Quantity").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeInteger_negativeValue_returnsError() {
|
||||||
|
assertFalse(Validator.isNonNegativeInteger("-1", "Quantity").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeInteger_decimal_returnsError() {
|
||||||
|
assertFalse(Validator.isNonNegativeInteger("1.5", "Quantity").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeInteger_nonNumeric_returnsError() {
|
||||||
|
assertFalse(Validator.isNonNegativeInteger("abc", "Quantity").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isNonNegativeInteger_nullInput_returnsError() {
|
||||||
|
assertFalse(Validator.isNonNegativeInteger(null, "Quantity").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
//isLessThanVarChars
|
||||||
|
@Test
|
||||||
|
void isLessThanVarChars_withinLimit_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isLessThanVarChars("Hello", "Name", 10).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isLessThanVarChars_exactlyAtLimit_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isLessThanVarChars("Hello", "Name", 5).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isLessThanVarChars_exceedsLimit_returnsError() {
|
||||||
|
assertFalse(Validator.isLessThanVarChars("Hello World", "Name", 5).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isLessThanVarChars_nullInput_returnsError() {
|
||||||
|
assertFalse(Validator.isLessThanVarChars(null, "Name", 10).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isLessThanVarChars_emptyString_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isLessThanVarChars("", "Name", 5).isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
//isValidEmail
|
||||||
|
@Test
|
||||||
|
void isValidEmail_validEmail_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isValidEmail("user@example.com", "Email").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidEmail_missingAtSign_returnsError() {
|
||||||
|
assertFalse(Validator.isValidEmail("userexample.com", "Email").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidEmail_missingDomain_returnsError() {
|
||||||
|
assertFalse(Validator.isValidEmail("user@", "Email").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidEmail_shortTld_returnsError() {
|
||||||
|
assertFalse(Validator.isValidEmail("user@example.c", "Email").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidEmail_nullInput_returnsError() {
|
||||||
|
assertFalse(Validator.isValidEmail(null, "Email").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidEmail_emptyString_returnsError() {
|
||||||
|
assertFalse(Validator.isValidEmail("", "Email").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
//isValidPhoneNumber
|
||||||
|
@Test
|
||||||
|
void isValidPhoneNumber_validFormat_returnsEmpty() {
|
||||||
|
assertTrue(Validator.isValidPhoneNumber("403-555-1234", "Phone").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidPhoneNumber_missingDashes_returnsError() {
|
||||||
|
assertFalse(Validator.isValidPhoneNumber("4035551234", "Phone").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidPhoneNumber_lettersPresent_returnsError() {
|
||||||
|
assertFalse(Validator.isValidPhoneNumber("abc-def-ghij", "Phone").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidPhoneNumber_wrongSegmentLength_returnsError() {
|
||||||
|
assertFalse(Validator.isValidPhoneNumber("40-5551-234", "Phone").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidPhoneNumber_nullInput_returnsError() {
|
||||||
|
assertFalse(Validator.isValidPhoneNumber(null, "Phone").isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void isValidPhoneNumber_emptyString_returnsError() {
|
||||||
|
assertFalse(Validator.isValidPhoneNumber("", "Phone").isEmpty());
|
||||||
|
}
|
||||||
|
}
|
||||||
42
web/.gitignore
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||||
|
|
||||||
|
# dependencies
|
||||||
|
/node_modules
|
||||||
|
/.pnp
|
||||||
|
.pnp.*
|
||||||
|
.yarn/*
|
||||||
|
!.yarn/patches
|
||||||
|
!.yarn/plugins
|
||||||
|
!.yarn/releases
|
||||||
|
!.yarn/versions
|
||||||
|
|
||||||
|
# testing
|
||||||
|
/coverage
|
||||||
|
|
||||||
|
# next.js
|
||||||
|
/.next/
|
||||||
|
/out/
|
||||||
|
|
||||||
|
# production
|
||||||
|
/build
|
||||||
|
|
||||||
|
# misc
|
||||||
|
.DS_Store
|
||||||
|
*.pem
|
||||||
|
|
||||||
|
# debug
|
||||||
|
nohup.out
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
.pnpm-debug.log*
|
||||||
|
|
||||||
|
# env files (can opt-in for committing if needed)
|
||||||
|
.env*
|
||||||
|
|
||||||
|
# vercel
|
||||||
|
.vercel
|
||||||
|
|
||||||
|
# typescript
|
||||||
|
*.tsbuildinfo
|
||||||
|
next-env.d.ts
|
||||||
36
web/README.md
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
First, run the development server:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run dev
|
||||||
|
# or
|
||||||
|
yarn dev
|
||||||
|
# or
|
||||||
|
pnpm dev
|
||||||
|
# or
|
||||||
|
bun dev
|
||||||
|
```
|
||||||
|
|
||||||
|
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||||
|
|
||||||
|
You can start editing the page by modifying `app/page.js`. The page auto-updates as you edit the file.
|
||||||
|
|
||||||
|
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||||
|
|
||||||
|
## Learn More
|
||||||
|
|
||||||
|
To learn more about Next.js, take a look at the following resources:
|
||||||
|
|
||||||
|
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||||
|
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||||
|
|
||||||
|
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||||
|
|
||||||
|
## Deploy on Vercel
|
||||||
|
|
||||||
|
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||||
|
|
||||||
|
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||||
BIN
web/app/favicon.ico
Normal file
|
After Width: | Height: | Size: 25 KiB |
290
web/app/globals.css
Normal file
@@ -0,0 +1,290 @@
|
|||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--background: #ffffff;
|
||||||
|
--foreground: #171717;
|
||||||
|
}
|
||||||
|
|
||||||
|
@theme inline {
|
||||||
|
--color-background: var(--background);
|
||||||
|
--color-foreground: var(--foreground);
|
||||||
|
--font-sans: var(--font-geist-sans);
|
||||||
|
--font-mono: var(--font-geist-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--background: #0a0a0a;
|
||||||
|
--foreground: #ededed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--foreground);
|
||||||
|
font-family: Arial, Helvetica, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navbar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
background: orange;
|
||||||
|
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
|
||||||
|
z-index: 1000;
|
||||||
|
padding: 0.5rem 2rem;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
height: 70px;
|
||||||
|
border-radius: 0px 0px 10px 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Add padding to body to account for fixed header */
|
||||||
|
body {
|
||||||
|
padding-top: 70px;
|
||||||
|
margin: 0;
|
||||||
|
font-family: Arial, sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Logo Styles */
|
||||||
|
#logo {
|
||||||
|
border-radius: 50%;
|
||||||
|
transition: transform 0.5s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
#logo:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Navigation Links Container */
|
||||||
|
.nav-links {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2rem;
|
||||||
|
margin-left: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Indivdual Link Styles */
|
||||||
|
.nav-link {
|
||||||
|
color: rgb(255, 255, 255);
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 1.1rem;
|
||||||
|
padding: 0.5rem 1rem;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Alternative Hover Effect - Background */
|
||||||
|
.nav-link:hover {
|
||||||
|
background-color: rgba(255, 255, 255, 0.171);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/* Home Page */
|
||||||
|
.home-page {
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Slideshow Styles */
|
||||||
|
.slideshow-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 500px;
|
||||||
|
overflow: hidden;
|
||||||
|
/* margin-bottom: 4rem; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 1s ease-in-out;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide.active {
|
||||||
|
opacity: 1;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slide-image {
|
||||||
|
object-fit: cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slideshow-indicators {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator {
|
||||||
|
width: 12px;
|
||||||
|
height: 12px;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid white;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator.active {
|
||||||
|
background: white;
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.indicator:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Four Image Links Section */
|
||||||
|
.image-links-section {
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-links-container {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, 1fr);
|
||||||
|
gap: 2rem;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-link-card {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-link-card:hover {
|
||||||
|
transform: translateY(-5px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-wrapper {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 20px;
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.linked-image {
|
||||||
|
object-fit: cover;
|
||||||
|
transition: transform 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-link-card:hover .linked-image {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-title {
|
||||||
|
text-align: center;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #333;
|
||||||
|
margin-top: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Centered Title Section */
|
||||||
|
.centered-title-section {
|
||||||
|
text-align: center;
|
||||||
|
padding: 4rem 2rem;
|
||||||
|
background: linear-gradient(to bottom, #f9f9f9, #ffffff);
|
||||||
|
/* margin-top: 2re; */
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 3rem;
|
||||||
|
color: #333;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: #666;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
font-weight: 300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title-decoration {
|
||||||
|
width: 100px;
|
||||||
|
height: 4px;
|
||||||
|
background: orange;
|
||||||
|
margin: 2rem auto 0;
|
||||||
|
border-radius: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Responsive Design */
|
||||||
|
@media (max-width: 1024px) {
|
||||||
|
.slideshow-container {
|
||||||
|
height: 400px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 2.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.slideshow-container {
|
||||||
|
height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-links-container {
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-links-section {
|
||||||
|
padding: 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 480px) {
|
||||||
|
.slideshow-container {
|
||||||
|
height: 250px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-links-container {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
gap: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main-title {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
font-size: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.centered-title-section {
|
||||||
|
padding: 2rem 1rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
web/app/layout.js
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { Geist, Geist_Mono } from "next/font/google";
|
||||||
|
import "./globals.css";
|
||||||
|
import DisplayNav from "@/components/Navigation";
|
||||||
|
|
||||||
|
export const metadata = {
|
||||||
|
title: "Leon's Pet Store",
|
||||||
|
description: "Generated by create next app",
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function RootLayout({ children }) {
|
||||||
|
return (
|
||||||
|
<html lang="en">
|
||||||
|
<body>
|
||||||
|
<DisplayNav />
|
||||||
|
{children}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
);
|
||||||
|
}
|
||||||
83
web/app/page.js
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import Image from "next/image";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useState, useEffect } from "react";
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
// Slideshow images array
|
||||||
|
const slideshowImages = [
|
||||||
|
{ src: "/images/home/slideshow/pet1.jpg", alt: "Happy pets" },
|
||||||
|
{ src: "/images/home/slideshow/pet2.jpg", alt: "Pet supplies" },
|
||||||
|
{ src: "/images/home/slideshow/pet3.jpg", alt: "Pet grooming" },
|
||||||
|
{ src: "/images/home/slideshow/pet4.jpg", alt: "Pet food" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const [currentSlide, setCurrentSlide] = useState(0);
|
||||||
|
|
||||||
|
// Auto-advance slideshow
|
||||||
|
useEffect(() => {
|
||||||
|
//Change slide every 7.5 seconds
|
||||||
|
const timer = setInterval(() => {setCurrentSlide((prev) => (prev + 1) % slideshowImages.length);}, 7500);
|
||||||
|
|
||||||
|
return () => clearInterval(timer);
|
||||||
|
}, [slideshowImages.length]);
|
||||||
|
|
||||||
|
// Four images that link to other pages
|
||||||
|
const navImages = [
|
||||||
|
{ src: "/images/home/navimages/adopt.jpg", alt: "Adopt a Pet", link: "/adopt", title: "Adopt a Pet" },
|
||||||
|
{ src: "/images/home/navimages/store.jpg", alt: "Online Store", link: "/store", title: "Online Store" },
|
||||||
|
{ src: "/images/home/navimages/appointments.jpg", alt: "Appointments", link: "/appointments", title: "Appointments" },
|
||||||
|
{ src: "/images/home/navimages/about.jpg", alt: "About Us", link: "/about", title: "About Us" },
|
||||||
|
];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<main className="home-page">
|
||||||
|
{/* Slideshow Section */}
|
||||||
|
<section className="slideshow-container">
|
||||||
|
{slideshowImages.map((image, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={`slide ${index === currentSlide ? "active" : ""}`}
|
||||||
|
>
|
||||||
|
<Image
|
||||||
|
src={image.src}
|
||||||
|
alt={image.alt}
|
||||||
|
fill
|
||||||
|
priority={index === 0}
|
||||||
|
className="slide-image"
|
||||||
|
sizes="100vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Centered Title Section */}
|
||||||
|
<section className="centered-title-section">
|
||||||
|
<h1 className="main-title">Welcome to Leon's Pet Store</h1>
|
||||||
|
<p className="subtitle">Your One-Stop Shop for All Things Pets</p>
|
||||||
|
<div className="title-decoration"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
{/* Four Image Links Section */}
|
||||||
|
<section className="image-links-section">
|
||||||
|
<div className="image-links-container">
|
||||||
|
{navImages.map((item, index) => (
|
||||||
|
<Link href={item.link} key={index} className="image-link-card">
|
||||||
|
<div className="image-wrapper">
|
||||||
|
<Image
|
||||||
|
src={item.src}
|
||||||
|
alt={item.alt}
|
||||||
|
fill
|
||||||
|
className="linked-image"
|
||||||
|
sizes="(max-width: 768px) 100vw, 25vw"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<h3 className="image-title">{item.title}</h3>
|
||||||
|
</Link>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
);
|
||||||
|
}
|
||||||
26
web/components/Navigation.js
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import Link from "next/link";
|
||||||
|
import Image from "next/image";
|
||||||
|
|
||||||
|
export default function DisplayNav() {
|
||||||
|
return (
|
||||||
|
<nav className="navbar">
|
||||||
|
<Image
|
||||||
|
className="mx-3"
|
||||||
|
src="/logo_simple.png"
|
||||||
|
alt="store_logo"
|
||||||
|
width={50}
|
||||||
|
height={50}
|
||||||
|
id="logo"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<div className="nav-links">
|
||||||
|
<a href="/" className="nav-link">Home</a>
|
||||||
|
<a href="/pets" className="nav-link">Adopt a Pet</a>
|
||||||
|
<a href="/" className="nav-link">Online Store</a>
|
||||||
|
<a href="/appointments" className="nav-link">Schedule an Appointment</a>
|
||||||
|
<a href="/contact" className="nav-link">Contact Us</a>
|
||||||
|
<a href="/aboutus" className="nav-link">About Us</a>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
);
|
||||||
|
}
|
||||||
16
web/eslint.config.mjs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { defineConfig, globalIgnores } from "eslint/config";
|
||||||
|
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||||
|
|
||||||
|
const eslintConfig = defineConfig([
|
||||||
|
...nextVitals,
|
||||||
|
// Override default ignores of eslint-config-next.
|
||||||
|
globalIgnores([
|
||||||
|
// Default ignores of eslint-config-next:
|
||||||
|
".next/**",
|
||||||
|
"out/**",
|
||||||
|
"build/**",
|
||||||
|
"next-env.d.ts",
|
||||||
|
]),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default eslintConfig;
|
||||||
7
web/jsconfig.json
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./*"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
7
web/next.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
/** @type {import('next').NextConfig} */
|
||||||
|
const nextConfig = {
|
||||||
|
/* config options here */
|
||||||
|
reactCompiler: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default nextConfig;
|
||||||
6556
web/package-lock.json
generated
Normal file
23
web/package.json
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
{
|
||||||
|
"name": "threaded-pets",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start",
|
||||||
|
"lint": "eslint"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"next": "16.1.6",
|
||||||
|
"react": "19.2.3",
|
||||||
|
"react-dom": "19.2.3"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@tailwindcss/postcss": "^4",
|
||||||
|
"babel-plugin-react-compiler": "1.0.0",
|
||||||
|
"eslint": "^9",
|
||||||
|
"eslint-config-next": "16.1.6",
|
||||||
|
"tailwindcss": "^4"
|
||||||
|
}
|
||||||
|
}
|
||||||
7
web/postcss.config.mjs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
const config = {
|
||||||
|
plugins: {
|
||||||
|
"@tailwindcss/postcss": {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default config;
|
||||||