From 77793071ce79b268b0b508a894ce527c8c43228b Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 16:13:50 -0600 Subject: [PATCH 1/7] add pet image support --- .../detailfragments/PetDetailFragment.java | 119 +++++++++++++++++- .../viewmodels/PetDetailViewModel.java | 8 ++ .../main/res/layout/fragment_pet_detail.xml | 25 ++++ 3 files changed, 149 insertions(+), 3 deletions(-) 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 index a22e2c0d..c5e435ef 100644 --- 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 @@ -1,5 +1,6 @@ package com.example.petstoremobile.fragments.listfragments.detailfragments; +import android.net.Uri; import android.os.Bundle; import androidx.annotation.NonNull; @@ -16,7 +17,10 @@ import android.widget.Spinner; import android.widget.TextView; import android.widget.Toast; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; import com.example.petstoremobile.R; +import com.example.petstoremobile.api.PetApi; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.databinding.FragmentPetDetailBinding; import com.example.petstoremobile.dtos.DropdownDTO; @@ -24,18 +28,26 @@ import com.example.petstoremobile.dtos.PetDTO; import com.example.petstoremobile.utils.ActivityLogger; import com.example.petstoremobile.utils.DateTimeUtils; import com.example.petstoremobile.utils.DialogUtils; +import com.example.petstoremobile.utils.FileUtils; +import com.example.petstoremobile.utils.GlideUtils; +import com.example.petstoremobile.utils.ImagePickerHelper; import com.example.petstoremobile.utils.InputValidator; import com.example.petstoremobile.utils.Resource; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.PetDetailViewModel; +import java.io.File; import java.util.List; import java.util.Locale; import javax.inject.Inject; +import javax.inject.Named; import dagger.hilt.android.AndroidEntryPoint; +import okhttp3.MediaType; +import okhttp3.MultipartBody; +import okhttp3.RequestBody; /** * Fragment for displaying and editing pet details. @@ -45,14 +57,45 @@ public class PetDetailFragment extends Fragment { private FragmentPetDetailBinding binding; private PetDetailViewModel viewModel; + private ImagePickerHelper imagePickerHelper; private boolean isUpdatingUI = false; + private boolean hasImage = false; + private boolean isImageChanged = false; + private boolean isImageRemoved = false; + private Uri photoUri; + + @Inject @Named("baseUrl") String baseUrl; @Inject TokenManager tokenManager; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(PetDetailViewModel.class); + + imagePickerHelper = new ImagePickerHelper(this, "pet_photo.jpg", new ImagePickerHelper.ImagePickerListener() { + @Override + public void onImagePicked(Uri uri) { + photoUri = uri; + Glide.with(PetDetailFragment.this) + .load(uri) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .skipMemoryCache(true) + .into(binding.ivPetImage); + hasImage = true; + isImageChanged = true; + isImageRemoved = false; + } + + @Override + public void onImageRemoved() { + photoUri = null; + hasImage = false; + isImageChanged = false; + isImageRemoved = true; + binding.ivPetImage.setImageResource(R.drawable.placeholder2); + } + }); } @Override @@ -74,6 +117,7 @@ public class PetDetailFragment extends Fragment { binding.btnBack.setOnClickListener(v -> navigateBack()); binding.btnSavePet.setOnClickListener(v -> savePet()); binding.btnDeletePet.setOnClickListener(v -> deletePet()); + binding.ivPetImage.setOnClickListener(v -> imagePickerHelper.showImagePickerDialog("Select Pet Image", hasImage)); } private void observeViewModel() { @@ -197,14 +241,18 @@ public class PetDetailFragment extends Fragment { if (resource == null) return; setLoading(resource.status == Resource.Status.LOADING); if (resource.status == Resource.Status.SUCCESS) { + if (resource.data != null) { + viewModel.setPetId(resource.data.getPetId()); + } + String msg; if (viewModel.isEditing()) { ActivityLogger.logChange(requireContext(), "Pet", "UPDATED", (int) viewModel.getPetId()); - Toast.makeText(getContext(), "Pet updated successfully!", Toast.LENGTH_SHORT).show(); + msg = "Pet updated successfully!"; } else { ActivityLogger.log(requireContext(), "Added new Pet: " + name); - Toast.makeText(getContext(), "Pet added successfully!", Toast.LENGTH_SHORT).show(); + msg = "Pet added successfully!"; } - navigateToPetList(); + performPendingImageActions(msg); } else if (resource.status == Resource.Status.ERROR) { Toast.makeText(getContext(), "Error: " + resource.message, Toast.LENGTH_SHORT).show(); } @@ -239,10 +287,75 @@ public class PetDetailFragment extends Fragment { if (getArguments() != null && getArguments().containsKey("petId")) { viewModel.setPetId(getArguments().getLong("petId")); loadPetData(); + loadPetImage(); return; } viewModel.setPetId(-1); + hasImage = false; + } + + private void loadPetImage() { + String imageUrl = baseUrl + String.format(Locale.US, PetApi.PET_IMAGE_PATH, viewModel.getPetId()); + String token = tokenManager.getToken(); + GlideUtils.loadImageWithToken(requireContext(), binding.ivPetImage, imageUrl, token, R.drawable.placeholder2, new GlideUtils.ImageLoadListener() { + @Override + public void onResourceReady() { + hasImage = true; + } + + @Override + public void onLoadFailed() { + hasImage = false; + } + }); + } + + private void performPendingImageActions(String successMsg) { + if (isImageRemoved) { + viewModel.deletePetImage().observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), successMsg + " (but image removal failed)", Toast.LENGTH_SHORT).show(); + } + navigateToPetList(); + } + }); + } else if (isImageChanged && photoUri != null) { + uploadPetImageAndNavigate(photoUri, successMsg); + } else { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + navigateToPetList(); + } + } + + private void uploadPetImageAndNavigate(Uri uri, String successMsg) { + File file = FileUtils.getFileFromUri(requireContext(), uri); + if (file == null) { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + navigateToPetList(); + return; + } + + RequestBody requestFile = RequestBody.create(file, MediaType.parse(requireContext().getContentResolver().getType(uri))); + MultipartBody.Part body = MultipartBody.Part.createFormData("image", file.getName(), requestFile); + + viewModel.uploadPetImage(body).observe(getViewLifecycleOwner(), resource -> { + if (resource == null) return; + setLoading(resource.status == Resource.Status.LOADING); + if (resource.status != Resource.Status.LOADING) { + if (resource.status == Resource.Status.SUCCESS) { + Toast.makeText(getContext(), successMsg, Toast.LENGTH_SHORT).show(); + } else { + Toast.makeText(getContext(), successMsg + " (but image upload failed)", Toast.LENGTH_SHORT).show(); + } + navigateToPetList(); + } + }); } private void loadPetData() { diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java index ddb868a7..b1f4a624 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/PetDetailViewModel.java @@ -239,6 +239,14 @@ public class PetDetailViewModel extends ViewModel { return petRepository.deletePet(petId); } + public LiveData> uploadPetImage(okhttp3.MultipartBody.Part image) { + return petRepository.uploadPetImage(petId, image); + } + + public LiveData> deletePetImage() { + return petRepository.deletePetImage(petId); + } + public LiveData> getCustomerList() { return customerList; } diff --git a/android/app/src/main/res/layout/fragment_pet_detail.xml b/android/app/src/main/res/layout/fragment_pet_detail.xml index e42d8e30..53be1557 100644 --- a/android/app/src/main/res/layout/fragment_pet_detail.xml +++ b/android/app/src/main/res/layout/fragment_pet_detail.xml @@ -70,6 +70,31 @@ android:layout_gravity="end" android:layout_marginBottom="8dp"/> + + + + + + + + Date: Wed, 15 Apr 2026 16:17:50 -0600 Subject: [PATCH 2/7] fix species-service validation --- .../backend/service/AppointmentService.java | 28 ------------------- .../db/migration/V7__fix_service_species.sql | 8 ++++++ web/app/appointments/page.js | 18 ++++++------ 3 files changed, 18 insertions(+), 36 deletions(-) create mode 100644 backend/src/main/resources/db/migration/V7__fix_service_species.sql diff --git a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java index 999568a8..9c63733a 100644 --- a/backend/src/main/java/com/petshop/backend/service/AppointmentService.java +++ b/backend/src/main/java/com/petshop/backend/service/AppointmentService.java @@ -127,8 +127,6 @@ public class AppointmentService { } } - validateSpeciesServiceCompatibility(pet, service); - validateStoreAccess(store.getStoreId(), authenticatedUser); validatePetServiceCompatibility(pet, service); validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null); @@ -387,32 +385,6 @@ public class AppointmentService { return true; } - private void validateSpeciesServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) { - if (pet == null || service == null) return; - String species = pet.getPetSpecies(); - if (species == null) return; - String serviceName = service.getServiceName().toLowerCase(); - - switch (species.toLowerCase()) { - case "bird": - if (!serviceName.contains("wing clipping") && !serviceName.contains("beak and nail")) { - throw new IllegalArgumentException( - "Service '" + service.getServiceName() + "' is not available for birds. " + - "Allowed services: Wing Clipping, Beak and Nail Care."); - } - break; - case "fish": - if (!serviceName.contains("aquarium health")) { - throw new IllegalArgumentException( - "Service '" + service.getServiceName() + "' is not available for fish. " + - "Allowed service: Aquarium Health Check."); - } - break; - default: - break; - } - } - private void validateStoreAccess(Long requestedStoreId, User user) { if (user.getRole() != User.Role.STAFF) { return; diff --git a/backend/src/main/resources/db/migration/V7__fix_service_species.sql b/backend/src/main/resources/db/migration/V7__fix_service_species.sql new file mode 100644 index 00000000..ef1faf06 --- /dev/null +++ b/backend/src/main/resources/db/migration/V7__fix_service_species.sql @@ -0,0 +1,8 @@ +DELETE FROM service_species WHERE serviceId = 2 AND species = 'Bird'; + +INSERT INTO service_species (serviceId, species) VALUES +(1, 'Guinea Pig'), +(2, 'Reptile'), +(2, 'Other'), +(4, 'Reptile'), +(4, 'Other'); diff --git a/web/app/appointments/page.js b/web/app/appointments/page.js index 58b741f9..876981c6 100644 --- a/web/app/appointments/page.js +++ b/web/app/appointments/page.js @@ -20,20 +20,22 @@ const SPECIES_BREEDS = { Other: ["Other"], }; -// Explicit allowlists for species with restricted service availability. -// Species not listed here may use all services. -const SPECIES_SERVICE_ALLOWLIST = { +const SPECIES_EXCLUSIVE_SERVICES = { Bird: ["wing clipping", "beak and nail"], Fish: ["aquarium health"], }; function getAvailableServices(services, species) { if (!species) return services; - const allowlist = SPECIES_SERVICE_ALLOWLIST[species]; - if (!allowlist) return services; - return services.filter((s) => - allowlist.some((kw) => s.serviceName.toLowerCase().includes(kw)) - ); + return services.filter((s) => { + const name = s.serviceName.toLowerCase(); + for (const [exclusiveSpecies, keywords] of Object.entries(SPECIES_EXCLUSIVE_SERVICES)) { + if (exclusiveSpecies !== species && keywords.some((kw) => name.includes(kw))) { + return false; + } + } + return true; + }); } const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; -- 2.49.1 From 6c8ea1c993961fe513a7de25e676f1d4058f1dd0 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 16:17:46 -0600 Subject: [PATCH 3/7] fix stripe key --- web/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/Dockerfile b/web/Dockerfile index e850494d..4b794483 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -3,7 +3,7 @@ WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . -ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY +ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51TK18lFQ95OLlFb7dNtKXlvhry8IOvHaWJWW7zUNFhicMgyJ2EgAFhiAocxsCyP95IKt7AeQg4cWe5iHF3qoheZyl0034Cd4yij ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY RUN npm run build -- 2.49.1 From 56b3e9932e0e59a959ca9a3d33d139cca8564a0c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 16:21:27 -0600 Subject: [PATCH 4/7] exclude next cache from build --- web/.dockerignore | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 web/.dockerignore diff --git a/web/.dockerignore b/web/.dockerignore new file mode 100644 index 00000000..b90a368f --- /dev/null +++ b/web/.dockerignore @@ -0,0 +1,2 @@ +node_modules +.next -- 2.49.1 From 8b39538305782c5f53f96ca9e31df027e114240c Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 16:21:37 -0600 Subject: [PATCH 5/7] add grooming for hamster, other --- .../src/main/resources/db/migration/V7__fix_service_species.sql | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/src/main/resources/db/migration/V7__fix_service_species.sql b/backend/src/main/resources/db/migration/V7__fix_service_species.sql index ef1faf06..2e2c64a6 100644 --- a/backend/src/main/resources/db/migration/V7__fix_service_species.sql +++ b/backend/src/main/resources/db/migration/V7__fix_service_species.sql @@ -2,6 +2,8 @@ DELETE FROM service_species WHERE serviceId = 2 AND species = 'Bird'; INSERT INTO service_species (serviceId, species) VALUES (1, 'Guinea Pig'), +(1, 'Hamster'), +(1, 'Other'), (2, 'Reptile'), (2, 'Other'), (4, 'Reptile'), -- 2.49.1 From 5b25a3f051af316a4e45af12395c240b136c0468 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 16:24:27 -0600 Subject: [PATCH 6/7] expand reptile and other services --- .../main/resources/db/migration/V7__fix_service_species.sql | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/backend/src/main/resources/db/migration/V7__fix_service_species.sql b/backend/src/main/resources/db/migration/V7__fix_service_species.sql index 2e2c64a6..4c22a1b0 100644 --- a/backend/src/main/resources/db/migration/V7__fix_service_species.sql +++ b/backend/src/main/resources/db/migration/V7__fix_service_species.sql @@ -6,5 +6,9 @@ INSERT INTO service_species (serviceId, species) VALUES (1, 'Other'), (2, 'Reptile'), (2, 'Other'), +(3, 'Reptile'), +(3, 'Other'), (4, 'Reptile'), -(4, 'Other'); +(4, 'Other'), +(5, 'Reptile'), +(5, 'Other'); -- 2.49.1 From b835770cb672284cd90d9436a25fde227fe2e7e9 Mon Sep 17 00:00:00 2001 From: Harkamal Randhawa Date: Wed, 15 Apr 2026 16:24:58 -0600 Subject: [PATCH 7/7] hardcode stripe key --- web/Dockerfile | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/web/Dockerfile b/web/Dockerfile index 4b794483..38bd4ff0 100644 --- a/web/Dockerfile +++ b/web/Dockerfile @@ -3,8 +3,7 @@ WORKDIR /app COPY package*.json ./ RUN npm ci COPY . . -ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51TK18lFQ95OLlFb7dNtKXlvhry8IOvHaWJWW7zUNFhicMgyJ2EgAFhiAocxsCyP95IKt7AeQg4cWe5iHF3qoheZyl0034Cd4yij -ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY +ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_51TK18lFQ95OLlFb7dNtKXlvhry8IOvHaWJWW7zUNFhicMgyJ2EgAFhiAocxsCyP95IKt7AeQg4cWe5iHF3qoheZyl0034Cd4yij RUN npm run build FROM node:22-alpine -- 2.49.1