Add role based access to android #48

Merged
RecentRunner merged 2 commits from AddRoleBasedAccessToAndroid into main 2026-03-26 16:50:02 -06:00
9 changed files with 210 additions and 43 deletions

View File

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

View File

@@ -39,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);
@@ -83,11 +89,20 @@ 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

View File

@@ -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;
@@ -67,4 +82,4 @@ public class RetrofitClient {
return getClient(context).create(MessageApi.class);
}
}
}

View File

@@ -5,15 +5,21 @@ 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);
@@ -26,4 +32,9 @@ public interface AuthApi {
@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);
}

View File

@@ -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;
@@ -49,6 +50,13 @@ public class ListFragment extends Fragment {
drawerInventory = view.findViewById(R.id.drawerInventory);
drawerProducts = view.findViewById(R.id.drawerProducts);
// 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);
@@ -142,4 +150,4 @@ public class ListFragment extends Fragment {
.addToBackStack(null)
.commit();
}
}
}

View File

@@ -26,6 +26,10 @@ 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;
@@ -38,9 +42,14 @@ 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;
@@ -73,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);
}
}
);
@@ -86,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);
}
}
);
@@ -167,7 +172,6 @@ public class ProfileFragment extends Fragment {
}
})
.show();
//TODO: UPDATE PHOTO IN DATABASE
});
//Edit email button
@@ -272,7 +276,31 @@ public class ProfileFragment extends Fragment {
tvProfileEmail.setText(currentUser.getEmail());
tvProfilePhone.setText(currentUser.getPhone());
tvProfileRole.setText(currentUser.getRole());
//TODO: LOAD PHOTO FROM DATABASE
// 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());
@@ -288,6 +316,62 @@ public class ProfileFragment extends Fragment {
});
}
//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());

View File

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

View File

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

View File

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