Merge branch 'AttachmentsToChat'
This commit is contained in:
@@ -10,11 +10,19 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.dtos.ActivityLogDTO;
|
import com.example.petstoremobile.dtos.ActivityLogDTO;
|
||||||
import com.example.petstoremobile.utils.DateTimeUtils;
|
|
||||||
|
|
||||||
|
import java.text.ParseException;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
|
|
||||||
public class ActivityLogAdapter extends RecyclerView.Adapter<ActivityLogAdapter.ViewHolder> {
|
public class ActivityLogAdapter extends RecyclerView.Adapter<ActivityLogAdapter.ViewHolder> {
|
||||||
|
|
||||||
|
private static final String SEPARATOR = " | ";
|
||||||
|
private static final SimpleDateFormat INPUT_FORMAT = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
|
||||||
|
private static final SimpleDateFormat OUTPUT_FORMAT = new SimpleDateFormat("MMM d, HH:mm", Locale.getDefault());
|
||||||
|
|
||||||
private final List<ActivityLogDTO> items;
|
private final List<ActivityLogDTO> items;
|
||||||
|
|
||||||
public ActivityLogAdapter(List<ActivityLogDTO> items) {
|
public ActivityLogAdapter(List<ActivityLogDTO> items) {
|
||||||
@@ -32,26 +40,77 @@ public class ActivityLogAdapter extends RecyclerView.Adapter<ActivityLogAdapter.
|
|||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
ActivityLogDTO log = items.get(position);
|
ActivityLogDTO log = items.get(position);
|
||||||
holder.tvActivity.setText(log.getActivity());
|
|
||||||
holder.tvUser.setText(log.getFullName() + " (" + log.getUsername() + ")");
|
String activity = log.getActivity() != null ? log.getActivity() : "";
|
||||||
holder.tvMeta.setText(log.getStoreName() + " · " + log.getRole());
|
int separatorIndex = activity.indexOf(SEPARATOR);
|
||||||
String timestamp = log.getLogTimestamp();
|
if (separatorIndex >= 0) {
|
||||||
String date = DateTimeUtils.extractDate(timestamp);
|
holder.tvActivity.setText(activity.substring(0, separatorIndex).trim());
|
||||||
String time = (timestamp != null && timestamp.length() >= 16) ? timestamp.substring(11, 16) : null;
|
holder.tvTechnical.setText(activity.substring(separatorIndex + SEPARATOR.length()).trim());
|
||||||
holder.tvTimestamp.setText(date != null && time != null ? date + " " + time : date);
|
holder.tvTechnical.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
holder.tvActivity.setText(activity);
|
||||||
|
holder.tvTechnical.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
String fullName = firstNonBlank(log.getFullName(), log.getFullNameSnapshot(), "Unknown");
|
||||||
|
String username = firstNonBlank(log.getUsername(), log.getUsernameSnapshot(), "");
|
||||||
|
holder.tvUser.setText(username.isEmpty() ? fullName : fullName + " (" + username + ")");
|
||||||
|
|
||||||
|
String role = firstNonBlank(log.getRole(), log.getRoleSnapshot(), "");
|
||||||
|
String store = firstNonBlank(log.getStoreName(), log.getStoreNameSnapshot(), "");
|
||||||
|
if (!role.isEmpty() && !store.isEmpty()) {
|
||||||
|
holder.tvMeta.setText(store + " · " + formatRole(role));
|
||||||
|
} else if (!role.isEmpty()) {
|
||||||
|
holder.tvMeta.setText(formatRole(role));
|
||||||
|
} else if (!store.isEmpty()) {
|
||||||
|
holder.tvMeta.setText(store);
|
||||||
|
} else {
|
||||||
|
holder.tvMeta.setText("");
|
||||||
|
}
|
||||||
|
|
||||||
|
holder.tvTimestamp.setText(formatTimestamp(log.getLogTimestamp()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemCount() { return items.size(); }
|
public int getItemCount() { return items.size(); }
|
||||||
|
|
||||||
|
private String formatTimestamp(String raw) {
|
||||||
|
if (raw == null) return "";
|
||||||
|
try {
|
||||||
|
String normalized = raw.length() > 19 ? raw.substring(0, 19) : raw;
|
||||||
|
Date date = INPUT_FORMAT.parse(normalized);
|
||||||
|
return date != null ? OUTPUT_FORMAT.format(date) : raw.substring(0, Math.min(16, raw.length())).replace("T", " ");
|
||||||
|
} catch (ParseException e) {
|
||||||
|
return raw.length() >= 16 ? raw.substring(0, 16).replace("T", " ") : raw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String formatRole(String role) {
|
||||||
|
if (role == null) return "";
|
||||||
|
switch (role.toUpperCase(Locale.ROOT)) {
|
||||||
|
case "ADMIN": return "Admin";
|
||||||
|
case "STAFF": return "Staff";
|
||||||
|
case "CUSTOMER": return "Customer";
|
||||||
|
default: return role;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstNonBlank(String... values) {
|
||||||
|
for (String v : values) {
|
||||||
|
if (v != null && !v.isBlank()) return v;
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
public static class ViewHolder extends RecyclerView.ViewHolder {
|
public static class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
TextView tvActivity, tvUser, tvMeta, tvTimestamp;
|
TextView tvActivity, tvTechnical, tvUser, tvMeta, tvTimestamp;
|
||||||
|
|
||||||
public ViewHolder(@NonNull View itemView) {
|
public ViewHolder(@NonNull View itemView) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
tvActivity = itemView.findViewById(R.id.tvLogActivity);
|
tvActivity = itemView.findViewById(R.id.tvLogActivity);
|
||||||
tvUser = itemView.findViewById(R.id.tvLogUser);
|
tvTechnical = itemView.findViewById(R.id.tvLogTechnical);
|
||||||
tvMeta = itemView.findViewById(R.id.tvLogMeta);
|
tvUser = itemView.findViewById(R.id.tvLogUser);
|
||||||
|
tvMeta = itemView.findViewById(R.id.tvLogMeta);
|
||||||
tvTimestamp = itemView.findViewById(R.id.tvLogTimestamp);
|
tvTimestamp = itemView.findViewById(R.id.tvLogTimestamp);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.example.petstoremobile.adapters;
|
package com.example.petstoremobile.adapters;
|
||||||
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
@@ -49,27 +48,6 @@ public class PurchaseOrderAdapter extends RecyclerView.Adapter<PurchaseOrderAdap
|
|||||||
binding.tvPOStore.setText("Store: " + (po.getStoreName() != null ? po.getStoreName() : ""));
|
binding.tvPOStore.setText("Store: " + (po.getStoreName() != null ? po.getStoreName() : ""));
|
||||||
binding.tvPODate.setText("Date: " + (po.getOrderDate() != null ? po.getOrderDate() : ""));
|
binding.tvPODate.setText("Date: " + (po.getOrderDate() != null ? po.getOrderDate() : ""));
|
||||||
|
|
||||||
String status = po.getStatus() != null ? po.getStatus() : "";
|
|
||||||
binding.tvPOStatus.setText(status);
|
|
||||||
|
|
||||||
switch (status.toUpperCase()) {
|
|
||||||
case "RECEIVED":
|
|
||||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#4CAF50"));
|
|
||||||
break;
|
|
||||||
case "PLACED":
|
|
||||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#2196F3"));
|
|
||||||
break;
|
|
||||||
case "PENDING":
|
|
||||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#FF9800"));
|
|
||||||
break;
|
|
||||||
case "CANCELLED":
|
|
||||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#F44336"));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
binding.tvPOStatus.setBackgroundColor(Color.parseColor("#9E9E9E"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
holder.itemView.setOnClickListener(v -> listener.onPurchaseOrderClick(position));
|
holder.itemView.setOnClickListener(v -> listener.onPurchaseOrderClick(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@ public interface ActivityLogApi {
|
|||||||
@Query("limit") int limit,
|
@Query("limit") int limit,
|
||||||
@Query("storeId") Long storeId,
|
@Query("storeId") Long storeId,
|
||||||
@Query("role") String role,
|
@Query("role") String role,
|
||||||
@Query("search") String search
|
@Query("search") String search,
|
||||||
|
@Query("startDate") String startDate,
|
||||||
|
@Query("endDate") String endDate
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.example.petstoremobile.fragments.listfragments;
|
package com.example.petstoremobile.fragments.listfragments;
|
||||||
|
|
||||||
|
import android.app.DatePickerDialog;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -10,6 +11,7 @@ import androidx.annotation.Nullable;
|
|||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
@@ -27,6 +29,7 @@ import com.example.petstoremobile.viewmodels.ActivityLogListViewModel;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
import dagger.hilt.android.AndroidEntryPoint;
|
||||||
@@ -38,6 +41,8 @@ public class ActivityLogFragment extends Fragment {
|
|||||||
private ActivityLogAdapter adapter;
|
private ActivityLogAdapter adapter;
|
||||||
private final List<ActivityLogDTO> logList = new ArrayList<>();
|
private final List<ActivityLogDTO> logList = new ArrayList<>();
|
||||||
private List<DropdownDTO> storeList = new ArrayList<>();
|
private List<DropdownDTO> storeList = new ArrayList<>();
|
||||||
|
private String selectedStartDate = null;
|
||||||
|
private String selectedEndDate = null;
|
||||||
|
|
||||||
@Inject TokenManager tokenManager;
|
@Inject TokenManager tokenManager;
|
||||||
|
|
||||||
@@ -74,6 +79,22 @@ public class ActivityLogFragment extends Fragment {
|
|||||||
adapter = new ActivityLogAdapter(logList);
|
adapter = new ActivityLogAdapter(logList);
|
||||||
binding.recyclerViewActivityLog.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewActivityLog.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewActivityLog.setAdapter(adapter);
|
binding.recyclerViewActivityLog.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewActivityLog.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewActivityLog.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
viewModel.loadLogs(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupFilters() {
|
private void setupFilters() {
|
||||||
@@ -89,6 +110,35 @@ public class ActivityLogFragment extends Fragment {
|
|||||||
? binding.spinnerRoleFilter.getSelectedItem().toString() : "All Roles"));
|
? binding.spinnerRoleFilter.getSelectedItem().toString() : "All Roles"));
|
||||||
|
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreFilter, this::onStoreSelected);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreFilter, this::onStoreSelected);
|
||||||
|
|
||||||
|
binding.btnStartDate.setOnClickListener(v -> showDatePicker(true));
|
||||||
|
binding.btnEndDate.setOnClickListener(v -> showDatePicker(false));
|
||||||
|
binding.btnClearDates.setOnClickListener(v -> {
|
||||||
|
selectedStartDate = null;
|
||||||
|
selectedEndDate = null;
|
||||||
|
binding.btnStartDate.setText("Start Date");
|
||||||
|
binding.btnEndDate.setText("End Date");
|
||||||
|
binding.btnClearDates.setVisibility(View.GONE);
|
||||||
|
viewModel.setDateRange(null, null);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void showDatePicker(boolean isStart) {
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
new DatePickerDialog(requireContext(), (view, year, month, day) -> {
|
||||||
|
String date = String.format("%04d-%02d-%02d", year, month + 1, day);
|
||||||
|
String label = String.format("%02d/%02d/%04d", day, month + 1, year);
|
||||||
|
if (isStart) {
|
||||||
|
selectedStartDate = date;
|
||||||
|
binding.btnStartDate.setText(label);
|
||||||
|
} else {
|
||||||
|
selectedEndDate = date;
|
||||||
|
binding.btnEndDate.setText(label);
|
||||||
|
}
|
||||||
|
binding.btnClearDates.setVisibility(
|
||||||
|
selectedStartDate != null || selectedEndDate != null ? View.VISIBLE : View.GONE);
|
||||||
|
viewModel.setDateRange(selectedStartDate, selectedEndDate);
|
||||||
|
}, cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH)).show();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onStoreSelected() {
|
private void onStoreSelected() {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.AdoptionAdapter;
|
import com.example.petstoremobile.adapters.AdoptionAdapter;
|
||||||
@@ -114,14 +115,14 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
|||||||
adapter,
|
adapter,
|
||||||
"adoption",
|
"adoption",
|
||||||
viewModel::bulkDeleteAdoptions,
|
viewModel::bulkDeleteAdoptions,
|
||||||
this::loadAdoptions
|
() -> loadAdoptions(true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
loadAdoptions();
|
loadAdoptions(true);
|
||||||
if (!isStaff()) viewModel.loadStores();
|
if (!isStaff()) viewModel.loadStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,7 +160,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
|||||||
} else {
|
} else {
|
||||||
selectedCalendarDay = null;
|
selectedCalendarDay = null;
|
||||||
}
|
}
|
||||||
loadAdoptions();
|
loadAdoptions(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -187,26 +188,42 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
|||||||
adapter = new AdoptionAdapter(adoptionList, this);
|
adapter = new AdoptionAdapter(adoptionList, this);
|
||||||
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewAdoptions.setAdapter(adapter);
|
binding.recyclerViewAdoptions.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewAdoptions.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewAdoptions.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
loadAdoptions(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions);
|
UIUtils.attachSearch(binding.etSearchAdoption, () -> loadAdoptions(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStatusFilter() {
|
private void setupStatusFilter() {
|
||||||
String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"};
|
String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"};
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, () -> loadAdoptions(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStoreFilter() {
|
private void setupStoreFilter() {
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, () -> loadAdoptions(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshAdoption.setOnRefreshListener(this::loadAdoptions);
|
binding.swipeRefreshAdoption.setOnRefreshListener(() -> loadAdoptions(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadAdoptions() {
|
private void loadAdoptions(boolean reset) {
|
||||||
String query = binding.etSearchAdoption.getText().toString().trim();
|
String query = binding.etSearchAdoption.getText().toString().trim();
|
||||||
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
|
String status = binding.spinnerStatusAdoption.getSelectedItem() != null ? binding.spinnerStatusAdoption.getSelectedItem().toString() : "All Statuses";
|
||||||
|
|
||||||
@@ -230,7 +247,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
|||||||
if (status.equals("All Statuses")) status = null;
|
if (status.equals("All Statuses")) status = null;
|
||||||
else status = status.toUpperCase();
|
else status = status.toUpperCase();
|
||||||
|
|
||||||
viewModel.loadAdoptions(true, query, status, storeId, selectedDateString, null);
|
viewModel.loadAdoptions(reset, query, status, storeId, selectedDateString, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetail(int position) {
|
private void openDetail(int position) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
@@ -123,14 +124,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
adapter,
|
adapter,
|
||||||
"appointment",
|
"appointment",
|
||||||
viewModel::bulkDeleteAppointments,
|
viewModel::bulkDeleteAppointments,
|
||||||
this::loadAppointmentData
|
() -> loadAppointmentData(true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
loadAppointmentData();
|
loadAppointmentData(true);
|
||||||
if (!isStaff()) viewModel.loadStores();
|
if (!isStaff()) viewModel.loadStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -143,7 +144,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
|
|
||||||
private void setupMyAppointmentFilter() {
|
private void setupMyAppointmentFilter() {
|
||||||
binding.btnMyAppointments.setOnClickListener(v -> {
|
binding.btnMyAppointments.setOnClickListener(v -> {
|
||||||
loadAppointmentData();
|
loadAppointmentData(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -177,7 +178,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
} else {
|
} else {
|
||||||
selectedCalendarDay = null;
|
selectedCalendarDay = null;
|
||||||
}
|
}
|
||||||
loadAppointmentData();
|
loadAppointmentData(true);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,20 +201,20 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData);
|
UIUtils.attachSearch(binding.etSearchAppointment, () -> loadAppointmentData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStatusFilter() {
|
private void setupStatusFilter() {
|
||||||
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
|
String[] statuses = {"All Statuses", "Booked", "Completed", "Cancelled", "Missed"};
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadAppointmentData);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadAppointmentData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStoreFilter() {
|
private void setupStoreFilter() {
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadAppointmentData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
|
binding.swipeRefreshAppointment.setOnRefreshListener(() -> loadAppointmentData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openAppointmentDetails(int position) {
|
private void openAppointmentDetails(int position) {
|
||||||
@@ -241,7 +242,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
|
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadAppointmentData() {
|
private void loadAppointmentData(boolean reset) {
|
||||||
String query = binding.etSearchAppointment.getText().toString().trim();
|
String query = binding.etSearchAppointment.getText().toString().trim();
|
||||||
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||||
|
|
||||||
@@ -270,13 +271,29 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
|||||||
if (status.equals("All Statuses")) status = null;
|
if (status.equals("All Statuses")) status = null;
|
||||||
else status = status.toUpperCase();
|
else status = status.toUpperCase();
|
||||||
|
|
||||||
viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId);
|
viewModel.loadAppointments(reset, query, status, storeId, selectedDateString, employeeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerView() {
|
||||||
adapter = new AppointmentAdapter(appointmentList, this);
|
adapter = new AppointmentAdapter(appointmentList, this);
|
||||||
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewAppointments.setAdapter(adapter);
|
binding.recyclerViewAppointments.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewAppointments.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewAppointments.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
loadAppointmentData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.Navigation;
|
import androidx.navigation.Navigation;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.CouponAdapter;
|
import com.example.petstoremobile.adapters.CouponAdapter;
|
||||||
@@ -46,7 +47,7 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl
|
|||||||
setupSwipeRefresh();
|
setupSwipeRefresh();
|
||||||
observeViewModel();
|
observeViewModel();
|
||||||
|
|
||||||
viewModel.loadCoupons(0, 100, null, null, null);
|
applyFilters(true);
|
||||||
|
|
||||||
binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1));
|
binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1));
|
||||||
binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete());
|
binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete());
|
||||||
@@ -74,38 +75,54 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl
|
|||||||
adapter = new CouponAdapter(couponList, this);
|
adapter = new CouponAdapter(couponList, this);
|
||||||
binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewCoupon.setAdapter(adapter);
|
binding.recyclerViewCoupon.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewCoupon.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewCoupon.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
applyFilters(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStatusFilter() {
|
private void setupStatusFilter() {
|
||||||
String[] statuses = {"All Statuses", "Active", "Inactive"};
|
String[] statuses = {"All Statuses", "Active", "Inactive"};
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, this::applyFilters);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusCoupon, statuses, () -> applyFilters(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupTypeFilter() {
|
private void setupTypeFilter() {
|
||||||
String[] types = {"All Types", "FIXED", "PERCENT"};
|
String[] types = {"All Types", "FIXED", "PERCENT"};
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, this::applyFilters);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerTypeCoupon, types, () -> applyFilters(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchCoupon, this::applyFilters);
|
UIUtils.attachSearch(binding.etSearchCoupon, () -> applyFilters(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshCoupon.setOnRefreshListener(this::applyFilters);
|
binding.swipeRefreshCoupon.setOnRefreshListener(() -> applyFilters(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyFilters() {
|
private void applyFilters(boolean reset) {
|
||||||
String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ?
|
String statusStr = binding.spinnerStatusCoupon.getSelectedItem() != null ?
|
||||||
binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses";
|
binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses";
|
||||||
Boolean active = null;
|
Boolean active = null;
|
||||||
if (statusStr.equals("Active")) active = true;
|
if (statusStr.equals("Active")) active = true;
|
||||||
else if (statusStr.equals("Inactive")) active = false;
|
else if (statusStr.equals("Inactive")) active = false;
|
||||||
|
|
||||||
String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ?
|
String typeStr = binding.spinnerTypeCoupon.getSelectedItem() != null ?
|
||||||
binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types";
|
binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types";
|
||||||
String discountType = typeStr.equals("All Types") ? null : typeStr;
|
String discountType = typeStr.equals("All Types") ? null : typeStr;
|
||||||
|
|
||||||
viewModel.loadCoupons(0, 100, active, discountType, null);
|
viewModel.loadCoupons(reset, active, discountType, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetail(long id) {
|
private void openDetail(long id) {
|
||||||
@@ -133,7 +150,7 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl
|
|||||||
viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> {
|
viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> {
|
||||||
if (resource.status == Resource.Status.SUCCESS) {
|
if (resource.status == Resource.Status.SUCCESS) {
|
||||||
adapter.setSelectionMode(false);
|
adapter.setSelectionMode(false);
|
||||||
applyFilters();
|
applyFilters(true);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
UIUtils.showToast(requireContext(), resource.message);
|
UIUtils.showToast(requireContext(), resource.message);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.CustomerAdapter;
|
import com.example.petstoremobile.adapters.CustomerAdapter;
|
||||||
import com.example.petstoremobile.api.auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
@@ -44,7 +45,7 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust
|
|||||||
setupSwipeRefresh();
|
setupSwipeRefresh();
|
||||||
observeViewModel();
|
observeViewModel();
|
||||||
|
|
||||||
viewModel.loadCustomers();
|
viewModel.loadCustomers(true);
|
||||||
|
|
||||||
binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1));
|
binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1));
|
||||||
|
|
||||||
@@ -73,6 +74,22 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust
|
|||||||
adapter.setToken(tokenManager.getToken());
|
adapter.setToken(tokenManager.getToken());
|
||||||
binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewCustomer.setAdapter(adapter);
|
binding.recyclerViewCustomer.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewCustomer.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewCustomer.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
viewModel.loadCustomers(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStatusFilter() {
|
private void setupStatusFilter() {
|
||||||
@@ -92,7 +109,7 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshCustomer.setOnRefreshListener(viewModel::loadCustomers);
|
binding.swipeRefreshCustomer.setOnRefreshListener(() -> viewModel.loadCustomers(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetail(int position) {
|
private void openDetail(int position) {
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.Toast;
|
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.PetAdapter;
|
import com.example.petstoremobile.adapters.PetAdapter;
|
||||||
@@ -21,7 +21,6 @@ import com.example.petstoremobile.databinding.FragmentPetBinding;
|
|||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
import com.example.petstoremobile.dtos.StoreDTO;
|
import com.example.petstoremobile.dtos.StoreDTO;
|
||||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
|
||||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||||
import com.example.petstoremobile.utils.UIUtils;
|
import com.example.petstoremobile.utils.UIUtils;
|
||||||
import com.example.petstoremobile.viewmodels.PetListViewModel;
|
import com.example.petstoremobile.viewmodels.PetListViewModel;
|
||||||
@@ -91,7 +90,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
|
|
||||||
viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> {
|
viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> {
|
||||||
String[] arr = options.toArray(new String[0]);
|
String[] arr = options.toArray(new String[0]);
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, this::loadPetData);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, arr, () -> loadPetData(true));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,14 +103,14 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
adapter,
|
adapter,
|
||||||
"pet",
|
"pet",
|
||||||
viewModel::bulkDeletePets,
|
viewModel::bulkDeletePets,
|
||||||
this::loadPetData
|
() -> loadPetData(true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
loadPetData();
|
loadPetData(true);
|
||||||
viewModel.loadSpecies();
|
viewModel.loadSpecies();
|
||||||
if (!isStaff()) viewModel.loadStores();
|
if (!isStaff()) viewModel.loadStores();
|
||||||
}
|
}
|
||||||
@@ -132,28 +131,28 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchPet, this::loadPetData);
|
UIUtils.attachSearch(binding.etSearchPet, () -> loadPetData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStatusFilter() {
|
private void setupStatusFilter() {
|
||||||
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"};
|
String[] statuses = {"All Statuses", "Available", "Adopted", "Owned", "Pending"};
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, this::loadPetData);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatus, statuses, () -> loadPetData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSpeciesFilter() {
|
private void setupSpeciesFilter() {
|
||||||
String[] initial = {"All Species"};
|
String[] initial = {"All Species"};
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, this::loadPetData);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, () -> loadPetData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStoreFilter() {
|
private void setupStoreFilter() {
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadPetData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshPet.setOnRefreshListener(this::loadPetData);
|
binding.swipeRefreshPet.setOnRefreshListener(() -> loadPetData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadPetData() {
|
private void loadPetData(boolean reset) {
|
||||||
String query = binding.etSearchPet.getText().toString().trim();
|
String query = binding.etSearchPet.getText().toString().trim();
|
||||||
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
String status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||||
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
|
String species = binding.spinnerSpecies.getSelectedItem() != null ? binding.spinnerSpecies.getSelectedItem().toString() : "All Species";
|
||||||
@@ -169,7 +168,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadPets(query, status, species, storeId);
|
viewModel.loadPets(reset, query, status, species, storeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerView() {
|
||||||
@@ -178,6 +177,22 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
|||||||
adapter.setToken(tokenManager.getToken());
|
adapter.setToken(tokenManager.getToken());
|
||||||
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewPets.setAdapter(adapter);
|
binding.recyclerViewPets.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewPets.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPets.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
loadPetData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openPetProfile(int position) {
|
private void openPetProfile(int position) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -85,28 +86,28 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
loadProductData();
|
loadProductData(true);
|
||||||
viewModel.loadCategories();
|
viewModel.loadCategories();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupFilterToggle() {
|
private void setupFilterToggle() {
|
||||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
|
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter,
|
||||||
binding.etSearchProduct, binding.spinnerCategory);
|
binding.etSearchProduct, binding.spinnerCategory);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData);
|
UIUtils.attachSearch(binding.etSearchProduct, () -> loadProductData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupCategoryFilter() {
|
private void setupCategoryFilter() {
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, () -> loadProductData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshProduct.setOnRefreshListener(this::loadProductData);
|
binding.swipeRefreshProduct.setOnRefreshListener(() -> loadProductData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadProductData() {
|
private void loadProductData(boolean reset) {
|
||||||
String query = binding.etSearchProduct.getText().toString().trim();
|
String query = binding.etSearchProduct.getText().toString().trim();
|
||||||
if (query.isEmpty()) query = null;
|
if (query.isEmpty()) query = null;
|
||||||
|
|
||||||
@@ -116,7 +117,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
|||||||
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId();
|
categoryId = categories.get(binding.spinnerCategory.getSelectedItemPosition() - 1).getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadProducts(query, categoryId);
|
viewModel.loadProducts(reset, query, categoryId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerView() {
|
||||||
@@ -124,6 +125,22 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
|||||||
adapter.setBaseUrl(baseUrl);
|
adapter.setBaseUrl(baseUrl);
|
||||||
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewProducts.setAdapter(adapter);
|
binding.recyclerViewProducts.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewProducts.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewProducts.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
loadProductData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openProductDetails(int position) {
|
private void openProductDetails(int position) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.ProductSupplierAdapter;
|
import com.example.petstoremobile.adapters.ProductSupplierAdapter;
|
||||||
@@ -34,7 +35,7 @@ public class ProductSupplierFragment extends Fragment
|
|||||||
|
|
||||||
private FragmentProductSupplierBinding binding;
|
private FragmentProductSupplierBinding binding;
|
||||||
private List<ProductSupplierDTO> psList = new ArrayList<>();
|
private List<ProductSupplierDTO> psList = new ArrayList<>();
|
||||||
|
|
||||||
private ProductSupplierAdapter adapter;
|
private ProductSupplierAdapter adapter;
|
||||||
private ProductSupplierListViewModel viewModel;
|
private ProductSupplierListViewModel viewModel;
|
||||||
private BulkDeleteHandler bulkDeleteHandler;
|
private BulkDeleteHandler bulkDeleteHandler;
|
||||||
@@ -97,14 +98,14 @@ public class ProductSupplierFragment extends Fragment
|
|||||||
adapter,
|
adapter,
|
||||||
"relationship",
|
"relationship",
|
||||||
viewModel::bulkDeleteProductSuppliers,
|
viewModel::bulkDeleteProductSuppliers,
|
||||||
this::loadData
|
() -> loadData(true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
loadData();
|
loadData(true);
|
||||||
viewModel.loadFilterData();
|
viewModel.loadFilterData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,25 +124,41 @@ public class ProductSupplierFragment extends Fragment
|
|||||||
adapter = new ProductSupplierAdapter(psList, this);
|
adapter = new ProductSupplierAdapter(psList, this);
|
||||||
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewPS.setAdapter(adapter);
|
binding.recyclerViewPS.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewPS.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPS.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
loadData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchPS, this::loadData);
|
UIUtils.attachSearch(binding.etSearchPS, () -> loadData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupProductFilter() {
|
private void setupProductFilter() {
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, () -> loadData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSupplierFilter() {
|
private void setupSupplierFilter() {
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, () -> loadData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshPS.setOnRefreshListener(this::loadData);
|
binding.swipeRefreshPS.setOnRefreshListener(() -> loadData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadData() {
|
private void loadData(boolean reset) {
|
||||||
String query = binding.etSearchPS.getText().toString().trim();
|
String query = binding.etSearchPS.getText().toString().trim();
|
||||||
if (query.isEmpty()) query = null;
|
if (query.isEmpty()) query = null;
|
||||||
|
|
||||||
@@ -157,7 +174,7 @@ public class ProductSupplierFragment extends Fragment
|
|||||||
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
|
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadProductSuppliers(query, productId, supplierId);
|
viewModel.loadProductSuppliers(reset, query, productId, supplierId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetail(int position) {
|
private void openDetail(int position) {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
|
import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
|
||||||
@@ -83,7 +84,7 @@ public class PurchaseOrderFragment extends Fragment
|
|||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
loadData();
|
loadData(true);
|
||||||
if (!isStaff()) viewModel.loadStores();
|
if (!isStaff()) viewModel.loadStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -101,24 +102,40 @@ public class PurchaseOrderFragment extends Fragment
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchPO, this::loadData);
|
UIUtils.attachSearch(binding.etSearchPO, () -> loadData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStoreFilter() {
|
private void setupStoreFilter() {
|
||||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData);
|
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerView() {
|
||||||
adapter = new PurchaseOrderAdapter(poList, this);
|
adapter = new PurchaseOrderAdapter(poList, this);
|
||||||
binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewPO.setAdapter(adapter);
|
binding.recyclerViewPO.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewPO.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewPO.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
loadData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshPO.setOnRefreshListener(this::loadData);
|
binding.swipeRefreshPO.setOnRefreshListener(() -> loadData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadData() {
|
private void loadData(boolean reset) {
|
||||||
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
|
String query = binding.etSearchPO != null ? binding.etSearchPO.getText().toString().trim() : "";
|
||||||
if (query.isEmpty()) query = null;
|
if (query.isEmpty()) query = null;
|
||||||
|
|
||||||
@@ -133,7 +150,7 @@ public class PurchaseOrderFragment extends Fragment
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
viewModel.loadPurchaseOrders(query, storeId);
|
viewModel.loadPurchaseOrders(reset, query, storeId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetail(int position) {
|
private void openDetail(int position) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.adapters.EmployeeAdapter;
|
import com.example.petstoremobile.adapters.EmployeeAdapter;
|
||||||
import com.example.petstoremobile.api.auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
@@ -39,15 +40,15 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
|||||||
Bundle savedInstanceState) {
|
Bundle savedInstanceState) {
|
||||||
binding = FragmentStaffBinding.inflate(inflater, container, false);
|
binding = FragmentStaffBinding.inflate(inflater, container, false);
|
||||||
viewModel = new ViewModelProvider(this).get(StaffListViewModel.class);
|
viewModel = new ViewModelProvider(this).get(StaffListViewModel.class);
|
||||||
|
|
||||||
setupRecyclerView();
|
setupRecyclerView();
|
||||||
setupSearch();
|
setupSearch();
|
||||||
setupStatusFilter();
|
setupStatusFilter();
|
||||||
setupStoreFilter();
|
setupStoreFilter();
|
||||||
setupSwipeRefresh();
|
setupSwipeRefresh();
|
||||||
observeViewModel();
|
observeViewModel();
|
||||||
|
|
||||||
viewModel.loadStaff();
|
viewModel.loadStaff(true);
|
||||||
viewModel.loadStores();
|
viewModel.loadStores();
|
||||||
|
|
||||||
binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
|
binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
|
||||||
@@ -82,6 +83,22 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
|||||||
adapter.setToken(tokenManager.getToken());
|
adapter.setToken(tokenManager.getToken());
|
||||||
binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewStaff.setAdapter(adapter);
|
binding.recyclerViewStaff.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewStaff.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewStaff.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
viewModel.loadStaff(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupStatusFilter() {
|
private void setupStatusFilter() {
|
||||||
@@ -99,9 +116,9 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
|||||||
|
|
||||||
private void applyFilters() {
|
private void applyFilters() {
|
||||||
String query = binding.etSearchStaff.getText().toString().trim();
|
String query = binding.etSearchStaff.getText().toString().trim();
|
||||||
String status = binding.spinnerStatusStaff.getSelectedItem() != null ?
|
String status = binding.spinnerStatusStaff.getSelectedItem() != null ?
|
||||||
binding.spinnerStatusStaff.getSelectedItem().toString() : "All Statuses";
|
binding.spinnerStatusStaff.getSelectedItem().toString() : "All Statuses";
|
||||||
|
|
||||||
Long storeId = null;
|
Long storeId = null;
|
||||||
List<StoreDTO> stores = viewModel.getStores().getValue();
|
List<StoreDTO> stores = viewModel.getStores().getValue();
|
||||||
if (binding.spinnerStoreStaff.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
if (binding.spinnerStoreStaff.getSelectedItemPosition() > 0 && stores != null && !stores.isEmpty()) {
|
||||||
@@ -112,7 +129,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff);
|
binding.swipeRefreshStaff.setOnRefreshListener(() -> viewModel.loadStaff(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openDetail(int position) {
|
private void openDetail(int position) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment;
|
|||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
|
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
@@ -52,8 +53,8 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
|||||||
setupFilterToggle();
|
setupFilterToggle();
|
||||||
setupBulkDelete();
|
setupBulkDelete();
|
||||||
observeViewModel();
|
observeViewModel();
|
||||||
|
|
||||||
loadSupplierData();
|
loadSupplierData(true);
|
||||||
|
|
||||||
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
|
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
|
||||||
|
|
||||||
@@ -83,7 +84,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
|||||||
adapter,
|
adapter,
|
||||||
"supplier",
|
"supplier",
|
||||||
viewModel::bulkDeleteSuppliers,
|
viewModel::bulkDeleteSuppliers,
|
||||||
this::loadSupplierData
|
() -> loadSupplierData(true)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,11 +99,11 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSearch() {
|
private void setupSearch() {
|
||||||
UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData);
|
UIUtils.attachSearch(binding.etSearchSupplier, () -> loadSupplierData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupSwipeRefresh() {
|
private void setupSwipeRefresh() {
|
||||||
binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData);
|
binding.swipeRefreshSupplier.setOnRefreshListener(() -> loadSupplierData(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openSupplierDetails(int position) {
|
private void openSupplierDetails(int position) {
|
||||||
@@ -126,15 +127,31 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadSupplierData() {
|
private void loadSupplierData(boolean reset) {
|
||||||
String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : "";
|
String query = binding.etSearchSupplier != null ? binding.etSearchSupplier.getText().toString().trim() : "";
|
||||||
if (query.isEmpty()) query = null;
|
if (query.isEmpty()) query = null;
|
||||||
viewModel.loadSuppliers(query);
|
viewModel.loadSuppliers(reset, query);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerView() {
|
||||||
adapter = new SupplierAdapter(supplierList, this);
|
adapter = new SupplierAdapter(supplierList, this);
|
||||||
binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
binding.recyclerViewSuppliers.setAdapter(adapter);
|
binding.recyclerViewSuppliers.setAdapter(adapter);
|
||||||
|
|
||||||
|
binding.recyclerViewSuppliers.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||||
|
@Override
|
||||||
|
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
if (dy <= 0) return;
|
||||||
|
LinearLayoutManager lm = (LinearLayoutManager) binding.recyclerViewSuppliers.getLayoutManager();
|
||||||
|
if (lm == null) return;
|
||||||
|
int visible = lm.getChildCount();
|
||||||
|
int total = lm.getItemCount();
|
||||||
|
int firstVis = lm.findFirstVisibleItemPosition();
|
||||||
|
Boolean isLoading = viewModel.getIsLoading().getValue();
|
||||||
|
if ((isLoading == null || !isLoading) && !viewModel.isLastPage() && (visible + firstVis) >= total - 3) {
|
||||||
|
loadSupplierData(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -114,9 +114,18 @@ public class ProductSupplierDetailFragment extends Fragment {
|
|||||||
viewModel.setEditMode(productId, supplierId);
|
viewModel.setEditMode(productId, supplierId);
|
||||||
preselectedProductId = productId;
|
preselectedProductId = productId;
|
||||||
preselectedSupplierId = supplierId;
|
preselectedSupplierId = supplierId;
|
||||||
|
|
||||||
binding.tvPSMode.setText("Edit Product Supplier");
|
binding.tvPSMode.setText("Edit Product Supplier");
|
||||||
binding.btnDeletePS.setVisibility(View.VISIBLE);
|
binding.btnDeletePS.setVisibility(View.VISIBLE);
|
||||||
|
|
||||||
|
viewModel.loadProductSupplier().observe(getViewLifecycleOwner(), resource -> {
|
||||||
|
if (resource == null) return;
|
||||||
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
|
if (resource.data.getCost() != null) {
|
||||||
|
binding.etPSCost.setText(resource.data.getCost().toPlainString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
binding.tvPSMode.setText("Add Product Supplier");
|
binding.tvPSMode.setText("Add Product Supplier");
|
||||||
binding.btnDeletePS.setVisibility(View.GONE);
|
binding.btnDeletePS.setVisibility(View.GONE);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||||
|
|
||||||
import android.graphics.Color;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.*;
|
import android.view.*;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
@@ -82,26 +81,6 @@ public class PurchaseOrderDetailFragment extends Fragment {
|
|||||||
binding.tvPODetailSupplier.setText(po.getSupplierName());
|
binding.tvPODetailSupplier.setText(po.getSupplierName());
|
||||||
binding.tvPODetailStore.setText(po.getStoreName() != null ? po.getStoreName() : "N/A");
|
binding.tvPODetailStore.setText(po.getStoreName() != null ? po.getStoreName() : "N/A");
|
||||||
binding.tvPODetailDate.setText(po.getOrderDate());
|
binding.tvPODetailDate.setText(po.getOrderDate());
|
||||||
|
|
||||||
String status = po.getStatus() != null ? po.getStatus() : "";
|
|
||||||
binding.tvPODetailStatus.setText(status);
|
|
||||||
switch (status.toUpperCase()) {
|
|
||||||
case "RECEIVED":
|
|
||||||
binding.tvPODetailStatus.setTextColor(Color.parseColor("#4CAF50"));
|
|
||||||
break;
|
|
||||||
case "PLACED":
|
|
||||||
binding.tvPODetailStatus.setTextColor(Color.parseColor("#2196F3"));
|
|
||||||
break;
|
|
||||||
case "PENDING":
|
|
||||||
binding.tvPODetailStatus.setTextColor(Color.parseColor("#FF9800"));
|
|
||||||
break;
|
|
||||||
case "CANCELLED":
|
|
||||||
binding.tvPODetailStatus.setTextColor(Color.parseColor("#F44336"));
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
binding.tvPODetailStatus.setTextColor(Color.parseColor("#9E9E9E"));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show();
|
Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ public class ActivityLogRepository extends BaseRepository {
|
|||||||
this.activityLogApi = activityLogApi;
|
this.activityLogApi = activityLogApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Resource<List<ActivityLogDTO>>> getActivityLogs(int limit, Long storeId, String role, String search) {
|
public LiveData<Resource<List<ActivityLogDTO>>> getActivityLogs(int limit, Long storeId, String role, String search, String startDate, String endDate) {
|
||||||
return executeCall(activityLogApi.getActivityLogs(limit, storeId, role, search));
|
return executeCall(activityLogApi.getActivityLogs(limit, storeId, role, search, startDate, endDate));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
|
|||||||
|
|
||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
public class ActivityLogListViewModel extends ViewModel {
|
public class ActivityLogListViewModel extends ViewModel {
|
||||||
private static final int LIMIT = 2000;
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
private final ActivityLogRepository repository;
|
private final ActivityLogRepository repository;
|
||||||
private final StoreRepository storeRepository;
|
private final StoreRepository storeRepository;
|
||||||
@@ -32,6 +32,11 @@ public class ActivityLogListViewModel extends ViewModel {
|
|||||||
private Long currentStoreId = null;
|
private Long currentStoreId = null;
|
||||||
private String currentRole = null;
|
private String currentRole = null;
|
||||||
private String currentSearch = null;
|
private String currentSearch = null;
|
||||||
|
private String currentStartDate = null;
|
||||||
|
private String currentEndDate = null;
|
||||||
|
|
||||||
|
private int currentLimit = PAGE_SIZE;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ActivityLogListViewModel(ActivityLogRepository repository, StoreRepository storeRepository) {
|
public ActivityLogListViewModel(ActivityLogRepository repository, StoreRepository storeRepository) {
|
||||||
@@ -42,10 +47,11 @@ public class ActivityLogListViewModel extends ViewModel {
|
|||||||
public LiveData<List<ActivityLogDTO>> getLogs() { return logs; }
|
public LiveData<List<ActivityLogDTO>> getLogs() { return logs; }
|
||||||
public LiveData<List<DropdownDTO>> getStoreOptions() { return storeOptions; }
|
public LiveData<List<DropdownDTO>> getStoreOptions() { return storeOptions; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
public void loadInitialData() {
|
public void loadInitialData() {
|
||||||
loadStores();
|
loadStores();
|
||||||
loadLogs();
|
loadLogs(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadStores() {
|
private void loadStores() {
|
||||||
@@ -56,12 +62,24 @@ public class ActivityLogListViewModel extends ViewModel {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void loadLogs() {
|
public void loadLogs(boolean reset) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentLimit = PAGE_SIZE;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(repository.getActivityLogs(LIMIT, currentStoreId, currentRole, currentSearch), resource -> {
|
observeOnce(repository.getActivityLogs(currentLimit, currentStoreId, currentRole, currentSearch, currentStartDate, currentEndDate), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
logs.setValue(resource.data);
|
List<ActivityLogDTO> result = resource.data;
|
||||||
|
logs.setValue(result);
|
||||||
|
isLastPage = result.size() < currentLimit;
|
||||||
|
if (!isLastPage) currentLimit += PAGE_SIZE;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
@@ -72,19 +90,26 @@ public class ActivityLogListViewModel extends ViewModel {
|
|||||||
|
|
||||||
public void setRoleFilter(String role) {
|
public void setRoleFilter(String role) {
|
||||||
currentRole = "All Roles".equals(role) ? null : role;
|
currentRole = "All Roles".equals(role) ? null : role;
|
||||||
loadLogs();
|
loadLogs(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setStoreFilter(Long storeId) {
|
public void setStoreFilter(Long storeId) {
|
||||||
currentStoreId = storeId;
|
currentStoreId = storeId;
|
||||||
loadLogs();
|
loadLogs(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSearchQuery(String query) {
|
public void setSearchQuery(String query) {
|
||||||
currentSearch = (query == null || query.trim().isEmpty()) ? null : query.trim();
|
currentSearch = (query == null || query.trim().isEmpty()) ? null : query.trim();
|
||||||
loadLogs();
|
loadLogs(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setDateRange(String startDate, String endDate) {
|
||||||
|
currentStartDate = startDate;
|
||||||
|
currentEndDate = endDate;
|
||||||
|
loadLogs(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
|
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
|
||||||
liveData.observeForever(new Observer<Resource<T>>() {
|
liveData.observeForever(new Observer<Resource<T>>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ public class AdoptionListViewModel extends ViewModel {
|
|||||||
isLastPage = false;
|
isLastPage = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
if ("All Statuses".equals(status)) status = null;
|
if ("All Statuses".equals(status)) status = null;
|
||||||
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
|
|||||||
@@ -29,6 +29,10 @@ public class AppointmentListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) {
|
public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) {
|
||||||
this.appointmentRepository = appointmentRepository;
|
this.appointmentRepository = appointmentRepository;
|
||||||
@@ -38,13 +42,27 @@ public class AppointmentListViewModel extends ViewModel {
|
|||||||
public LiveData<List<AppointmentDTO>> getAppointments() { return appointments; }
|
public LiveData<List<AppointmentDTO>> getAppointments() { return appointments; }
|
||||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadAppointments(boolean reset, String query, String status, Long storeId, String date, Long employeeId) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadAppointments(String query, String status, Long storeId, String date, Long employeeId) {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(appointmentRepository.getAllAppointments(0, 500, query, status, storeId, date, employeeId, "appointmentId,desc"), resource -> {
|
observeOnce(appointmentRepository.getAllAppointments(currentPage, PAGE_SIZE, query, status, storeId, date, employeeId, "appointmentId,desc"), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
appointments.setValue(resource.data.getContent());
|
List<AppointmentDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(appointments.getValue());
|
||||||
|
currentList.addAll(resource.data.getContent());
|
||||||
|
appointments.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData;
|
|||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
import com.example.petstoremobile.dtos.CouponDTO;
|
import com.example.petstoremobile.dtos.CouponDTO;
|
||||||
import com.example.petstoremobile.dtos.PageResponse;
|
|
||||||
import com.example.petstoremobile.repositories.CouponRepository;
|
import com.example.petstoremobile.repositories.CouponRepository;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
import com.example.petstoremobile.utils.Resource;
|
||||||
|
|
||||||
@@ -24,6 +23,10 @@ public class CouponListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<CouponDTO>> coupons = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<CouponDTO>> coupons = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public CouponListViewModel(CouponRepository repository) {
|
public CouponListViewModel(CouponRepository repository) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
@@ -31,13 +34,27 @@ public class CouponListViewModel extends ViewModel {
|
|||||||
|
|
||||||
public LiveData<List<CouponDTO>> getCoupons() { return coupons; }
|
public LiveData<List<CouponDTO>> getCoupons() { return coupons; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadCoupons(boolean reset, Boolean active, String discountType, String sort) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadCoupons(int page, int size, Boolean active, String discountType, String sort) {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(repository.getAllCoupons(page, size, active, discountType, sort), resource -> {
|
observeOnce(repository.getAllCoupons(currentPage, PAGE_SIZE, active, discountType, sort), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
coupons.setValue(resource.data.getContent());
|
List<CouponDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(coupons.getValue());
|
||||||
|
currentList.addAll(resource.data.getContent());
|
||||||
|
coupons.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ public class CustomerListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<CustomerDTO>> filteredCustomers = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<CustomerDTO>> filteredCustomers = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
private String lastQuery = "";
|
private String lastQuery = "";
|
||||||
private String lastStatus = "All Statuses";
|
private String lastStatus = "All Statuses";
|
||||||
|
|
||||||
@@ -35,14 +39,28 @@ public class CustomerListViewModel extends ViewModel {
|
|||||||
|
|
||||||
public LiveData<List<CustomerDTO>> getFilteredCustomers() { return filteredCustomers; }
|
public LiveData<List<CustomerDTO>> getFilteredCustomers() { return filteredCustomers; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadCustomers(boolean reset) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadCustomers() {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(repository.getAllCustomers(0, 200), resource -> {
|
observeOnce(repository.getAllCustomers(currentPage, PAGE_SIZE), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
customers.setValue(resource.data.getContent());
|
List<CustomerDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(customers.getValue());
|
||||||
filter(lastQuery, lastStatus);
|
currentList.addAll(resource.data.getContent());
|
||||||
|
customers.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
|
applyFilter(currentList);
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
@@ -66,23 +84,25 @@ public class CustomerListViewModel extends ViewModel {
|
|||||||
public void filter(String query, String status) {
|
public void filter(String query, String status) {
|
||||||
this.lastQuery = query;
|
this.lastQuery = query;
|
||||||
this.lastStatus = status;
|
this.lastStatus = status;
|
||||||
|
applyFilter(customers.getValue());
|
||||||
|
}
|
||||||
|
|
||||||
List<CustomerDTO> all = customers.getValue();
|
private void applyFilter(List<CustomerDTO> all) {
|
||||||
if (all == null) return;
|
if (all == null) return;
|
||||||
|
|
||||||
List<CustomerDTO> filtered = new ArrayList<>();
|
List<CustomerDTO> filtered = new ArrayList<>();
|
||||||
String lowerQuery = query.toLowerCase();
|
String lowerQuery = lastQuery.toLowerCase();
|
||||||
|
|
||||||
for (CustomerDTO c : all) {
|
for (CustomerDTO c : all) {
|
||||||
boolean matchesQuery = query.isEmpty() ||
|
boolean matchesQuery = lastQuery.isEmpty() ||
|
||||||
(c.getFullName() != null && c.getFullName().toLowerCase().contains(lowerQuery)) ||
|
(c.getFullName() != null && c.getFullName().toLowerCase().contains(lowerQuery)) ||
|
||||||
(c.getUsername() != null && c.getUsername().toLowerCase().contains(lowerQuery)) ||
|
(c.getUsername() != null && c.getUsername().toLowerCase().contains(lowerQuery)) ||
|
||||||
(c.getEmail() != null && c.getEmail().toLowerCase().contains(lowerQuery)) ||
|
(c.getEmail() != null && c.getEmail().toLowerCase().contains(lowerQuery)) ||
|
||||||
(c.getPhone() != null && c.getPhone().toLowerCase().contains(lowerQuery));
|
(c.getPhone() != null && c.getPhone().toLowerCase().contains(lowerQuery));
|
||||||
|
|
||||||
boolean matchesStatus = status.equals("All Statuses") ||
|
boolean matchesStatus = lastStatus.equals("All Statuses") ||
|
||||||
(status.equals("Active") && Boolean.TRUE.equals(c.getActive())) ||
|
(lastStatus.equals("Active") && Boolean.TRUE.equals(c.getActive())) ||
|
||||||
(status.equals("Inactive") && Boolean.FALSE.equals(c.getActive()));
|
(lastStatus.equals("Inactive") && Boolean.FALSE.equals(c.getActive()));
|
||||||
|
|
||||||
if (matchesQuery && matchesStatus) {
|
if (matchesQuery && matchesStatus) {
|
||||||
filtered.add(c);
|
filtered.add(c);
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel;
|
|||||||
|
|
||||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||||
import com.example.petstoremobile.dtos.PageResponse;
|
|
||||||
import com.example.petstoremobile.dtos.PetDTO;
|
import com.example.petstoremobile.dtos.PetDTO;
|
||||||
import com.example.petstoremobile.dtos.StoreDTO;
|
import com.example.petstoremobile.dtos.StoreDTO;
|
||||||
import com.example.petstoremobile.repositories.PetRepository;
|
import com.example.petstoremobile.repositories.PetRepository;
|
||||||
@@ -32,6 +31,10 @@ public class PetListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<String>> speciesOptions = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<String>> speciesOptions = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) {
|
public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) {
|
||||||
this.petRepository = petRepository;
|
this.petRepository = petRepository;
|
||||||
@@ -42,16 +45,32 @@ public class PetListViewModel extends ViewModel {
|
|||||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||||
public LiveData<List<String>> getSpeciesOptions() { return speciesOptions; }
|
public LiveData<List<String>> getSpeciesOptions() { return speciesOptions; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadPets(boolean reset, String query, String status, String species, Long storeId) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadPets(String query, String status, String species, Long storeId) {
|
|
||||||
if ("All Statuses".equals(status)) status = null;
|
if ("All Statuses".equals(status)) status = null;
|
||||||
if ("All Species".equals(species)) species = null;
|
if ("All Species".equals(species)) species = null;
|
||||||
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(petRepository.getAllPets(0, 100, query, status, species, storeId, null, "petName"), resource -> {
|
final String finalStatus = status;
|
||||||
|
final String finalSpecies = species;
|
||||||
|
observeOnce(petRepository.getAllPets(currentPage, PAGE_SIZE, query, finalStatus, finalSpecies, storeId, null, "petName"), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
pets.setValue(resource.data.getContent());
|
List<PetDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(pets.getValue());
|
||||||
|
currentList.addAll(resource.data.getContent());
|
||||||
|
pets.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ public class ProductListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<DropdownDTO>> categories = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<DropdownDTO>> categories = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
|
public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
@@ -37,13 +41,27 @@ public class ProductListViewModel extends ViewModel {
|
|||||||
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
||||||
public LiveData<List<DropdownDTO>> getCategories() { return categories; }
|
public LiveData<List<DropdownDTO>> getCategories() { return categories; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadProducts(boolean reset, String query, Long categoryId) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadProducts(String query, Long categoryId) {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(productRepository.getAllProducts(query, categoryId, 0, 100, "prodName"), resource -> {
|
observeOnce(productRepository.getAllProducts(query, categoryId, currentPage, PAGE_SIZE, "prodName"), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
products.setValue(resource.data.getContent());
|
List<ProductDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(products.getValue());
|
||||||
|
currentList.addAll(resource.data.getContent());
|
||||||
|
products.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
|
|||||||
@@ -46,6 +46,10 @@ public class ProductSupplierDetailViewModel extends ViewModel {
|
|||||||
this.editSupplierId = supplierId;
|
this.editSupplierId = supplierId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public LiveData<Resource<ProductSupplierDTO>> loadProductSupplier() {
|
||||||
|
return psRepository.getProductSupplierById(editProductId, editSupplierId);
|
||||||
|
}
|
||||||
|
|
||||||
public boolean isEditing() { return isEditing; }
|
public boolean isEditing() { return isEditing; }
|
||||||
public long getEditProductId() { return editProductId; }
|
public long getEditProductId() { return editProductId; }
|
||||||
public long getEditSupplierId() { return editSupplierId; }
|
public long getEditSupplierId() { return editSupplierId; }
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ public class ProductSupplierListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) {
|
public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) {
|
||||||
this.psRepository = psRepository;
|
this.psRepository = psRepository;
|
||||||
@@ -44,13 +48,27 @@ public class ProductSupplierListViewModel extends ViewModel {
|
|||||||
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
||||||
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadProductSuppliers(boolean reset, String query, Long productId, Long supplierId) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadProductSuppliers(String query, Long productId, Long supplierId) {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(psRepository.getAllProductSuppliers(0, 100, query, productId, supplierId, "productName"), resource -> {
|
observeOnce(psRepository.getAllProductSuppliers(currentPage, PAGE_SIZE, query, productId, supplierId, "productName"), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
productSuppliers.setValue(resource.data.getContent());
|
List<ProductSupplierDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(productSuppliers.getValue());
|
||||||
|
currentList.addAll(resource.data.getContent());
|
||||||
|
productSuppliers.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
|
|||||||
@@ -28,6 +28,10 @@ public class PurchaseOrderListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) {
|
public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) {
|
||||||
this.purchaseOrderRepository = purchaseOrderRepository;
|
this.purchaseOrderRepository = purchaseOrderRepository;
|
||||||
@@ -37,13 +41,27 @@ public class PurchaseOrderListViewModel extends ViewModel {
|
|||||||
public LiveData<List<PurchaseOrderDTO>> getPurchaseOrders() { return purchaseOrders; }
|
public LiveData<List<PurchaseOrderDTO>> getPurchaseOrders() { return purchaseOrders; }
|
||||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadPurchaseOrders(boolean reset, String query, Long storeId) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadPurchaseOrders(String query, Long storeId) {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(purchaseOrderRepository.getAllPurchaseOrders(0, 100, query, storeId, "purchaseOrderId,desc"), resource -> {
|
observeOnce(purchaseOrderRepository.getAllPurchaseOrders(currentPage, PAGE_SIZE, query, storeId, "purchaseOrderId,desc"), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
purchaseOrders.setValue(resource.data.getContent());
|
List<PurchaseOrderDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(purchaseOrders.getValue());
|
||||||
|
currentList.addAll(resource.data.getContent());
|
||||||
|
purchaseOrders.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
|
|||||||
@@ -28,6 +28,11 @@ public class StaffListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
private String lastQuery = "";
|
private String lastQuery = "";
|
||||||
private Long lastStoreId = null;
|
private Long lastStoreId = null;
|
||||||
private String lastStatus = "All Statuses";
|
private String lastStatus = "All Statuses";
|
||||||
@@ -41,14 +46,28 @@ public class StaffListViewModel extends ViewModel {
|
|||||||
public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
|
public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
|
||||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadStaff(boolean reset) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadStaff() {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(repository.getAllEmployees(0, 100), resource -> {
|
observeOnce(repository.getAllEmployees(currentPage, PAGE_SIZE), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
employees.setValue(resource.data.getContent());
|
List<EmployeeDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(employees.getValue());
|
||||||
filter(lastQuery, lastStoreId, lastStatus);
|
currentList.addAll(resource.data.getContent());
|
||||||
|
employees.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
|
applyFilter(currentList);
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
@@ -81,28 +100,27 @@ public class StaffListViewModel extends ViewModel {
|
|||||||
this.lastQuery = query;
|
this.lastQuery = query;
|
||||||
this.lastStoreId = storeId;
|
this.lastStoreId = storeId;
|
||||||
this.lastStatus = status;
|
this.lastStatus = status;
|
||||||
|
applyFilter(employees.getValue());
|
||||||
List<EmployeeDTO> all = employees.getValue();
|
}
|
||||||
|
|
||||||
|
private void applyFilter(List<EmployeeDTO> all) {
|
||||||
if (all == null) return;
|
if (all == null) return;
|
||||||
|
|
||||||
List<EmployeeDTO> filtered = new ArrayList<>();
|
List<EmployeeDTO> filtered = new ArrayList<>();
|
||||||
String lowerQuery = query.toLowerCase();
|
String lowerQuery = lastQuery.toLowerCase();
|
||||||
|
|
||||||
for (EmployeeDTO e : all) {
|
for (EmployeeDTO e : all) {
|
||||||
// Search Query Filter
|
boolean matchesQuery = lastQuery.isEmpty() ||
|
||||||
boolean matchesQuery = query.isEmpty() ||
|
|
||||||
(e.getFullName() != null && e.getFullName().toLowerCase().contains(lowerQuery)) ||
|
(e.getFullName() != null && e.getFullName().toLowerCase().contains(lowerQuery)) ||
|
||||||
(e.getUsername() != null && e.getUsername().toLowerCase().contains(lowerQuery)) ||
|
(e.getUsername() != null && e.getUsername().toLowerCase().contains(lowerQuery)) ||
|
||||||
(e.getEmail() != null && e.getEmail().toLowerCase().contains(lowerQuery)) ||
|
(e.getEmail() != null && e.getEmail().toLowerCase().contains(lowerQuery)) ||
|
||||||
(e.getPhone() != null && e.getPhone().toLowerCase().contains(lowerQuery));
|
(e.getPhone() != null && e.getPhone().toLowerCase().contains(lowerQuery));
|
||||||
|
|
||||||
// Store Filter
|
boolean matchesStore = lastStoreId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(lastStoreId));
|
||||||
boolean matchesStore = storeId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(storeId));
|
|
||||||
|
|
||||||
// Status Filter
|
boolean matchesStatus = lastStatus.equals("All Statuses") ||
|
||||||
boolean matchesStatus = status.equals("All Statuses") ||
|
(lastStatus.equals("Active") && Boolean.TRUE.equals(e.getActive())) ||
|
||||||
(status.equals("Active") && Boolean.TRUE.equals(e.getActive())) ||
|
(lastStatus.equals("Inactive") && Boolean.FALSE.equals(e.getActive()));
|
||||||
(status.equals("Inactive") && Boolean.FALSE.equals(e.getActive()));
|
|
||||||
|
|
||||||
if (matchesQuery && matchesStore && matchesStatus) {
|
if (matchesQuery && matchesStore && matchesStatus) {
|
||||||
filtered.add(e);
|
filtered.add(e);
|
||||||
|
|||||||
@@ -25,6 +25,10 @@ public class SupplierListViewModel extends ViewModel {
|
|||||||
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<SupplierDTO>> suppliers = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
|
private int currentPage = 0;
|
||||||
|
private boolean isLastPage = false;
|
||||||
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SupplierListViewModel(SupplierRepository repository) {
|
public SupplierListViewModel(SupplierRepository repository) {
|
||||||
this.repository = repository;
|
this.repository = repository;
|
||||||
@@ -32,13 +36,27 @@ public class SupplierListViewModel extends ViewModel {
|
|||||||
|
|
||||||
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
|
public boolean isLastPage() { return isLastPage; }
|
||||||
|
|
||||||
|
public void loadSuppliers(boolean reset, String query) {
|
||||||
|
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
|
if (reset) {
|
||||||
|
currentPage = 0;
|
||||||
|
isLastPage = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLastPage) return;
|
||||||
|
|
||||||
public void loadSuppliers(String query) {
|
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
observeOnce(repository.getAllSuppliers(0, 100, query, "supCompany"), resource -> {
|
observeOnce(repository.getAllSuppliers(currentPage, PAGE_SIZE, query, "supCompany"), resource -> {
|
||||||
if (resource != null) {
|
if (resource != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
suppliers.setValue(resource.data.getContent());
|
List<SupplierDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(suppliers.getValue());
|
||||||
|
currentList.addAll(resource.data.getContent());
|
||||||
|
suppliers.setValue(currentList);
|
||||||
|
isLastPage = resource.data.isLast();
|
||||||
|
if (!isLastPage) currentPage++;
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
|
|||||||
@@ -88,6 +88,57 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
<!-- Date range pickers -->
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:layout_marginTop="8dp">
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnStartDate"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="Start Date"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:backgroundTint="@color/white"
|
||||||
|
android:textColor="@color/text_dark"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="8dp"
|
||||||
|
android:layout_height="0dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnEndDate"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:text="End Date"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:backgroundTint="@color/white"
|
||||||
|
android:textColor="@color/text_dark"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:maxLines="1"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="8dp"
|
||||||
|
android:layout_height="0dp"/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
android:id="@+id/btnClearDates"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="44dp"
|
||||||
|
android:text="Clear"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:backgroundTint="#e2e8f0"
|
||||||
|
android:textColor="@color/text_dark"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
<!-- Role and Store spinners -->
|
<!-- Role and Store spinners -->
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
@@ -119,6 +170,7 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
|||||||
@@ -105,19 +105,6 @@
|
|||||||
android:textSize="15sp"
|
android:textSize="15sp"
|
||||||
android:layout_marginBottom="16dp"/>
|
android:layout_marginBottom="16dp"/>
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="Status"
|
|
||||||
android:textColor="@color/text_light"
|
|
||||||
android:textSize="12sp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvPODetailStatus"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="15sp"
|
|
||||||
android:layout_marginBottom="16dp"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
|
|||||||
@@ -13,17 +13,17 @@
|
|||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
android:layout_marginBottom="4dp">
|
android:gravity="center_vertical"
|
||||||
|
android:layout_marginBottom="2dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvLogActivity"
|
android:id="@+id/tvLogActivity"
|
||||||
android:layout_width="0dp"
|
android:layout_width="0dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:textSize="13sp"
|
android:textSize="14sp"
|
||||||
android:textColor="@color/text_dark"
|
android:textColor="@color/text_dark"
|
||||||
android:textStyle="bold"
|
android:textStyle="bold"/>
|
||||||
android:fontFamily="monospace"/>
|
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvLogTimestamp"
|
android:id="@+id/tvLogTimestamp"
|
||||||
@@ -36,18 +36,42 @@
|
|||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/tvLogUser"
|
android:id="@+id/tvLogTechnical"
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:textSize="12sp"
|
|
||||||
android:textColor="@color/text_dark"
|
|
||||||
android:layout_marginBottom="2dp"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvLogMeta"
|
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:textSize="11sp"
|
android:textSize="11sp"
|
||||||
android:textColor="@color/text_light"/>
|
android:textColor="@color/text_light"
|
||||||
|
android:fontFamily="monospace"
|
||||||
|
android:layout_marginBottom="6dp"
|
||||||
|
android:visibility="gone"/>
|
||||||
|
|
||||||
|
<View
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="1dp"
|
||||||
|
android:background="#F0F0F0"
|
||||||
|
android:layout_marginBottom="6dp"/>
|
||||||
|
|
||||||
|
<LinearLayout
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:orientation="horizontal"
|
||||||
|
android:gravity="center_vertical">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLogUser"
|
||||||
|
android:layout_width="0dp"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:layout_weight="1"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:textColor="@color/text_dark"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/tvLogMeta"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textSize="11sp"
|
||||||
|
android:textColor="@color/text_light"/>
|
||||||
|
|
||||||
|
</LinearLayout>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -27,19 +27,6 @@
|
|||||||
android:textSize="18sp"
|
android:textSize="18sp"
|
||||||
android:textStyle="bold" />
|
android:textStyle="bold" />
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tvPOStatus"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingStart="8dp"
|
|
||||||
android:paddingTop="3dp"
|
|
||||||
android:paddingEnd="8dp"
|
|
||||||
android:paddingBottom="3dp"
|
|
||||||
android:text="Status"
|
|
||||||
android:textAllCaps="true"
|
|
||||||
android:textColor="@color/white"
|
|
||||||
android:textSize="11sp" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
|||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@Order(Ordered.LOWEST_PRECEDENCE - 20)
|
@Order(Ordered.LOWEST_PRECEDENCE - 20)
|
||||||
public class ActivityLoggingFilter extends OncePerRequestFilter {
|
public class ActivityLoggingFilter extends OncePerRequestFilter {
|
||||||
|
|
||||||
private final ActivityLogService activityLogService;
|
private final ActivityLogService activityLogService;
|
||||||
|
|
||||||
@@ -30,13 +30,8 @@ import java.io.IOException;
|
|||||||
@Override
|
@Override
|
||||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||||
String uri = request.getRequestURI();
|
String uri = request.getRequestURI();
|
||||||
if (uri == null || uri.isBlank()) {
|
if (uri == null || uri.isBlank()) return true;
|
||||||
return true;
|
if (!uri.startsWith("/api/")) return true;
|
||||||
}
|
|
||||||
if (!uri.startsWith("/api/")) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
String lower = uri.toLowerCase(java.util.Locale.ROOT);
|
String lower = uri.toLowerCase(java.util.Locale.ROOT);
|
||||||
return lower.startsWith("/api/v1/health")
|
return lower.startsWith("/api/v1/health")
|
||||||
|| lower.startsWith("/api/v1/activity-logs")
|
|| lower.startsWith("/api/v1/activity-logs")
|
||||||
@@ -46,16 +41,15 @@ import java.io.IOException;
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
|
||||||
|
throws ServletException, IOException {
|
||||||
filterChain.doFilter(request, response);
|
filterChain.doFilter(request, response);
|
||||||
recordActivity(request, response);
|
recordActivity(request, response);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void recordActivity(HttpServletRequest request, HttpServletResponse response) {
|
private void recordActivity(HttpServletRequest request, HttpServletResponse response) {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
if (authentication == null || !authentication.isAuthenticated()) {
|
if (authentication == null || !authentication.isAuthenticated()) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Long userId = null;
|
Long userId = null;
|
||||||
User.Role role = null;
|
User.Role role = null;
|
||||||
@@ -69,11 +63,136 @@ import java.io.IOException;
|
|||||||
role = appPrincipal.getRole();
|
role = appPrincipal.getRole();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (userId == null || role == null || role == User.Role.CUSTOMER) {
|
if (userId == null || role == null) return;
|
||||||
return;
|
if ("GET".equalsIgnoreCase(request.getMethod())) return;
|
||||||
}
|
|
||||||
|
String method = request.getMethod();
|
||||||
|
String uri = request.getRequestURI();
|
||||||
|
int status = response.getStatus();
|
||||||
|
|
||||||
|
String technical = method + " " + uri + " → " + status;
|
||||||
|
String description = describe(method, uri, status);
|
||||||
|
String activity = description != null ? description + " | " + technical : technical;
|
||||||
|
|
||||||
String activity = String.format("%s %s -> %d", request.getMethod(), request.getRequestURI(), response.getStatus());
|
|
||||||
activityLogService.record(userId, activity);
|
activityLogService.record(userId, activity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String describe(String method, String rawUri, int status) {
|
||||||
|
String uri = rawUri.contains("?") ? rawUri.substring(0, rawUri.indexOf('?')) : rawUri;
|
||||||
|
String[] parts = uri.replaceFirst("^/+", "").split("/");
|
||||||
|
if (parts.length < 3) return null;
|
||||||
|
|
||||||
|
String r = parts[2];
|
||||||
|
String seg3 = parts.length > 3 ? parts[3] : null;
|
||||||
|
String seg4 = parts.length > 4 ? parts[4] : null;
|
||||||
|
String seg5 = parts.length > 5 ? parts[5] : null;
|
||||||
|
|
||||||
|
boolean seg3IsId = seg3 != null && seg3.matches("\\d+");
|
||||||
|
boolean seg4IsId = seg4 != null && seg4.matches("\\d+");
|
||||||
|
String id = seg3IsId ? seg3 : null;
|
||||||
|
String sub = seg3IsId ? seg4 : seg3;
|
||||||
|
String subsub = seg3IsId ? seg5 : seg4;
|
||||||
|
|
||||||
|
boolean isGet = "GET".equalsIgnoreCase(method);
|
||||||
|
boolean isPost = "POST".equalsIgnoreCase(method);
|
||||||
|
boolean isPut = "PUT".equalsIgnoreCase(method);
|
||||||
|
boolean isPatch = "PATCH".equalsIgnoreCase(method);
|
||||||
|
boolean isDelete = "DELETE".equalsIgnoreCase(method);
|
||||||
|
boolean failed = status >= 400;
|
||||||
|
|
||||||
|
switch (r) {
|
||||||
|
case "auth" -> {
|
||||||
|
if ("login".equals(seg3) && isPost) return failed ? "Failed login attempt" : "Logged in";
|
||||||
|
if ("logout".equals(seg3) && isPost) return "Logged out";
|
||||||
|
if ("register".equals(seg3) && isPost) return failed ? "Failed registration attempt" : "Registered a new account";
|
||||||
|
if ("forgot-password".equals(seg3) && isPost) return "Requested a password reset";
|
||||||
|
if ("reset-password".equals(seg3) && isPost) return "Reset their password";
|
||||||
|
if ("me".equals(seg3) && isPut) return "Updated their profile";
|
||||||
|
if ("me".equals(seg3) && "avatar".equals(sub) && isPost) return "Uploaded a profile picture";
|
||||||
|
if ("me".equals(seg3) && "avatar".equals(sub) && isDelete) return "Removed their profile picture";
|
||||||
|
}
|
||||||
|
case "cart" -> {
|
||||||
|
if ("add".equals(seg3) && isPost) return "Added an item to cart";
|
||||||
|
if ("update".equals(seg3) && isPut) return "Updated a cart item";
|
||||||
|
if ("remove".equals(seg3) && isDelete) return "Removed an item from cart";
|
||||||
|
if ("clear".equals(seg3) && isDelete) return "Cleared their cart";
|
||||||
|
if ("apply-coupon".equals(seg3) && isPost) return "Applied a coupon to cart";
|
||||||
|
if ("checkout".equals(seg3)) {
|
||||||
|
if ("complete".equals(seg4) && isPost) return "Completed a purchase";
|
||||||
|
if ("cancel".equals(seg4) && isPost) return "Cancelled checkout";
|
||||||
|
if (isPost) return "Started checkout";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "chat" -> {
|
||||||
|
if ("conversations".equals(seg3) && isPost && seg4 == null) return "Started a new chat conversation";
|
||||||
|
if ("conversations".equals(seg3) && seg4IsId) {
|
||||||
|
String convId = seg4;
|
||||||
|
String chatSub = parts.length > 5 ? parts[5] : null;
|
||||||
|
if ("messages".equals(seg5) && isPost) return "Sent a chat message";
|
||||||
|
if ("attachments".equals(seg5) && isPost) return "Sent a file in chat";
|
||||||
|
if ("request-human".equals(seg5) && isPost) return "Requested human support in chat";
|
||||||
|
if (chatSub == null && isPut) return "Updated chat conversation #" + convId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "ai-chat" -> {
|
||||||
|
if ("message".equals(seg3) && isPost) return "Sent a message to the AI assistant";
|
||||||
|
}
|
||||||
|
case "product-suppliers" -> {
|
||||||
|
if (isPost && seg3 == null) return "Linked a product to a supplier";
|
||||||
|
if (seg3IsId && seg4IsId && isPut) return "Updated product-supplier link";
|
||||||
|
if (seg3IsId && seg4IsId && isDelete) return "Removed product-supplier link";
|
||||||
|
if (isDelete && !seg3IsId) return "Removed multiple product-supplier links";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String label = resourceLabel(r);
|
||||||
|
if (label == null) return null;
|
||||||
|
|
||||||
|
if ("image".equals(sub) && id != null) {
|
||||||
|
if (isPost) return "Uploaded image for " + label + " #" + id;
|
||||||
|
if (isDelete) return "Removed image from " + label + " #" + id;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("cancel".equals(sub) && id != null && isPatch) return "Cancelled " + label + " #" + id;
|
||||||
|
|
||||||
|
if (isPost && id == null) {
|
||||||
|
if ("request".equals(seg3)) return "Submitted an adoption request";
|
||||||
|
if ("bulk-delete".equals(seg3)) return "Deleted multiple " + plural(label);
|
||||||
|
return "Created a new " + label;
|
||||||
|
}
|
||||||
|
if ((isPut || isPatch) && id != null && sub == null) return "Updated " + label + " #" + id;
|
||||||
|
if (isDelete && id != null && sub == null) return "Deleted " + label + " #" + id;
|
||||||
|
if (isDelete && id == null) return "Deleted multiple " + plural(label);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String resourceLabel(String resource) {
|
||||||
|
return switch (resource) {
|
||||||
|
case "products" -> "product";
|
||||||
|
case "categories" -> "category";
|
||||||
|
case "customers" -> "customer";
|
||||||
|
case "employees" -> "employee";
|
||||||
|
case "users" -> "user";
|
||||||
|
case "pets" -> "pet";
|
||||||
|
case "my-pets" -> "pet";
|
||||||
|
case "appointments" -> "appointment";
|
||||||
|
case "adoptions" -> "adoption";
|
||||||
|
case "sales" -> "sale";
|
||||||
|
case "refunds" -> "refund";
|
||||||
|
case "inventory" -> "inventory record";
|
||||||
|
case "services" -> "service";
|
||||||
|
case "suppliers" -> "supplier";
|
||||||
|
case "purchase-orders" -> "purchase order";
|
||||||
|
case "coupons" -> "coupon";
|
||||||
|
case "stores" -> "store";
|
||||||
|
default -> null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private String plural(String label) {
|
||||||
|
if (label.endsWith("y")) return label.substring(0, label.length() - 1) + "ies";
|
||||||
|
if (label.endsWith("s")) return label + "es";
|
||||||
|
return label + "s";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,7 +137,7 @@ public class AuthController {
|
|||||||
|
|
||||||
String token = jwtUtil.generateToken(user);
|
String token = jwtUtil.generateToken(user);
|
||||||
if (user.getRole() != User.Role.CUSTOMER) {
|
if (user.getRole() != User.Role.CUSTOMER) {
|
||||||
activityLogService.record(user.getId(), "POST /api/v1/auth/login -> 200");
|
activityLogService.record(user.getId(), "Logged in | POST /api/v1/auth/login → 200");
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(new LoginResponse(
|
return ResponseEntity.ok(new LoginResponse(
|
||||||
|
|||||||
@@ -158,10 +158,10 @@ public class EmailService {
|
|||||||
.html(html)
|
.html(html)
|
||||||
.build();
|
.build();
|
||||||
resend.emails().send(options);
|
resend.emails().send(options);
|
||||||
activityLogService.record(recipientUserId, "Email sent: " + subject + " → " + to);
|
activityLogService.record(recipientUserId, "Sent an email | Email sent: " + subject + " → " + to);
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.error("Failed to send email '{}' to {}: {}", subject, to, ex.getMessage());
|
log.error("Failed to send email '{}' to {}: {}", subject, to, ex.getMessage());
|
||||||
activityLogService.record(recipientUserId, "Email failed: " + subject + " → " + to);
|
activityLogService.record(recipientUserId, "Failed to send email | Email failed: " + subject + " → " + to);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
import java.io.ByteArrayOutputStream;
|
import java.io.ByteArrayOutputStream;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.SequenceInputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@@ -150,26 +153,33 @@ public class ApiClient {
|
|||||||
String mimeType = Files.probeContentType(filePath);
|
String mimeType = Files.probeContentType(filePath);
|
||||||
if (mimeType == null || mimeType.isBlank()) mimeType = "application/octet-stream";
|
if (mimeType == null || mimeType.isBlank()) mimeType = "application/octet-stream";
|
||||||
|
|
||||||
byte[] fileBytes = Files.readAllBytes(filePath);
|
|
||||||
String fileName = filePath.getFileName().toString();
|
String fileName = filePath.getFileName().toString();
|
||||||
|
|
||||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
byte[] fileHeader = ("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + filePartName
|
||||||
out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + filePartName
|
|
||||||
+ "\"; filename=\"" + fileName + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n")
|
+ "\"; filename=\"" + fileName + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n")
|
||||||
.getBytes(StandardCharsets.UTF_8));
|
.getBytes(StandardCharsets.UTF_8);
|
||||||
out.write(fileBytes);
|
|
||||||
out.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
ByteArrayOutputStream tail = new ByteArrayOutputStream();
|
||||||
|
tail.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
||||||
if (textContent != null && !textContent.isBlank()) {
|
if (textContent != null && !textContent.isBlank()) {
|
||||||
out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + textPartName
|
tail.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + textPartName
|
||||||
+ "\"\r\n\r\n" + textContent + "\r\n").getBytes(StandardCharsets.UTF_8));
|
+ "\"\r\n\r\n" + textContent + "\r\n").getBytes(StandardCharsets.UTF_8));
|
||||||
}
|
}
|
||||||
out.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
|
tail.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
InputStream body = new SequenceInputStream(
|
||||||
|
java.util.Collections.enumeration(java.util.Arrays.asList(
|
||||||
|
new ByteArrayInputStream(fileHeader),
|
||||||
|
Files.newInputStream(filePath),
|
||||||
|
new ByteArrayInputStream(tail.toByteArray())
|
||||||
|
))
|
||||||
|
);
|
||||||
|
|
||||||
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||||
.uri(URI.create(baseUrl + path))
|
.uri(URI.create(baseUrl + path))
|
||||||
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||||
.POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray()))
|
.POST(HttpRequest.BodyPublishers.ofInputStream(() -> body))
|
||||||
.timeout(Duration.ofSeconds(30));
|
.timeout(Duration.ofSeconds(120));
|
||||||
addAuthHeader(builder);
|
addAuthHeader(builder);
|
||||||
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
||||||
return handleResponse(response, responseClass);
|
return handleResponse(response, responseClass);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ public class MessageResponse {
|
|||||||
private Boolean isRead;
|
private Boolean isRead;
|
||||||
private String attachmentName;
|
private String attachmentName;
|
||||||
private String attachmentUrl;
|
private String attachmentUrl;
|
||||||
|
private String attachmentMimeType;
|
||||||
|
|
||||||
public MessageResponse() {
|
public MessageResponse() {
|
||||||
}
|
}
|
||||||
@@ -105,4 +106,12 @@ public class MessageResponse {
|
|||||||
public void setAttachmentUrl(String attachmentUrl) {
|
public void setAttachmentUrl(String attachmentUrl) {
|
||||||
this.attachmentUrl = attachmentUrl;
|
this.attachmentUrl = attachmentUrl;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAttachmentMimeType() {
|
||||||
|
return attachmentMimeType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttachmentMimeType(String attachmentMimeType) {
|
||||||
|
this.attachmentMimeType = attachmentMimeType;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,10 +8,14 @@ import javafx.event.ActionEvent;
|
|||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.CheckBox;
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.DatePicker;
|
import javafx.scene.control.DatePicker;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.TableCell;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import org.example.petshopdesktop.api.dto.activity.ActivityLogResponse;
|
import org.example.petshopdesktop.api.dto.activity.ActivityLogResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.ActivityLogApi;
|
import org.example.petshopdesktop.api.endpoints.ActivityLogApi;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
@@ -20,73 +24,105 @@ import org.example.petshopdesktop.util.TableViewSupport;
|
|||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.TreeSet;
|
||||||
|
|
||||||
public class ActivityLogController {
|
public class ActivityLogController {
|
||||||
|
|
||||||
@FXML
|
private static final String SEPARATOR_NEW = " | ";
|
||||||
private TableView<ActivityLogResponse> tvActivityLogs;
|
private static final String SEPARATOR_OLD = " \u00b7 ";
|
||||||
|
private static final DateTimeFormatter DISPLAY_FORMATTER = DateTimeFormatter.ofPattern("MMM d, HH:mm");
|
||||||
|
private static final int DEFAULT_LIMIT = 2000;
|
||||||
|
|
||||||
@FXML
|
@FXML private TableView<ActivityLogResponse> tvActivityLogs;
|
||||||
private TableColumn<ActivityLogResponse, Object> colTimestamp;
|
@FXML private TableColumn<ActivityLogResponse, Object> colTimestamp;
|
||||||
|
@FXML private TableColumn<ActivityLogResponse, String> colUser;
|
||||||
@FXML
|
@FXML private TableColumn<ActivityLogResponse, String> colRole;
|
||||||
private TableColumn<ActivityLogResponse, String> colUser;
|
@FXML private TableColumn<ActivityLogResponse, String> colStore;
|
||||||
|
@FXML private TableColumn<ActivityLogResponse, String> colActivity;
|
||||||
@FXML
|
@FXML private Button btnRefresh;
|
||||||
private TableColumn<ActivityLogResponse, String> colRole;
|
@FXML private DatePicker dpStart;
|
||||||
|
@FXML private DatePicker dpEnd;
|
||||||
@FXML
|
@FXML private CheckBox chkHideViewOnly;
|
||||||
private TableColumn<ActivityLogResponse, String> colStore;
|
@FXML private TextField tfSearch;
|
||||||
|
@FXML private ComboBox<String> cbRoleFilter;
|
||||||
@FXML
|
@FXML private ComboBox<String> cbStoreFilter;
|
||||||
private TableColumn<ActivityLogResponse, String> colActivity;
|
@FXML private Label lblStatus;
|
||||||
|
@FXML private Label lblError;
|
||||||
@FXML
|
|
||||||
private Button btnRefresh;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private DatePicker dpStart;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private DatePicker dpEnd;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private CheckBox chkHideViewOnly;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblStatus;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblError;
|
|
||||||
|
|
||||||
private final ObservableList<ActivityLogResponse> activityLogs = FXCollections.observableArrayList();
|
private final ObservableList<ActivityLogResponse> activityLogs = FXCollections.observableArrayList();
|
||||||
private FilteredList<ActivityLogResponse> filteredLogs;
|
private FilteredList<ActivityLogResponse> filteredLogs;
|
||||||
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
||||||
private static final int DEFAULT_LIMIT = 2000;
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
colTimestamp.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLogTimestamp()));
|
colTimestamp.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getLogTimestamp()));
|
||||||
colTimestamp.setCellFactory(column -> new javafx.scene.control.TableCell<>() {
|
colTimestamp.setCellFactory(col -> new TableCell<>() {
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(Object item, boolean empty) {
|
protected void updateItem(Object item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
if (empty || item == null) {
|
if (empty || item == null) {
|
||||||
setText(null);
|
setText(null);
|
||||||
} else if (item instanceof java.time.LocalDateTime time) {
|
} else if (item instanceof java.time.LocalDateTime time) {
|
||||||
setText(time.format(formatter));
|
setText(time.format(DISPLAY_FORMATTER));
|
||||||
} else {
|
} else {
|
||||||
setText(item.toString());
|
setText(item.toString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
colUser.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayUser(data.getValue())));
|
colUser.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayUser(data.getValue())));
|
||||||
colRole.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayRole(data.getValue())));
|
colRole.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(formatRole(displayRole(data.getValue()))));
|
||||||
colStore.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayStore(data.getValue())));
|
colStore.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayStore(data.getValue())));
|
||||||
|
|
||||||
colActivity.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(nullToBlank(data.getValue().getActivity())));
|
colActivity.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(nullToBlank(data.getValue().getActivity())));
|
||||||
|
colActivity.setCellFactory(col -> new TableCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null || item.isBlank()) {
|
||||||
|
setGraphic(null);
|
||||||
|
setText(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
int sep = item.indexOf(SEPARATOR_NEW);
|
||||||
|
int sepLen = SEPARATOR_NEW.length();
|
||||||
|
if (sep < 0) {
|
||||||
|
sep = item.indexOf(SEPARATOR_OLD);
|
||||||
|
sepLen = SEPARATOR_OLD.length();
|
||||||
|
}
|
||||||
|
if (sep >= 0) {
|
||||||
|
String description = item.substring(0, sep).trim();
|
||||||
|
String technical = item.substring(sep + sepLen).trim();
|
||||||
|
Label lblDesc = new Label(description);
|
||||||
|
lblDesc.setStyle("-fx-font-weight: bold; -fx-text-fill: #1f2937;");
|
||||||
|
lblDesc.setWrapText(true);
|
||||||
|
Label lblTech = new Label(technical);
|
||||||
|
lblTech.setStyle("-fx-font-size: 11px; -fx-text-fill: #94a3b8; -fx-font-family: monospace;");
|
||||||
|
lblTech.setWrapText(true);
|
||||||
|
VBox box = new VBox(2, lblDesc, lblTech);
|
||||||
|
setGraphic(box);
|
||||||
|
setText(null);
|
||||||
|
} else {
|
||||||
|
setGraphic(null);
|
||||||
|
setText(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
tvActivityLogs.setFixedCellSize(52);
|
||||||
|
|
||||||
|
cbRoleFilter.setItems(FXCollections.observableArrayList("All Roles", "ADMIN", "STAFF", "CUSTOMER"));
|
||||||
|
cbRoleFilter.getSelectionModel().selectFirst();
|
||||||
|
cbRoleFilter.setOnAction(e -> applyFilters());
|
||||||
|
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList("All Stores"));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStoreFilter.setOnAction(e -> applyFilters());
|
||||||
|
|
||||||
|
tfSearch.textProperty().addListener((obs, oldVal, newVal) -> applyFilters());
|
||||||
|
|
||||||
filteredLogs = new FilteredList<>(activityLogs, a -> true);
|
filteredLogs = new FilteredList<>(activityLogs, a -> true);
|
||||||
TableViewSupport.bindSortedItems(tvActivityLogs, filteredLogs);
|
TableViewSupport.bindSortedItems(tvActivityLogs, filteredLogs);
|
||||||
|
|
||||||
dpStart.setValue(LocalDate.now().minusDays(30));
|
dpStart.setValue(LocalDate.now().minusDays(30));
|
||||||
dpEnd.setValue(LocalDate.now());
|
dpEnd.setValue(LocalDate.now());
|
||||||
loadLogs();
|
loadLogs();
|
||||||
@@ -103,22 +139,59 @@ public class ActivityLogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void onHideViewOnlyChanged(ActionEvent event) {
|
void onFilterChanged(ActionEvent event) {
|
||||||
applyViewOnlyFilter();
|
applyFilters();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyViewOnlyFilter() {
|
private void applyFilters() {
|
||||||
boolean hide = chkHideViewOnly != null && chkHideViewOnly.isSelected();
|
String search = tfSearch != null && tfSearch.getText() != null ? tfSearch.getText().trim().toLowerCase() : "";
|
||||||
if (filteredLogs != null) {
|
String role = cbRoleFilter != null ? cbRoleFilter.getValue() : null;
|
||||||
filteredLogs.setPredicate(hide
|
String store = cbStoreFilter != null ? cbStoreFilter.getValue() : null;
|
||||||
? a -> {
|
boolean hideViewOnly = chkHideViewOnly != null && chkHideViewOnly.isSelected();
|
||||||
String act = a.getActivity();
|
|
||||||
if (act == null) return true;
|
if (filteredLogs == null) return;
|
||||||
String trimmed = act.stripLeading();
|
|
||||||
return !trimmed.startsWith("View");
|
filteredLogs.setPredicate(log -> {
|
||||||
|
if (hideViewOnly) {
|
||||||
|
String act = log.getActivity();
|
||||||
|
if (act != null && act.stripLeading().startsWith("View")) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role != null && !role.equals("All Roles")) {
|
||||||
|
String logRole = displayRole(log);
|
||||||
|
if (!role.equalsIgnoreCase(logRole)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (store != null && !store.equals("All Stores")) {
|
||||||
|
if (!store.equals(displayStore(log))) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!search.isEmpty()) {
|
||||||
|
String activity = nullToBlank(log.getActivity()).toLowerCase();
|
||||||
|
String user = displayUser(log).toLowerCase();
|
||||||
|
String username = nullToBlank(log.getUsername()).toLowerCase();
|
||||||
|
String usernameSnap = nullToBlank(log.getUsernameSnapshot()).toLowerCase();
|
||||||
|
if (!activity.contains(search) && !user.contains(search)
|
||||||
|
&& !username.contains(search) && !usernameSnap.contains(search)) {
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
: a -> true);
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void populateStoreFilter(List<ActivityLogResponse> logs) {
|
||||||
|
TreeSet<String> stores = new TreeSet<>();
|
||||||
|
for (ActivityLogResponse log : logs) {
|
||||||
|
String s = displayStore(log);
|
||||||
|
if (!s.isBlank()) stores.add(s);
|
||||||
}
|
}
|
||||||
|
String current = cbStoreFilter.getValue();
|
||||||
|
ObservableList<String> options = FXCollections.observableArrayList("All Stores");
|
||||||
|
options.addAll(stores);
|
||||||
|
cbStoreFilter.setItems(options);
|
||||||
|
cbStoreFilter.setValue(options.contains(current) ? current : "All Stores");
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadLogs() {
|
private void loadLogs() {
|
||||||
@@ -126,8 +199,8 @@ public class ActivityLogController {
|
|||||||
tvActivityLogs.setDisable(true);
|
tvActivityLogs.setDisable(true);
|
||||||
btnRefresh.setDisable(true);
|
btnRefresh.setDisable(true);
|
||||||
|
|
||||||
java.time.LocalDate startDate = dpStart != null ? dpStart.getValue() : null;
|
LocalDate startDate = dpStart != null ? dpStart.getValue() : null;
|
||||||
java.time.LocalDate endDate = dpEnd != null ? dpEnd.getValue() : null;
|
LocalDate endDate = dpEnd != null ? dpEnd.getValue() : null;
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
@@ -136,7 +209,8 @@ public class ActivityLogController {
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
activityLogs.setAll(safeContent);
|
activityLogs.setAll(safeContent);
|
||||||
applyViewOnlyFilter();
|
populateStoreFilter(safeContent);
|
||||||
|
applyFilters();
|
||||||
tvActivityLogs.setDisable(false);
|
tvActivityLogs.setDisable(false);
|
||||||
btnRefresh.setDisable(false);
|
btnRefresh.setDisable(false);
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -145,8 +219,8 @@ public class ActivityLogController {
|
|||||||
ActivityLogger.getInstance().logException("ActivityLogController.loadLogs", e, "Loading activity logs");
|
ActivityLogger.getInstance().logException("ActivityLogController.loadLogs", e, "Loading activity logs");
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
lblError.setText(e.getMessage() == null || e.getMessage().isBlank()
|
lblError.setText(e.getMessage() == null || e.getMessage().isBlank()
|
||||||
? "Could not load activity logs."
|
? "Could not load activity logs."
|
||||||
: "Could not load activity logs: " + e.getMessage());
|
: "Could not load activity logs: " + e.getMessage());
|
||||||
tvActivityLogs.setDisable(false);
|
tvActivityLogs.setDisable(false);
|
||||||
btnRefresh.setDisable(false);
|
btnRefresh.setDisable(false);
|
||||||
});
|
});
|
||||||
@@ -155,41 +229,33 @@ public class ActivityLogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String displayUser(ActivityLogResponse log) {
|
private String displayUser(ActivityLogResponse log) {
|
||||||
if (log == null) {
|
if (log == null) return "";
|
||||||
return "";
|
if (log.getFullNameSnapshot() != null && !log.getFullNameSnapshot().isBlank()) return log.getFullNameSnapshot();
|
||||||
}
|
if (log.getFullName() != null && !log.getFullName().isBlank()) return log.getFullName();
|
||||||
if (log.getFullNameSnapshot() != null && !log.getFullNameSnapshot().isBlank()) {
|
if (log.getUsernameSnapshot() != null && !log.getUsernameSnapshot().isBlank()) return log.getUsernameSnapshot();
|
||||||
return log.getFullNameSnapshot();
|
if (log.getUsername() != null && !log.getUsername().isBlank()) return log.getUsername();
|
||||||
}
|
|
||||||
if (log.getFullName() != null && !log.getFullName().isBlank()) {
|
|
||||||
return log.getFullName();
|
|
||||||
}
|
|
||||||
if (log.getUsernameSnapshot() != null && !log.getUsernameSnapshot().isBlank()) {
|
|
||||||
return log.getUsernameSnapshot();
|
|
||||||
}
|
|
||||||
if (log.getUsername() != null && !log.getUsername().isBlank()) {
|
|
||||||
return log.getUsername();
|
|
||||||
}
|
|
||||||
return log.getUserId() != null ? String.valueOf(log.getUserId()) : "";
|
return log.getUserId() != null ? String.valueOf(log.getUserId()) : "";
|
||||||
}
|
}
|
||||||
|
|
||||||
private String displayRole(ActivityLogResponse log) {
|
private String displayRole(ActivityLogResponse log) {
|
||||||
if (log == null) {
|
if (log == null) return "";
|
||||||
return "";
|
if (log.getRoleSnapshot() != null && !log.getRoleSnapshot().isBlank()) return log.getRoleSnapshot();
|
||||||
}
|
|
||||||
if (log.getRoleSnapshot() != null && !log.getRoleSnapshot().isBlank()) {
|
|
||||||
return log.getRoleSnapshot();
|
|
||||||
}
|
|
||||||
return nullToBlank(log.getRole());
|
return nullToBlank(log.getRole());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String formatRole(String role) {
|
||||||
|
if (role == null) return "";
|
||||||
|
return switch (role.toUpperCase()) {
|
||||||
|
case "ADMIN" -> "Admin";
|
||||||
|
case "STAFF" -> "Staff";
|
||||||
|
case "CUSTOMER" -> "Customer";
|
||||||
|
default -> role;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private String displayStore(ActivityLogResponse log) {
|
private String displayStore(ActivityLogResponse log) {
|
||||||
if (log == null) {
|
if (log == null) return "";
|
||||||
return "";
|
if (log.getStoreNameSnapshot() != null && !log.getStoreNameSnapshot().isBlank()) return log.getStoreNameSnapshot();
|
||||||
}
|
|
||||||
if (log.getStoreNameSnapshot() != null && !log.getStoreNameSnapshot().isBlank()) {
|
|
||||||
return log.getStoreNameSnapshot();
|
|
||||||
}
|
|
||||||
return nullToBlank(log.getStoreName());
|
return nullToBlank(log.getStoreName());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import javafx.scene.layout.VBox;
|
|||||||
import javafx.scene.paint.ImagePattern;
|
import javafx.scene.paint.ImagePattern;
|
||||||
import javafx.scene.shape.Circle;
|
import javafx.scene.shape.Circle;
|
||||||
import javafx.scene.image.Image;
|
import javafx.scene.image.Image;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
import org.example.petshopdesktop.api.ChatRealtimeClient;
|
||||||
import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
||||||
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
||||||
@@ -203,11 +204,18 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
lblChatStatus.setText("Message sent");
|
lblChatStatus.setText("Message sent");
|
||||||
});
|
});
|
||||||
|
} catch (OutOfMemoryError e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
txtMessage.setText(content);
|
||||||
|
btnSend.setDisable(false);
|
||||||
|
lblChatStatus.setText("File too large to send");
|
||||||
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
txtMessage.setText(content);
|
txtMessage.setText(content);
|
||||||
btnSend.setDisable(false);
|
btnSend.setDisable(false);
|
||||||
lblChatStatus.setText("Chat send failed");
|
String msg = e.getMessage();
|
||||||
|
lblChatStatus.setText(msg != null && !msg.isBlank() ? msg : "Chat send failed");
|
||||||
ActivityLogger.getInstance().logException(
|
ActivityLogger.getInstance().logException(
|
||||||
"ChatController.sendMessage",
|
"ChatController.sendMessage",
|
||||||
e,
|
e,
|
||||||
@@ -222,6 +230,16 @@ public class ChatController {
|
|||||||
File file = org.example.petshopdesktop.util.FilePickerSupport.pickAnyFile(btnAttachment.getScene().getWindow());
|
File file = org.example.petshopdesktop.util.FilePickerSupport.pickAnyFile(btnAttachment.getScene().getWindow());
|
||||||
if (file == null) return;
|
if (file == null) return;
|
||||||
|
|
||||||
|
long maxBytes = 5 * 1024 * 1024;
|
||||||
|
if (file.length() > maxBytes) {
|
||||||
|
javafx.scene.control.Alert alert = new javafx.scene.control.Alert(javafx.scene.control.Alert.AlertType.WARNING);
|
||||||
|
alert.setTitle("File Too Large");
|
||||||
|
alert.setHeaderText("The selected file exceeds the 5 MB limit.");
|
||||||
|
alert.setContentText("Please choose a smaller file and try again.");
|
||||||
|
alert.showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
selectedAttachmentFile = file;
|
selectedAttachmentFile = file;
|
||||||
btnAttachment.setText("📎 " + file.getName());
|
btnAttachment.setText("📎 " + file.getName());
|
||||||
btnAttachment.setStyle("-fx-background-color: #dcfce7; -fx-background-radius: 12; -fx-text-fill: #166534; -fx-cursor: hand;");
|
btnAttachment.setStyle("-fx-background-color: #dcfce7; -fx-background-radius: 12; -fx-text-fill: #166534; -fx-cursor: hand;");
|
||||||
@@ -539,10 +557,31 @@ public class ChatController {
|
|||||||
bubble.getChildren().add(content);
|
bubble.getChildren().add(content);
|
||||||
}
|
}
|
||||||
if (message.getAttachmentName() != null && !message.getAttachmentName().isBlank()) {
|
if (message.getAttachmentName() != null && !message.getAttachmentName().isBlank()) {
|
||||||
Label attachmentLabel = new Label("\uD83D\uDCCE " + message.getAttachmentName());
|
String mimeType = message.getAttachmentMimeType();
|
||||||
attachmentLabel.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#475569") + "; -fx-font-size: 12px;");
|
boolean isImage = mimeType != null && mimeType.startsWith("image/");
|
||||||
attachmentLabel.setWrapText(true);
|
if (isImage && message.getId() != null) {
|
||||||
bubble.getChildren().add(attachmentLabel);
|
ImageView imageView = new ImageView();
|
||||||
|
imageView.setFitWidth(280);
|
||||||
|
imageView.setFitHeight(280);
|
||||||
|
imageView.setPreserveRatio(true);
|
||||||
|
imageView.setSmooth(true);
|
||||||
|
bubble.getChildren().add(imageView);
|
||||||
|
String attachmentPath = "/api/v1/chat/messages/" + message.getId() + "/attachment";
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
byte[] bytes = ApiClient.getInstance().getBytes(attachmentPath);
|
||||||
|
Image img = new Image(new java.io.ByteArrayInputStream(bytes));
|
||||||
|
if (!img.isError()) {
|
||||||
|
Platform.runLater(() -> imageView.setImage(img));
|
||||||
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}).start();
|
||||||
|
} else {
|
||||||
|
Label attachmentLabel = new Label("\uD83D\uDCCE " + message.getAttachmentName());
|
||||||
|
attachmentLabel.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#475569") + "; -fx-font-size: 12px;");
|
||||||
|
attachmentLabel.setWrapText(true);
|
||||||
|
bubble.getChildren().add(attachmentLabel);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bubble.getChildren().add(timestamp);
|
bubble.getChildren().add(timestamp);
|
||||||
|
|||||||
@@ -3,57 +3,79 @@
|
|||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.CheckBox?>
|
<?import javafx.scene.control.CheckBox?>
|
||||||
|
<?import javafx.scene.control.ComboBox?>
|
||||||
<?import javafx.scene.control.DatePicker?>
|
<?import javafx.scene.control.DatePicker?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.TableColumn?>
|
<?import javafx.scene.control.TableColumn?>
|
||||||
<?import javafx.scene.control.TableView?>
|
<?import javafx.scene.control.TableView?>
|
||||||
|
<?import javafx.scene.control.TextField?>
|
||||||
<?import javafx.scene.layout.HBox?>
|
<?import javafx.scene.layout.HBox?>
|
||||||
|
<?import javafx.scene.layout.Priority?>
|
||||||
|
<?import javafx.scene.layout.Region?>
|
||||||
<?import javafx.scene.layout.VBox?>
|
<?import javafx.scene.layout.VBox?>
|
||||||
<?import javafx.scene.text.Font?>
|
<?import javafx.scene.text.Font?>
|
||||||
|
|
||||||
<VBox spacing="18.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.ActivityLogController">
|
<VBox spacing="12.0" style="-fx-font-size: 14px;" xmlns="http://javafx.com/javafx/25" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.example.petshopdesktop.controllers.ActivityLogController">
|
||||||
<padding>
|
<padding>
|
||||||
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
<Insets bottom="20.0" left="20.0" right="20.0" top="20.0" />
|
||||||
</padding>
|
</padding>
|
||||||
|
|
||||||
<children>
|
<children>
|
||||||
<HBox alignment="CENTER_LEFT" spacing="20.0">
|
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="12.0">
|
||||||
<children>
|
<children>
|
||||||
<Label text="Activity Logs" textFill="#2c3e50">
|
<Label text="Activity Logs" textFill="#2c3e50">
|
||||||
<font>
|
<font>
|
||||||
<Font name="System Bold" size="30.0" />
|
<Font name="System Bold" size="30.0" />
|
||||||
</font>
|
</font>
|
||||||
</Label>
|
</Label>
|
||||||
<Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefreshClicked" prefHeight="44.0" prefWidth="118.0" style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;" text="Refresh" textFill="WHITE">
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
<font>
|
<Button fx:id="btnRefresh" mnemonicParsing="false" onAction="#btnRefreshClicked"
|
||||||
<Font name="System Bold" size="14.0" />
|
prefHeight="36.0" prefWidth="100.0"
|
||||||
</font>
|
style="-fx-background-color: #4ECDC4; -fx-cursor: hand; -fx-background-radius: 8;"
|
||||||
<padding>
|
text="Refresh" textFill="WHITE">
|
||||||
<Insets bottom="12.0" left="24.0" right="24.0" top="12.0" />
|
<font><Font name="System Bold" size="13.0" /></font>
|
||||||
</padding>
|
|
||||||
</Button>
|
</Button>
|
||||||
<Label text="From" textFill="#64748b" />
|
|
||||||
<DatePicker fx:id="dpStart" onAction="#onDateChanged" promptText="Start date" prefWidth="140.0" />
|
|
||||||
<Label text="To" textFill="#64748b" />
|
|
||||||
<DatePicker fx:id="dpEnd" onAction="#onDateChanged" promptText="End date" prefWidth="140.0" />
|
|
||||||
<CheckBox fx:id="chkHideViewOnly" onAction="#onHideViewOnlyChanged" text="Hide view-only" />
|
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
|
||||||
<Label text="Showing activity for the selected date range" textFill="#64748b" />
|
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<TextField fx:id="tfSearch" promptText="Search activity or user…"
|
||||||
|
prefHeight="36.0" HBox.hgrow="ALWAYS"
|
||||||
|
style="-fx-background-radius: 8; -fx-border-radius: 8; -fx-border-color: #e2e8f0;" />
|
||||||
|
|
||||||
|
<ComboBox fx:id="cbRoleFilter" prefHeight="36.0" prefWidth="130.0"
|
||||||
|
style="-fx-background-radius: 8;" />
|
||||||
|
|
||||||
|
<ComboBox fx:id="cbStoreFilter" prefHeight="36.0" prefWidth="160.0"
|
||||||
|
style="-fx-background-radius: 8;" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
|
<HBox alignment="CENTER_LEFT" spacing="10.0">
|
||||||
|
<children>
|
||||||
|
<Label text="From" textFill="#64748b" />
|
||||||
|
<DatePicker fx:id="dpStart" onAction="#onDateChanged" promptText="Start date" prefWidth="140.0" prefHeight="36.0" />
|
||||||
|
<Label text="To" textFill="#64748b" />
|
||||||
|
<DatePicker fx:id="dpEnd" onAction="#onDateChanged" promptText="End date" prefWidth="140.0" prefHeight="36.0" />
|
||||||
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<CheckBox fx:id="chkHideViewOnly" onAction="#onFilterChanged" text="Hide view-only" />
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
|
|
||||||
<TableView fx:id="tvActivityLogs" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
<TableView fx:id="tvActivityLogs" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||||
<columns>
|
<columns>
|
||||||
<TableColumn fx:id="colTimestamp" prefWidth="170.0" text="Timestamp" />
|
<TableColumn fx:id="colTimestamp" prefWidth="150.0" text="Timestamp" />
|
||||||
<TableColumn fx:id="colUser" prefWidth="170.0" text="User" />
|
<TableColumn fx:id="colUser" prefWidth="160.0" text="User" />
|
||||||
<TableColumn fx:id="colRole" prefWidth="90.0" text="Role" />
|
<TableColumn fx:id="colRole" prefWidth="90.0" text="Role" />
|
||||||
<TableColumn fx:id="colStore" prefWidth="160.0" text="Store" />
|
<TableColumn fx:id="colStore" prefWidth="140.0" text="Store" />
|
||||||
<TableColumn fx:id="colActivity" prefWidth="520.0" text="Activity" />
|
<TableColumn fx:id="colActivity" prefWidth="560.0" text="Activity" />
|
||||||
</columns>
|
</columns>
|
||||||
</TableView>
|
</TableView>
|
||||||
|
|
||||||
<Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="true" />
|
<Label fx:id="lblStatus" text="" textFill="#64748b" visible="false" managed="true" />
|
||||||
|
|
||||||
<Label fx:id="lblError" text="" textFill="#FF6B6B" wrapText="true" />
|
<Label fx:id="lblError" text="" textFill="#FF6B6B" wrapText="true" />
|
||||||
|
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
|
|||||||
Reference in New Issue
Block a user