Merge main into nomorebreaking
4
android/app/.gitignore
vendored
@@ -1,3 +1,7 @@
|
||||
/build
|
||||
/nohup.out
|
||||
/.classpath
|
||||
/.project
|
||||
/.settings/
|
||||
/src/test/
|
||||
/src/androidTest/
|
||||
|
||||
@@ -56,6 +56,9 @@ dependencies {
|
||||
implementation("io.reactivex.rxjava2:rxjava:2.2.21")
|
||||
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)
|
||||
androidTestImplementation(libs.ext.junit)
|
||||
androidTestImplementation(libs.espresso.core)
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||
android:maxSdkVersion="32" />
|
||||
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES" />
|
||||
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||
|
||||
<uses-feature
|
||||
android:name="android.hardware.camera"
|
||||
@@ -24,6 +25,11 @@
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
android:supportsRtl="true"
|
||||
android:theme="@style/Theme.PetStoreMobile">
|
||||
|
||||
<service
|
||||
android:name=".services.ChatNotificationService"
|
||||
android:exported="false" />
|
||||
|
||||
<activity
|
||||
android:name=".activities.HomeActivity"
|
||||
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
|
||||
public void 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;
|
||||
|
||||
import android.Manifest;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
|
||||
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.core.content.ContextCompat;
|
||||
import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
import androidx.core.view.WindowInsetsControllerCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.fragments.ChatFragment;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.ProfileFragment;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.google.android.material.bottomnavigation.BottomNavigationView;
|
||||
|
||||
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
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
@@ -34,17 +49,15 @@ public class HomeActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
//get the bottom navbar from the layout
|
||||
BottomNavigationView bottomNav = findViewById(R.id.bottom_navigation);
|
||||
|
||||
// Load ListFragment by default only if this is a fresh start
|
||||
bottomNav = findViewById(R.id.bottom_navigation);
|
||||
|
||||
//load the list fragment by default if it's a fresh start
|
||||
if (savedInstanceState == null) {
|
||||
loadFragment(new ListFragment());
|
||||
bottomNav.setSelectedItemId(R.id.nav_list);
|
||||
handleIntent(getIntent());
|
||||
}
|
||||
|
||||
//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 -> {
|
||||
|
||||
if (item.getItemId() == R.id.nav_list) {
|
||||
loadFragment(new ListFragment());
|
||||
return true;
|
||||
@@ -57,13 +70,59 @@ public class HomeActivity extends AppCompatActivity {
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Start the notification service and request for notification permission
|
||||
startNotificationService();
|
||||
requestNotificationPermission();
|
||||
}
|
||||
|
||||
// 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 load a fragment
|
||||
// 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) {
|
||||
getSupportFragmentManager()
|
||||
.beginTransaction()
|
||||
.replace(R.id.fragment_container, fragment)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.dtos.AuthDTO;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
@@ -38,11 +39,17 @@ public class MainActivity extends AppCompatActivity {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Check if user is already logged in
|
||||
if (TokenManager.getInstance(this).isLoggedIn()) {
|
||||
Intent intent = new Intent(this, HomeActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
TokenManager tokenManager = TokenManager.getInstance(this);
|
||||
if (tokenManager.isLoggedIn()) {
|
||||
if ("CUSTOMER".equalsIgnoreCase(tokenManager.getRole())) {
|
||||
// If a customer somehow remained logged in, clear them out
|
||||
tokenManager.clearLoginData();
|
||||
} else {
|
||||
Intent intent = new Intent(this, HomeActivity.class);
|
||||
startActivity(intent);
|
||||
finish();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
EdgeToEdge.enable(this);
|
||||
@@ -82,19 +89,28 @@ public class MainActivity extends AppCompatActivity {
|
||||
@Override
|
||||
public void onResponse(Call<AuthDTO.LoginResponse> call, Response<AuthDTO.LoginResponse> response) {
|
||||
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
|
||||
TokenManager.getInstance(MainActivity.this).saveLoginData(
|
||||
response.body().getToken(),
|
||||
response.body().getUsername(),
|
||||
response.body().getRole()
|
||||
role
|
||||
);
|
||||
|
||||
//fetch user id from api then login to home activity
|
||||
RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser()
|
||||
.enqueue(new Callback<AuthDTO.UserResponse>() {
|
||||
RetrofitClient.getAuthApi(MainActivity.this).getMe()
|
||||
.enqueue(new Callback<UserDTO>() {
|
||||
@Override
|
||||
public void onResponse(Call<AuthDTO.UserResponse> call,
|
||||
Response<AuthDTO.UserResponse> response) {
|
||||
public void onResponse(Call<UserDTO> call,
|
||||
Response<UserDTO> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
TokenManager.getInstance(MainActivity.this)
|
||||
.saveUserId(response.body().getId());
|
||||
@@ -106,7 +122,7 @@ public class MainActivity extends AppCompatActivity {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<AuthDTO.UserResponse> call,
|
||||
public void onFailure(Call<UserDTO> call,
|
||||
Throwable t) {
|
||||
Log.e("MainActivity", "Failed to fetch userId", t);
|
||||
|
||||
@@ -129,4 +145,4 @@ public class MainActivity extends AppCompatActivity {
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,17 +3,23 @@ package com.example.petstoremobile.api;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Part;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
//api calls to CRUD pets
|
||||
public interface PetApi {
|
||||
// endpoint for downloading the pet's image file
|
||||
String PET_IMAGE_PATH = "api/v1/pets/%d/image";
|
||||
|
||||
// Get all pets
|
||||
@GET("api/v1/pets")
|
||||
Call<PageResponse<PetDTO>> getAllPets(
|
||||
@@ -37,4 +43,9 @@ public interface PetApi {
|
||||
@DELETE("api/v1/pets/{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;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.api.auth.AuthInterceptor;
|
||||
@@ -12,9 +13,23 @@ import retrofit2.converter.gson.GsonConverterFactory;
|
||||
|
||||
//Retrofit client Used for API calls
|
||||
public class RetrofitClient {
|
||||
//base URL
|
||||
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
|
||||
public static final String BASE_URL = getBaseUrl();
|
||||
|
||||
// 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;
|
||||
|
||||
@@ -95,7 +110,6 @@ public class RetrofitClient {
|
||||
return getClient(context).create(MessageApi.class);
|
||||
}
|
||||
|
||||
|
||||
public static StoreApi getStoreApi(Context context) {
|
||||
return getClient(context).create(StoreApi.class);
|
||||
}
|
||||
|
||||
@@ -1,17 +1,40 @@
|
||||
package com.example.petstoremobile.api.auth;
|
||||
|
||||
import com.example.petstoremobile.dtos.AuthDTO;
|
||||
import com.example.petstoremobile.dtos.UserDTO;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import okhttp3.MultipartBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Part;
|
||||
|
||||
//Api for logging in and getting current user
|
||||
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")
|
||||
Call<AuthDTO.LoginResponse> login(@Body AuthDTO.LoginRequest loginRequest);
|
||||
|
||||
//get current user endpoint
|
||||
@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.models.Chat;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import com.example.petstoremobile.services.ChatNotificationService;
|
||||
import com.example.petstoremobile.websocket.StompChatManager;
|
||||
import java.util.*;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -40,6 +41,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
private RecyclerView rvChatList, rvMessages;
|
||||
private EditText etMessage;
|
||||
private Button btnSend;
|
||||
private TextView tvChatTitle;
|
||||
|
||||
// Adapters
|
||||
private ChatAdapter chatAdapter;
|
||||
@@ -75,6 +77,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
rvMessages = view.findViewById(R.id.rvMessages);
|
||||
etMessage = view.findViewById(R.id.etMessage);
|
||||
btnSend = view.findViewById(R.id.btnSend);
|
||||
tvChatTitle = view.findViewById(R.id.tvChatTitle);
|
||||
|
||||
ImageButton hamburger = view.findViewById(R.id.btnHamburger);
|
||||
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");
|
||||
}
|
||||
|
||||
if (getArguments() != null && getArguments().containsKey("conversation_id")) {
|
||||
activeConversationId = getArguments().getLong("conversation_id");
|
||||
}
|
||||
|
||||
loadCustomers();
|
||||
}
|
||||
|
||||
@@ -165,7 +172,21 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
.collect(Collectors.toList());
|
||||
chatList.addAll(loaded);
|
||||
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();
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
setConversationActive(false);
|
||||
@@ -186,6 +207,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
public void onChatClick(Chat chat) {
|
||||
activeConversationId = Long.parseLong(chat.getChatId());
|
||||
setConversationActive(true);
|
||||
tvChatTitle.setText(chat.getCustomerName());
|
||||
drawerLayout.closeDrawer(GravityCompat.START);
|
||||
|
||||
if (stompChatManager != null) {
|
||||
@@ -305,6 +327,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getId())) {
|
||||
setConversationActive(true);
|
||||
tvChatTitle.setText(name);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +409,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
etMessage.setEnabled(active);
|
||||
if (!active) {
|
||||
activeConversationId = null;
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
if (tvChatTitle != null) tvChatTitle.setText("Customer Chat");
|
||||
if (stompChatManager != null) {
|
||||
stompChatManager.clearConversationSubscription();
|
||||
}
|
||||
@@ -395,6 +420,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
etMessage.setHint("Select a chat to start messaging");
|
||||
} else {
|
||||
etMessage.setHint("Type a message...");
|
||||
ChatNotificationService.activeConversationIdInUi = activeConversationId;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,6 +428,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
super.onDestroyView();
|
||||
ChatNotificationService.activeConversationIdInUi = null;
|
||||
if (stompChatManager != null) stompChatManager.disconnect();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import android.widget.LinearLayout;
|
||||
|
||||
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.ServiceFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
||||
@@ -56,6 +57,13 @@ public class ListFragment extends Fragment {
|
||||
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
|
||||
touchBlocker = view.findViewById(R.id.touchBlocker);
|
||||
|
||||
@@ -170,4 +178,4 @@ public class ListFragment extends Fragment {
|
||||
.addToBackStack(null)
|
||||
.commit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ import androidx.fragment.app.Fragment;
|
||||
|
||||
import android.provider.MediaStore;
|
||||
import android.text.InputType;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
@@ -23,20 +24,43 @@ import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageView;
|
||||
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.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.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.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 {
|
||||
|
||||
//initialize the view/controls
|
||||
private ImageView imgProfile;
|
||||
private TextView tvProfileName, tvProfileEmail, tvProfilePhone, tvProfileRole;
|
||||
private Button btnChangePhoto, btnEditEmail, btnEditPhone, btnLogout;
|
||||
private Uri photoUri;
|
||||
private UserDTO currentUser;
|
||||
|
||||
//Initialize the launchers for camera and gallery
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
@@ -58,8 +82,7 @@ public class ProfileFragment extends Fragment {
|
||||
&& result.getData() != null) {
|
||||
//get the selected image and set the image to the profile
|
||||
Uri selectedImage = result.getData().getData();
|
||||
imgProfile.setImageURI(selectedImage);
|
||||
//TODO: SAVE CHANGED PHOTO TO DATABASE
|
||||
uploadAvatar(selectedImage);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -71,10 +94,7 @@ public class ProfileFragment extends Fragment {
|
||||
success -> {
|
||||
//if a photo is taken set the image profile to it otherwise do nothing
|
||||
if (success) {
|
||||
//Clear the old image and set the new one
|
||||
imgProfile.setImageURI(null);
|
||||
imgProfile.setImageURI(photoUri);
|
||||
//TODO: SAVE CHANGED PHOTO TO DATABASE
|
||||
uploadAvatar(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -107,7 +127,6 @@ public class ProfileFragment extends Fragment {
|
||||
);
|
||||
}
|
||||
|
||||
//TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
@@ -119,10 +138,13 @@ public class ProfileFragment extends Fragment {
|
||||
tvProfileEmail = view.findViewById(R.id.tvProfileEmail);
|
||||
tvProfilePhone = view.findViewById(R.id.tvProfilePhone);
|
||||
tvProfileRole = view.findViewById(R.id.tvProfileRole);
|
||||
btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
||||
btnEditEmail = view.findViewById(R.id.btnEditEmail);
|
||||
btnEditPhone = view.findViewById(R.id.btnEditPhone);
|
||||
btnLogout = view.findViewById(R.id.btnLogout);
|
||||
Button btnChangePhoto = view.findViewById(R.id.btnChangePhoto);
|
||||
Button btnEditEmail = view.findViewById(R.id.btnEditEmail);
|
||||
Button btnEditPhone = view.findViewById(R.id.btnEditPhone);
|
||||
Button btnLogout = view.findViewById(R.id.btnLogout);
|
||||
|
||||
//Load Profile Data from backend
|
||||
loadProfileData();
|
||||
|
||||
//Set up listeners for the buttons
|
||||
//Change photo button
|
||||
@@ -150,7 +172,6 @@ public class ProfileFragment extends Fragment {
|
||||
}
|
||||
})
|
||||
.show();
|
||||
//TODO: UPDATE PHOTO IN DATABASE
|
||||
});
|
||||
|
||||
//Edit email button
|
||||
@@ -170,19 +191,10 @@ public class ProfileFragment extends Fragment {
|
||||
.setTitle("Edit Email")
|
||||
.setView(input)
|
||||
.setPositiveButton("Save", (dialog, which) -> {
|
||||
String newEmail = input.getText().toString();
|
||||
//if the new value is a valid email then set the email to the new value
|
||||
if (android.util.Patterns.EMAIL_ADDRESS.matcher(newEmail).matches()) {
|
||||
tvProfileEmail.setText(newEmail);
|
||||
//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();
|
||||
if (InputValidator.isValidEmail(input)) {
|
||||
updateProfileField("email", input.getText().toString());
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Email is invalid", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
@@ -210,19 +222,10 @@ public class ProfileFragment extends Fragment {
|
||||
.setTitle("Edit Phone Number")
|
||||
.setView(input)
|
||||
.setPositiveButton("Save", (dialog, which) -> {
|
||||
String newPhone = input.getText().toString();
|
||||
//if the new value is format: (XXX) XXX-XXXX then set the phone to the new value
|
||||
if (newPhone.matches("\\(\\d{3}\\) \\d{3}-\\d{4}")) { //TODO MAKE VALIDATION CLASS INSTEAD FOR THIS
|
||||
tvProfilePhone.setText(newPhone);
|
||||
//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();
|
||||
if (InputValidator.isValidPhone(input)) {
|
||||
updateProfileField("phone", input.getText().toString());
|
||||
} else {
|
||||
Toast.makeText(requireContext(), "Phone number is invalid", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
})
|
||||
.setNegativeButton("Cancel", null)
|
||||
@@ -231,6 +234,10 @@ public class ProfileFragment extends Fragment {
|
||||
|
||||
//Logout button
|
||||
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
|
||||
//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);
|
||||
@@ -252,4 +259,152 @@ public class ProfileFragment extends Fragment {
|
||||
//launch the camera to capture the photo and save the photo to 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.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ImageButton;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
@@ -43,6 +46,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
private PetApi api;
|
||||
private SwipeRefreshLayout swipeRefreshLayout;
|
||||
private EditText etSearch;
|
||||
private Spinner spinnerStatus;
|
||||
|
||||
//load pet view
|
||||
@Override
|
||||
@@ -57,6 +61,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
|
||||
setupRecyclerView(view);
|
||||
setupSearch(view);
|
||||
setupStatusFilter(view);
|
||||
setupSwipeRefresh(view);
|
||||
loadPetData();
|
||||
|
||||
@@ -82,24 +87,48 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
etSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {}
|
||||
@Override public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
filterPets(s.toString());
|
||||
filterPets();
|
||||
}
|
||||
@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();
|
||||
if (query.isEmpty()) {
|
||||
filteredList.addAll(petList);
|
||||
} else {
|
||||
String lower = query.toLowerCase();
|
||||
for (PetDTO p : petList) {
|
||||
if (p.getPetName().toLowerCase().contains(lower)
|
||||
|| p.getPetSpecies().toLowerCase().contains(lower)
|
||||
|| p.getPetBreed().toLowerCase().contains(lower)) {
|
||||
filteredList.add(p);
|
||||
}
|
||||
for (PetDTO p : petList) {
|
||||
boolean matchesSearch = query.isEmpty() ||
|
||||
p.getPetName().toLowerCase().contains(query) ||
|
||||
p.getPetSpecies().toLowerCase().contains(query) ||
|
||||
p.getPetBreed().toLowerCase().contains(query);
|
||||
|
||||
boolean matchesStatus = selectedStatus.equals("All Statuses") ||
|
||||
p.getPetStatus().equalsIgnoreCase(selectedStatus);
|
||||
|
||||
if (matchesSearch && matchesStatus) {
|
||||
filteredList.add(p);
|
||||
}
|
||||
}
|
||||
adapter.notifyDataSetChanged();
|
||||
@@ -173,7 +202,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
petList.clear();
|
||||
petList.addAll(response.body().getContent());
|
||||
filterPets(etSearch.getText().toString());
|
||||
filterPets();
|
||||
|
||||
} else {
|
||||
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.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.PetFragment;
|
||||
import com.example.petstoremobile.utils.ActivityLogger;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
@@ -65,26 +67,27 @@ public class PetDetailFragment extends Fragment {
|
||||
|
||||
//Method to Update or Add a pet
|
||||
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
|
||||
String name = etPetName.getText().toString().trim();
|
||||
String species = etPetSpecies.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 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
|
||||
PetDTO petDTO = new PetDTO();
|
||||
petDTO.setPetName(name);
|
||||
petDTO.setPetSpecies(species);
|
||||
petDTO.setPetBreed(breed);
|
||||
petDTO.setPetAge(Integer.parseInt(ageStr));
|
||||
petDTO.setPetAge(age);
|
||||
petDTO.setPetPrice(priceStr);
|
||||
petDTO.setPetStatus(status);
|
||||
|
||||
@@ -98,6 +101,7 @@ public class PetDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", petId);
|
||||
Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -107,6 +111,7 @@ public class PetDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<PetDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "PetDetailFragment.updatePet", new Exception(t));
|
||||
Log.e("PetDetailFragment", "Error updating pet", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -117,6 +122,7 @@ public class PetDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<PetDTO> call, Response<PetDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.log(requireContext(), "Added new Pet: " + name);
|
||||
Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -126,6 +132,7 @@ public class PetDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<PetDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "PetDetailFragment.createPet", new Exception(t));
|
||||
Log.e("PetDetailFragment", "Error adding pet", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -146,6 +153,7 @@ public class PetDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Pet", "DELETED", petId);
|
||||
Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -155,6 +163,7 @@ public class PetDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "PetDetailFragment.deletePet", new Exception(t));
|
||||
Log.e("PetDetailFragment", "Error deleting pet", t);
|
||||
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.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.ServiceFragment;
|
||||
import com.example.petstoremobile.utils.ActivityLogger;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
@@ -58,24 +60,24 @@ public class ServiceDetailFragment extends Fragment {
|
||||
|
||||
//Method to Update or Add a service
|
||||
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
|
||||
String name = etServiceName.getText().toString().trim();
|
||||
String desc = etServiceDesc.getText().toString().trim();
|
||||
String durationStr = etServiceDuration.getText().toString().trim();
|
||||
String priceStr = 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;
|
||||
}
|
||||
int duration = Integer.parseInt(etServiceDuration.getText().toString().trim());
|
||||
double price = Double.parseDouble(etServicePrice.getText().toString().trim());
|
||||
|
||||
//create a service object to send to the API
|
||||
ServiceDTO serviceDTO = new ServiceDTO();
|
||||
serviceDTO.setServiceName(name);
|
||||
serviceDTO.setServiceDesc(desc);
|
||||
serviceDTO.setServiceDuration(Integer.parseInt(durationStr));
|
||||
serviceDTO.setServicePrice(Double.parseDouble(priceStr));
|
||||
serviceDTO.setServiceDuration(duration);
|
||||
serviceDTO.setServicePrice(price);
|
||||
|
||||
ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext());
|
||||
|
||||
@@ -87,6 +89,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Service", "UPDATED", serviceId);
|
||||
Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -96,6 +99,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.updateService", new Exception(t));
|
||||
Log.e("ServiceDetailFragment", "Error updating service", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -106,6 +110,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<ServiceDTO> call, Response<ServiceDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.log(requireContext(), "Added new Service: " + name);
|
||||
Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -115,6 +120,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ServiceDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.createService", new Exception(t));
|
||||
Log.e("ServiceDetailFragment", "Error adding service", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -134,6 +140,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Service", "DELETED", serviceId);
|
||||
Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -143,6 +150,7 @@ public class ServiceDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "ServiceDetailFragment.deleteService", new Exception(t));
|
||||
Log.e("ServiceDetailFragment", "Error deleting service", t);
|
||||
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.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.SupplierFragment;
|
||||
import com.example.petstoremobile.utils.ActivityLogger;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
@@ -58,6 +60,13 @@ public class SupplierDetailFragment extends Fragment {
|
||||
|
||||
//Method to Update or Add a supplier
|
||||
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
|
||||
String company = etSupCompany.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 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
|
||||
SupplierDTO supplierDTO = new SupplierDTO();
|
||||
supplierDTO.setSupCompany(company);
|
||||
@@ -89,6 +92,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Supplier", "UPDATED", supId);
|
||||
Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -98,6 +102,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.updateSupplier", new Exception(t));
|
||||
Log.e("SupplierDetailFragment", "Error updating supplier", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -108,6 +113,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<SupplierDTO> call, Response<SupplierDTO> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.log(requireContext(), "Added new Supplier: " + company);
|
||||
Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -117,6 +123,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<SupplierDTO> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.createSupplier", new Exception(t));
|
||||
Log.e("SupplierDetailFragment", "Error adding supplier", t);
|
||||
Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
@@ -136,6 +143,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
if (response.isSuccessful()) {
|
||||
ActivityLogger.logChange(requireContext(), "Supplier", "DELETED", supId);
|
||||
Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show();
|
||||
navigateBack();
|
||||
} else {
|
||||
@@ -145,6 +153,7 @@ public class SupplierDetailFragment extends Fragment {
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
ActivityLogger.logException(requireContext(), "SupplierDetailFragment.deleteSupplier", new Exception(t));
|
||||
Log.e("SupplierDetailFragment", "Error deleting supplier", t);
|
||||
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);
|
||||
etSupEmail = view.findViewById(R.id.etSupEmail);
|
||||
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);
|
||||
btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier);
|
||||
btnBack = view.findViewById(R.id.btnBack);
|
||||
|
||||
@@ -16,26 +16,42 @@ import androidx.core.content.FileProvider;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import android.provider.MediaStore;
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
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.api.PetApi;
|
||||
import com.example.petstoremobile.api.RetrofitClient;
|
||||
import com.example.petstoremobile.fragments.ListFragment;
|
||||
import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
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 {
|
||||
|
||||
private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice;
|
||||
private Button btnBack, btnEditPet, btnChangePhoto;
|
||||
private ImageView imgPet;
|
||||
private Uri photoUri;
|
||||
private int petId;
|
||||
|
||||
// launchers for camera and gallery
|
||||
private ActivityResultLauncher<Intent> galleryLauncher;
|
||||
@@ -53,8 +69,7 @@ public class PetProfileFragment extends Fragment {
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) {
|
||||
Uri selectedImage = result.getData().getData();
|
||||
imgPet.setImageURI(selectedImage);
|
||||
// TODO: SAVE CHANGED PHOTO TO DATABASE
|
||||
uploadPetImage(selectedImage);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -64,9 +79,7 @@ public class PetProfileFragment extends Fragment {
|
||||
new ActivityResultContracts.TakePicture(),
|
||||
success -> {
|
||||
if (success) {
|
||||
imgPet.setImageURI(null);
|
||||
imgPet.setImageURI(photoUri);
|
||||
// TODO: SAVE CHANGED PHOTO TO DATABASE
|
||||
uploadPetImage(photoUri);
|
||||
}
|
||||
}
|
||||
);
|
||||
@@ -112,11 +125,15 @@ public class PetProfileFragment extends Fragment {
|
||||
|
||||
// Set pet details to display
|
||||
if (getArguments() != null) {
|
||||
petId = getArguments().getInt("petId");
|
||||
tvPetName.setText(getArguments().getString("petName"));
|
||||
tvPetSpecies.setText(getArguments().getString("petSpecies"));
|
||||
tvPetBreed.setText(getArguments().getString("petBreed"));
|
||||
tvPetAge.setText(String.format(Locale.getDefault(), "%d yr(s)", getArguments().getInt("petAge")));
|
||||
tvPetPrice.setText(String.format(Locale.getDefault(), "$%.2f", getArguments().getDouble("petPrice")));
|
||||
|
||||
// Load pet image from backend
|
||||
loadPetImage(petId);
|
||||
}
|
||||
|
||||
//set button click listeners
|
||||
@@ -169,6 +186,74 @@ public class PetProfileFragment extends Fragment {
|
||||
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() {
|
||||
File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg");
|
||||
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;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
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.requestFocus();
|
||||
return false;
|
||||
@@ -94,4 +95,3 @@ public class InputValidator {
|
||||
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"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvChatTitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Customer Chat"
|
||||
android:textColor="@color/white"
|
||||
android:textSize="20sp"
|
||||
android:textStyle="bold"/>
|
||||
android:textStyle="bold"
|
||||
android:paddingStart="8dp"
|
||||
android:paddingEnd="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
@@ -38,18 +38,34 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etSearchPet"
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_margin="8dp"
|
||||
android:hint="Search by name, species or breed..."
|
||||
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"/>
|
||||
android:orientation="horizontal"
|
||||
android:padding="8dp"
|
||||
android:gravity="center_vertical">
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etSearchPet"
|
||||
android:layout_width="0dp"
|
||||
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
|
||||
android:id="@+id/swipeRefreshPet"
|
||||
|
||||
@@ -83,7 +83,7 @@
|
||||
android:id="@+id/tvProfileEmail"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="example@email.com"
|
||||
android:text="No email loaded"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="16sp" />
|
||||
|
||||
@@ -129,7 +129,7 @@
|
||||
android:id="@+id/tvProfilePhone"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="(123) 123-1234"
|
||||
android:text="No phone loaded"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="16sp" />
|
||||
|
||||
@@ -174,7 +174,7 @@
|
||||
android:id="@+id/tvProfileRole"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Manager"
|
||||
android:text="No role loaded"
|
||||
android:textSize="16sp"
|
||||
android:textColor="@color/accent_coral"/>
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</adaptive-icon>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<background android:drawable="@drawable/ic_launcher_background" />
|
||||
<foreground android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
|
||||
<background android:drawable="@color/ic_launcher_background"/>
|
||||
<foreground android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
<monochrome android:drawable="@mipmap/ic_launcher_foreground"/>
|
||||
</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>
|
||||