diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 00000000..faf530b2 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,10 @@ +*.iml +.gradle +/local.properties +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/android/app/.gitignore b/android/app/.gitignore new file mode 100644 index 00000000..42afabfd --- /dev/null +++ b/android/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts new file mode 100644 index 00000000..d1cc3c30 --- /dev/null +++ b/android/app/build.gradle.kts @@ -0,0 +1,62 @@ +plugins { + alias(libs.plugins.android.application) +} + +android { + namespace = "com.example.petstoremobile" + compileSdk = 36 + + defaultConfig { + applicationId = "com.example.petstoremobile" + minSdk = 24 + targetSdk = 36 + versionCode = 1 + versionName = "1.0" + + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + isMinifyEnabled = false + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + implementation(libs.appcompat) + implementation(libs.material) + implementation(libs.activity) + implementation(libs.constraintlayout) + + implementation("com.squareup.retrofit2:retrofit:2.9.0") + implementation("com.squareup.retrofit2:converter-gson:2.9.0") + implementation("com.squareup.okhttp3:logging-interceptor:4.9.1") + implementation("com.squareup.okhttp3:okhttp:4.12.0") + + implementation("com.google.android.material:material:1.11.0") + implementation("androidx.viewpager2:viewpager2:1.1.0") + + implementation("androidx.camera:camera-core:1.4.0") + implementation("androidx.camera:camera-camera2:1.4.0") + implementation("androidx.camera:camera-lifecycle:1.4.0") + implementation("androidx.camera:camera-view:1.4.0") + implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0") + implementation(libs.swiperefreshlayout) + + implementation("com.github.NaikSoftware:StompProtocolAndroid:1.6.6") + implementation("io.reactivex.rxjava2:rxjava:2.2.21") + implementation("io.reactivex.rxjava2:rxandroid:2.1.1") + + testImplementation(libs.junit) + androidTestImplementation(libs.ext.junit) + androidTestImplementation(libs.espresso.core) +} \ No newline at end of file diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 00000000..481bb434 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..633ea6ba --- /dev/null +++ b/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java b/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java new file mode 100644 index 00000000..6134d221 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/PetStoreApplication.java @@ -0,0 +1,13 @@ +package com.example.petstoremobile; + +import android.app.Application; +import com.example.petstoremobile.api.auth.TokenManager; + +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(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java new file mode 100644 index 00000000..1dee2f66 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -0,0 +1,69 @@ +package com.example.petstoremobile.activities; + +import android.os.Bundle; + +import androidx.activity.EdgeToEdge; +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.google.android.material.bottomnavigation.BottomNavigationView; + +public class HomeActivity extends AppCompatActivity { + + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_home); + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + //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 + if (savedInstanceState == null) { + loadFragment(new ListFragment()); + bottomNav.setSelectedItemId(R.id.nav_list); + } + + //when an item in the bar is selected, load the corresponding fragment + bottomNav.setOnItemSelectedListener(item -> { + + if (item.getItemId() == R.id.nav_list) { + loadFragment(new ListFragment()); + return true; + } else if (item.getItemId() == R.id.nav_chat) { + loadFragment(new ChatFragment()); + return true; + } else if (item.getItemId() == R.id.nav_profile) { + loadFragment(new ProfileFragment()); + return true; + } + return false; + }); + } + + //helper function to load a fragment + private void loadFragment(Fragment fragment) { + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.fragment_container, fragment) + .commit(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java new file mode 100644 index 00000000..6bc65f23 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/activities/MainActivity.java @@ -0,0 +1,132 @@ +package com.example.petstoremobile.activities; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.activity.EdgeToEdge; +import androidx.appcompat.app.AppCompatActivity; +import androidx.core.graphics.Insets; +import androidx.core.view.ViewCompat; +import androidx.core.view.WindowInsetsCompat; + +import com.example.petstoremobile.R; +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 retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +//The login screen activity +public class MainActivity extends AppCompatActivity { + + private EditText etUser; + private EditText etPassword; + private Button btnLogin; + private TextView tvLoginStatus; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + 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; + } + + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + + ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> { + Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars()); + v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom); + return insets; + }); + + //get all controls from layout + tvLoginStatus = findViewById(R.id.tvLoginStatus); + etUser = findViewById(R.id.etUser); + etPassword = findViewById(R.id.etPassword); + btnLogin = findViewById(R.id.btnLogin); + //clear login status + tvLoginStatus.setText(""); + + //Set click listener for login button + btnLogin.setOnClickListener(v -> { + //Get username and password from text fields + String username = etUser.getText().toString(); + String password = etPassword.getText().toString(); + + //check if fields are empty + if (username.isEmpty() || password.isEmpty()) { + Toast.makeText(this, "Please enter username and password", Toast.LENGTH_SHORT).show(); + tvLoginStatus.setText("Please enter username and password"); + return; + } + + AuthApi authApi = RetrofitClient.getAuthApi(this); + + //Call login from api and get response + authApi.login(new AuthDTO.LoginRequest(username,password)).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful() && response.body() != null) { + //save login data in shared preferences + TokenManager.getInstance(MainActivity.this).saveLoginData( + response.body().getToken(), + response.body().getUsername(), + response.body().getRole() + ); + + //fetch user id from api then login to home activity + RetrofitClient.getAuthApi(MainActivity.this).getCurrentUser() + .enqueue(new Callback() { + @Override + public void onResponse(Call call, + Response response) { + if (response.isSuccessful() && response.body() != null) { + TokenManager.getInstance(MainActivity.this) + .saveUserId(response.body().getId()); + } + + Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); + startActivity(new Intent(MainActivity.this, HomeActivity.class)); + finish(); + } + + @Override + public void onFailure(Call call, + Throwable t) { + Log.e("MainActivity", "Failed to fetch userId", t); + + Toast.makeText(MainActivity.this, "Login successful", Toast.LENGTH_SHORT).show(); + startActivity(new Intent(MainActivity.this, HomeActivity.class)); + finish(); + } + }); + } else { + Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show(); + tvLoginStatus.setText("Login failed"); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Toast.makeText(MainActivity.this, "Login failed", Toast.LENGTH_SHORT).show(); + tvLoginStatus.setText("Login failed"); + } + }); + }); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java new file mode 100644 index 00000000..98e88256 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AdoptionAdapter.java @@ -0,0 +1,79 @@ +package com.example.petstoremobile.adapters; + + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Adoption; +import java.util.List; + +public class AdoptionAdapter extends RecyclerView.Adapter { + + private List adoptionList; + private OnAdoptionClickListener adoptionClickListener; + + // Interface for adoption click on recycler view + public interface OnAdoptionClickListener { + void onAdoptionClick(int position); + } + + // Constructor + public AdoptionAdapter(List adoptionList, OnAdoptionClickListener adoptionClickListener) { + this.adoptionList = adoptionList; + this.adoptionClickListener = adoptionClickListener; + } + + // Get the controls of each row in recycler view + public static class AdoptionViewHolder extends RecyclerView.ViewHolder { + TextView tvAdopterName, tvPetName, tvAdoptionDate, tvAdoptionStatus; + + public AdoptionViewHolder(@NonNull View v) { + super(v); + tvAdopterName = v.findViewById(R.id.tvAdopterName); + tvPetName = v.findViewById(R.id.tvAdoptionPetName); + tvAdoptionDate = v.findViewById(R.id.tvAdoptionDate); + tvAdoptionStatus = v.findViewById(R.id.tvAdoptionStatus); + } + } + + // Create a new row view + @NonNull + @Override + public AdoptionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_adoption, parent, false); + return new AdoptionViewHolder(v); + } + + // Populate the row with adoption data + @Override + public void onBindViewHolder(@NonNull AdoptionViewHolder holder, int position) { + Adoption adoption = adoptionList.get(position); + + holder.tvAdopterName.setText(adoption.getAdopterName()); + holder.tvPetName.setText("Pet: " + adoption.getPetName()); + holder.tvAdoptionDate.setText("Date: " + adoption.getAdoptionDate()); + holder.tvAdoptionStatus.setText(adoption.getStatus()); + + // Set the status color depending on adoption status + if (adoption.getStatus().equals("Approved")) { + holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + } else if (adoption.getStatus().equals("Pending")) { + holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#FF9800")); + } else { + holder.tvAdoptionStatus.setBackgroundColor(Color.parseColor("#F44336")); + } + + // When a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> adoptionClickListener.onAdoptionClick(position)); + } + + @Override + public int getItemCount() { + return adoptionList.size(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java new file mode 100644 index 00000000..7924c12b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/AppointmentAdapter.java @@ -0,0 +1,86 @@ +package com.example.petstoremobile.adapters; + + + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Appointment; +import java.util.List; + +public class AppointmentAdapter extends RecyclerView.Adapter { + + private List appointmentList; + private OnAppointmentClickListener appointmentClickListener; + + // Interface for appointment click on recycler view + public interface OnAppointmentClickListener { + void onAppointmentClick(int position); + } + + // Constructor + public AppointmentAdapter(List appointmentList, OnAppointmentClickListener appointmentClickListener) { + this.appointmentList = appointmentList; + this.appointmentClickListener = appointmentClickListener; + } + + // Get the controls of each row in recycler view + public static class AppointmentViewHolder extends RecyclerView.ViewHolder { + TextView tvCustomerName, tvPetName, tvServiceType, tvDateTime, tvAppointmentStatus; + + public AppointmentViewHolder(@NonNull View v) { + super(v); + tvCustomerName = v.findViewById(R.id.tvCustomerName); + tvPetName = v.findViewById(R.id.tvApptPetName); + tvServiceType = v.findViewById(R.id.tvServiceType); + tvDateTime = v.findViewById(R.id.tvDateTime); + tvAppointmentStatus = v.findViewById(R.id.tvAppointmentStatus); + } + } + + // Create a new row view + @NonNull + @Override + public AppointmentViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_appointment, parent, false); + return new AppointmentViewHolder(v); + } + + // Populate the row with appointment data + @Override + public void onBindViewHolder(@NonNull AppointmentViewHolder holder, int position) { + Appointment appointment = appointmentList.get(position); + + holder.tvCustomerName.setText(appointment.getCustomerName()); + holder.tvPetName.setText("Pet: " + appointment.getPetName()); + holder.tvServiceType.setText(appointment.getServiceType()); + holder.tvDateTime.setText(appointment.getAppointmentDate() + " at " + appointment.getAppointmentTime()); + holder.tvAppointmentStatus.setText(appointment.getStatus()); + + // Set the status color depending on appointment status + switch (appointment.getStatus()) { + case "Confirmed": + holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + break; + case "Pending": + holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#FF9800")); + break; + default: + holder.tvAppointmentStatus.setBackgroundColor(Color.parseColor("#F44336")); + break; + } + + // When a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> appointmentClickListener.onAppointmentClick(position)); + } + + @Override + public int getItemCount() { + return appointmentList.size(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java new file mode 100644 index 00000000..93e6d902 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ChatAdapter.java @@ -0,0 +1,59 @@ +package com.example.petstoremobile.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Chat; + +import java.util.List; + +public class ChatAdapter extends RecyclerView.Adapter { + + private List chatList; + private OnChatClickListener listener; + + public interface OnChatClickListener { + void onChatClick(Chat chat); + } + + public ChatAdapter(List chatList, OnChatClickListener listener) { + this.chatList = chatList; + this.listener = listener; + } + + @NonNull + @Override + public ChatViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_chat, parent, false); + return new ChatViewHolder(view); + } + + @Override + public void onBindViewHolder(@NonNull ChatViewHolder holder, int position) { + Chat chat = chatList.get(position); + holder.tvCustomerName.setText(chat.getCustomerName()); + holder.tvLastMessage.setText(chat.getLastMessage()); + holder.itemView.setOnClickListener(v -> listener.onChatClick(chat)); + } + + @Override + public int getItemCount() { + return chatList.size(); + } + + public static class ChatViewHolder extends RecyclerView.ViewHolder { + TextView tvCustomerName, tvLastMessage; + + public ChatViewHolder(@NonNull View itemView) { + super(itemView); + tvCustomerName = itemView.findViewById(R.id.tvCustomerName); + tvLastMessage = itemView.findViewById(R.id.tvLastMessage); + } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java new file mode 100644 index 00000000..7ae36dc5 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/InventoryAdapter.java @@ -0,0 +1,79 @@ +package com.example.petstoremobile.adapters; + + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Inventory; +import java.util.List; + +public class InventoryAdapter extends RecyclerView.Adapter { + + private List inventoryList; + private OnInventoryClickListener inventoryClickListener; + + // Interface for inventory click on recycler view + public interface OnInventoryClickListener { + void onInventoryClick(int position); + } + + // Constructor + public InventoryAdapter(List inventoryList, OnInventoryClickListener inventoryClickListener) { + this.inventoryList = inventoryList; + this.inventoryClickListener = inventoryClickListener; + } + + // Get the controls of each row in recycler view + public static class InventoryViewHolder extends RecyclerView.ViewHolder { + TextView tvItemName, tvCategory, tvQuantity, tvUnitPrice, tvSupplier; + + public InventoryViewHolder(@NonNull View v) { + super(v); + tvItemName = v.findViewById(R.id.tvItemName); + tvCategory = v.findViewById(R.id.tvCategory); + tvQuantity = v.findViewById(R.id.tvQuantity); + tvUnitPrice = v.findViewById(R.id.tvUnitPrice); + tvSupplier = v.findViewById(R.id.tvInvSupplier); + } + } + + // Create a new row view + @NonNull + @Override + public InventoryViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_inventory, parent, false); + return new InventoryViewHolder(v); + } + + // Populate the row with inventory data + @Override + public void onBindViewHolder(@NonNull InventoryViewHolder holder, int position) { + Inventory inventory = inventoryList.get(position); + + holder.tvItemName.setText(inventory.getItemName()); + holder.tvCategory.setText(inventory.getCategory()); + holder.tvQuantity.setText("Qty: " + inventory.getQuantity()); + holder.tvUnitPrice.setText("$" + String.format("%.2f", inventory.getUnitPrice())); + holder.tvSupplier.setText("Supplier: " + inventory.getSupplier()); + + // Highlight low stock items in red + if (inventory.getQuantity() <= 5) { + holder.tvQuantity.setTextColor(Color.parseColor("#F44336")); + } else { + holder.tvQuantity.setTextColor(Color.parseColor("#4CAF50")); + } + + // When a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> inventoryClickListener.onInventoryClick(position)); + } + + @Override + public int getItemCount() { + return inventoryList.size(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java new file mode 100644 index 00000000..0354a1fd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/MessageAdapter.java @@ -0,0 +1,78 @@ +package com.example.petstoremobile.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Message; +import java.util.List; + +public class MessageAdapter extends RecyclerView.Adapter { + + private static final int TYPE_SENT = 1; + private static final int TYPE_RECEIVED = 2; + + private final List messages; + private Long currentUserId; + + public MessageAdapter(List messages, Long currentUserId) { + this.messages = messages; + this.currentUserId = currentUserId; + } + + public void setCurrentUserId(Long id) { + this.currentUserId = id; + notifyDataSetChanged(); + } + + @Override + public int getItemViewType(int position) { + Message m = messages.get(position); + if (currentUserId != null && currentUserId.equals(m.getSenderId())) { + return TYPE_SENT; + } + return TYPE_RECEIVED; + } + + @NonNull @Override + public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + LayoutInflater inf = LayoutInflater.from(parent.getContext()); + if (viewType == TYPE_SENT) { + View v = inf.inflate(R.layout.item_message_sent, parent, false); + return new SentHolder(v); + } else { + View v = inf.inflate(R.layout.item_message_received, parent, false); + return new ReceivedHolder(v); + } + } + + @Override + public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { + Message m = messages.get(position); + if (holder instanceof SentHolder) ((SentHolder) holder).bind(m); + if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m); + } + + @Override public int getItemCount() { return messages.size(); } + + static class SentHolder extends RecyclerView.ViewHolder { + TextView tvMessage; + SentHolder(View v) { + super(v); + tvMessage = v.findViewById(R.id.tvMessageContent); // updated + } + void bind(Message m) { tvMessage.setText(m.getContent()); } + } + + static class ReceivedHolder extends RecyclerView.ViewHolder { + TextView tvMessage; + ReceivedHolder(View v) { + super(v); + tvMessage = v.findViewById(R.id.tvMessageContent); // updated + } + void bind(Message m) { tvMessage.setText(m.getContent()); } + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java new file mode 100644 index 00000000..c78cc7cc --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/PetAdapter.java @@ -0,0 +1,85 @@ +package com.example.petstoremobile.adapters; + +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.dtos.PetDTO; +import java.util.List; + +public class PetAdapter extends RecyclerView.Adapter { + + private List petList; + private OnPetClickListener petClickListener; + + // Interface for pet click on recycler view + public interface OnPetClickListener { + void onPetClick(int position); + } + + //Constructor + public PetAdapter(List petList, OnPetClickListener petClickListener) { + this.petList = petList; + this.petClickListener = petClickListener; + } + + // Get the controls of each row in recycler view + public static class PetViewHolder extends RecyclerView.ViewHolder { + TextView tvPetName, tvPetSpeciesBreed, tvPetAge, tvPetPrice, tvPetStatus; + + public PetViewHolder(@NonNull View v) { + super(v); + tvPetName = v.findViewById(R.id.tvPetName); + tvPetSpeciesBreed = v.findViewById(R.id.tvPetSpeciesBreed); + tvPetAge = v.findViewById(R.id.tvPetAge); + tvPetPrice = v.findViewById(R.id.tvPetPrice); + tvPetStatus = v.findViewById(R.id.tvPetStatus); + } + } + + // Create a new row view + @NonNull + @Override + public PetViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_pet, parent, false); + return new PetViewHolder(v); + } + + //populate the row with pet data + @Override + public void onBindViewHolder(@NonNull PetViewHolder holder, int position) { + PetDTO pet = petList.get(position); + + holder.tvPetName.setText(pet.getPetName()); + holder.tvPetSpeciesBreed.setText(pet.getPetSpecies() + " - " + pet.getPetBreed()); + holder.tvPetAge.setText("Age: " + pet.getPetAge() + " yr(s)"); + + try { + double price = Double.parseDouble(pet.getPetPrice()); + holder.tvPetPrice.setText("$" + String.format("%.2f", price)); + } catch (Exception e) { + holder.tvPetPrice.setText("$" + pet.getPetPrice()); + } + + holder.tvPetStatus.setText(pet.getPetStatus()); + + //Set the status color depending on availability. If available, green, otherwise red + if (pet.getPetStatus() != null && pet.getPetStatus().equals("Available")) { + holder.tvPetStatus.setBackgroundColor(Color.parseColor("#4CAF50")); + } else { + holder.tvPetStatus.setBackgroundColor(Color.parseColor("#F44336")); + } + + //when a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> petClickListener.onPetClick(position)); + } + + @Override + public int getItemCount() { + return petList.size(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java new file mode 100644 index 00000000..a4e5e770 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ProductAdapter.java @@ -0,0 +1,72 @@ +package com.example.petstoremobile.adapters; + + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.models.Product; +import java.util.List; + +public class ProductAdapter extends RecyclerView.Adapter { + + private List productList; + private OnProductClickListener productClickListener; + + // Interface for product click on recycler view + public interface OnProductClickListener { + void onProductClick(int position); + } + + // Constructor + public ProductAdapter(List productList, OnProductClickListener productClickListener) { + this.productList = productList; + this.productClickListener = productClickListener; + } + + // Get the controls of each row in recycler view + public static class ProductViewHolder extends RecyclerView.ViewHolder { + TextView tvProductName, tvProductDesc, tvCategory, tvProductPrice, tvStockQuantity; + + public ProductViewHolder(@NonNull View v) { + super(v); + tvProductName = v.findViewById(R.id.tvProductName); + tvProductDesc = v.findViewById(R.id.tvProductDesc); + tvCategory = v.findViewById(R.id.tvProductCategory); + tvProductPrice = v.findViewById(R.id.tvProductPrice); + tvStockQuantity = v.findViewById(R.id.tvStockQuantity); + } + } + + // Create a new row view + @NonNull + @Override + public ProductViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_product, parent, false); + return new ProductViewHolder(v); + } + + // Populate the row with product data + @Override + public void onBindViewHolder(@NonNull ProductViewHolder holder, int position) { + Product product = productList.get(position); + + holder.tvProductName.setText(product.getProductName()); + holder.tvProductDesc.setText(product.getProductDesc()); + holder.tvCategory.setText(product.getCategory()); + holder.tvProductPrice.setText("$" + String.format("%.2f", product.getProductPrice())); + holder.tvStockQuantity.setText("Stock: " + product.getStockQuantity()); + + // When a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> productClickListener.onProductClick(position)); + } + + @Override + public int getItemCount() { + return productList.size(); + } +} + diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java new file mode 100644 index 00000000..e3cc6d1c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/ServiceAdapter.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.dtos.ServiceDTO; +import java.util.List; + +public class ServiceAdapter extends RecyclerView.Adapter { + + private List serviceList; + private OnServiceClickListener serviceClickListener; + + // Interface for service click on recycler view + public interface OnServiceClickListener { + void onServiceClick(int position); + } + + //Constructor + public ServiceAdapter(List serviceList, OnServiceClickListener serviceClickListener) { + this.serviceList = serviceList; + this.serviceClickListener = serviceClickListener; + } + + // Get the controls of each row in recycler view + public static class ServiceViewHolder extends RecyclerView.ViewHolder { + TextView tvServiceName, tvServiceDesc, tvServiceDuration, tvServicePrice; + + public ServiceViewHolder(@NonNull View v) { + super(v); + tvServiceName = v.findViewById(R.id.tvServiceName); + tvServiceDesc = v.findViewById(R.id.tvServiceDesc); + tvServiceDuration = v.findViewById(R.id.tvServiceDuration); + tvServicePrice = v.findViewById(R.id.tvServicePrice); + } + } + + // Create a new row view + @NonNull + @Override + public ServiceViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_service, parent, false); + return new ServiceViewHolder(v); + } + + //populate the row with service data + @Override + public void onBindViewHolder(@NonNull ServiceViewHolder holder, int position) { + ServiceDTO service = serviceList.get(position); + + holder.tvServiceName.setText(service.getServiceName()); + holder.tvServiceDesc.setText(service.getServiceDesc()); + holder.tvServiceDuration.setText("Duration: " + service.getServiceDuration() + " min"); + holder.tvServicePrice.setText("$" + String.format("%.2f", service.getServicePrice())); + + //when a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> serviceClickListener.onServiceClick(position)); + } + + @Override + public int getItemCount() { + return serviceList.size(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java new file mode 100644 index 00000000..e134f5b2 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SupplierAdapter.java @@ -0,0 +1,68 @@ +package com.example.petstoremobile.adapters; + +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.dtos.SupplierDTO; +import java.util.List; + +public class SupplierAdapter extends RecyclerView.Adapter { + + private List supplierList; + private OnSupplierClickListener supplierClickListener; + + // Interface for supplier click on recycler view + public interface OnSupplierClickListener { + void onSupplierClick(int position); + } + + //Constructor + public SupplierAdapter(List supplierList, OnSupplierClickListener supplierClickListener) { + this.supplierList = supplierList; + this.supplierClickListener = supplierClickListener; + } + + // Get the controls of each row in recycler view + public static class SupplierViewHolder extends RecyclerView.ViewHolder { + TextView tvSupCompany, tvSupContactName, tvSupEmail, tvSupPhone; + + public SupplierViewHolder(@NonNull View v) { + super(v); + tvSupCompany = v.findViewById(R.id.tvSupCompany); + tvSupContactName = v.findViewById(R.id.tvSupContactName); + tvSupEmail = v.findViewById(R.id.tvSupEmail); + tvSupPhone = v.findViewById(R.id.tvSupPhone); + } + } + + // Create a new row view + @NonNull + @Override + public SupplierViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_supplier, parent, false); + return new SupplierViewHolder(v); + } + + //populate the row with supplier data + @Override + public void onBindViewHolder(@NonNull SupplierViewHolder holder, int position) { + SupplierDTO supplier = supplierList.get(position); + + holder.tvSupCompany.setText(supplier.getSupCompany()); + holder.tvSupContactName.setText(supplier.getSupContactFirstName() + " " + supplier.getSupContactLastName()); + holder.tvSupEmail.setText(supplier.getSupEmail()); + holder.tvSupPhone.setText(supplier.getSupPhone()); + + //when a row is clicked, open the detail view + holder.itemView.setOnClickListener(v -> supplierClickListener.onSupplierClick(position)); + } + + @Override + public int getItemCount() { + return supplierList.size(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java new file mode 100644 index 00000000..63f5ac51 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/ChatApi.java @@ -0,0 +1,23 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.dtos.MessageDTO; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +//api calls to get conversations +public interface ChatApi { + + @GET("api/v1/chat/conversations") + Call> getAllConversations(); + + @GET("api/v1/chat/conversations/{conversationId}") + Call getConversationById(@Path("conversationId") Long conversationId); + +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java new file mode 100644 index 00000000..02700075 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/CustomerApi.java @@ -0,0 +1,21 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.CustomerDTO; +import com.example.petstoremobile.dtos.PageResponse; + +import java.util.List; + +import retrofit2.Call; +import retrofit2.http.GET; +import retrofit2.http.Path; +import retrofit2.http.Query; + +//api calls to get customers +public interface CustomerApi { + + @GET("api/v1/customers") + Call> getAllCustomers(@Query("page") int page, @Query("size") int size); + + @GET("api/v1/customers/{customerId}") + Call getCustomerById(@Path("customerId") Long customerId); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java new file mode 100644 index 00000000..13df781f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/MessageApi.java @@ -0,0 +1,20 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.MessageDTO; +import com.example.petstoremobile.dtos.SendMessageRequest; +import java.util.List; +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.Path; + +//api calls to get and send messages +public interface MessageApi { + + @GET("api/v1/chat/conversations/{id}/messages") + Call> getMessages(@Path("id") Long conversationId); + + @POST("api/v1/chat/conversations/{id}/messages") + Call sendMessage(@Path("id") Long conversationId, @Body SendMessageRequest request); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java new file mode 100644 index 00000000..35fccfbc --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/PetApi.java @@ -0,0 +1,40 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +//api calls to CRUD pets +public interface PetApi { + // Get all pets + @GET("api/v1/pets") + Call> getAllPets( + @Query("page") int page, + @Query("size") int size + ); + + // Get pet by id + @GET("api/v1/pets/{id}") + Call getPetById(@Path("id") Long id); + + // Create pet + @POST("api/v1/pets") + Call createPet(@Body PetDTO pet); + + // Update pet + @PUT("api/v1/pets/{id}") + Call updatePet(@Path("id") Long id, @Body PetDTO pet); + + // Delete pet + @DELETE("api/v1/pets/{id}") + Call deletePet(@Path("id") Long id); + +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java new file mode 100644 index 00000000..8a7f48bc --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java @@ -0,0 +1,70 @@ +package com.example.petstoremobile.api; + +import android.content.Context; + +import com.example.petstoremobile.api.auth.AuthApi; +import com.example.petstoremobile.api.auth.AuthInterceptor; + +import okhttp3.OkHttpClient; +import okhttp3.logging.HttpLoggingInterceptor; +import retrofit2.Retrofit; +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 + + private static Retrofit retrofit = null; + + public static Retrofit getClient(Context context) { + //create an http logging using an interceptor + HttpLoggingInterceptor interceptor = new HttpLoggingInterceptor(); + interceptor.setLevel(HttpLoggingInterceptor.Level.BODY); + + OkHttpClient client = new OkHttpClient.Builder() + .addInterceptor(interceptor) + .addInterceptor(new AuthInterceptor(context)) + .build(); + + //build the retrofit object with all needed properties + retrofit = new Retrofit.Builder() + .baseUrl(BASE_URL) + .addConverterFactory(GsonConverterFactory.create()) //JSON converter + .client(client) //logging interceptor - OkHttpClient + .build(); + + return retrofit; + } + + //associate the retrofit object with the API interface + public static PetApi getPetApi(Context context) { + return getClient(context).create(PetApi.class); + } + + public static ServiceApi getServiceApi(Context context) { + return getClient(context).create(ServiceApi.class); + } + + public static SupplierApi getSupplierApi(Context context) { + return getClient(context).create(SupplierApi.class); + } + + public static AuthApi getAuthApi(Context context) { + return getClient(context).create(AuthApi.class); + } + + public static ChatApi getChatApi(Context context) { + return getClient(context).create(ChatApi.class); + } + + public static CustomerApi getCustomerApi(Context context) { + return getClient(context).create(CustomerApi.class); + } + + public static MessageApi getMessageApi(Context context) { + return getClient(context).create(MessageApi.class); + } + +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java new file mode 100644 index 00000000..a8e4ed32 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/ServiceApi.java @@ -0,0 +1,39 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +//api calls to CRUD services +public interface ServiceApi { + // Get all services + @GET("api/v1/services") + Call> getAllServices( + @Query("page") int page, + @Query("size") int size + ); + + // Get service by id + @GET("api/v1/services/{id}") + Call getServiceById(@Path("id") Long id); + + // Create service + @POST("api/v1/services") + Call createService(@Body ServiceDTO service); + + // Update service + @PUT("api/v1/services/{id}") + Call updateService(@Path("id") Long id, @Body ServiceDTO service); + + // Delete service + @DELETE("api/v1/services/{id}") + Call deleteService(@Path("id") Long id); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java new file mode 100644 index 00000000..47d4e1e3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/SupplierApi.java @@ -0,0 +1,39 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SupplierDTO; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.DELETE; +import retrofit2.http.GET; +import retrofit2.http.POST; +import retrofit2.http.PUT; +import retrofit2.http.Path; +import retrofit2.http.Query; + +//api calls to CRUD suppliers +public interface SupplierApi { + // Get all suppliers + @GET("api/v1/suppliers") + Call> getAllSuppliers( + @Query("page") int page, + @Query("size") int size + ); + + // Get supplier by id + @GET("api/v1/suppliers/{id}") + Call getSupplierById(@Path("id") Long id); + + // Create supplier + @POST("api/v1/suppliers") + Call createSupplier(@Body SupplierDTO supplier); + + // Update supplier + @PUT("api/v1/suppliers/{id}") + Call updateSupplier(@Path("id") Long id, @Body SupplierDTO supplier); + + // Delete supplier + @DELETE("api/v1/suppliers/{id}") + Call deleteSupplier(@Path("id") Long id); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java new file mode 100644 index 00000000..cf82623f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthApi.java @@ -0,0 +1,17 @@ +package com.example.petstoremobile.api.auth; + +import com.example.petstoremobile.dtos.AuthDTO; + +import retrofit2.Call; +import retrofit2.http.Body; +import retrofit2.http.GET; +import retrofit2.http.POST; + +//Api for logging in and getting current user +public interface AuthApi { + @POST("api/v1/auth/login") + Call login(@Body AuthDTO.LoginRequest loginRequest); + + @GET("api/v1/auth/me") + Call getCurrentUser(); +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java new file mode 100644 index 00000000..dd17fffd --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/AuthInterceptor.java @@ -0,0 +1,43 @@ +package com.example.petstoremobile.api.auth; + +import android.content.Context; + +import androidx.annotation.NonNull; + +import java.io.IOException; + +import okhttp3.Interceptor; +import okhttp3.Request; +import okhttp3.Response; + +//Used to get the token from the backend for authenticated api calls +public class AuthInterceptor implements Interceptor { + + private final TokenManager tokenManager; + + public AuthInterceptor(Context context) { + this.tokenManager = TokenManager.getInstance(context); + } + + @NonNull + @Override + public Response intercept(@NonNull Chain chain) throws IOException { + String token = tokenManager.getToken(); + String url = chain.request().url().toString(); + + if (url.contains("auth/login") || url.contains("auth/register")) { + return chain.proceed(chain.request()); + } + + //If we have a token then add it to the request + if (token != null) { + Request request = chain.request().newBuilder() + .addHeader("Authorization", "Bearer " + token) + .build(); + return chain.proceed(request); + } + + //If no token then just pass the request + return chain.proceed(chain.request()); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java new file mode 100644 index 00000000..b0f90508 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/auth/TokenManager.java @@ -0,0 +1,70 @@ +package com.example.petstoremobile.api.auth; + +import android.content.Context; +import android.content.SharedPreferences; + +//Store login token in shared preferences +public class TokenManager { + private static final String TOKEN_KEY = "token"; + private static final String USERNAME_KEY = "username"; + private static final String ROLE_KEY = "role"; + private static final String PREFS_NAME = "auth_prefs"; + private static final String USER_ID_KEY = "user_id"; + + private static TokenManager instance; + private SharedPreferences prefs; + + private TokenManager(Context context) { + prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); + } + + public static TokenManager getInstance(Context context) { + if (instance == null) { + instance = new TokenManager(context); + } + return instance; + } + + //save login data after login + public void saveLoginData(String token, String username, String role) { + prefs.edit() + .putString(TOKEN_KEY, token) + .putString(USERNAME_KEY, username) + .putString(ROLE_KEY, role) + .apply(); + } + + //Getters + public String getToken() { + return prefs.getString(TOKEN_KEY, null); + } + + public String getUsername() { + return prefs.getString(USERNAME_KEY, null); + } + + public String getRole() { + return prefs.getString(ROLE_KEY, null); + } + + public Long getUserId() { + long id = prefs.getLong(USER_ID_KEY, -1L); + return id == -1L ? null : id; + } + + public void saveUserId(Long userId) { + prefs.edit().putLong(USER_ID_KEY, userId).apply(); + } + + //Check if logged in + public boolean isLoggedIn() { + return getToken() != null; + } + + //Clear login data + public void clearLoginData() { + prefs.edit().clear().apply(); + } + + +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java new file mode 100644 index 00000000..6aecdbc3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/AuthDTO.java @@ -0,0 +1,49 @@ +package com.example.petstoremobile.dtos; + +//Used to send login data to the backend +public class AuthDTO { + public static class LoginRequest { + private String username; + private String password; + + public LoginRequest(String username, String password) { + this.username = username; + this.password = password; + } + + public String getUsername() { return username; } + public String getPassword() { return password; } + } + + //Used to receive login data from the backend + public static class LoginResponse { + private String token; + private String username; + private String role; + + public String getToken() { return token; } + public String getUsername() { return username; } + public String getRole() { return role; } + } + + //Used to get logged in profile + public static class UserResponse { + private Long id; + private String username; + private String email; + private String fullName; + private String avatarUrl; + private String role; + private Long storeId; + private String storeName; + + public Long getId() { return id; } + public String getUsername() { return username; } + public String getEmail() { return email; } + public String getFullName() { return fullName; } + public String getAvatarUrl() { return avatarUrl; } + public String getRole() { return role; } + public Long getStoreId() { return storeId; } + public String getStoreName() { return storeName; } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java new file mode 100644 index 00000000..316aa467 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ConversationDTO.java @@ -0,0 +1,78 @@ +package com.example.petstoremobile.dtos; + +public class ConversationDTO { + private Long id; + private Long customerId; + private Long staffId; + private String status; + private String mode; + private String lastMessage; + private String createdAt; + private String updatedAt; + + public ConversationDTO() {} + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public Long getStaffId() { + return staffId; + } + + public void setStaffId(Long staffId) { + this.staffId = staffId; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } + + public String getMode() { + return mode; + } + + public void setMode(String mode) { + this.mode = mode; + } + + public String getLastMessage() { + return lastMessage; + } + + public void setLastMessage(String lastMessage) { + this.lastMessage = lastMessage; + } + + public String getCreatedAt() { + return createdAt; + } + + public void setCreatedAt(String createdAt) { + this.createdAt = createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } + + public void setUpdatedAt(String updatedAt) { + this.updatedAt = updatedAt; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java new file mode 100644 index 00000000..1a135a6d --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/CustomerDTO.java @@ -0,0 +1,59 @@ +package com.example.petstoremobile.dtos; + +import com.google.gson.annotations.SerializedName; + +public class CustomerDTO { + @SerializedName("customerId") + private Long customerId; + + private String firstName; + private String lastName; + private String email; + private String phone; + + public CustomerDTO() {} + + public Long getCustomerId() { + return customerId; + } + + public void setCustomerId(Long customerId) { + this.customerId = customerId; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + + public String getFullName() { + return (firstName != null ? firstName : "") + " " + (lastName != null ? lastName : ""); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java new file mode 100644 index 00000000..1080b600 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/MessageDTO.java @@ -0,0 +1,44 @@ +package com.example.petstoremobile.dtos; + +import com.google.gson.annotations.SerializedName; + +public class MessageDTO { + + @SerializedName("id") + private Long id; + + @SerializedName("conversationId") + private Long conversationId; + + @SerializedName("senderId") + private Long senderId; + + @SerializedName("content") + private String content; + + @SerializedName("timestamp") + private String timestamp; + + @SerializedName("isRead") + private Boolean isRead; + + public MessageDTO() {} + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Long getConversationId() { return conversationId; } + public void setConversationId(Long conversationId) { this.conversationId = conversationId; } + + public Long getSenderId() { return senderId; } + public void setSenderId(Long senderId) { this.senderId = senderId; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public String getTimestamp() { return timestamp; } + public void setTimestamp(String timestamp) { this.timestamp = timestamp; } + + public Boolean getIsRead() { return isRead; } + public void setIsRead(Boolean isRead) { this.isRead = isRead; } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java new file mode 100644 index 00000000..7237105e --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PageResponse.java @@ -0,0 +1,32 @@ +package com.example.petstoremobile.dtos; + +import java.util.List; + +//Used to get data from the API +public class PageResponse { + private List content; + private int totalPages; + private long totalElements; + private int number; + private int size; + private boolean last; + + public List getContent() { + return content; + } + public int getTotalPages() { + return totalPages; + } + public long getTotalElements() { + return totalElements; + } + public int getNumber() { + return number; + } + public int getSize() { + return size; + } + public boolean isLast() { + return last; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java new file mode 100644 index 00000000..d76a8509 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/PetDTO.java @@ -0,0 +1,40 @@ +package com.example.petstoremobile.dtos; + +public class PetDTO { + private Long petId; + private String petName; + private String petSpecies; + private String petBreed; + private Integer petAge; + private String petStatus; + private String petPrice; + private String createdAt; + private String updatedAt; + + public Long getPetId() { return petId; } + public void setPetId(Long petId) { this.petId = petId; } + + public String getPetName() { return petName; } + public void setPetName(String petName) { this.petName = petName; } + + public String getPetSpecies() { return petSpecies; } + public void setPetSpecies(String petSpecies) { this.petSpecies = petSpecies; } + + public String getPetBreed() { return petBreed; } + public void setPetBreed(String petBreed) { this.petBreed = petBreed; } + + public Integer getPetAge() { return petAge; } + public void setPetAge(Integer petAge) { this.petAge = petAge; } + + public String getPetStatus() { return petStatus; } + public void setPetStatus(String petStatus) { this.petStatus = petStatus; } + + public String getPetPrice() { return petPrice; } + public void setPetPrice(String petPrice) { this.petPrice = petPrice; } + + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + + public String getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java new file mode 100644 index 00000000..7a521de3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SendMessageRequest.java @@ -0,0 +1,13 @@ +package com.example.petstoremobile.dtos; + +public class SendMessageRequest { + + private String content; + + public SendMessageRequest(String content) { + this.content = content; + } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java new file mode 100644 index 00000000..56e44371 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/ServiceDTO.java @@ -0,0 +1,32 @@ +package com.example.petstoremobile.dtos; + +public class ServiceDTO { + private Long serviceId; + private String serviceName; + private String serviceDesc; + private Integer serviceDuration; + private Double servicePrice; + private String createdAt; + private String updatedAt; + + public Long getServiceId() { return serviceId; } + public void setServiceId(Long serviceId) { this.serviceId = serviceId; } + + public String getServiceName() { return serviceName; } + public void setServiceName(String serviceName) { this.serviceName = serviceName; } + + public String getServiceDesc() { return serviceDesc; } + public void setServiceDesc(String serviceDesc) { this.serviceDesc = serviceDesc; } + + public Integer getServiceDuration() { return serviceDuration; } + public void setServiceDuration(Integer serviceDuration) { this.serviceDuration = serviceDuration; } + + public Double getServicePrice() { return servicePrice; } + public void setServicePrice(Double servicePrice) { this.servicePrice = servicePrice; } + + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + + public String getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java new file mode 100644 index 00000000..e34816c1 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SupplierDTO.java @@ -0,0 +1,36 @@ +package com.example.petstoremobile.dtos; + +public class SupplierDTO { + private Long supId; + private String supCompany; + private String supContactFirstName; + private String supContactLastName; + private String supEmail; + private String supPhone; + private String createdAt; + private String updatedAt; + + public Long getSupId() { return supId; } + public void setSupId(Long supId) { this.supId = supId; } + + public String getSupCompany() { return supCompany; } + public void setSupCompany(String supCompany) { this.supCompany = supCompany; } + + public String getSupContactFirstName() { return supContactFirstName; } + public void setSupContactFirstName(String supContactFirstName) { this.supContactFirstName = supContactFirstName; } + + public String getSupContactLastName() { return supContactLastName; } + public void setSupContactLastName(String supContactLastName) { this.supContactLastName = supContactLastName; } + + public String getSupEmail() { return supEmail; } + public void setSupEmail(String supEmail) { this.supEmail = supEmail; } + + public String getSupPhone() { return supPhone; } + public void setSupPhone(String supPhone) { this.supPhone = supPhone; } + + public String getCreatedAt() { return createdAt; } + public void setCreatedAt(String createdAt) { this.createdAt = createdAt; } + + public String getUpdatedAt() { return updatedAt; } + public void setUpdatedAt(String updatedAt) { this.updatedAt = updatedAt; } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java new file mode 100644 index 00000000..b73fdc6b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -0,0 +1,407 @@ +package com.example.petstoremobile.fragments; + +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.annotation.NonNull; +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.ChatAdapter; +import com.example.petstoremobile.adapters.MessageAdapter; +import com.example.petstoremobile.api.auth.TokenManager; +import com.example.petstoremobile.api.ChatApi; +import com.example.petstoremobile.api.CustomerApi; +import com.example.petstoremobile.api.MessageApi; +import com.example.petstoremobile.api.RetrofitClient; +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.dtos.SendMessageRequest; +import com.example.petstoremobile.models.Chat; +import com.example.petstoremobile.models.Message; +import com.example.petstoremobile.websocket.StompChatManager; +import java.util.*; +import java.util.stream.Collectors; +import retrofit2.*; + +public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickListener, StompChatManager.MessageListener, + StompChatManager.ConversationListener, StompChatManager.ConnectionListener { + + private static final String TAG = "ChatFragment"; + + // View + private DrawerLayout drawerLayout; + private RecyclerView rvChatList, rvMessages; + private EditText etMessage; + private Button btnSend; + + // Adapters + private ChatAdapter chatAdapter; + private MessageAdapter messageAdapter; + + // Data + private final List chatList = new ArrayList<>(); + private final List messageList = new ArrayList<>(); + private final Map customerNames = new HashMap<>(); + + // APIs + private ChatApi chatApi; + private CustomerApi customerApi; + private MessageApi messageApi; + + // chat + private Long currentUserId; + private Long activeConversationId; + private StompChatManager stompChatManager; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, + ViewGroup container, Bundle savedInstanceState) { + + View view = inflater.inflate(R.layout.fragment_chat, container, false); + + chatApi = RetrofitClient.getChatApi(requireContext()); + customerApi = RetrofitClient.getCustomerApi(requireContext()); + messageApi = RetrofitClient.getMessageApi(requireContext()); + + drawerLayout = view.findViewById(R.id.chatDrawerLayout); + rvChatList = view.findViewById(R.id.rvChatList); + rvMessages = view.findViewById(R.id.rvMessages); + etMessage = view.findViewById(R.id.etMessage); + btnSend = view.findViewById(R.id.btnSend); + + ImageButton hamburger = view.findViewById(R.id.btnHamburger); + hamburger.setOnClickListener(v -> drawerLayout.openDrawer(GravityCompat.START)); + btnSend.setOnClickListener(v -> sendMessage()); + + setupRecyclerViews(); + loadInitialData(); + + return view; + } + + private void setupRecyclerViews() { + // Set up Drawer menu to select conversation + chatAdapter = new ChatAdapter(chatList, this); + rvChatList.setLayoutManager(new LinearLayoutManager(getContext())); + rvChatList.setAdapter(chatAdapter); + + // set up RecyclerView for selected chat to show messages + messageAdapter = new MessageAdapter(messageList, null); + LinearLayoutManager lm = new LinearLayoutManager(getContext()); + lm.setStackFromEnd(true); + rvMessages.setLayoutManager(lm); + rvMessages.setAdapter(messageAdapter); + setConversationActive(false); + } + + //Helper function to load token and user id then connect to websocket + private void loadInitialData() { + TokenManager tm = TokenManager.getInstance(requireContext()); + String token = tm.getToken(); + currentUserId = tm.getUserId(); + String role = tm.getRole(); + + messageAdapter.setCurrentUserId(currentUserId); + + // if token exist then connect to websocket + if (token != null) { + stompChatManager = new StompChatManager(token, role); + stompChatManager.setMessageListener(this); + stompChatManager.setConversationListener(this); + stompChatManager.setConnectionListener(this); + stompChatManager.connect(); + } else { + Log.e(TAG, "No token found"); + } + + loadCustomers(); + } + + //Helper function to load customer names for it to be displayed on drawer menu + private void loadCustomers() { + customerApi.getAllCustomers(0, 100).enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null) { + for (CustomerDTO c : response.body().getContent()) { + customerNames.put(c.getCustomerId(), c.getFullName()); + } + } + loadConversations(); + } + + @Override + public void onFailure(@NonNull Call> call, + @NonNull Throwable t) { + loadConversations(); + } + }); + } + + //helper function to load conversations entities to display with customer names in drawer menu + private void loadConversations() { + chatApi.getAllConversations().enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null) { + chatList.clear(); + List loaded = response.body().stream() + .map(dto -> { + String name = customerNames.getOrDefault( + dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + return new Chat(String.valueOf(dto.getId()), + name, dto.getLastMessage(), + dto.getCustomerId(), dto.getStaffId()); + }) + .collect(Collectors.toList()); + chatList.addAll(loaded); + chatAdapter.notifyDataSetChanged(); + if (activeConversationId == null) { + messageList.clear(); + messageAdapter.notifyDataSetChanged(); + setConversationActive(false); + } + } + } + @Override + public void onFailure(@NonNull Call> call, + @NonNull Throwable t) { + Log.e(TAG, "Error loading conversations", t); + } + }); + } + + // Called when user taps a chat in the drawer + // Loads messages for that chat selected + @Override + public void onChatClick(Chat chat) { + activeConversationId = Long.parseLong(chat.getChatId()); + setConversationActive(true); + drawerLayout.closeDrawer(GravityCompat.START); + + if (stompChatManager != null) { + stompChatManager.subscribeToConversation(activeConversationId); + } + + loadMessageHistory(activeConversationId); + } + + //helper function to load messages for selected chat + private void loadMessageHistory(Long conversationId) { + messageApi.getMessages(conversationId).enqueue(new Callback>() { + @Override + public void onResponse(@NonNull Call> call, + @NonNull Response> response) { + if (response.isSuccessful() && response.body() != null) { + messageList.clear(); + for (MessageDTO dto : response.body()) { + messageList.add(dtoToModel(dto)); + } + messageAdapter.notifyDataSetChanged(); + scrollToBottom(); + } + } + @Override + public void onFailure(@NonNull Call> call, + @NonNull Throwable t) { + Log.e(TAG, "Error loading messages", t); + } + }); + } + + //Helper function to send a message to the chat + private void sendMessage() { + //check if a chat is selected + if (activeConversationId == null) return; + + //get the message from text field + String text = etMessage.getText().toString().trim(); + if (text.isEmpty()) return; + + //clear text field after sending + etMessage.setText(""); + + //calls api to send the message + messageApi.sendMessage(activeConversationId, new SendMessageRequest(text)) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, + @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + messageList.add(dtoToModel(response.body())); + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + loadConversations(); + } + } + @Override + public void onFailure(@NonNull Call call, + @NonNull Throwable t) { + Log.e(TAG, "Send failed", t); + } + }); + } + + // When a message is received updates the chat preview + @Override + public void onMessageReceived(MessageDTO dto) { + //if there is no active selected conversation or the message received is for another chat, then just update the preview of last message + if (activeConversationId == null || !activeConversationId.equals(dto.getConversationId())) { + updateConversationPreview(dto.getConversationId(), dto.getContent()); + return; + } + updateConversationPreview(dto.getConversationId(), dto.getContent()); + + if (currentUserId != null && currentUserId.equals(dto.getSenderId())) return; + + //else add the message to the active chat if it's not from the current user + messageList.add(dtoToModel(dto)); + messageAdapter.notifyItemInserted(messageList.size() - 1); + scrollToBottom(); + } + + // When a new conversation is added, updates the chat preview + @Override + public void onConversationUpdated(ConversationDTO dto) { + boolean updated = false; + String name = customerNames.getOrDefault( + dto.getCustomerId(), "Customer #" + dto.getCustomerId()); + + for (int i = 0; i < chatList.size(); i++) { + Chat existing = chatList.get(i); + if (existing.getChatId().equals(String.valueOf(dto.getId()))) { + chatList.set(i, new Chat( + String.valueOf(dto.getId()), + name, + dto.getLastMessage(), + dto.getCustomerId(), + dto.getStaffId() + )); + chatAdapter.notifyItemChanged(i); + updated = true; + break; + } + } + + if (!updated) { + chatList.add(0, new Chat( + String.valueOf(dto.getId()), + name, + dto.getLastMessage(), + dto.getCustomerId(), + dto.getStaffId() + )); + chatAdapter.notifyItemInserted(0); + } + + if (activeConversationId != null && activeConversationId.equals(dto.getId())) { + setConversationActive(true); + } + } + + @Override + public void onSocketOpened() { + if (!isAdded()) { + return; + } + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + } + + @Override + public void onSocketClosed() { + if (!isAdded()) { + return; + } + loadConversations(); + } + + @Override + public void onSocketError() { + if (!isAdded()) { + return; + } + loadConversations(); + if (activeConversationId != null) { + loadMessageHistory(activeConversationId); + } + } + + // Helper function to convert DTO to message + private Message dtoToModel(MessageDTO dto) { + Message m = new Message(); + m.setId(dto.getId()); + m.setConversationId(dto.getConversationId()); + m.setSenderId(dto.getSenderId()); + m.setContent(dto.getContent()); + m.setTimestamp(dto.getTimestamp()); + m.setIsRead(dto.getIsRead()); + return m; + } + + //Helper function to scroll to bottom of the chat + private void scrollToBottom() { + if (!messageList.isEmpty()) { + rvMessages.post(() -> + rvMessages.smoothScrollToPosition(messageList.size() - 1)); + } + } + + // Helper function to update the chat preview last message + private void updateConversationPreview(Long conversationId, String lastMessage) { + if (conversationId == null) { + return; + } + for (int i = 0; i < chatList.size(); i++) { + Chat existing = chatList.get(i); + if (existing.getChatId().equals(String.valueOf(conversationId))) { + Chat updated = new Chat( + existing.getChatId(), + existing.getCustomerName(), + lastMessage, + existing.getCustomerId(), + existing.getStaffId() + ); + chatList.set(i, updated); + chatAdapter.notifyItemChanged(i); + return; + } + } + } + + //Helper function to enable or disable the send button when there is no active chat + private void setConversationActive(boolean active) { + btnSend.setEnabled(active); + etMessage.setEnabled(active); + if (!active) { + activeConversationId = null; + if (stompChatManager != null) { + stompChatManager.clearConversationSubscription(); + } + messageList.clear(); + messageAdapter.notifyDataSetChanged(); + etMessage.setText(""); + etMessage.setHint("Select a chat to start messaging"); + } else { + etMessage.setHint("Type a message..."); + } + } + + // When fragment is destroyed, disconnect from websocket + @Override + public void onDestroyView() { + super.onDestroyView(); + if (stompChatManager != null) stompChatManager.disconnect(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java new file mode 100644 index 00000000..cd07df63 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -0,0 +1,145 @@ +package com.example.petstoremobile.fragments; + +import android.os.Bundle; + +import androidx.core.view.GravityCompat; +import androidx.drawerlayout.widget.DrawerLayout; +import androidx.fragment.app.Fragment; + +import android.view.LayoutInflater; + +import android.view.View; +import android.view.ViewGroup; +import android.widget.LinearLayout; + +import com.example.petstoremobile.R; + +import com.example.petstoremobile.fragments.listfragments.PetFragment; +import com.example.petstoremobile.fragments.listfragments.ServiceFragment; +import com.example.petstoremobile.fragments.listfragments.SupplierFragment; +import com.example.petstoremobile.fragments.listfragments.AdoptionFragment; +import com.example.petstoremobile.fragments.listfragments.AppointmentFragment; +import com.example.petstoremobile.fragments.listfragments.InventoryFragment; +import com.example.petstoremobile.fragments.listfragments.ProductFragment; + +//The Fragment for the displaying the list of entities to be viewed +public class ListFragment extends Fragment { + + private DrawerLayout drawerLayout; + private LinearLayout drawerPets, drawerServices, drawerSuppliers; + private View touchBlocker; + + // Adoptions, Appointments, Inventory, Products + + private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts; + + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_list, container, false); + + //get controls from the layout + drawerLayout = view.findViewById(R.id.drawerLayout); + drawerPets = view.findViewById(R.id.drawerPets); + drawerServices = view.findViewById(R.id.drawerServices); + drawerSuppliers = view.findViewById(R.id.drawerSuppliers); + drawerAdoptions = view.findViewById(R.id.drawerAdoptions); + drawerAppointments = view.findViewById(R.id.drawerAppointments); + drawerInventory = view.findViewById(R.id.drawerInventory); + drawerProducts = view.findViewById(R.id.drawerProducts); + + //needed to disable touches on the innerContainer while the drawer is open + touchBlocker = view.findViewById(R.id.touchBlocker); + + //Display pets fragment by default + if (savedInstanceState == null) { + loadFragment(new PetFragment()); + } + + //add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments) + //while the drawer is open + drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { + + //When the drawer is opened, disable touches on the background + @Override + public void onDrawerOpened(View drawerView) { + touchBlocker.setVisibility(View.VISIBLE); + touchBlocker.setClickable(true); + } + + //When the drawer is closed, enable touches again + @Override + public void onDrawerClosed(View drawerView) { + touchBlocker.setVisibility(View.GONE); + touchBlocker.setClickable(false); + } + + //unused methods + @Override + public void onDrawerSlide(View drawerView, float slideOffset) {} + @Override + public void onDrawerStateChanged(int newState) {} + }); + + // Click listeners for each drawer + //Pets + drawerPets.setOnClickListener(v -> { + loadFragment(new PetFragment()); + drawerLayout.closeDrawers(); + }); + + //Services + drawerServices.setOnClickListener(v -> { + loadFragment(new ServiceFragment()); + drawerLayout.closeDrawers(); + }); + + //Suppliers + drawerSuppliers.setOnClickListener(v -> { + loadFragment(new SupplierFragment()); + drawerLayout.closeDrawers(); + }); + + //Adoptions + + drawerAdoptions.setOnClickListener(v -> { + loadFragment(new AdoptionFragment()); + drawerLayout.closeDrawers(); + }); + + //Appoinment + drawerAppointments.setOnClickListener(v -> { + loadFragment(new AppointmentFragment()); + drawerLayout.closeDrawers(); + }); + + //Inventory + drawerInventory.setOnClickListener(v -> { + loadFragment(new InventoryFragment()); + drawerLayout.closeDrawers(); + }); + + //Products + drawerProducts.setOnClickListener(v -> { + loadFragment(new ProductFragment()); + drawerLayout.closeDrawers(); + }); + + return view; + } + + //helper function to open the drawer + public void openDrawer() { + drawerLayout.openDrawer(GravityCompat.START); + } + + // helper function to load the fragment into the display + public void loadFragment(Fragment fragment) { + getChildFragmentManager() + .beginTransaction() + .replace(R.id.inner_fragment_container, fragment) + .addToBackStack(null) + .commit(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java new file mode 100644 index 00000000..2bb92e4c --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ProfileFragment.java @@ -0,0 +1,255 @@ +package com.example.petstoremobile.fragments; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.net.Uri; +import android.os.Bundle; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.fragment.app.Fragment; + +import android.provider.MediaStore; +import android.text.InputType; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.ImageView; +import android.widget.TextView; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.activities.MainActivity; +import com.example.petstoremobile.api.auth.TokenManager; + +import java.io.File; + +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; + + //Initialize the launchers for camera and gallery + private ActivityResultLauncher galleryLauncher; + private ActivityResultLauncher cameraLauncher; + private ActivityResultLauncher permissionLauncher; + + //Called when the fragment is created, sets up the launchers is set profile image + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Launcher to open gallery to select profile image + galleryLauncher = registerForActivityResult( + //open gallery + new ActivityResultContracts.StartActivityForResult(), + result -> { + //if the user selects an image and its not null + if (result.getResultCode() == Activity.RESULT_OK + && 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 + } + } + ); + + // Launcher for camera to open and capture profile image + cameraLauncher = registerForActivityResult( + //open camera + new ActivityResultContracts.TakePicture(), + 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 + } + } + ); + + // Launcher to request camera permission + permissionLauncher = registerForActivityResult( + //ask user for camera permission + new ActivityResultContracts.RequestPermission(), + granted -> { + //if the permission is granted launch the camera + if (granted) { + launchCamera(); + } + else { + //if the permission is denied then tell the user to grant it + new AlertDialog.Builder(requireContext()) + .setTitle("Permission Permission Required") + .setMessage("Please grant camera permission to use this feature") + .setPositiveButton("Open Settings", (dialog, which) ->{ + //open the settings page to grant the permission when they click open settings + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null)); + startActivity(intent); + }) + //close the dialog when the user clicks cancel + .setNegativeButton("Cancel", null) + .show(); + } + } + ); + } + + //TODO: MAKE PROFILE VIEW DISPLAY PROFILE DATA FROM DATABASE + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_profile, container, false); + + //get all the controls from the view + imgProfile = view.findViewById(R.id.imgProfile); + tvProfileName = view.findViewById(R.id.tvProfileName); + 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); + + //Set up listeners for the buttons + //Change photo button + btnChangePhoto.setOnClickListener(v -> { + //Show alert dialog to user to select from gallery or camera + new AlertDialog.Builder(requireContext()) + .setTitle("Change Profile Photo") + //set the options for the alert dialog + .setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> { + if (which == 0) { + // Choose Camera + //Checks if the user has granted the camera permission already + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + //if the permission is already granted then launch the camera + launchCamera(); + } else { + //otherwise request the permission + permissionLauncher.launch(Manifest.permission.CAMERA); + } + } else { + // Choose Gallery + Intent intent = new Intent(Intent.ACTION_PICK, + MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + galleryLauncher.launch(intent); + } + }) + .show(); + //TODO: UPDATE PHOTO IN DATABASE + }); + + //Edit email button + //When clicked open a dialog to change email + btnEditEmail.setOnClickListener(v -> { + //Make a text field for the user to enter the new email + EditText input = new EditText(requireContext()); + input.setPadding(30,30,30,30); + input.setText(tvProfileEmail.getText().toString()); + + //set input type to email + input.setInputType(android.text.InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS); + + + //Show alert dialog to user to enter new email + new AlertDialog.Builder(requireContext()) + .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(); + } + }) + .setNegativeButton("Cancel", null) + .show(); + }); + + //Edit phone button + //When clicked open a dialog to change phone + btnEditPhone.setOnClickListener(v -> { + //Make a text field for the user to enter the new email + EditText input = new EditText(requireContext()); + input.setPadding(30,30,30,30); + input.setText(tvProfilePhone.getText().toString()); + + //set input type to phone number + input.setInputType(InputType.TYPE_CLASS_PHONE); + + //add canada phone number formatting to input (XXX) XXX-XXXX + input.addTextChangedListener(new android.telephony.PhoneNumberFormattingTextWatcher("CA")); + input.setFilters(new android.text.InputFilter[]{new android.text.InputFilter.LengthFilter(14)}); + + + //Show alert dialog to user to enter new phone + new AlertDialog.Builder(requireContext()) + .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(); + } + }) + .setNegativeButton("Cancel", null) + .show(); + }); + + //Logout button + btnLogout.setOnClickListener(v -> { + 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); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK); + //start the activity to go to login page and finish the current activity + startActivity(intent); + requireActivity().finish(); + }); + + return view; + } + + //Helper function create a file in the cache directory to store the photo in then launch the camera to capture the photo + private void launchCamera() { + //create a file in the cache directory to store the photo in + File photoFile = new File(requireContext().getCacheDir(), "profile_photo.jpg"); + //get the uri for the file made + photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); + //launch the camera to capture the photo and save the photo to photoUri + cameraLauncher.launch(photoUri); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java new file mode 100644 index 00000000..f72d6836 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AdoptionFragment.java @@ -0,0 +1,163 @@ +package com.example.petstoremobile.fragments.listfragments; + +// Added search/filter bar to filter adoptions by adopter name or pet name. +// Added pull-to-refresh using SwipeRefreshLayout. + +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.AdoptionAdapter; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.AdoptionDetailFragment; +import com.example.petstoremobile.models.Adoption; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.ArrayList; +import java.util.List; + +public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdoptionClickListener { + + private List adoptionList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private AdoptionAdapter adapter; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + private ImageButton hamburger; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_adoption, container, false); + + hamburger = view.findViewById(R.id.btnHamburger); + + loadAdoptionData(); + // Replace with actual API call when backend is ready + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + + FloatingActionButton fabAddAdoption = view.findViewById(R.id.fabAddAdoption); + fabAddAdoption.setOnClickListener(v -> openAdoptionDetails(-1)); + + //Make the hamburger button open the drawer from listFragment + hamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + //if list fragment is found then use its helper function to open the drawer + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + + return view; + } + + // Filters adoption list by adopter name or pet name + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchAdoption); + 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) { + filterAdoptions(s.toString()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + private void filterAdoptions(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(adoptionList); + } else { + String lower = query.toLowerCase(); + for (Adoption a : adoptionList) { + if (a.getAdopterName().toLowerCase().contains(lower) + || a.getPetName().toLowerCase().contains(lower) + || a.getStatus().toLowerCase().contains(lower)) { + filteredList.add(a); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAdoption); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadAdoptionData(); // TODO: Replace with actual API call + filterAdoptions(etSearch.getText().toString()); + swipeRefreshLayout.setRefreshing(false); + }); + } + + private void openAdoptionDetails(int position) { + AdoptionDetailFragment detailFragment = new AdoptionDetailFragment(); + Bundle args = new Bundle(); + args.putInt("position", position); + + if (position != -1) { + Adoption adoption = filteredList.get(position); + int realPosition = adoptionList.indexOf(adoption); + args.putInt("position", realPosition); + args.putInt("adoptionId", adoption.getAdoptionId()); + args.putString("adopterName", adoption.getAdopterName()); + args.putString("adopterEmail", adoption.getAdopterEmail()); + args.putString("adopterPhone", adoption.getAdopterPhone()); + args.putString("petName", adoption.getPetName()); + args.putString("adoptionDate", adoption.getAdoptionDate()); + args.putString("status", adoption.getStatus()); + } + + detailFragment.setArguments(args); + detailFragment.setAdoptionFragment(this); + + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.loadFragment(detailFragment); + } + + public void onAdoptionSaved(int position, Adoption adoption) { + if (position == -1) { + adoptionList.add(adoption); + } else { + adoptionList.set(position, adoption); + } + filterAdoptions(etSearch.getText().toString()); + } + + public void onAdoptionDeleted(int position) { + adoptionList.remove(position); + filterAdoptions(etSearch.getText().toString()); + } + + @Override + public void onAdoptionClick(int position) { + openAdoptionDetails(position); + } + + private void loadAdoptionData() { + adoptionList.clear(); + adoptionList.add(new Adoption(1, "Sarah Connor", "sarah@email.com", "555-1234", "Luna", "2026-03-01", "Approved")); + adoptionList.add(new Adoption(2, "Tom Hardy", "tom@email.com", "555-5678", "Bella", "2026-03-05", "Pending")); + adoptionList.add(new Adoption(3, "Emily Clark", "emily@email.com", "555-9012", "Charlie", "2026-03-07", "Pending")); + adoptionList.add(new Adoption(4, "Mike Ross", "mike@email.com", "555-3456", "Milo", "2026-02-20", "Rejected")); + filteredList.clear(); + filteredList.addAll(adoptionList); + } + + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAdoptions); + adapter = new AdoptionAdapter(filteredList, this); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java new file mode 100644 index 00000000..09ad489b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AppointmentFragment.java @@ -0,0 +1,167 @@ +package com.example.petstoremobile.fragments.listfragments; + +// Added search/filter bar to filter appointments by customer name or service type. +// Added pull-to-refresh using SwipeRefreshLayout. + +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.AppointmentAdapter; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.AppointmentDetailFragment; +import com.example.petstoremobile.models.Appointment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.ArrayList; +import java.util.List; + +public class AppointmentFragment extends Fragment implements AppointmentAdapter.OnAppointmentClickListener { + + private List appointmentList = new ArrayList<>(); // full data list + private List filteredList = new ArrayList<>(); // filtered display list + private AppointmentAdapter adapter; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + private ImageButton hamburger; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_appointment, container, false); + + hamburger = view.findViewById(R.id.btnHamburger); + + loadAppointmentData(); // TODO: Replace with actual API call when backend is ready + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + + FloatingActionButton fabAddAppointment = view.findViewById(R.id.fabAddAppointment); + fabAddAppointment.setOnClickListener(v -> openAppointmentDetails(-1)); + + //Make the hamburger button open the drawer from listFragment + hamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + //if list fragment is found then use its helper function to open the drawer + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + + return view; + } + + // Sets up the search bar to filter appointments by customer name or service type + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchAppointment); + 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) { + filterAppointments(s.toString()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + // Filters the appointment list based on the search query + private void filterAppointments(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(appointmentList); + } else { + String lower = query.toLowerCase(); + for (Appointment a : appointmentList) { + if (a.getCustomerName().toLowerCase().contains(lower) + || a.getServiceType().toLowerCase().contains(lower) + || a.getPetName().toLowerCase().contains(lower)) { + filteredList.add(a); + } + } + } + adapter.notifyDataSetChanged(); + } + + // Sets up pull-to-refresh: reloads data when user swipes down + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshAppointment); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadAppointmentData(); // TODO: Replace with actual API call when backend is ready + filterAppointments(etSearch.getText().toString()); + swipeRefreshLayout.setRefreshing(false); + }); + } + + private void openAppointmentDetails(int position) { + AppointmentDetailFragment detailFragment = new AppointmentDetailFragment(); + Bundle args = new Bundle(); + args.putInt("position", position); + + if (position != -1) { + Appointment appointment = filteredList.get(position); + // Find the real position in the full list for save/delete callbacks + int realPosition = appointmentList.indexOf(appointment); + args.putInt("position", realPosition); + args.putInt("appointmentId", appointment.getAppointmentId()); + args.putString("customerName", appointment.getCustomerName()); + args.putString("petName", appointment.getPetName()); + args.putString("serviceType", appointment.getServiceType()); + args.putString("appointmentDate", appointment.getAppointmentDate()); + args.putString("appointmentTime", appointment.getAppointmentTime()); + args.putString("status", appointment.getStatus()); + } + + detailFragment.setArguments(args); + detailFragment.setAppointmentFragment(this); + + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.loadFragment(detailFragment); + } + + public void onAppointmentSaved(int position, Appointment appointment) { + if (position == -1) { + appointmentList.add(appointment); + } else { + appointmentList.set(position, appointment); + } + filterAppointments(etSearch.getText().toString()); + } + + public void onAppointmentDeleted(int position) { + appointmentList.remove(position); + filterAppointments(etSearch.getText().toString()); + } + + @Override + public void onAppointmentClick(int position) { + openAppointmentDetails(position); + } + + // Helper function to load hardcoded sample data + // Replace with API call + private void loadAppointmentData() { + appointmentList.clear(); + appointmentList.add(new Appointment(1, "John Smith", "Buddy", "Grooming", "2026-03-10", "10:00 AM", "Confirmed")); + appointmentList.add(new Appointment(2, "Jane Doe", "Luna", "Vet Checkup", "2026-03-11", "02:00 PM", "Pending")); + appointmentList.add(new Appointment(3, "Bob Lee", "Max", "Training", "2026-03-12", "11:00 AM", "Confirmed")); + appointmentList.add(new Appointment(4, "Alice Brown", "Milo", "Grooming", "2026-03-13", "03:00 PM", "Cancelled")); + filteredList.clear(); + filteredList.addAll(appointmentList); + } + + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewAppointments); + adapter = new AppointmentAdapter(filteredList, this); // adapter uses filteredList + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java new file mode 100644 index 00000000..38546e83 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/InventoryFragment.java @@ -0,0 +1,162 @@ +package com.example.petstoremobile.fragments.listfragments; + +// Added search/filter bar to filter inventory by item name or category. +// Added pull-to-refresh using SwipeRefreshLayout. + +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.InventoryAdapter; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.InventoryDetailFragment; +import com.example.petstoremobile.models.Inventory; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.ArrayList; +import java.util.List; + +public class InventoryFragment extends Fragment implements InventoryAdapter.OnInventoryClickListener { + + private List inventoryList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private InventoryAdapter adapter; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + private ImageButton hamburger; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_inventory, container, false); + + hamburger = view.findViewById(R.id.btnHamburger); + + loadInventoryData(); // TODO: Replace with actual API call when backend is ready + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + + FloatingActionButton fabAddInventory = view.findViewById(R.id.fabAddInventory); + fabAddInventory.setOnClickListener(v -> openInventoryDetails(-1)); + + //Make the hamburger button open the drawer from listFragment + hamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + //if list fragment is found then use its helper function to open the drawer + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + + return view; + } + + // Filters inventory list by item name or category + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchInventory); + 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) { + filterInventory(s.toString()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + private void filterInventory(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(inventoryList); + } else { + String lower = query.toLowerCase(); + for (Inventory i : inventoryList) { + if (i.getItemName().toLowerCase().contains(lower) + || i.getCategory().toLowerCase().contains(lower) + || i.getSupplier().toLowerCase().contains(lower)) { + filteredList.add(i); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshInventory); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadInventoryData(); // TODO: Replace with actual API call + filterInventory(etSearch.getText().toString()); + swipeRefreshLayout.setRefreshing(false); + }); + } + + private void openInventoryDetails(int position) { + InventoryDetailFragment detailFragment = new InventoryDetailFragment(); + Bundle args = new Bundle(); + args.putInt("position", position); + + if (position != -1) { + Inventory inventory = filteredList.get(position); + int realPosition = inventoryList.indexOf(inventory); + args.putInt("position", realPosition); + args.putInt("inventoryId", inventory.getInventoryId()); + args.putString("itemName", inventory.getItemName()); + args.putString("category", inventory.getCategory()); + args.putInt("quantity", inventory.getQuantity()); + args.putDouble("unitPrice", inventory.getUnitPrice()); + args.putString("supplier", inventory.getSupplier()); + } + + detailFragment.setArguments(args); + detailFragment.setInventoryFragment(this); + + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.loadFragment(detailFragment); + } + + public void onInventorySaved(int position, Inventory inventory) { + if (position == -1) { + inventoryList.add(inventory); + } else { + inventoryList.set(position, inventory); + } + filterInventory(etSearch.getText().toString()); + } + + public void onInventoryDeleted(int position) { + inventoryList.remove(position); + filterInventory(etSearch.getText().toString()); + } + + @Override + public void onInventoryClick(int position) { + openInventoryDetails(position); + } + + private void loadInventoryData() { + inventoryList.clear(); + inventoryList.add(new Inventory(1, "Dog Food - Large", "Food", 50, 25.99, "PetSupplies Co.")); + inventoryList.add(new Inventory(2, "Cat Litter", "Hygiene", 30, 12.99, "CleanPaws Ltd.")); + inventoryList.add(new Inventory(3, "Dog Leash", "Accessories", 4, 15.99, "PetGear Inc.")); + inventoryList.add(new Inventory(4, "Bird Cage - Medium", "Housing", 8, 79.99, "BirdWorld")); + inventoryList.add(new Inventory(5, "Flea Treatment", "Medicine", 2, 34.99, "VetCare Supply")); + filteredList.clear(); + filteredList.addAll(inventoryList); + } + + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewInventory); + adapter = new InventoryAdapter(filteredList, this); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java new file mode 100644 index 00000000..6448b5f3 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/PetFragment.java @@ -0,0 +1,202 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.Toast; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.PetAdapter; +import com.example.petstoremobile.api.PetApi; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.PetDTO; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; +import com.example.petstoremobile.fragments.listfragments.listprofilefragments.PetProfileFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class PetFragment extends Fragment implements PetAdapter.OnPetClickListener { + private List petList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private ImageButton hamburger; + private PetAdapter adapter; + private PetApi api; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + + //load pet view + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_pet, container, false); + + //get retrofit + api = RetrofitClient.getPetApi(requireContext()); + + hamburger = view.findViewById(R.id.btnHamburger); + + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + loadPetData(); + + + //Add button to opens the add dialog + FloatingActionButton fabAddPet = view.findViewById(R.id.fabAddPet); + fabAddPet.setOnClickListener(v -> openPetDetails(-1)); + + //Make the hamburger button open the drawer from listFragment + hamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + //if list fragment is found then use its helper function to open the drawer + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + + return view; + } + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchPet); + 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()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + private void filterPets(String query) { + 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); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshPet); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadPetData(); + }); + } + + //Open pet profile + private void openPetProfile(int position) { + PetProfileFragment profileFragment = new PetProfileFragment(); + + //Make a bundle to pass data to the profile fragment + Bundle args = new Bundle(); + PetDTO pet = filteredList.get(position); + args.putInt("petId", pet.getPetId().intValue()); + args.putString("petName", pet.getPetName()); + args.putString("petSpecies", pet.getPetSpecies()); + args.putString("petBreed", pet.getPetBreed()); + args.putInt("petAge", pet.getPetAge()); + args.putString("petStatus", pet.getPetStatus()); + + try { + args.putDouble("petPrice", Double.parseDouble(pet.getPetPrice())); + } catch (Exception e) { + args.putDouble("petPrice", 0.0); + } + + //send the bundle to the profile fragment to display + profileFragment.setArguments(args); + + //get ListFragment to load the the pet profile view + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.loadFragment(profileFragment); + } + } + + //Open the pet detail view for adding + private void openPetDetails(int position) { + PetDetailFragment detailFragment = new PetDetailFragment(); + + //get ListFragment to load the detail view + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.loadFragment(detailFragment); + } + } + + // Called by PetAdapter when a row is clicked to open the details view + @Override + public void onPetClick(int position) { + openPetProfile(position); + } + + // Helper function to get a list of all pets from the backend + private void loadPetData() { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(true); + } + api.getAllPets(0, 100).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + if (response.isSuccessful() && response.body() != null) { + petList.clear(); + petList.addAll(response.body().getContent()); + filterPets(etSearch.getText().toString()); + + } else { + Log.e("onResponse: ", response.message()); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + Toast.makeText(getContext(), + "Failed to load pets", Toast.LENGTH_SHORT).show(); + Log.e("onFailure: ", t.getMessage()); + } + }); + } + + //set up the recyclerview and adapter + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewPets); + adapter = new PetAdapter(filteredList, this); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java new file mode 100644 index 00000000..65e14d4b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ProductFragment.java @@ -0,0 +1,162 @@ +package com.example.petstoremobile.fragments.listfragments; + +// Added search/filter bar to filter products by name or category. +// Added pull-to-refresh using SwipeRefreshLayout. + +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; +import android.text.Editable; +import android.text.TextWatcher; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.ProductAdapter; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.ProductDetailFragment; +import com.example.petstoremobile.models.Product; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import java.util.ArrayList; +import java.util.List; + +public class ProductFragment extends Fragment implements ProductAdapter.OnProductClickListener { + + private List productList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private ProductAdapter adapter; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + private ImageButton hamburger; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_product, container, false); + + hamburger = view.findViewById(R.id.btnHamburger); + + loadProductData(); // TODO: Replace with actual API call when backend is ready + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + + FloatingActionButton fabAddProduct = view.findViewById(R.id.fabAddProduct); + fabAddProduct.setOnClickListener(v -> openProductDetails(-1)); + + //Make the hamburger button open the drawer from listFragment + hamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + //if list fragment is found then use its helper function to open the drawer + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + + return view; + } + + // Filters products by name, description, or category + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchProduct); + 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) { + filterProducts(s.toString()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + private void filterProducts(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(productList); + } else { + String lower = query.toLowerCase(); + for (Product p : productList) { + if (p.getProductName().toLowerCase().contains(lower) + || p.getCategory().toLowerCase().contains(lower) + || p.getProductDesc().toLowerCase().contains(lower)) { + filteredList.add(p); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshProduct); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadProductData(); // TODO: Replace with actual API call + filterProducts(etSearch.getText().toString()); + swipeRefreshLayout.setRefreshing(false); + }); + } + + private void openProductDetails(int position) { + ProductDetailFragment detailFragment = new ProductDetailFragment(); + Bundle args = new Bundle(); + args.putInt("position", position); + + if (position != -1) { + Product product = filteredList.get(position); + int realPosition = productList.indexOf(product); + args.putInt("position", realPosition); + args.putInt("productId", product.getProductId()); + args.putString("productName", product.getProductName()); + args.putString("productDesc", product.getProductDesc()); + args.putString("category", product.getCategory()); + args.putDouble("productPrice", product.getProductPrice()); + args.putInt("stockQuantity", product.getStockQuantity()); + } + + detailFragment.setArguments(args); + detailFragment.setProductFragment(this); + + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.loadFragment(detailFragment); + } + + public void onProductSaved(int position, Product product) { + if (position == -1) { + productList.add(product); + } else { + productList.set(position, product); + } + filterProducts(etSearch.getText().toString()); + } + + public void onProductDeleted(int position) { + productList.remove(position); + filterProducts(etSearch.getText().toString()); + } + + @Override + public void onProductClick(int position) { + openProductDetails(position); + } + + private void loadProductData() { + productList.clear(); + productList.add(new Product(1, "Premium Dog Food", "High protein dry food for adult dogs", "Food", 45.99, 25)); + productList.add(new Product(2, "Cat Toy Bundle", "Set of 5 interactive toys", "Toys", 19.99, 40)); + productList.add(new Product(3, "Pet Shampoo", "Gentle formula for all breeds", "Grooming", 12.99, 60)); + productList.add(new Product(4, "Dog Bed - Large", "Memory foam orthopedic bed", "Bedding", 89.99, 10)); + productList.add(new Product(5, "Aquarium Starter Kit", "20-gallon tank with filter and light", "Aquatic", 129.99, 5)); + filteredList.clear(); + filteredList.addAll(productList); + } + + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewProducts); + adapter = new ProductAdapter(filteredList, this); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java new file mode 100644 index 00000000..1f204114 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ServiceFragment.java @@ -0,0 +1,189 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.Toast; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.ServiceAdapter; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.api.ServiceApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.ServiceDTO; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.ServiceDetailFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class ServiceFragment extends Fragment implements ServiceAdapter.OnServiceClickListener { + + private List serviceList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private ServiceAdapter adapter; + private ImageButton hamburger; + private ServiceApi api; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + + //load service view + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_service, container, false); + + api = RetrofitClient.getServiceApi(requireContext()); + hamburger = view.findViewById(R.id.btnHamburger); + + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + loadServiceData(); + + //Add button to opens the add dialog + FloatingActionButton fabAddService = view.findViewById(R.id.fabAddService); + fabAddService.setOnClickListener(v -> openServiceDetails(-1)); + + //Make the hamburger button open the drawer from listFragment + hamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + //if list fragment is found then use its helper function to open the drawer + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + + return view; + } + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchService); + 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) { + filterServices(s.toString()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + private void filterServices(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(serviceList); + } else { + String lower = query.toLowerCase(); + for (ServiceDTO s : serviceList) { + if (s.getServiceName().toLowerCase().contains(lower) + || s.getServiceDesc().toLowerCase().contains(lower)) { + filteredList.add(s); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshService); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadServiceData(); + }); + } + + //Open the service detail view depending on the mode + private void openServiceDetails(int position) { + ServiceDetailFragment detailFragment = new ServiceDetailFragment(); + + //Make a bundle to pass data to the detail fragment + Bundle args = new Bundle(); + args.putInt("position", position); + + //if editing a service, add the service data to the bundle + if (position != -1) { + ServiceDTO service = filteredList.get(position); + args.putInt("serviceId", service.getServiceId().intValue()); + args.putString("serviceName", service.getServiceName()); + args.putString("serviceDesc", service.getServiceDesc()); + args.putInt("serviceDuration", service.getServiceDuration()); + args.putDouble("servicePrice", service.getServicePrice()); + } + + //send the bundle to the detail fragment to display + detailFragment.setArguments(args); + //set the service fragment to the parent so we refer back to service view when save or delete is done + detailFragment.setServiceFragment(this); + + //get ListFragment to load the the detail view + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.loadFragment(detailFragment); + } + } + + // Called by ServiceAdapter when a row is clicked to open the details view + @Override + public void onServiceClick(int position) { + openServiceDetails(position); + } + + // Helper function to get a list of all services from the backend + private void loadServiceData() { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(true); + } + api.getAllServices(0, 100).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + if (response.isSuccessful() && response.body() != null) { + serviceList.clear(); + serviceList.addAll(response.body().getContent()); + filterServices(etSearch.getText().toString()); + + } else { + Log.e("onResponse: ", response.message()); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + if (getContext() != null) { + Toast.makeText(getContext(), + "Failed to load services", Toast.LENGTH_SHORT).show(); + } + Log.e("onFailure: ", t.getMessage()); + } + }); + } + + //set up the recyclerview and adapter + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewServices); + adapter = new ServiceAdapter(filteredList, this); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java new file mode 100644 index 00000000..0d75da78 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SupplierFragment.java @@ -0,0 +1,192 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.os.Bundle; + +import androidx.fragment.app.Fragment; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; +import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; + +import android.text.Editable; +import android.text.TextWatcher; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; +import android.widget.ImageButton; +import android.widget.Toast; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.adapters.SupplierAdapter; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.api.SupplierApi; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SupplierDTO; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.SupplierDetailFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.ArrayList; +import java.util.List; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupplierClickListener { + + private List supplierList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); + private SupplierAdapter adapter; + private ImageButton hamburger; + private SupplierApi api; + private SwipeRefreshLayout swipeRefreshLayout; + private EditText etSearch; + + //load supplier view + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_supplier, container, false); + + api = RetrofitClient.getSupplierApi(requireContext()); + hamburger = view.findViewById(R.id.btnHamburger); + + setupRecyclerView(view); + setupSearch(view); + setupSwipeRefresh(view); + loadSupplierData(); + + //Add button to opens the add dialog + FloatingActionButton fabAddSupplier = view.findViewById(R.id.fabAddSupplier); + fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1)); + + //Make the hamburger button open the drawer from listFragment + hamburger.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + //if list fragment is found then use its helper function to open the drawer + if (listFragment != null) { + listFragment.openDrawer(); + } + }); + + return view; + } + + private void setupSearch(View view) { + etSearch = view.findViewById(R.id.etSearchSupplier); + 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) { + filterSuppliers(s.toString()); + } + @Override public void afterTextChanged(Editable s) {} + }); + } + + private void filterSuppliers(String query) { + filteredList.clear(); + if (query.isEmpty()) { + filteredList.addAll(supplierList); + } else { + String lower = query.toLowerCase(); + for (SupplierDTO s : supplierList) { + if (s.getSupCompany().toLowerCase().contains(lower) + || s.getSupContactFirstName().toLowerCase().contains(lower) + || s.getSupContactLastName().toLowerCase().contains(lower)) { + filteredList.add(s); + } + } + } + adapter.notifyDataSetChanged(); + } + + private void setupSwipeRefresh(View view) { + swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSupplier); + swipeRefreshLayout.setOnRefreshListener(() -> { + loadSupplierData(); + }); + } + + //Open the supplier detail view depending on the mode + private void openSupplierDetails(int position) { + SupplierDetailFragment detailFragment = new SupplierDetailFragment(); + + //Make a bundle to pass data to the detail fragment + Bundle args = new Bundle(); + args.putInt("position", position); + + //if editing a supplier, add the supplier data to the bundle + if (position != -1) { + SupplierDTO supplier = filteredList.get(position); + args.putInt("supId", supplier.getSupId().intValue()); + args.putString("supCompany", supplier.getSupCompany()); + args.putString("supContactFirstName", supplier.getSupContactFirstName()); + args.putString("supContactLastName", supplier.getSupContactLastName()); + args.putString("supEmail", supplier.getSupEmail()); + args.putString("supPhone", supplier.getSupPhone()); + } + + //send the bundle to the detail fragment to display + detailFragment.setArguments(args); + //set the supplier fragment to the parent so we refer back to supplier view when save or delete is done + detailFragment.setSupplierFragment(this); + + //get ListFragment to load the the detail view + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.loadFragment(detailFragment); + } + } + + + // Called by SupplierAdapter when a row is clicked to open the details view + @Override + public void onSupplierClick(int position) { + openSupplierDetails(position); + } + + // Helper function to get a list of all suppliers from the backend + private void loadSupplierData() { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(true); + } + api.getAllSuppliers(0, 100).enqueue(new Callback>() { + @Override + public void onResponse(Call> call, Response> response) { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + if (response.isSuccessful() && response.body() != null) { + supplierList.clear(); + supplierList.addAll(response.body().getContent()); + filterSuppliers(etSearch.getText().toString()); + + } else { + Log.e("onResponse: ", response.message()); + } + } + + @Override + public void onFailure(Call> call, Throwable t) { + if (swipeRefreshLayout != null) { + swipeRefreshLayout.setRefreshing(false); + } + if (getContext() != null) { + Toast.makeText(getContext(), + "Failed to load suppliers", Toast.LENGTH_SHORT).show(); + } + Log.e("onFailure: ", t.getMessage()); + } + }); + } + + //set up the recyclerview and adapter + private void setupRecyclerView(View view) { + RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSuppliers); + adapter = new SupplierAdapter(filteredList, this); + recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); + recyclerView.setAdapter(adapter); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java new file mode 100644 index 00000000..32c06996 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AdoptionDetailFragment.java @@ -0,0 +1,172 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + + +// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. + +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import com.example.petstoremobile.R; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.AdoptionFragment; +import com.example.petstoremobile.models.Adoption; +import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.InputValidator; + +public class AdoptionDetailFragment extends Fragment { + + private TextView tvMode, tvAdoptionId; + private EditText etAdopterName, etAdopterEmail, etAdopterPhone, etPetName, etAdoptionDate; + private Spinner spinnerAdoptionStatus; + private Button btnSaveAdoption, btnDeleteAdoption, btnBack; + private int adoptionId; + private int position; + private boolean isEditing = false; + private AdoptionFragment adoptionFragment; + + // Set the adoption fragment as parent so we refer back when save or delete is done + public void setAdoptionFragment(AdoptionFragment fragment) { + this.adoptionFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_adoption_detail, container, false); + + initViews(view); + setupSpinner(); + handleArguments(); + + btnBack.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + }); + btnSaveAdoption.setOnClickListener(v -> saveAdoption()); + btnDeleteAdoption.setOnClickListener(v -> deleteAdoption()); + + return view; + } + + // Validates all fields using InputValidator, then saves the adoption record + private void saveAdoption() { + if (!InputValidator.isNotEmpty(etAdopterName, "Adopter Name")) return; + if (!InputValidator.isValidEmail(etAdopterEmail)) return; + if (!InputValidator.isValidPhone(etAdopterPhone)) return; + if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return; + if (!InputValidator.isValidDate(etAdoptionDate)) return; + + String adopterName = etAdopterName.getText().toString().trim(); + String adopterEmail = etAdopterEmail.getText().toString().trim(); + String adopterPhone = etAdopterPhone.getText().toString().trim(); + String petName = etPetName.getText().toString().trim(); + String adoptionDate = etAdoptionDate.getText().toString().trim(); + String status = spinnerAdoptionStatus.getSelectedItem().toString(); + + try { + if (isEditing) { + // TODO: Replace with actual API PUT call when backend is ready + Adoption updated = new Adoption(adoptionId, adopterName, adopterEmail, adopterPhone, petName, adoptionDate, status); + if (adoptionFragment != null) adoptionFragment.onAdoptionSaved(position, updated); + ActivityLogger.logChange(requireContext(), "Adoption", "UPDATED", adoptionId); + Toast.makeText(getContext(), "Adoption record updated.", Toast.LENGTH_SHORT).show(); + } else { + // TODO: Replace with actual API POST call when backend is ready + Adoption newAdoption = new Adoption(0, adopterName, adopterEmail, adopterPhone, petName, adoptionDate, status); + if (adoptionFragment != null) adoptionFragment.onAdoptionSaved(-1, newAdoption); + ActivityLogger.log(requireContext(), "Added new Adoption record for: " + adopterName + " adopting " + petName); + Toast.makeText(getContext(), "Adoption record added.", Toast.LENGTH_SHORT).show(); + } + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "AdoptionDetailFragment.saveAdoption", e); + Toast.makeText(getContext(), "Error saving adoption record.", Toast.LENGTH_SHORT).show(); + } + } + + // Deletes the adoption record and logs the action + private void deleteAdoption() { + try { + // TODO: Replace with actual API DELETE call when backend is ready + if (adoptionFragment != null) adoptionFragment.onAdoptionDeleted(position); + ActivityLogger.logChange(requireContext(), "Adoption", "DELETED", adoptionId); + Toast.makeText(getContext(), "Adoption record deleted.", Toast.LENGTH_SHORT).show(); + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "AdoptionDetailFragment.deleteAdoption", e); + Toast.makeText(getContext(), "Error deleting adoption record.", Toast.LENGTH_SHORT).show(); + } + } + + private void handleArguments() { + if (getArguments() != null && getArguments().containsKey("adoptionId")) { + isEditing = true; + adoptionId = getArguments().getInt("adoptionId"); + position = getArguments().getInt("position"); + tvMode.setText("Edit Adoption"); + tvAdoptionId.setText("ID: " + adoptionId); + etAdopterName.setText(getArguments().getString("adopterName")); + etAdopterEmail.setText(getArguments().getString("adopterEmail")); + etAdopterPhone.setText(getArguments().getString("adopterPhone")); + etPetName.setText(getArguments().getString("petName")); + etAdoptionDate.setText(getArguments().getString("adoptionDate")); + String status = getArguments().getString("status"); + if ("Approved".equals(status)) spinnerAdoptionStatus.setSelection(0); + else if ("Pending".equals(status)) spinnerAdoptionStatus.setSelection(1); + else spinnerAdoptionStatus.setSelection(2); + btnDeleteAdoption.setVisibility(View.VISIBLE); + } else { + isEditing = false; + tvMode.setText("Add Adoption"); + tvAdoptionId.setVisibility(View.GONE); + btnDeleteAdoption.setVisibility(View.GONE); + btnSaveAdoption.setText("Add"); + } + } + + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvAdoptionMode); + tvAdoptionId = view.findViewById(R.id.tvAdoptionId); + etAdopterName = view.findViewById(R.id.etAdopterName); + etAdopterEmail = view.findViewById(R.id.etAdopterEmail); + etAdopterPhone = view.findViewById(R.id.etAdopterPhone); + etPetName = view.findViewById(R.id.etAdoptionPetName); + etAdoptionDate = view.findViewById(R.id.etAdoptionDate); + spinnerAdoptionStatus = view.findViewById(R.id.spinnerAdoptionStatus); + btnSaveAdoption = view.findViewById(R.id.btnSaveAdoption); + btnDeleteAdoption = view.findViewById(R.id.btnDeleteAdoption); + btnBack = view.findViewById(R.id.btnAdoptionBack); + } + + private void setupSpinner() { + ArrayAdapter adapter = new ArrayAdapter(requireContext(), + android.R.layout.simple_spinner_item, + new String[]{"Approved", "Pending", "Rejected"}) { + + //Override the getView method for the spinner to make the text color darker for more readability + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark)); + return view; + } + }; + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerAdoptionStatus.setAdapter(adapter); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java new file mode 100644 index 00000000..bfd2d6b5 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/AppointmentDetailFragment.java @@ -0,0 +1,178 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + + +// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. + +import android.content.res.Configuration; +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; +import com.example.petstoremobile.R; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.AppointmentFragment; +import com.example.petstoremobile.models.Appointment; +import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.InputValidator; + +public class AppointmentDetailFragment extends Fragment { + + private TextView tvMode, tvAppointmentId; + private EditText etCustomerName, etPetName, etServiceType, etAppointmentDate, etAppointmentTime; + private Spinner spinnerStatus; + private Button btnSaveAppointment, btnDeleteAppointment, btnBack; + private int appointmentId; + private int position; + private boolean isEditing = false; + private AppointmentFragment appointmentFragment; + + // Set the appointment fragment as parent so we refer back when save or delete is done + public void setAppointmentFragment(AppointmentFragment fragment) { + this.appointmentFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_appointment_detail, container, false); + + initViews(view); + setupSpinner(); + handleArguments(); + + btnBack.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.getChildFragmentManager().popBackStack(); + } + }); + btnSaveAppointment.setOnClickListener(v -> saveAppointment()); + btnDeleteAppointment.setOnClickListener(v -> deleteAppointment()); + + return view; + } + + // Validates all fields using InputValidator, then saves the appointment + private void saveAppointment() { + // Validate all inputs using InputValidator utility + if (!InputValidator.isNotEmpty(etCustomerName, "Customer Name")) return; + if (!InputValidator.isNotEmpty(etPetName, "Pet Name")) return; + if (!InputValidator.isNotEmpty(etServiceType, "Service Type")) return; + if (!InputValidator.isValidDate(etAppointmentDate)) return; + if (!InputValidator.isValidTime(etAppointmentTime)) return; + + String customerName = etCustomerName.getText().toString().trim(); + String petName = etPetName.getText().toString().trim(); + String serviceType = etServiceType.getText().toString().trim(); + String date = etAppointmentDate.getText().toString().trim(); + String time = etAppointmentTime.getText().toString().trim(); + String status = spinnerStatus.getSelectedItem().toString(); + + try { + if (isEditing) { + // TODO: Replace with actual API PUT call when backend is ready + Appointment updated = new Appointment(appointmentId, customerName, petName, serviceType, date, time, status); + if (appointmentFragment != null) appointmentFragment.onAppointmentSaved(position, updated); + ActivityLogger.logChange(requireContext(), "Appointment", "UPDATED", appointmentId); + Toast.makeText(getContext(), "Appointment updated.", Toast.LENGTH_SHORT).show(); + } else { + // TODO: Replace with actual API POST call when backend is ready + Appointment newAppt = new Appointment(0, customerName, petName, serviceType, date, time, status); + if (appointmentFragment != null) appointmentFragment.onAppointmentSaved(-1, newAppt); + ActivityLogger.log(requireContext(), "Added new Appointment for customer: " + customerName); + Toast.makeText(getContext(), "Appointment added.", Toast.LENGTH_SHORT).show(); + } + // Go back to list + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "AppointmentDetailFragment.saveAppointment", e); + Toast.makeText(getContext(), "Error saving appointment.", Toast.LENGTH_SHORT).show(); + } + } + + // Deletes the appointment and logs the action + private void deleteAppointment() { + try { + // TODO: Replace with actual API DELETE call when backend is ready + if (appointmentFragment != null) appointmentFragment.onAppointmentDeleted(position); + ActivityLogger.logChange(requireContext(), "Appointment", "DELETED", appointmentId); + Toast.makeText(getContext(), "Appointment deleted.", Toast.LENGTH_SHORT).show(); + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "AppointmentDetailFragment.deleteAppointment", e); + Toast.makeText(getContext(), "Error deleting appointment.", Toast.LENGTH_SHORT).show(); + } + } + + // Determines if the fragment is in add or edit mode and populates fields accordingly + private void handleArguments() { + if (getArguments() != null && getArguments().containsKey("appointmentId")) { + isEditing = true; + appointmentId = getArguments().getInt("appointmentId"); + position = getArguments().getInt("position"); + tvMode.setText("Edit Appointment"); + tvAppointmentId.setText("ID: " + appointmentId); + etCustomerName.setText(getArguments().getString("customerName")); + etPetName.setText(getArguments().getString("petName")); + etServiceType.setText(getArguments().getString("serviceType")); + etAppointmentDate.setText(getArguments().getString("appointmentDate")); + etAppointmentTime.setText(getArguments().getString("appointmentTime")); + String status = getArguments().getString("status"); + if ("Confirmed".equals(status)) spinnerStatus.setSelection(0); + else if ("Pending".equals(status)) spinnerStatus.setSelection(1); + else spinnerStatus.setSelection(2); + btnDeleteAppointment.setVisibility(View.VISIBLE); + } else { + isEditing = false; + tvMode.setText("Add Appointment"); + tvAppointmentId.setVisibility(View.GONE); + btnDeleteAppointment.setVisibility(View.GONE); + btnSaveAppointment.setText("Add"); + } + } + + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvApptMode); + tvAppointmentId = view.findViewById(R.id.tvAppointmentId); + etCustomerName = view.findViewById(R.id.etCustomerName); + etPetName = view.findViewById(R.id.etApptPetName); + etServiceType = view.findViewById(R.id.etServiceType); + etAppointmentDate = view.findViewById(R.id.etAppointmentDate); + etAppointmentTime = view.findViewById(R.id.etAppointmentTime); + spinnerStatus = view.findViewById(R.id.spinnerAppointmentStatus); + btnSaveAppointment = view.findViewById(R.id.btnSaveAppointment); + btnDeleteAppointment = view.findViewById(R.id.btnDeleteAppointment); + btnBack = view.findViewById(R.id.btnApptBack); + } + + private void setupSpinner() { + ArrayAdapter adapter = new ArrayAdapter(requireContext(), + android.R.layout.simple_spinner_item, + new String[]{"Confirmed", "Pending", "Cancelled"}) { + + //Override the getView method for the spinner to make the text color darker for more readability + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark)); + return view; + } + }; + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerStatus.setAdapter(adapter); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java new file mode 100644 index 00000000..175feb91 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/InventoryDetailFragment.java @@ -0,0 +1,139 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. + +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import com.example.petstoremobile.R; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.InventoryFragment; +import com.example.petstoremobile.models.Inventory; +import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.InputValidator; + +public class InventoryDetailFragment extends Fragment { + + private TextView tvMode, tvInventoryId; + private EditText etItemName, etCategory, etQuantity, etUnitPrice, etSupplier; + private Button btnSaveInventory, btnDeleteInventory, btnBack; + private int inventoryId; + private int position; + private boolean isEditing = false; + private InventoryFragment inventoryFragment; + + // Set the inventory fragment as parent so we refer back when save or delete is done + public void setInventoryFragment(InventoryFragment fragment) { + this.inventoryFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_inventory_detail, container, false); + + initViews(view); + handleArguments(); + + btnBack.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + }); + btnSaveInventory.setOnClickListener(v -> saveInventory()); + btnDeleteInventory.setOnClickListener(v -> deleteInventory()); + + return view; + } + + // Validates all fields using InputValidator, then saves the inventory item + private void saveInventory() { + if (!InputValidator.isNotEmpty(etItemName, "Item Name")) return; + if (!InputValidator.isNotEmpty(etCategory, "Category")) return; + if (!InputValidator.isPositiveInteger(etQuantity, "Quantity")) return; + if (!InputValidator.isPositiveDecimal(etUnitPrice, "Unit Price")) return; + if (!InputValidator.isNotEmpty(etSupplier, "Supplier")) return; + + String itemName = etItemName.getText().toString().trim(); + String category = etCategory.getText().toString().trim(); + int quantity = Integer.parseInt(etQuantity.getText().toString().trim()); + double unitPrice = Double.parseDouble(etUnitPrice.getText().toString().trim()); + String supplier = etSupplier.getText().toString().trim(); + + try { + if (isEditing) { + // TODO: Replace with actual API PUT call when backend is ready + Inventory updated = new Inventory(inventoryId, itemName, category, quantity, unitPrice, supplier); + if (inventoryFragment != null) inventoryFragment.onInventorySaved(position, updated); + ActivityLogger.logChange(requireContext(), "Inventory", "UPDATED", inventoryId); + Toast.makeText(getContext(), "Inventory item updated.", Toast.LENGTH_SHORT).show(); + } else { + // TODO: Replace with actual API POST call when backend is ready + Inventory newItem = new Inventory(0, itemName, category, quantity, unitPrice, supplier); + if (inventoryFragment != null) inventoryFragment.onInventorySaved(-1, newItem); + ActivityLogger.log(requireContext(), "Added new Inventory item: " + itemName); + Toast.makeText(getContext(), "Inventory item added.", Toast.LENGTH_SHORT).show(); + } + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "InventoryDetailFragment.saveInventory", e); + Toast.makeText(getContext(), "Error saving inventory item.", Toast.LENGTH_SHORT).show(); + } + } + + // Deletes the inventory item and logs the action + private void deleteInventory() { + try { + // TODO: Replace with actual API DELETE call when backend is ready + if (inventoryFragment != null) inventoryFragment.onInventoryDeleted(position); + ActivityLogger.logChange(requireContext(), "Inventory", "DELETED", inventoryId); + Toast.makeText(getContext(), "Inventory item deleted.", Toast.LENGTH_SHORT).show(); + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "InventoryDetailFragment.deleteInventory", e); + Toast.makeText(getContext(), "Error deleting inventory item.", Toast.LENGTH_SHORT).show(); + } + } + + private void handleArguments() { + if (getArguments() != null && getArguments().containsKey("inventoryId")) { + isEditing = true; + inventoryId = getArguments().getInt("inventoryId"); + position = getArguments().getInt("position"); + tvMode.setText("Edit Inventory Item"); + tvInventoryId.setText("ID: " + inventoryId); + etItemName.setText(getArguments().getString("itemName")); + etCategory.setText(getArguments().getString("category")); + etQuantity.setText(String.valueOf(getArguments().getInt("quantity"))); + etUnitPrice.setText(String.valueOf(getArguments().getDouble("unitPrice"))); + etSupplier.setText(getArguments().getString("supplier")); + btnDeleteInventory.setVisibility(View.VISIBLE); + } else { + isEditing = false; + tvMode.setText("Add Inventory Item"); + tvInventoryId.setVisibility(View.GONE); + btnDeleteInventory.setVisibility(View.GONE); + btnSaveInventory.setText("Add"); + } + } + + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvInventoryMode); + tvInventoryId = view.findViewById(R.id.tvInventoryId); + etItemName = view.findViewById(R.id.etItemName); + etCategory = view.findViewById(R.id.etInventoryCategory); + etQuantity = view.findViewById(R.id.etQuantity); + etUnitPrice = view.findViewById(R.id.etUnitPrice); + etSupplier = view.findViewById(R.id.etInventorySupplier); + btnSaveInventory = view.findViewById(R.id.btnSaveInventory); + btnDeleteInventory = view.findViewById(R.id.btnDeleteInventory); + btnBack = view.findViewById(R.id.btnInventoryBack); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java new file mode 100644 index 00000000..6906bc73 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/PetDetailFragment.java @@ -0,0 +1,245 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.os.Bundle; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.fragment.app.Fragment; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.PetApi; +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 retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class PetDetailFragment extends Fragment { + + private TextView tvMode, tvPetId; + private EditText etPetName, etPetSpecies, etPetBreed, etPetAge, etPetPrice; + private Spinner spinnerPetStatus; + private Button btnSavePet, btnDeletePet, btnBack; + private int petId; + private boolean isEditing = false; + private PetFragment petFragment; + + //set the pet fragment to the parent so we refer back to pet view when save or delete is done + public void setPetFragment(PetFragment fragment) { + this.petFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_pet_detail, container, false); + + //set up spinner and get controls from layout and display the view depending on the mode + initViews(view); + setupSpinner(); + handleArguments(); + + //set button click listeners + btnBack.setOnClickListener(v -> navigateBack()); + btnSavePet.setOnClickListener(v -> savePet()); + btnDeletePet.setOnClickListener(v -> deletePet()); + + return view; + } + + //Method to Update or Add a pet + private void savePet() { + //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(); + 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.setPetPrice(priceStr); + petDTO.setPetStatus(status); + + PetApi petApi = RetrofitClient.getPetApi(requireContext()); + + //check if the pet is being edited or added + if (isEditing) { + // Update existing pet + petDTO.setPetId((long) petId); + petApi.updatePet((long) petId, petDTO).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to update pet: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("PetDetailFragment", "Error updating pet", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } else { + // Add new pet + petApi.createPet(petDTO).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to add pet: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("PetDetailFragment", "Error adding pet", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + } + + //Method to Delete a pet + private void deletePet() { + //Alert the user to confirm the delete + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Pet") + .setMessage("Are you sure you want to delete " + etPetName.getText().toString() + "?") + .setPositiveButton("Delete", (dialog, which) -> { + PetApi petApi = RetrofitClient.getPetApi(requireContext()); + //if they say yes then delete the pet + petApi.deletePet((long) petId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Pet deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to delete pet: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("PetDetailFragment", "Error deleting pet", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + //Helper method to navigate back to the list + private void navigateBack() { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + // If editing pop back twice to get back to PetDetail Fragment instead of PetProfileFragment + if (isEditing) { + listFragment.getChildFragmentManager().popBackStack(); + } + listFragment.getChildFragmentManager().popBackStack(); + } + } + + //helper function to check if pet is being edited or added and show the view accordingly + private void handleArguments() { + // Pet is being edited if the bundle contains a petId + if (getArguments() != null && getArguments().containsKey("petId")) { + // Get pet data from arguments and populate fields + isEditing = true; + petId = getArguments().getInt("petId"); + tvMode.setText("Edit Pet"); + tvPetId.setText("ID: " + petId); + etPetName.setText(getArguments().getString("petName")); + etPetSpecies.setText(getArguments().getString("petSpecies")); + etPetBreed.setText(getArguments().getString("petBreed")); + etPetAge.setText(String.valueOf(getArguments().getInt("petAge"))); + etPetPrice.setText(String.valueOf(getArguments().getDouble("petPrice"))); + String status = getArguments().getString("petStatus"); + if ("Available".equals(status)) { + spinnerPetStatus.setSelection(0); + } else { + spinnerPetStatus.setSelection(1); + } + btnDeletePet.setVisibility(View.VISIBLE); + } else { + // Pet is being added + // Set default values for add a new pet + isEditing = false; + tvMode.setText("Add Pet"); + tvPetId.setVisibility(View.GONE); + btnDeletePet.setVisibility(View.GONE); + btnSavePet.setText("Add"); + } + } + + //helper function to get controls from layout + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvMode); + tvPetId = view.findViewById(R.id.tvPetId); + etPetName = view.findViewById(R.id.etPetName); + etPetSpecies = view.findViewById(R.id.etPetSpecies); + etPetBreed = view.findViewById(R.id.etPetBreed); + etPetAge = view.findViewById(R.id.etPetAge); + etPetPrice = view.findViewById(R.id.etPetPrice); + spinnerPetStatus = view.findViewById(R.id.spinnerPetStatus); + btnSavePet = view.findViewById(R.id.btnSavePet); + btnDeletePet = view.findViewById(R.id.btnDeletePet); + btnBack = view.findViewById(R.id.btnBack); + } + + //helper function to set up the spinner menu for pet status + private void setupSpinner() { + ArrayAdapter adapter = new ArrayAdapter(requireContext(), + android.R.layout.simple_spinner_item, + new String[]{"Available", "Adopted"}) { + + //Override the getView method for the spinner to make the text color darker for more readability + @NonNull + @Override + public View getView(int position, @Nullable View convertView, @NonNull ViewGroup parent) { + View view = super.getView(position, convertView, parent); + ((TextView) view).setTextColor(ContextCompat.getColor(requireContext(), R.color.text_dark)); + return view; + } + }; + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinnerPetStatus.setAdapter(adapter); + } + +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java new file mode 100644 index 00000000..4179c4f5 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ProductDetailFragment.java @@ -0,0 +1,139 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +// Uses InputValidator for detailed field validation and ActivityLogger to log all changes. + +import android.os.Bundle; +import androidx.fragment.app.Fragment; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import com.example.petstoremobile.R; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.ProductFragment; +import com.example.petstoremobile.models.Product; +import com.example.petstoremobile.utils.ActivityLogger; +import com.example.petstoremobile.utils.InputValidator; + +public class ProductDetailFragment extends Fragment { + + private TextView tvMode, tvProductId; + private EditText etProductName, etProductDesc, etCategory, etProductPrice, etStockQuantity; + private Button btnSaveProduct, btnDeleteProduct, btnBack; + private int productId; + private int position; + private boolean isEditing = false; + private ProductFragment productFragment; + + // Set the product fragment as parent so we refer back when save or delete is done + public void setProductFragment(ProductFragment fragment) { + this.productFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_product_detail, container, false); + + initViews(view); + handleArguments(); + + btnBack.setOnClickListener(v -> { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + }); + btnSaveProduct.setOnClickListener(v -> saveProduct()); + btnDeleteProduct.setOnClickListener(v -> deleteProduct()); + + return view; + } + + // Validates all fields using InputValidator, then saves the product + private void saveProduct() { + if (!InputValidator.isNotEmpty(etProductName, "Product Name")) return; + if (!InputValidator.isNotEmpty(etProductDesc, "Description")) return; + if (!InputValidator.isNotEmpty(etCategory, "Category")) return; + if (!InputValidator.isPositiveDecimal(etProductPrice, "Price")) return; + if (!InputValidator.isPositiveInteger(etStockQuantity, "Stock Quantity")) return; + + String productName = etProductName.getText().toString().trim(); + String productDesc = etProductDesc.getText().toString().trim(); + String category = etCategory.getText().toString().trim(); + double productPrice = Double.parseDouble(etProductPrice.getText().toString().trim()); + int stockQuantity = Integer.parseInt(etStockQuantity.getText().toString().trim()); + + try { + if (isEditing) { + // TODO: Replace with actual API PUT call when backend is ready + Product updated = new Product(productId, productName, productDesc, category, productPrice, stockQuantity); + if (productFragment != null) productFragment.onProductSaved(position, updated); + ActivityLogger.logChange(requireContext(), "Product", "UPDATED", productId); + Toast.makeText(getContext(), "Product updated.", Toast.LENGTH_SHORT).show(); + } else { + // TODO: Replace with actual API POST call when backend is ready + Product newProduct = new Product(0, productName, productDesc, category, productPrice, stockQuantity); + if (productFragment != null) productFragment.onProductSaved(-1, newProduct); + ActivityLogger.log(requireContext(), "Added new Product: " + productName); + Toast.makeText(getContext(), "Product added.", Toast.LENGTH_SHORT).show(); + } + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "ProductDetailFragment.saveProduct", e); + Toast.makeText(getContext(), "Error saving product.", Toast.LENGTH_SHORT).show(); + } + } + + // Deletes the product and logs the action + private void deleteProduct() { + try { + // TODO: Replace with actual API DELETE call when backend is ready + if (productFragment != null) productFragment.onProductDeleted(position); + ActivityLogger.logChange(requireContext(), "Product", "DELETED", productId); + Toast.makeText(getContext(), "Product deleted.", Toast.LENGTH_SHORT).show(); + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) listFragment.getChildFragmentManager().popBackStack(); + } catch (Exception e) { + ActivityLogger.logException(requireContext(), "ProductDetailFragment.deleteProduct", e); + Toast.makeText(getContext(), "Error deleting product.", Toast.LENGTH_SHORT).show(); + } + } + + private void handleArguments() { + if (getArguments() != null && getArguments().containsKey("productId")) { + isEditing = true; + productId = getArguments().getInt("productId"); + position = getArguments().getInt("position"); + tvMode.setText("Edit Product"); + tvProductId.setText("ID: " + productId); + etProductName.setText(getArguments().getString("productName")); + etProductDesc.setText(getArguments().getString("productDesc")); + etCategory.setText(getArguments().getString("category")); + etProductPrice.setText(String.valueOf(getArguments().getDouble("productPrice"))); + etStockQuantity.setText(String.valueOf(getArguments().getInt("stockQuantity"))); + btnDeleteProduct.setVisibility(View.VISIBLE); + } else { + isEditing = false; + tvMode.setText("Add Product"); + tvProductId.setVisibility(View.GONE); + btnDeleteProduct.setVisibility(View.GONE); + btnSaveProduct.setText("Add"); + } + } + + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvProductMode); + tvProductId = view.findViewById(R.id.tvProductId); + etProductName = view.findViewById(R.id.etProductName); + etProductDesc = view.findViewById(R.id.etProductDesc); + etCategory = view.findViewById(R.id.etProductCategory); + etProductPrice = view.findViewById(R.id.etProductPrice); + etStockQuantity = view.findViewById(R.id.etStockQuantity); + btnSaveProduct = view.findViewById(R.id.btnSaveProduct); + btnDeleteProduct = view.findViewById(R.id.btnDeleteProduct); + btnBack = view.findViewById(R.id.btnProductBack); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java new file mode 100644 index 00000000..2defbb69 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/ServiceDetailFragment.java @@ -0,0 +1,200 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.RetrofitClient; +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 retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class ServiceDetailFragment extends Fragment { + + private TextView tvMode, tvServiceId; + private EditText etServiceName, etServiceDesc, etServiceDuration, etServicePrice; + private Button btnSaveService, btnDeleteService, btnBack; + private int serviceId; + private boolean isEditing = false; + private ServiceFragment serviceFragment; + + //set the service fragment to the parent so we refer back to service view when save or delete is done + public void setServiceFragment(ServiceFragment fragment) { + this.serviceFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_service_detail, container, false); + + //get controls from layout and display the view depending on the mode + initViews(view); + handleArguments(); + + //set button click listeners + btnBack.setOnClickListener(v -> navigateBack()); + btnSaveService.setOnClickListener(v -> saveService()); + btnDeleteService.setOnClickListener(v -> deleteService()); + + return view; + } + + //Method to Update or Add a service + private void saveService() { + //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; + } + + //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)); + + ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext()); + + //check if the service is being edited or added + if (isEditing) { + // Update existing service + serviceDTO.setServiceId((long) serviceId); + serviceApi.updateService((long) serviceId, serviceDTO).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Service updated successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to update service: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("ServiceDetailFragment", "Error updating service", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } else { + // Add new service + serviceApi.createService(serviceDTO).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Service added successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to add service: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("ServiceDetailFragment", "Error adding service", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + } + + //Method to Delete a service + private void deleteService() { + //Alert the user to confirm the delete + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Service") + .setMessage("Are you sure you want to delete " + etServiceName.getText().toString() + "?") + .setPositiveButton("Delete", (dialog, which) -> { + ServiceApi serviceApi = RetrofitClient.getServiceApi(requireContext()); + serviceApi.deleteService((long) serviceId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Service deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to delete service: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("ServiceDetailFragment", "Error deleting service", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + //Helper method to navigate back to the list + private void navigateBack() { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.getChildFragmentManager().popBackStack(); + } + } + + //helper function to check if service is being edited or added and show the view accordingly + private void handleArguments() { + // Service is being edited if the bundle contains a serviceId + if (getArguments() != null && getArguments().containsKey("serviceId")) { + // Get service data from arguments and populate fields + isEditing = true; + serviceId = getArguments().getInt("serviceId"); + tvMode.setText("Edit Service"); + tvServiceId.setText("ID: " + serviceId); + etServiceName.setText(getArguments().getString("serviceName")); + etServiceDesc.setText(getArguments().getString("serviceDesc")); + etServiceDuration.setText(String.valueOf(getArguments().getInt("serviceDuration"))); + etServicePrice.setText(String.valueOf(getArguments().getDouble("servicePrice"))); + btnDeleteService.setVisibility(View.VISIBLE); + } else { + // Service is being added + // Set default values for add a new service + isEditing = false; + tvMode.setText("Add Service"); + tvServiceId.setVisibility(View.GONE); + btnDeleteService.setVisibility(View.GONE); + btnSaveService.setText("Add"); + } + } + + //helper function to get controls from layout + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvMode); + tvServiceId = view.findViewById(R.id.tvServiceId); + etServiceName = view.findViewById(R.id.etServiceName); + etServiceDesc = view.findViewById(R.id.etServiceDesc); + etServiceDuration = view.findViewById(R.id.etServiceDuration); + etServicePrice = view.findViewById(R.id.etServicePrice); + btnSaveService = view.findViewById(R.id.btnSaveService); + btnDeleteService = view.findViewById(R.id.btnDeleteService); + btnBack = view.findViewById(R.id.btnBack); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java new file mode 100644 index 00000000..8537d6c2 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SupplierDetailFragment.java @@ -0,0 +1,204 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.os.Bundle; + +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; + +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.RetrofitClient; +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 retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + +public class SupplierDetailFragment extends Fragment { + + private TextView tvMode, tvSupId; + private EditText etSupCompany, etSupContactFirstName, etSupContactLastName, etSupEmail, etSupPhone; + private Button btnSaveSupplier, btnDeleteSupplier, btnBack; + private int supId; + private boolean isEditing = false; + private SupplierFragment supplierFragment; + + //set the supplier fragment to the parent so we refer back to supplier view when save or delete is done + public void setSupplierFragment(SupplierFragment fragment) { + this.supplierFragment = fragment; + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_supplier_detail, container, false); + + //get controls from layout and display the view depending on the mode + initViews(view); + handleArguments(); + + //set button click listeners + btnBack.setOnClickListener(v -> navigateBack()); + btnSaveSupplier.setOnClickListener(v -> saveSupplier()); + btnDeleteSupplier.setOnClickListener(v -> deleteSupplier()); + + return view; + } + + //Method to Update or Add a supplier + private void saveSupplier() { + //get all the values from the fields + String company = etSupCompany.getText().toString().trim(); + String firstName = etSupContactFirstName.getText().toString().trim(); + String lastName = etSupContactLastName.getText().toString().trim(); + 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); + supplierDTO.setSupContactFirstName(firstName); + supplierDTO.setSupContactLastName(lastName); + supplierDTO.setSupEmail(email); + supplierDTO.setSupPhone(phone); + + SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext()); + + //check if the supplier is being edited or added + if (isEditing) { + // Update existing supplier + supplierDTO.setSupId((long) supId); + supplierApi.updateSupplier((long) supId, supplierDTO).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Supplier updated successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to update supplier: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("SupplierDetailFragment", "Error updating supplier", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } else { + // Add new supplier + supplierApi.createSupplier(supplierDTO).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Supplier added successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to add supplier: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("SupplierDetailFragment", "Error adding supplier", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + } + } + + //Method to Delete a supplier + private void deleteSupplier() { + //Alert the user to confirm the delete + new AlertDialog.Builder(requireContext()) + .setTitle("Delete Supplier") + .setMessage("Are you sure you want to delete " + etSupCompany.getText().toString() + "?") + .setPositiveButton("Delete", (dialog, which) -> { + SupplierApi supplierApi = RetrofitClient.getSupplierApi(requireContext()); + supplierApi.deleteSupplier((long) supId).enqueue(new Callback() { + @Override + public void onResponse(Call call, Response response) { + if (response.isSuccessful()) { + Toast.makeText(getContext(), "Supplier deleted successfully!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + Toast.makeText(getContext(), "Failed to delete supplier: " + response.code(), Toast.LENGTH_SHORT).show(); + } + } + + @Override + public void onFailure(Call call, Throwable t) { + Log.e("SupplierDetailFragment", "Error deleting supplier", t); + Toast.makeText(getContext(), "Error: " + t.getMessage(), Toast.LENGTH_SHORT).show(); + } + }); + }) + .setNegativeButton("Cancel", null) + .show(); + } + + //Helper method to navigate back to the list + private void navigateBack() { + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.getChildFragmentManager().popBackStack(); + } + } + + //helper function to check if supplier is being edited or added and show the view accordingly + private void handleArguments() { + // Supplier is being edited if the bundle contains a supId + if (getArguments() != null && getArguments().containsKey("supId")) { + // Get supplier data from arguments and populate fields + isEditing = true; + supId = getArguments().getInt("supId"); + tvMode.setText("Edit Supplier"); + tvSupId.setText("ID: " + supId); + etSupCompany.setText(getArguments().getString("supCompany")); + etSupContactFirstName.setText(getArguments().getString("supContactFirstName")); + etSupContactLastName.setText(getArguments().getString("supContactLastName")); + etSupEmail.setText(getArguments().getString("supEmail")); + etSupPhone.setText(getArguments().getString("supPhone")); + btnDeleteSupplier.setVisibility(View.VISIBLE); + } else { + // Supplier is being added + // Set default values for add a new supplier + isEditing = false; + tvMode.setText("Add Supplier"); + tvSupId.setVisibility(View.GONE); + btnDeleteSupplier.setVisibility(View.GONE); + btnSaveSupplier.setText("Add"); + } + } + + //helper function to get controls from layout + private void initViews(View view) { + tvMode = view.findViewById(R.id.tvMode); + tvSupId = view.findViewById(R.id.tvSupId); + etSupCompany = view.findViewById(R.id.etSupCompany); + etSupContactFirstName = view.findViewById(R.id.etSupContactFirstName); + etSupContactLastName = view.findViewById(R.id.etSupContactLastName); + etSupEmail = view.findViewById(R.id.etSupEmail); + etSupPhone = view.findViewById(R.id.etSupPhone); + btnSaveSupplier = view.findViewById(R.id.btnSaveSupplier); + btnDeleteSupplier = view.findViewById(R.id.btnDeleteSupplier); + btnBack = view.findViewById(R.id.btnBack); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java new file mode 100644 index 00000000..280789e8 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/listprofilefragments/PetProfileFragment.java @@ -0,0 +1,177 @@ +package com.example.petstoremobile.fragments.listfragments.listprofilefragments; + +import android.Manifest; +import android.app.Activity; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Color; +import android.net.Uri; +import android.os.Bundle; + +import androidx.activity.result.ActivityResultLauncher; +import androidx.activity.result.contract.ActivityResultContracts; +import androidx.appcompat.app.AlertDialog; +import androidx.core.content.ContextCompat; +import androidx.core.content.FileProvider; +import androidx.fragment.app.Fragment; + +import android.provider.MediaStore; +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 com.example.petstoremobile.R; +import com.example.petstoremobile.fragments.ListFragment; +import com.example.petstoremobile.fragments.listfragments.detailfragments.PetDetailFragment; + +import java.io.File; +import java.util.Locale; + +public class PetProfileFragment extends Fragment { + + private TextView tvPetName, tvPetSpecies, tvPetBreed, tvPetAge, tvPetPrice; + private Button btnBack, btnEditPet, btnChangePhoto; + private ImageView imgPet; + private Uri photoUri; + + // launchers for camera and gallery + private ActivityResultLauncher galleryLauncher; + private ActivityResultLauncher cameraLauncher; + private ActivityResultLauncher permissionLauncher; + + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + // Launcher to open gallery to select image + galleryLauncher = registerForActivityResult( + new ActivityResultContracts.StartActivityForResult(), + result -> { + if (result.getResultCode() == Activity.RESULT_OK && result.getData() != null) { + Uri selectedImage = result.getData().getData(); + imgPet.setImageURI(selectedImage); + // TODO: SAVE CHANGED PHOTO TO DATABASE + } + } + ); + + // Launcher for camera to open and capture image + cameraLauncher = registerForActivityResult( + new ActivityResultContracts.TakePicture(), + success -> { + if (success) { + imgPet.setImageURI(null); + imgPet.setImageURI(photoUri); + // TODO: SAVE CHANGED PHOTO TO DATABASE + } + } + ); + + // Launcher to request camera permission + permissionLauncher = registerForActivityResult( + new ActivityResultContracts.RequestPermission(), + granted -> { + if (granted) { + launchCamera(); + } else { + new AlertDialog.Builder(requireContext()) + .setTitle("Permission Required") + .setMessage("Please grant camera permission to use this feature") + .setPositiveButton("Open Settings", (dialog, which) -> { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", requireContext().getPackageName(), null)); + startActivity(intent); + }) + .setNegativeButton("Cancel", null) + .show(); + } + } + ); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_pet_profile, container, false); + + // Initialize views + tvPetName = view.findViewById(R.id.tvPetName); + tvPetSpecies = view.findViewById(R.id.tvPetSpecies); + tvPetBreed = view.findViewById(R.id.tvPetBreed); + tvPetAge = view.findViewById(R.id.tvPetAge); + tvPetPrice = view.findViewById(R.id.tvPetPrice); + btnBack = view.findViewById(R.id.btnBack); + btnEditPet = view.findViewById(R.id.btnEditPet); + btnChangePhoto = view.findViewById(R.id.btnChangePhoto); + imgPet = view.findViewById(R.id.imgPet); + + + // Set pet details to display + if (getArguments() != null) { + 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"))); + } + + //set button click listeners + btnBack.setOnClickListener(v -> { + //get the list fragment and pop the back stack to return to the previous view (PetFragment) + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.getChildFragmentManager().popBackStack(); + } + }); + + //Make the edit button go to the pet detail view + btnEditPet.setOnClickListener(v -> { + if (getArguments() == null) return; + + PetDetailFragment detailFragment = new PetDetailFragment(); + //send the bundle to the pet detail fragment + detailFragment.setArguments(getArguments()); + + //get ListFragment to load the the detail view + ListFragment listFragment = (ListFragment) getParentFragment(); + if (listFragment != null) { + listFragment.loadFragment(detailFragment); + } + }); + + //Make change photo button ask user to select a new photo + btnChangePhoto.setOnClickListener(v -> { + new AlertDialog.Builder(requireContext()) + .setTitle("Change Pet Photo") + .setItems(new String[]{"Take Photo", "Choose from Gallery"}, (dialog, which) -> { + if (which == 0) { + // Choose Camera + //Checks if the user has granted the camera permission already + if (ContextCompat.checkSelfPermission(requireContext(), Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED) { + //if the permission is already granted then launch the camera + launchCamera(); + } else { + //otherwise request the permission + permissionLauncher.launch(Manifest.permission.CAMERA); + } + } else { + Intent intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI); + galleryLauncher.launch(intent); + } + }) + .show(); + }); + + return view; + } + + private void launchCamera() { + File photoFile = new File(requireContext().getCacheDir(), "pet_photo.jpg"); + photoUri = FileProvider.getUriForFile(requireContext(), requireContext().getPackageName() + ".fileprovider", photoFile); + cameraLauncher.launch(photoUri); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java b/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java new file mode 100644 index 00000000..e227bc9b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/models/Adoption.java @@ -0,0 +1,79 @@ +package com.example.petstoremobile.models; + +public class Adoption { + private int adoptionId; + private String adopterName; + private String adopterEmail; + private String adopterPhone; + private String petName; + private String adoptionDate; + private String status; + + // Constructor + public Adoption(int adoptionId, String adopterName, String adopterEmail, String adopterPhone, String petName, String adoptionDate, String status) { + this.adoptionId = adoptionId; + this.adopterName = adopterName; + this.adopterEmail = adopterEmail; + this.adopterPhone = adopterPhone; + this.petName = petName; + this.adoptionDate = adoptionDate; + this.status = status; + } + + // Getters and setters + public int getAdoptionId() { + return adoptionId; + } + + public void setAdoptionId(int adoptionId) { + this.adoptionId = adoptionId; + } + + public String getAdopterName() { + return adopterName; + } + + public void setAdopterName(String adopterName) { + this.adopterName = adopterName; + } + + public String getAdopterEmail() { + return adopterEmail; + } + + public void setAdopterEmail(String adopterEmail) { + this.adopterEmail = adopterEmail; + } + + public String getAdopterPhone() { + return adopterPhone; + } + + public void setAdopterPhone(String adopterPhone) { + this.adopterPhone = adopterPhone; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getAdoptionDate() { + return adoptionDate; + } + + public void setAdoptionDate(String adoptionDate) { + this.adoptionDate = adoptionDate; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java b/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java new file mode 100644 index 00000000..38e8da10 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/models/Appointment.java @@ -0,0 +1,76 @@ +package com.example.petstoremobile.models; + + +public class Appointment { + private int appointmentId; + private String customerName; + private String petName; + private String serviceType; + private String appointmentDate; + private String appointmentTime; + private String status; + + // Constructor + public Appointment(int appointmentId, String customerName, String petName, String serviceType, String appointmentDate, String appointmentTime, String status) { + this.appointmentId = appointmentId; + this.customerName = customerName; + this.petName = petName; + this.serviceType = serviceType; + this.appointmentDate = appointmentDate; + this.appointmentTime = appointmentTime; + this.status = status; + } + + // Getters and setters + public int getAppointmentId() { + return appointmentId; + } + + public String getCustomerName() { + return customerName; + } + + public void setCustomerName(String customerName) { + this.customerName = customerName; + } + + public String getPetName() { + return petName; + } + + public void setPetName(String petName) { + this.petName = petName; + } + + public String getServiceType() { + return serviceType; + } + + public void setServiceType(String serviceType) { + this.serviceType = serviceType; + } + + public String getAppointmentDate() { + return appointmentDate; + } + + public void setAppointmentDate(String appointmentDate) { + this.appointmentDate = appointmentDate; + } + + public String getAppointmentTime() { + return appointmentTime; + } + + public void setAppointmentTime(String appointmentTime) { + this.appointmentTime = appointmentTime; + } + + public String getStatus() { + return status; + } + + public void setStatus(String status) { + this.status = status; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Chat.java b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java new file mode 100644 index 00000000..f3a9a4eb --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/models/Chat.java @@ -0,0 +1,37 @@ +package com.example.petstoremobile.models; + +public class Chat { + private String chatId; + private String customerName; + private String lastMessage; + private Long customerId; + private Long staffId; + + public Chat(String chatId, String customerName, String lastMessage, Long customerId, Long staffId) { + this.chatId = chatId; + this.customerName = customerName; + this.lastMessage = lastMessage; + this.customerId = customerId; + this.staffId = staffId; + } + + public String getChatId() { + return chatId; + } + + public String getCustomerName() { + return customerName; + } + + public String getLastMessage() { + return lastMessage; + } + + public Long getCustomerId() { + return customerId; + } + + public Long getStaffId() { + return staffId; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java b/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java new file mode 100644 index 00000000..7aacd2df --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/models/Inventory.java @@ -0,0 +1,66 @@ +package com.example.petstoremobile.models; + + +public class Inventory { + private int inventoryId; + private String itemName; + private String category; + private int quantity; + private double unitPrice; + private String supplier; + + // Constructor + public Inventory(int inventoryId, String itemName, String category, int quantity, double unitPrice, String supplier) { + this.inventoryId = inventoryId; + this.itemName = itemName; + this.category = category; + this.quantity = quantity; + this.unitPrice = unitPrice; + this.supplier = supplier; + } + + // Getters and setters + public int getInventoryId() { + return inventoryId; + } + + public String getItemName() { + return itemName; + } + + public void setItemName(String itemName) { + this.itemName = itemName; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public int getQuantity() { + return quantity; + } + + public void setQuantity(int quantity) { + this.quantity = quantity; + } + + public double getUnitPrice() { + return unitPrice; + } + + public void setUnitPrice(double unitPrice) { + this.unitPrice = unitPrice; + } + + public String getSupplier() { + return supplier; + } + + public void setSupplier(String supplier) { + this.supplier = supplier; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Message.java b/android/app/src/main/java/com/example/petstoremobile/models/Message.java new file mode 100644 index 00000000..18ec549a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/models/Message.java @@ -0,0 +1,36 @@ +package com.example.petstoremobile.models; + +public class Message { + private Long id; + private Long conversationId; + private Long senderId; + private String content; + private String timestamp; + private Boolean isRead; + + public Message() {} + + public Message(Long conversationId, Long senderId, String content) { + this.conversationId = conversationId; + this.senderId = senderId; + this.content = content; + } + + public Long getId() { return id; } + public void setId(Long id) { this.id = id; } + + public Long getConversationId() { return conversationId; } + public void setConversationId(Long conversationId) { this.conversationId = conversationId; } + + public Long getSenderId() { return senderId; } + public void setSenderId(Long senderId) { this.senderId = senderId; } + + public String getContent() { return content; } + public void setContent(String content) { this.content = content; } + + public String getTimestamp() { return timestamp; } + public void setTimestamp(String timestamp) { this.timestamp = timestamp; } + + public Boolean getIsRead() { return isRead; } + public void setIsRead(Boolean isRead) { this.isRead = isRead; } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/models/Product.java b/android/app/src/main/java/com/example/petstoremobile/models/Product.java new file mode 100644 index 00000000..90a56eab --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/models/Product.java @@ -0,0 +1,66 @@ +package com.example.petstoremobile.models; + + +public class Product { + private int productId; + private String productName; + private String productDesc; + private String category; + private double productPrice; + private int stockQuantity; + + // Constructor + public Product(int productId, String productName, String productDesc, String category, double productPrice, int stockQuantity) { + this.productId = productId; + this.productName = productName; + this.productDesc = productDesc; + this.category = category; + this.productPrice = productPrice; + this.stockQuantity = stockQuantity; + } + + // Getters and setters + public int getProductId() { + return productId; + } + + public String getProductName() { + return productName; + } + + public void setProductName(String productName) { + this.productName = productName; + } + + public String getProductDesc() { + return productDesc; + } + + public void setProductDesc(String productDesc) { + this.productDesc = productDesc; + } + + public String getCategory() { + return category; + } + + public void setCategory(String category) { + this.category = category; + } + + public double getProductPrice() { + return productPrice; + } + + public void setProductPrice(double productPrice) { + this.productPrice = productPrice; + } + + public int getStockQuantity() { + return stockQuantity; + } + + public void setStockQuantity(int stockQuantity) { + this.stockQuantity = stockQuantity; + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/ActivityLogger.java b/android/app/src/main/java/com/example/petstoremobile/utils/ActivityLogger.java new file mode 100644 index 00000000..f474508f --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/ActivityLogger.java @@ -0,0 +1,45 @@ +package com.example.petstoremobile.utils; + +import android.content.Context; +import java.io.FileWriter; +import java.io.IOException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +public class ActivityLogger { + + private static final String LOG_FILE = "log.txt"; + + // Logs a general message with a timestamp + public static void log(Context context, String message) { + String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); + String entry = timestamp + " | INFO | " + message + "\n"; + writeToFile(context, entry); + } + + // Logs a database change (ADD, UPDATE, DELETE) with entity type and ID + public static void logChange(Context context, String entity, String action, int id) { + String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); + String entry = timestamp + " | DB CHANGE | " + action + " " + entity + " ID: " + id + "\n"; + writeToFile(context, entry); + } + + // Logs an exception with location info + public static void logException(Context context, String location, Exception e) { + String timestamp = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()).format(new Date()); + String entry = timestamp + " | ERROR | " + location + ": " + e.getMessage() + "\n"; + writeToFile(context, entry); + } + + // Writes the log entry to log.txt in internal storage + private static void writeToFile(Context context, String entry) { + try { + FileWriter fw = new FileWriter(context.getFilesDir() + "/" + LOG_FILE, true); + fw.write(entry); + fw.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java new file mode 100644 index 00000000..8182912b --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/utils/InputValidator.java @@ -0,0 +1,97 @@ +package com.example.petstoremobile.utils; + +import android.widget.EditText; + +public class InputValidator { + + // Checks if an EditText field is not empty + public static boolean isNotEmpty(EditText field, String fieldName) { + if (field.getText().toString().trim().isEmpty()) { + field.setError(fieldName + " is required"); + field.requestFocus(); + return false; + } + return true; + } + + // Checks if the value is a positive integer + public static boolean isPositiveInteger(EditText field, String fieldName) { + String value = field.getText().toString().trim(); + try { + int num = Integer.parseInt(value); + if (num < 0) { + field.setError(fieldName + " must be a positive number"); + field.requestFocus(); + return false; + } + return true; + } catch (NumberFormatException e) { + field.setError(fieldName + " must be a whole number"); + field.requestFocus(); + return false; + } + } + + // Checks if the value is a positive decimal number + public static boolean isPositiveDecimal(EditText field, String fieldName) { + String value = field.getText().toString().trim(); + try { + double num = Double.parseDouble(value); + if (num < 0) { + field.setError(fieldName + " must be a positive number"); + field.requestFocus(); + return false; + } + return true; + } catch (NumberFormatException e) { + field.setError(fieldName + " must be a number"); + field.requestFocus(); + return false; + } + } + + // Checks if the email address is valid + public static boolean isValidEmail(EditText field) { + String email = field.getText().toString().trim(); + if (email.isEmpty() || !android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()) { + field.setError("Enter a valid email address"); + field.requestFocus(); + return false; + } + return true; + } + + // Checks if the phone number is valid (digits, spaces, dashes, brackets allowed) + public static boolean isValidPhone(EditText field) { + String phone = field.getText().toString().trim(); + if (phone.isEmpty() || !phone.matches("[0-9\\-\\s\\(\\)\\+]+")) { + field.setError("Enter a valid phone number"); + field.requestFocus(); + return false; + } + return true; + } + + // Checks if the date is in YYYY-MM-DD format + public static boolean isValidDate(EditText field) { + String date = field.getText().toString().trim(); + if (date.isEmpty() || !date.matches("\\d{4}-\\d{2}-\\d{2}")) { + field.setError("Date must be in YYYY-MM-DD format"); + field.requestFocus(); + return false; + } + return true; + } + + // Checks if the time format is valid (e.g. 10:00 AM) + public static boolean isValidTime(EditText field) { + String time = field.getText().toString().trim(); + if (time.isEmpty() || !time.matches("\\d{1,2}:\\d{2}\\s?(AM|PM|am|pm)?")) { + field.setError("Enter a valid time (e.g. 10:00 AM)"); + field.requestFocus(); + return false; + } + return true; + } +} + diff --git a/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java new file mode 100644 index 00000000..af4264db --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/websocket/StompChatManager.java @@ -0,0 +1,295 @@ +package com.example.petstoremobile.websocket; + +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.ConversationDTO; +import com.example.petstoremobile.dtos.MessageDTO; +import com.google.gson.Gson; +import java.util.HashMap; +import java.util.Collections; +import java.util.Locale; +import java.util.Map; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import ua.naiksoftware.stomp.Stomp; +import ua.naiksoftware.stomp.StompClient; +import ua.naiksoftware.stomp.dto.StompHeader; + +//Used to handle the websocket connection for the chat +public class StompChatManager { + + private static final String TAG = "StompChatManager"; + + //Interface for when a message is received + public interface MessageListener { + void onMessageReceived(MessageDTO message); + } + + //Interface for when a conversation is created or updated + public interface ConversationListener { + void onConversationUpdated(ConversationDTO conversation); + } + + //Interface for when the websocket connection is opened, closed, or has an error + public interface ConnectionListener { + void onSocketOpened(); + void onSocketClosed(); + void onSocketError(); + } + + private StompClient stompClient; + private final CompositeDisposable compositeDisposable = new CompositeDisposable(); + private Disposable topicDisposable; + private Disposable conversationsDisposable; + private Disposable userConversationsDisposable; + private Disposable errorQueueDisposable; + private final Gson gson = new Gson(); + private final Handler reconnectHandler = new Handler(Looper.getMainLooper()); + private MessageListener messageListener; + private ConversationListener conversationListener; + private ConnectionListener connectionListener; + private final String authToken; + private final String role; + private boolean isConnected; + private boolean isConnecting; + private boolean manualDisconnect; + private Long pendingConversationId; + + public StompChatManager(String authToken, String role) { + this.authToken = authToken; + this.role = role == null ? "" : role.trim().toUpperCase(Locale.ROOT); + } + + public void setMessageListener(MessageListener listener) { + this.messageListener = listener; + } + + public void setConversationListener(ConversationListener listener) { + this.conversationListener = listener; + } + + public void setConnectionListener(ConnectionListener listener) { + this.connectionListener = listener; + } + + // Set up a stomp connection + public void connect() { + if (authToken == null || authToken.isBlank()) { + Log.e(TAG, "Cannot connect websocket without token"); + return; + } + if (isConnected || isConnecting) { + return; + } + manualDisconnect = false; + isConnecting = true; + reconnectHandler.removeCallbacksAndMessages(null); + String webSocketUrl = buildWebSocketUrl(); + + Map headers = new HashMap<>(); + headers.put("Authorization", "Bearer " + authToken); + + stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, webSocketUrl, headers); + + compositeDisposable.add( + stompClient.lifecycle() + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(event -> { + switch (event.getType()) { + case OPENED: + isConnected = true; + isConnecting = false; + Log.d(TAG, "WebSocket opened"); + if (connectionListener != null) { + connectionListener.onSocketOpened(); + } + subscribeToErrorQueue(); + subscribeToConversationFeeds(); + if (pendingConversationId != null) { + subscribeToTopic(pendingConversationId); + } + break; + case CLOSED: + isConnected = false; + isConnecting = false; + Log.d(TAG, "WebSocket closed"); + if (connectionListener != null) { + connectionListener.onSocketClosed(); + } + scheduleReconnect(); + break; + case ERROR: + isConnected = false; + isConnecting = false; + Log.e(TAG, "WebSocket error: " + event.getException()); + if (connectionListener != null) { + connectionListener.onSocketError(); + } + scheduleReconnect(); + break; + } + }) + ); + + stompClient.connect(Collections.singletonList( + new StompHeader("Authorization", "Bearer " + authToken) + )); + } + + // Subscribes to updates for a specific conversation + public void subscribeToConversation(Long conversationId) { + pendingConversationId = conversationId; + if (!isConnected || stompClient == null) { + Log.d(TAG, "Delaying subscription until socket opens for conversation " + conversationId); + connect(); + return; + } + subscribeToTopic(conversationId); + } + + // Clears the current conversation subscription + public void clearConversationSubscription() { + pendingConversationId = null; + if (topicDisposable != null && !topicDisposable.isDisposed()) { + topicDisposable.dispose(); + topicDisposable = null; + } + } + + //helper function to subscribe to a specific conversation topic + private void subscribeToTopic(Long conversationId) { + if (topicDisposable != null && !topicDisposable.isDisposed()) { + topicDisposable.dispose(); + } + + String topic = "/topic/chat/conversations/" + conversationId; + Log.d(TAG, "Subscribing to topic " + topic); + + topicDisposable = stompClient.topic(topic) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> { + MessageDTO message = gson.fromJson( + stompMessage.getPayload(), MessageDTO.class); + if (messageListener != null) { + messageListener.onMessageReceived(message); + } + }, + throwable -> Log.e(TAG, "Topic error", throwable) + ); + + compositeDisposable.add(topicDisposable); + } + + // Listens for conversation updates and refresh the chat list + private void subscribeToConversationFeeds() { + if (conversationsDisposable != null && !conversationsDisposable.isDisposed()) { + conversationsDisposable.dispose(); + } + if (userConversationsDisposable != null && !userConversationsDisposable.isDisposed()) { + userConversationsDisposable.dispose(); + } + + userConversationsDisposable = stompClient.topic("/user/queue/chat/conversations") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> { + Log.d(TAG, "Queue conversation update: " + stompMessage.getPayload()); + ConversationDTO conversation = gson.fromJson( + stompMessage.getPayload(), ConversationDTO.class); + if (conversationListener != null) { + conversationListener.onConversationUpdated(conversation); + } + }, + throwable -> Log.e(TAG, "Conversation queue error", throwable) + ); + + compositeDisposable.add(userConversationsDisposable); + + if (isCustomer()) { + return; + } + + conversationsDisposable = stompClient.topic("/topic/chat/conversations") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> { + Log.d(TAG, "Broadcast conversation update: " + stompMessage.getPayload()); + ConversationDTO conversation = gson.fromJson( + stompMessage.getPayload(), ConversationDTO.class); + if (conversationListener != null) { + conversationListener.onConversationUpdated(conversation); + } + }, + throwable -> Log.e(TAG, "Conversation topic error", throwable) + ); + + compositeDisposable.add(conversationsDisposable); + } + + // Log any error from stomp + private void subscribeToErrorQueue() { + if (errorQueueDisposable != null && !errorQueueDisposable.isDisposed()) { + errorQueueDisposable.dispose(); + } + + errorQueueDisposable = stompClient.topic("/user/queue/chat/errors") + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + stompMessage -> Log.e(TAG, "WebSocket queue error payload: " + stompMessage.getPayload()), + throwable -> Log.e(TAG, "WebSocket error queue subscription failed", throwable) + ); + + compositeDisposable.add(errorQueueDisposable); + } + + // Disconnects the stomp connection + public void disconnect() { + manualDisconnect = true; + isConnected = false; + isConnecting = false; + pendingConversationId = null; + reconnectHandler.removeCallbacksAndMessages(null); + compositeDisposable.clear(); + if (stompClient != null) { + stompClient.disconnect(); + } + } + + // Make the URL for the websocket connection + private String buildWebSocketUrl() { + String baseUrl = RetrofitClient.BASE_URL.endsWith("/") + ? RetrofitClient.BASE_URL.substring(0, RetrofitClient.BASE_URL.length() - 1) + : RetrofitClient.BASE_URL; + if (baseUrl.startsWith("https://")) { + return "wss://" + baseUrl.substring("https://".length()) + "/ws/chat"; + } + if (baseUrl.startsWith("http://")) { + return "ws://" + baseUrl.substring("http://".length()) + "/ws/chat"; + } + return baseUrl + "/ws/chat"; + } + + // Helper to check if the current user is a customer + private boolean isCustomer() { + return "CUSTOMER".equals(role); + } + + // if connection drops, try to reconnect after 1 second + private void scheduleReconnect() { + if (manualDisconnect) { + return; + } + reconnectHandler.removeCallbacksAndMessages(null); + reconnectHandler.postDelayed(this::connect, 1000); + } +} diff --git a/android/app/src/main/res/color/bottom_nav_colors.xml b/android/app/src/main/res/color/bottom_nav_colors.xml new file mode 100644 index 00000000..c170f0da --- /dev/null +++ b/android/app/src/main/res/color/bottom_nav_colors.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/baseline_menu_36.xml b/android/app/src/main/res/drawable/baseline_menu_36.xml new file mode 100644 index 00000000..f25be2a9 --- /dev/null +++ b/android/app/src/main/res/drawable/baseline_menu_36.xml @@ -0,0 +1,10 @@ + + + diff --git a/android/app/src/main/res/drawable/bg_message_received.xml b/android/app/src/main/res/drawable/bg_message_received.xml new file mode 100644 index 00000000..7cc459d5 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_message_received.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/bg_message_sent.xml b/android/app/src/main/res/drawable/bg_message_sent.xml new file mode 100644 index 00000000..a2013587 --- /dev/null +++ b/android/app/src/main/res/drawable/bg_message_sent.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/circle_image.xml b/android/app/src/main/res/drawable/circle_image.xml new file mode 100644 index 00000000..8635e87b --- /dev/null +++ b/android/app/src/main/res/drawable/circle_image.xml @@ -0,0 +1,8 @@ + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/ic_launcher_background.xml b/android/app/src/main/res/drawable/ic_launcher_background.xml new file mode 100644 index 00000000..07d5da9c --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/android/app/src/main/res/drawable/ic_launcher_foreground.xml b/android/app/src/main/res/drawable/ic_launcher_foreground.xml new file mode 100644 index 00000000..2b068d11 --- /dev/null +++ b/android/app/src/main/res/drawable/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/drawable/petstore_logo.png b/android/app/src/main/res/drawable/petstore_logo.png new file mode 100644 index 00000000..131282c1 Binary files /dev/null and b/android/app/src/main/res/drawable/petstore_logo.png differ diff --git a/android/app/src/main/res/drawable/placeholder.png b/android/app/src/main/res/drawable/placeholder.png new file mode 100644 index 00000000..e7bdbefd Binary files /dev/null and b/android/app/src/main/res/drawable/placeholder.png differ diff --git a/android/app/src/main/res/drawable/rounded_card.xml b/android/app/src/main/res/drawable/rounded_card.xml new file mode 100644 index 00000000..125ee629 --- /dev/null +++ b/android/app/src/main/res/drawable/rounded_card.xml @@ -0,0 +1,9 @@ + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_home.xml b/android/app/src/main/res/layout/activity_home.xml new file mode 100644 index 00000000..c7b9f3ea --- /dev/null +++ b/android/app/src/main/res/layout/activity_home.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/android/app/src/main/res/layout/activity_main.xml b/android/app/src/main/res/layout/activity_main.xml new file mode 100644 index 00000000..9322e775 --- /dev/null +++ b/android/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + +