From e6e8dc1b23a259ff460e335d9ae3731c4fedbb34 Mon Sep 17 00:00:00 2001 From: Alex <78383757+Lextical@users.noreply.github.com> Date: Sun, 12 Apr 2026 17:28:40 -0600 Subject: [PATCH] Fixed Log filters and fixed chat attachment download --- .../petstoremobile/api/ActivityLogApi.java | 8 +- .../fragments/ChatFragment.java | 57 +++++----- .../listfragments/ActivityLogFragment.java | 30 +++-- .../repositories/ActivityLogRepository.java | 4 +- .../viewmodels/ActivityLogListViewModel.java | 106 ++++++------------ .../controller/ActivityLogController.java | 7 +- .../repository/ActivityLogRepository.java | 6 +- .../backend/service/ActivityLogService.java | 38 ++++++- 8 files changed, 140 insertions(+), 116 deletions(-) diff --git a/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java b/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java index 7aaedffe..d6b7e2ae 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/ActivityLogApi.java @@ -6,9 +6,15 @@ import java.util.List; import retrofit2.Call; import retrofit2.http.GET; +import retrofit2.http.Query; public interface ActivityLogApi { @GET("api/v1/activity-logs") - Call> getActivityLogs(); + Call> getActivityLogs( + @Query("limit") int limit, + @Query("storeId") Long storeId, + @Query("role") String role, + @Query("search") String search + ); } 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 index 5b59de80..c9171db2 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ChatFragment.java @@ -227,43 +227,46 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis } private void saveFileToDownloads(ResponseBody body, String fileName, String mimeType) { - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - ContentValues values = new ContentValues(); - values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); - values.put(MediaStore.Downloads.MIME_TYPE, mimeType); - values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); + android.os.Handler mainHandler = new android.os.Handler(android.os.Looper.getMainLooper()); + new Thread(() -> { + try { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + ContentValues values = new ContentValues(); + values.put(MediaStore.Downloads.DISPLAY_NAME, fileName); + values.put(MediaStore.Downloads.MIME_TYPE, mimeType); + values.put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS); - Uri uri = requireContext().getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); - if (uri != null) { - try (OutputStream outputStream = requireContext().getContentResolver().openOutputStream(uri); + Uri uri = requireContext().getContentResolver().insert(MediaStore.Downloads.EXTERNAL_CONTENT_URI, values); + if (uri != null) { + try (OutputStream outputStream = requireContext().getContentResolver().openOutputStream(uri); + InputStream inputStream = body.byteStream()) { + byte[] buffer = new byte[4096]; + int bytesRead; + while ((bytesRead = inputStream.read(buffer)) != -1) { + outputStream.write(buffer, 0, bytesRead); + } + } + mainHandler.post(() -> Toast.makeText(requireContext(), "File saved to Downloads", Toast.LENGTH_SHORT).show()); + } + } else { + File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); + File file = new File(downloadsDir, fileName); + try (OutputStream outputStream = new FileOutputStream(file); InputStream inputStream = body.byteStream()) { byte[] buffer = new byte[4096]; int bytesRead; while ((bytesRead = inputStream.read(buffer)) != -1) { outputStream.write(buffer, 0, bytesRead); } - Toast.makeText(requireContext(), "File saved to Downloads", Toast.LENGTH_SHORT).show(); } + mainHandler.post(() -> Toast.makeText(requireContext(), "File saved to Downloads: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show()); } - } else { - File downloadsDir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS); - File file = new File(downloadsDir, fileName); - try (OutputStream outputStream = new FileOutputStream(file); - InputStream inputStream = body.byteStream()) { - byte[] buffer = new byte[4096]; - int bytesRead; - while ((bytesRead = inputStream.read(buffer)) != -1) { - outputStream.write(buffer, 0, bytesRead); - } - Toast.makeText(requireContext(), "File saved to Downloads: " + file.getAbsolutePath(), Toast.LENGTH_LONG).show(); - } + body.close(); + } catch (Exception e) { + Log.e(TAG, "Error saving file", e); + mainHandler.post(() -> Toast.makeText(requireContext(), "Error saving file", Toast.LENGTH_SHORT).show()); } - body.close(); - } catch (Exception e) { - Log.e(TAG, "Error saving file", e); - Toast.makeText(requireContext(), "Error saving file", Toast.LENGTH_SHORT).show(); - } + }).start(); } private void observeViewModel() { diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java index 8ce58e95..85d77668 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/ActivityLogFragment.java @@ -14,6 +14,7 @@ import androidx.recyclerview.widget.LinearLayoutManager; import com.example.petstoremobile.adapters.ActivityLogAdapter; import com.example.petstoremobile.databinding.FragmentActivityLogBinding; import com.example.petstoremobile.dtos.ActivityLogDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.utils.SpinnerUtils; import com.example.petstoremobile.utils.UIUtils; import com.example.petstoremobile.viewmodels.ActivityLogListViewModel; @@ -29,6 +30,7 @@ public class ActivityLogFragment extends Fragment { private ActivityLogListViewModel viewModel; private ActivityLogAdapter adapter; private final List logList = new ArrayList<>(); + private List storeList = new ArrayList<>(); @Override public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, @@ -40,7 +42,7 @@ public class ActivityLogFragment extends Fragment { setupFilters(); observeViewModel(); - binding.swipeRefreshActivityLog.setOnRefreshListener(() -> viewModel.loadLogs()); + binding.swipeRefreshActivityLog.setOnRefreshListener(() -> viewModel.loadInitialData()); UIUtils.setupHamburgerMenu(binding.btnHamburgerActivityLog, this); return binding.getRoot(); @@ -49,7 +51,7 @@ public class ActivityLogFragment extends Fragment { @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); - viewModel.loadLogs(); + viewModel.loadInitialData(); } private void setupRecyclerView() { @@ -65,10 +67,18 @@ public class ActivityLogFragment extends Fragment { UIUtils.attachSearch(binding.etSearchLog, () -> viewModel.setSearchQuery(binding.etSearchLog.getText().toString())); - String[] roles = {"All Roles", "ADMIN", "STAFF"}; + String[] roles = {"All Roles", "Admin", "Staff", "Customer"}; SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRoleFilter, roles, () -> viewModel.setRoleFilter(binding.spinnerRoleFilter.getSelectedItem() != null ? binding.spinnerRoleFilter.getSelectedItem().toString() : "All Roles")); + + SpinnerUtils.setupFilterSpinner(binding.spinnerStoreFilter, this::onStoreSelected); + } + + private void onStoreSelected() { + int pos = binding.spinnerStoreFilter.getSelectedItemPosition(); + Long storeId = (pos > 0 && !storeList.isEmpty()) ? storeList.get(pos - 1).getId() : null; + viewModel.setStoreFilter(storeId); } private void observeViewModel() { @@ -78,16 +88,14 @@ public class ActivityLogFragment extends Fragment { adapter.notifyDataSetChanged(); }); - viewModel.getStoreOptions().observe(getViewLifecycleOwner(), options -> { - String[] arr = options.toArray(new String[0]); - SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStoreFilter, arr, () -> - viewModel.setStoreFilter(binding.spinnerStoreFilter.getSelectedItem() != null - ? binding.spinnerStoreFilter.getSelectedItem().toString() : "All Stores")); + viewModel.getStoreOptions().observe(getViewLifecycleOwner(), stores -> { + storeList = stores; + SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerStoreFilter, + stores, DropdownDTO::getLabel, "All Stores", -1L, DropdownDTO::getId); }); - viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> { - binding.swipeRefreshActivityLog.setRefreshing(loading); - }); + viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> + binding.swipeRefreshActivityLog.setRefreshing(loading)); } @Override diff --git a/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java b/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java index 53af40e5..866ea2ad 100644 --- a/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java +++ b/android/app/src/main/java/com/example/petstoremobile/repositories/ActivityLogRepository.java @@ -21,7 +21,7 @@ public class ActivityLogRepository extends BaseRepository { this.activityLogApi = activityLogApi; } - public LiveData>> getActivityLogs() { - return executeCall(activityLogApi.getActivityLogs()); + public LiveData>> getActivityLogs(int limit, Long storeId, String role, String search) { + return executeCall(activityLogApi.getActivityLogs(limit, storeId, role, search)); } } diff --git a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java index 33053cfc..8bcb5cc4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java +++ b/android/app/src/main/java/com/example/petstoremobile/viewmodels/ActivityLogListViewModel.java @@ -6,13 +6,13 @@ import androidx.lifecycle.Observer; import androidx.lifecycle.ViewModel; import com.example.petstoremobile.dtos.ActivityLogDTO; +import com.example.petstoremobile.dtos.DropdownDTO; import com.example.petstoremobile.repositories.ActivityLogRepository; +import com.example.petstoremobile.repositories.StoreRepository; import com.example.petstoremobile.utils.Resource; import java.util.ArrayList; -import java.util.LinkedHashSet; import java.util.List; -import java.util.Locale; import javax.inject.Inject; @@ -20,35 +20,48 @@ import dagger.hilt.android.lifecycle.HiltViewModel; @HiltViewModel public class ActivityLogListViewModel extends ViewModel { - private final ActivityLogRepository repository; + private static final int LIMIT = 2000; + + private final ActivityLogRepository repository; + private final StoreRepository storeRepository; - private final List allLogs = new ArrayList<>(); private final MutableLiveData> logs = new MutableLiveData<>(new ArrayList<>()); - private final MutableLiveData> storeOptions = new MutableLiveData<>(new ArrayList<>()); + private final MutableLiveData> storeOptions = new MutableLiveData<>(new ArrayList<>()); private final MutableLiveData isLoading = new MutableLiveData<>(false); - private String searchQuery = ""; - private String roleFilter = "All Roles"; - private String storeFilter = "All Stores"; + private Long currentStoreId = null; + private String currentRole = null; + private String currentSearch = null; @Inject - public ActivityLogListViewModel(ActivityLogRepository repository) { + public ActivityLogListViewModel(ActivityLogRepository repository, StoreRepository storeRepository) { this.repository = repository; + this.storeRepository = storeRepository; } public LiveData> getLogs() { return logs; } - public LiveData> getStoreOptions() { return storeOptions; } + public LiveData> getStoreOptions() { return storeOptions; } public LiveData getIsLoading() { return isLoading; } + public void loadInitialData() { + loadStores(); + loadLogs(); + } + + private void loadStores() { + observeOnce(storeRepository.getStoreDropdowns(), resource -> { + if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) { + storeOptions.setValue(resource.data); + } + }); + } + public void loadLogs() { isLoading.setValue(true); - observeOnce(repository.getActivityLogs(), resource -> { + observeOnce(repository.getActivityLogs(LIMIT, currentStoreId, currentRole, currentSearch), resource -> { if (resource != null) { if (resource.status == Resource.Status.SUCCESS && resource.data != null) { - allLogs.clear(); - allLogs.addAll(resource.data); - buildStoreOptions(); - applyFilters(); + logs.setValue(resource.data); isLoading.setValue(false); } else if (resource.status == Resource.Status.ERROR) { isLoading.setValue(false); @@ -57,66 +70,19 @@ public class ActivityLogListViewModel extends ViewModel { }); } - public void setSearchQuery(String query) { - searchQuery = query == null ? "" : query.trim(); - applyFilters(); - } - public void setRoleFilter(String role) { - roleFilter = role == null ? "All Roles" : role; - applyFilters(); + currentRole = "All Roles".equals(role) ? null : role; + loadLogs(); } - public void setStoreFilter(String store) { - storeFilter = store == null ? "All Stores" : store; - applyFilters(); + public void setStoreFilter(Long storeId) { + currentStoreId = storeId; + loadLogs(); } - private void buildStoreOptions() { - LinkedHashSet names = new LinkedHashSet<>(); - for (ActivityLogDTO log : allLogs) { - String name = log.getStoreNameSnapshot() != null ? log.getStoreNameSnapshot() : log.getStoreName(); - if (name != null && !name.isEmpty()) names.add(name); - } - List options = new ArrayList<>(); - options.add("All Stores"); - options.addAll(names); - storeOptions.setValue(options); - } - - private void applyFilters() { - List filtered = new ArrayList<>(); - String query = searchQuery.toLowerCase(Locale.ROOT); - - for (ActivityLogDTO log : allLogs) { - // Role filter - if (!"All Roles".equals(roleFilter)) { - String role = log.getRoleSnapshot() != null ? log.getRoleSnapshot() : log.getRole(); - if (!roleFilter.equalsIgnoreCase(role)) continue; - } - - // Store filter - if (!"All Stores".equals(storeFilter)) { - String store = log.getStoreNameSnapshot() != null ? log.getStoreNameSnapshot() : log.getStoreName(); - if (!storeFilter.equals(store)) continue; - } - - // Search filter - if (!query.isEmpty()) { - String activity = log.getActivity() != null ? log.getActivity().toLowerCase(Locale.ROOT) : ""; - String fullName = log.getFullNameSnapshot() != null - ? log.getFullNameSnapshot().toLowerCase(Locale.ROOT) - : (log.getFullName() != null ? log.getFullName().toLowerCase(Locale.ROOT) : ""); - String username = log.getUsernameSnapshot() != null - ? log.getUsernameSnapshot().toLowerCase(Locale.ROOT) - : (log.getUsername() != null ? log.getUsername().toLowerCase(Locale.ROOT) : ""); - if (!activity.contains(query) && !fullName.contains(query) && !username.contains(query)) continue; - } - - filtered.add(log); - } - - logs.setValue(filtered); + public void setSearchQuery(String query) { + currentSearch = (query == null || query.trim().isEmpty()) ? null : query.trim(); + loadLogs(); } private void observeOnce(LiveData> liveData, Observer> handler) { diff --git a/backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java b/backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java index 650fe6f0..49e4cc85 100644 --- a/backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java +++ b/backend/src/main/java/com/petshop/backend/controller/ActivityLogController.java @@ -23,8 +23,11 @@ public class ActivityLogController { @GetMapping public ResponseEntity> getActivityLogs( - @RequestParam(defaultValue = "2000") int limit) { + @RequestParam(defaultValue = "2000") int limit, + @RequestParam(required = false) Long storeId, + @RequestParam(required = false) String role, + @RequestParam(required = false) String search) { int safeLimit = Math.min(Math.max(1, limit), 10000); - return ResponseEntity.ok(activityLogService.getLogs(safeLimit)); + return ResponseEntity.ok(activityLogService.getLogs(safeLimit, storeId, role, search)); } } diff --git a/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java b/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java index fa79d06a..09bf817a 100644 --- a/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java +++ b/backend/src/main/java/com/petshop/backend/repository/ActivityLogRepository.java @@ -2,13 +2,15 @@ package com.petshop.backend.repository; import com.petshop.backend.entity.ActivityLog; import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.Query; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; import org.springframework.stereotype.Repository; + import java.util.List; @Repository -public interface ActivityLogRepository extends JpaRepository { +public interface ActivityLogRepository extends JpaRepository, JpaSpecificationExecutor { boolean existsByUser_Id(Long userId); @Query("select a from ActivityLog a order by a.logTimestamp desc, a.logId desc") diff --git a/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java b/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java index f5cd942c..76acaa6c 100644 --- a/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java +++ b/backend/src/main/java/com/petshop/backend/service/ActivityLogService.java @@ -6,12 +6,16 @@ import com.petshop.backend.entity.StoreLocation; import com.petshop.backend.entity.User; import com.petshop.backend.repository.ActivityLogRepository; import com.petshop.backend.repository.UserRepository; +import jakarta.persistence.criteria.Predicate; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.ArrayList; import java.util.List; @Service @@ -60,9 +64,41 @@ public class ActivityLogService { record(user.getId(), activity); } + @Transactional(readOnly = true) + public List getLogs(int limit, Long storeId, String role, String search) { + Specification spec = (root, query, cb) -> { + List predicates = new ArrayList<>(); + + if (storeId != null) { + predicates.add(cb.equal(root.get("store").get("storeId"), storeId)); + } + + if (role != null && !role.isBlank()) { + predicates.add(cb.equal(root.get("roleSnapshot"), role)); + } + + if (search != null && !search.isBlank()) { + String pattern = "%" + search.toLowerCase() + "%"; + Predicate searchPredicate = cb.or( + cb.like(cb.lower(root.get("activity")), pattern), + cb.like(cb.lower(root.get("fullNameSnapshot")), pattern), + cb.like(cb.lower(root.get("usernameSnapshot")), pattern) + ); + predicates.add(searchPredicate); + } + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + PageRequest pageRequest = PageRequest.of(0, limit, Sort.by(Sort.Direction.DESC, "logTimestamp", "logId")); + return activityLogRepository.findAll(spec, pageRequest).stream() + .map(this::toResponse) + .toList(); + } + @Transactional(readOnly = true) public List getLogs(int limit) { - return activityLogRepository.findRecent(PageRequest.of(0, limit)).stream().map(this::toResponse).toList(); + return getLogs(limit, null, null, null); } private ActivityLogResponse toResponse(ActivityLog entry) {