merge main into websitefinal
This commit is contained in:
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Bash(grep -E \"\\\\.\\(tsx?|jsx?\\)$\")",
|
||||
"Bash(grep -E \"\\\\.js$|^d\")"
|
||||
]
|
||||
}
|
||||
}
|
||||
73
.github/workflows/deploy.yml
vendored
Normal file
73
.github/workflows/deploy.yml
vendored
Normal file
@@ -0,0 +1,73 @@
|
||||
name: Build and Deploy
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, azure-deploy]
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
build-and-deploy:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set image names (lowercase)
|
||||
run: |
|
||||
OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "BACKEND_IMAGE=ghcr.io/${OWNER}/petshop-backend" >> $GITHUB_ENV
|
||||
echo "FRONTEND_IMAGE=ghcr.io/${OWNER}/petshop-web" >> $GITHUB_ENV
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push backend image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./backend
|
||||
push: true
|
||||
tags: ${{ env.BACKEND_IMAGE }}:latest
|
||||
|
||||
- name: Build and push frontend image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: ./web
|
||||
push: true
|
||||
tags: ${{ env.FRONTEND_IMAGE }}:latest
|
||||
build-args: |
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
|
||||
|
||||
- name: Log in to Azure
|
||||
uses: azure/login@v1
|
||||
with:
|
||||
creds: ${{ secrets.AZURE_CREDENTIALS }}
|
||||
|
||||
- name: Deploy backend
|
||||
run: |
|
||||
az containerapp update \
|
||||
--name ${{ secrets.AZURE_BACKEND_APP_NAME }} \
|
||||
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
||||
--image ${{ env.BACKEND_IMAGE }}:latest \
|
||||
--registry-server ${{ env.REGISTRY }} \
|
||||
--registry-username ${{ github.actor }} \
|
||||
--registry-password ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Deploy frontend
|
||||
run: |
|
||||
az containerapp update \
|
||||
--name ${{ secrets.AZURE_FRONTEND_APP_NAME }} \
|
||||
--resource-group ${{ secrets.AZURE_RESOURCE_GROUP }} \
|
||||
--image ${{ env.FRONTEND_IMAGE }}:latest \
|
||||
--registry-server ${{ env.REGISTRY }} \
|
||||
--registry-username ${{ github.actor }} \
|
||||
--registry-password ${{ secrets.GITHUB_TOKEN }}
|
||||
1570
.idea/caches/deviceStreaming.xml
generated
Normal file
1570
.idea/caches/deviceStreaming.xml
generated
Normal file
File diff suppressed because it is too large
Load Diff
8
.idea/markdown.xml
generated
Normal file
8
.idea/markdown.xml
generated
Normal file
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="MarkdownSettings">
|
||||
<option name="previewPanelProviderInfo">
|
||||
<ProviderInfo name="Compose (experimental)" className="com.intellij.markdown.compose.preview.ComposePanelProvider" />
|
||||
</option>
|
||||
</component>
|
||||
</project>
|
||||
@@ -16,9 +16,9 @@ val localProperties = Properties().apply {
|
||||
fun quoted(value: String): String = "\"$value\""
|
||||
|
||||
val emulatorBackendUrl =
|
||||
(localProperties.getProperty("petstore.backend.emulatorUrl") ?: "http://10.0.2.2:8080/").trim()
|
||||
(localProperties.getProperty("petstore.backend.emulatorUrl") ?: "https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/").trim()
|
||||
val deviceBackendUrl =
|
||||
(localProperties.getProperty("petstore.backend.deviceUrl") ?: "http://10.0.0.200:8080/").trim()
|
||||
(localProperties.getProperty("petstore.backend.deviceUrl") ?: "https://petshop-backend.nicepond-c7280126.westus2.azurecontainerapps.io/").trim()
|
||||
|
||||
android {
|
||||
namespace = "com.example.petstoremobile"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.example.petstoremobile.activities;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.View;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.EdgeToEdge;
|
||||
@@ -9,14 +10,26 @@ import androidx.core.graphics.Insets;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.view.WindowInsetsCompat;
|
||||
|
||||
import com.example.petstoremobile.api.auth.AuthApi;
|
||||
import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding;
|
||||
import com.example.petstoremobile.utils.InputValidator;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
@AndroidEntryPoint
|
||||
public class ForgotPasswordActivity extends AppCompatActivity {
|
||||
|
||||
@Inject
|
||||
AuthApi authApi;
|
||||
|
||||
private ActivityForgotPasswordBinding binding;
|
||||
|
||||
@Override
|
||||
@@ -33,14 +46,39 @@ public class ForgotPasswordActivity extends AppCompatActivity {
|
||||
});
|
||||
|
||||
binding.btnSubmit.setOnClickListener(v -> {
|
||||
if (InputValidator.isValidEmail(binding.etEmail)) {
|
||||
String email = binding.etEmail.getText().toString().trim();
|
||||
// TODO: Implement password reset logic here
|
||||
Toast.makeText(this, "If this email is linked, a reset email will be sent.", Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
if (!InputValidator.isValidEmail(binding.etEmail)) return;
|
||||
String email = binding.etEmail.getText().toString().trim();
|
||||
sendResetLink(email);
|
||||
});
|
||||
|
||||
binding.btnBackToLogin.setOnClickListener(v -> finish());
|
||||
}
|
||||
|
||||
private void sendResetLink(String email) {
|
||||
binding.btnSubmit.setEnabled(false);
|
||||
|
||||
Map<String, String> body = new HashMap<>();
|
||||
body.put("usernameOrEmail", email);
|
||||
|
||||
authApi.forgotPassword(body).enqueue(new Callback<Void>() {
|
||||
@Override
|
||||
public void onResponse(Call<Void> call, Response<Void> response) {
|
||||
if (binding == null) return;
|
||||
binding.btnSubmit.setEnabled(true);
|
||||
Toast.makeText(ForgotPasswordActivity.this,
|
||||
"If this email is registered, a reset link will be sent.",
|
||||
Toast.LENGTH_LONG).show();
|
||||
finish();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Void> call, Throwable t) {
|
||||
if (binding == null) return;
|
||||
binding.btnSubmit.setEnabled(true);
|
||||
Toast.makeText(ForgotPasswordActivity.this,
|
||||
"Could not send reset link. Please try again.",
|
||||
Toast.LENGTH_LONG).show();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,11 +10,19 @@ import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
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.Locale;
|
||||
|
||||
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;
|
||||
|
||||
public ActivityLogAdapter(List<ActivityLogDTO> items) {
|
||||
@@ -32,26 +40,77 @@ public class ActivityLogAdapter extends RecyclerView.Adapter<ActivityLogAdapter.
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
ActivityLogDTO log = items.get(position);
|
||||
holder.tvActivity.setText(log.getActivity());
|
||||
holder.tvUser.setText(log.getFullName() + " (" + log.getUsername() + ")");
|
||||
holder.tvMeta.setText(log.getStoreName() + " · " + log.getRole());
|
||||
String timestamp = log.getLogTimestamp();
|
||||
String date = DateTimeUtils.extractDate(timestamp);
|
||||
String time = (timestamp != null && timestamp.length() >= 16) ? timestamp.substring(11, 16) : null;
|
||||
holder.tvTimestamp.setText(date != null && time != null ? date + " " + time : date);
|
||||
|
||||
String activity = log.getActivity() != null ? log.getActivity() : "";
|
||||
int separatorIndex = activity.indexOf(SEPARATOR);
|
||||
if (separatorIndex >= 0) {
|
||||
holder.tvActivity.setText(activity.substring(0, separatorIndex).trim());
|
||||
holder.tvTechnical.setText(activity.substring(separatorIndex + SEPARATOR.length()).trim());
|
||||
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
|
||||
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 {
|
||||
TextView tvActivity, tvUser, tvMeta, tvTimestamp;
|
||||
TextView tvActivity, tvTechnical, tvUser, tvMeta, tvTimestamp;
|
||||
|
||||
public ViewHolder(@NonNull View itemView) {
|
||||
super(itemView);
|
||||
tvActivity = itemView.findViewById(R.id.tvLogActivity);
|
||||
tvUser = itemView.findViewById(R.id.tvLogUser);
|
||||
tvMeta = itemView.findViewById(R.id.tvLogMeta);
|
||||
tvActivity = itemView.findViewById(R.id.tvLogActivity);
|
||||
tvTechnical = itemView.findViewById(R.id.tvLogTechnical);
|
||||
tvUser = itemView.findViewById(R.id.tvLogUser);
|
||||
tvMeta = itemView.findViewById(R.id.tvLogMeta);
|
||||
tvTimestamp = itemView.findViewById(R.id.tvLogTimestamp);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,15 @@ import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.load.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemMessageReceivedBinding;
|
||||
import com.example.petstoremobile.databinding.ItemMessageSentBinding;
|
||||
import com.example.petstoremobile.models.Message;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
|
||||
@@ -28,6 +32,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
|
||||
private final List<Message> messages;
|
||||
private Long currentUserId;
|
||||
private Long staffId;
|
||||
private String token;
|
||||
private String baseUrl;
|
||||
private OnAttachmentClickListener attachmentClickListener;
|
||||
@@ -35,6 +40,13 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
public MessageAdapter(List<Message> messages, Long currentUserId) {
|
||||
this.messages = messages;
|
||||
this.currentUserId = currentUserId;
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
Message m = messages.get(position);
|
||||
return m.getId() != null ? m.getId() : position;
|
||||
}
|
||||
|
||||
public void setCurrentUserId(Long id) {
|
||||
@@ -42,6 +54,11 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setStaffId(Long id) {
|
||||
this.staffId = id;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setToken(String token) {
|
||||
this.token = token;
|
||||
}
|
||||
@@ -79,7 +96,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
Message m = messages.get(position);
|
||||
if (holder instanceof SentHolder) ((SentHolder) holder).bind(m, token, baseUrl, attachmentClickListener);
|
||||
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token, baseUrl, attachmentClickListener);
|
||||
if (holder instanceof ReceivedHolder) ((ReceivedHolder) holder).bind(m, token, baseUrl, attachmentClickListener, staffId);
|
||||
}
|
||||
|
||||
@Override public int getItemCount() { return messages.size(); }
|
||||
@@ -91,7 +108,9 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
this.binding = binding;
|
||||
}
|
||||
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
|
||||
// Check for Text
|
||||
binding.tvSenderName.setText("You");
|
||||
binding.tvTimestamp.setText(formatTimestamp(m.getTimestamp()));
|
||||
|
||||
if (m.getContent() != null && !m.getContent().isEmpty()) {
|
||||
binding.tvMessageContent.setVisibility(View.VISIBLE);
|
||||
binding.tvMessageContent.setText(m.getContent());
|
||||
@@ -99,7 +118,6 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
binding.tvMessageContent.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Check for Attachment
|
||||
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
|
||||
|
||||
View.OnClickListener click = v -> {
|
||||
@@ -116,8 +134,10 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
super(binding.getRoot());
|
||||
this.binding = binding;
|
||||
}
|
||||
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener) {
|
||||
// Check for Text
|
||||
void bind(Message m, String token, String baseUrl, OnAttachmentClickListener listener, Long staffId) {
|
||||
binding.tvSenderName.setText(resolveSenderName(m, staffId));
|
||||
binding.tvTimestamp.setText(formatTimestamp(m.getTimestamp()));
|
||||
|
||||
if (m.getContent() != null && !m.getContent().isEmpty()) {
|
||||
binding.tvMessageContent.setVisibility(View.VISIBLE);
|
||||
binding.tvMessageContent.setText(m.getContent());
|
||||
@@ -125,7 +145,6 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
binding.tvMessageContent.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
// Check for Attachment
|
||||
displayAttachment(m, binding.ivAttachment, binding.tvAttachmentName, token, baseUrl);
|
||||
|
||||
View.OnClickListener click = v -> {
|
||||
@@ -136,7 +155,29 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
}
|
||||
}
|
||||
|
||||
// helper function to display the attachment to the chat bubble
|
||||
private static String resolveSenderName(Message m, Long staffId) {
|
||||
if ("BOT".equalsIgnoreCase(m.getSenderRole())) {
|
||||
return (m.getSenderDisplayName() != null && !m.getSenderDisplayName().isEmpty())
|
||||
? m.getSenderDisplayName() : "AI Bot";
|
||||
}
|
||||
if (staffId != null && staffId.equals(m.getSenderId())) {
|
||||
return "Staff";
|
||||
}
|
||||
return "Customer";
|
||||
}
|
||||
|
||||
private static String formatTimestamp(String timestamp) {
|
||||
if (timestamp == null || timestamp.isEmpty()) return "";
|
||||
try {
|
||||
String normalized = timestamp.length() > 19 ? timestamp.substring(0, 19) : timestamp;
|
||||
SimpleDateFormat input = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss", Locale.getDefault());
|
||||
Date date = input.parse(normalized);
|
||||
return new SimpleDateFormat("MMM d, HH:mm", Locale.getDefault()).format(date);
|
||||
} catch (Exception e) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
|
||||
private static void displayAttachment(Message m, ImageView iv, TextView tvName, String token, String baseUrl) {
|
||||
// Check if there's an attachment by looking at name or mime type
|
||||
if (m.getAttachmentName() != null || m.getAttachmentMimeType() != null) {
|
||||
@@ -150,6 +191,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
}
|
||||
|
||||
if (url == null) {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.GONE);
|
||||
return;
|
||||
@@ -166,18 +208,25 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
.build());
|
||||
}
|
||||
|
||||
// Use a signature to prevent Glide from showing stale cached images for the same URL/ID
|
||||
String signatureKey = (m.getTimestamp() != null ? m.getTimestamp() : "") + m.getId();
|
||||
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
Glide.with(iv.getContext())
|
||||
.load(loadTarget)
|
||||
.signature(new ObjectKey(signatureKey))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(iv);
|
||||
} else {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.VISIBLE);
|
||||
tvName.setText(m.getAttachmentName() != null ? m.getAttachmentName() : "Attachment");
|
||||
}
|
||||
} else {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.example.petstoremobile.adapters;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.ViewGroup;
|
||||
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.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));
|
||||
}
|
||||
|
||||
|
||||
@@ -48,6 +48,12 @@ public class SaleAdapter extends RecyclerView.Adapter<SaleAdapter.SaleViewHolder
|
||||
|
||||
binding.tvSaleId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : ""));
|
||||
binding.tvSaleEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : ""));
|
||||
if (s.getCustomerName() != null && !s.getCustomerName().isEmpty()) {
|
||||
binding.tvSaleCustomer.setText("Customer: " + s.getCustomerName());
|
||||
binding.tvSaleCustomer.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.tvSaleCustomer.setVisibility(View.GONE);
|
||||
}
|
||||
binding.tvSaleDate.setText(s.getSaleDate() != null ? s.getSaleDate().substring(0, Math.min(10, s.getSaleDate().length())) : "");
|
||||
binding.tvSalePayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : "");
|
||||
binding.tvSaleTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : "");
|
||||
|
||||
@@ -15,6 +15,8 @@ public interface ActivityLogApi {
|
||||
@Query("limit") int limit,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("role") String role,
|
||||
@Query("search") String search
|
||||
@Query("search") String search,
|
||||
@Query("startDate") String startDate,
|
||||
@Query("endDate") String endDate
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ public interface SaleApi {
|
||||
@Query("paymentMethod") String paymentMethod,
|
||||
@Query("storeId") Long storeId,
|
||||
@Query("isRefund") Boolean isRefund,
|
||||
@Query("customerId") Long customerId,
|
||||
@Query("sort") String sort);
|
||||
|
||||
@GET("api/v1/sales/{id}")
|
||||
|
||||
@@ -43,4 +43,8 @@ public interface AuthApi {
|
||||
@DELETE("api/v1/auth/me/avatar")
|
||||
Call<Void> deleteAvatar();
|
||||
|
||||
//forgot password endpoint
|
||||
@POST("api/v1/auth/forgot-password")
|
||||
Call<Void> forgotPassword(@Body Map<String, String> body);
|
||||
|
||||
}
|
||||
|
||||
@@ -34,6 +34,12 @@ public class MessageDTO {
|
||||
@SerializedName("attachmentSizeBytes")
|
||||
private Long attachmentSizeBytes;
|
||||
|
||||
@SerializedName("senderRole")
|
||||
private String senderRole;
|
||||
|
||||
@SerializedName("senderDisplayName")
|
||||
private String senderDisplayName;
|
||||
|
||||
public MessageDTO() {}
|
||||
|
||||
public Long getId() { return id; }
|
||||
@@ -65,4 +71,10 @@ public class MessageDTO {
|
||||
|
||||
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
|
||||
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
|
||||
|
||||
public String getSenderRole() { return senderRole; }
|
||||
public void setSenderRole(String senderRole) { this.senderRole = senderRole; }
|
||||
|
||||
public String getSenderDisplayName() { return senderDisplayName; }
|
||||
public void setSenderDisplayName(String senderDisplayName) { this.senderDisplayName = senderDisplayName; }
|
||||
}
|
||||
@@ -182,6 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
LinearLayoutManager lm = new LinearLayoutManager(getContext());
|
||||
lm.setStackFromEnd(true);
|
||||
binding.rvMessages.setLayoutManager(lm);
|
||||
binding.rvMessages.setItemAnimator(null);
|
||||
binding.rvMessages.setAdapter(messageAdapter);
|
||||
setConversationActive(false, null);
|
||||
}
|
||||
@@ -285,9 +286,14 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
});
|
||||
|
||||
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
|
||||
int prevSize = messageList.size();
|
||||
messageList.clear();
|
||||
messageList.addAll(list);
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
if (prevSize > 0 && list.size() == prevSize + 1) {
|
||||
messageAdapter.notifyItemInserted(list.size() - 1);
|
||||
} else {
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
}
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
@@ -356,6 +362,8 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
binding.tvChatTitle.setText(chat.getCustomerName());
|
||||
binding.chatDrawerLayout.closeDrawer(GravityCompat.START);
|
||||
|
||||
messageAdapter.setStaffId(chat.getStaffId());
|
||||
|
||||
if (stompChatManager != null) stompChatManager.subscribeToConversation(activeConversationId);
|
||||
viewModel.loadMessageHistory(activeConversationId);
|
||||
}
|
||||
@@ -464,9 +472,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
public void onMessageReceived(MessageDTO dto) {
|
||||
requireActivity().runOnUiThread(() -> {
|
||||
if (activeConversationId != null && activeConversationId.equals(dto.getConversationId())) {
|
||||
if (!tokenManager.getUserId().equals(dto.getSenderId())) {
|
||||
viewModel.addMessageLocally(dto);
|
||||
}
|
||||
viewModel.addMessageLocally(dto);
|
||||
}
|
||||
viewModel.updateConversationLocally(new ConversationDTO(dto.getConversationId(), 0L, 0L, dto.getContent(), ""));
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.example.petstoremobile.fragments.listfragments;
|
||||
|
||||
import android.app.DatePickerDialog;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -10,6 +11,7 @@ import androidx.annotation.Nullable;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.widget.Toast;
|
||||
|
||||
@@ -27,6 +29,7 @@ import com.example.petstoremobile.viewmodels.ActivityLogListViewModel;
|
||||
import javax.inject.Inject;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.List;
|
||||
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
@@ -38,6 +41,8 @@ public class ActivityLogFragment extends Fragment {
|
||||
private ActivityLogAdapter adapter;
|
||||
private final List<ActivityLogDTO> logList = new ArrayList<>();
|
||||
private List<DropdownDTO> storeList = new ArrayList<>();
|
||||
private String selectedStartDate = null;
|
||||
private String selectedEndDate = null;
|
||||
|
||||
@Inject TokenManager tokenManager;
|
||||
|
||||
@@ -74,6 +79,22 @@ public class ActivityLogFragment extends Fragment {
|
||||
adapter = new ActivityLogAdapter(logList);
|
||||
binding.recyclerViewActivityLog.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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() {
|
||||
@@ -89,6 +110,35 @@ public class ActivityLogFragment extends Fragment {
|
||||
? binding.spinnerRoleFilter.getSelectedItem().toString() : "All Roles"));
|
||||
|
||||
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() {
|
||||
|
||||
@@ -14,6 +14,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.AdoptionAdapter;
|
||||
@@ -114,14 +115,14 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
adapter,
|
||||
"adoption",
|
||||
viewModel::bulkDeleteAdoptions,
|
||||
this::loadAdoptions
|
||||
() -> loadAdoptions(true)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadAdoptions();
|
||||
loadAdoptions(true);
|
||||
if (!isStaff()) viewModel.loadStores();
|
||||
}
|
||||
|
||||
@@ -159,7 +160,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
} else {
|
||||
selectedCalendarDay = null;
|
||||
}
|
||||
loadAdoptions();
|
||||
loadAdoptions(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,26 +188,42 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
||||
adapter = new AdoptionAdapter(adoptionList, this);
|
||||
binding.recyclerViewAdoptions.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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() {
|
||||
UIUtils.attachSearch(binding.etSearchAdoption, this::loadAdoptions);
|
||||
UIUtils.attachSearch(binding.etSearchAdoption, () -> loadAdoptions(true));
|
||||
}
|
||||
|
||||
private void setupStatusFilter() {
|
||||
String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
|
||||
String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, () -> loadAdoptions(true));
|
||||
}
|
||||
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, this::loadAdoptions);
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStoreAdoption, () -> loadAdoptions(true));
|
||||
}
|
||||
|
||||
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 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;
|
||||
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) {
|
||||
|
||||
@@ -7,11 +7,13 @@ import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import javax.inject.Inject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
@@ -19,6 +21,9 @@ import java.util.*;
|
||||
@AndroidEntryPoint
|
||||
public class AnalyticsFragment extends Fragment {
|
||||
|
||||
@Inject
|
||||
TokenManager tokenManager;
|
||||
|
||||
private FragmentAnalyticsBinding binding;
|
||||
private AnalyticsViewModel viewModel;
|
||||
private boolean filtersExpanded = false;
|
||||
@@ -33,6 +38,7 @@ public class AnalyticsFragment extends Fragment {
|
||||
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
|
||||
|
||||
setupFilterPanel();
|
||||
setupViewModeToggle();
|
||||
observeViewModel();
|
||||
viewModel.loadAnalytics();
|
||||
|
||||
@@ -42,6 +48,39 @@ public class AnalyticsFragment extends Fragment {
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private static final int COLOR_SELECTED = 0xFF4ECDC4;
|
||||
private static final int COLOR_UNSELECTED = 0xFFCBD5E1;
|
||||
|
||||
private void setupViewModeToggle() {
|
||||
updateViewModeButtonStyles(viewModel.getViewMode());
|
||||
|
||||
binding.btnMyAnalytics.setOnClickListener(v -> {
|
||||
viewModel.setViewMode("mine");
|
||||
updateViewModeButtonStyles("mine");
|
||||
updateStoreFilterVisibility("mine");
|
||||
});
|
||||
|
||||
binding.btnStoreAnalytics.setOnClickListener(v -> {
|
||||
viewModel.setViewMode("store");
|
||||
updateViewModeButtonStyles("store");
|
||||
updateStoreFilterVisibility("store");
|
||||
});
|
||||
}
|
||||
|
||||
private void updateViewModeButtonStyles(String mode) {
|
||||
binding.btnMyAnalytics.setBackgroundTintList(
|
||||
android.content.res.ColorStateList.valueOf(mode.equals("mine") ? COLOR_SELECTED : COLOR_UNSELECTED));
|
||||
binding.btnStoreAnalytics.setBackgroundTintList(
|
||||
android.content.res.ColorStateList.valueOf(mode.equals("store") ? COLOR_SELECTED : COLOR_UNSELECTED));
|
||||
}
|
||||
|
||||
private void updateStoreFilterVisibility(String mode) {
|
||||
boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole());
|
||||
int vis = (isAdmin && mode.equals("store")) ? View.VISIBLE : View.GONE;
|
||||
binding.tvStoreFilterLabel.setVisibility(vis);
|
||||
binding.spinnerFilterStore.setVisibility(vis);
|
||||
}
|
||||
|
||||
// Filter Panel
|
||||
|
||||
private void setupFilterPanel() {
|
||||
@@ -96,6 +135,9 @@ public class AnalyticsFragment extends Fragment {
|
||||
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
|
||||
filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
||||
|
||||
Object store = binding.spinnerFilterStore.getSelectedItem();
|
||||
viewModel.setStoreFilter(store != null ? store.toString() : "All Stores");
|
||||
|
||||
updateFilterSummary();
|
||||
viewModel.applyFilter(filter);
|
||||
}
|
||||
@@ -104,8 +146,8 @@ public class AnalyticsFragment extends Fragment {
|
||||
binding.etFilterStartDate.setText("");
|
||||
binding.etFilterEndDate.setText("");
|
||||
binding.spinnerTopN.setSelection(0);
|
||||
// Reset payment method to "All"
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, "All Stores");
|
||||
updateFilterSummary();
|
||||
viewModel.resetFilter();
|
||||
}
|
||||
@@ -162,6 +204,16 @@ public class AnalyticsFragment extends Fragment {
|
||||
methods.toArray(new String[0]));
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection);
|
||||
});
|
||||
|
||||
viewModel.getAvailableStores().observe(getViewLifecycleOwner(), stores -> {
|
||||
if (stores == null || stores.isEmpty()) return;
|
||||
String currentSelection = binding.spinnerFilterStore.getSelectedItem() != null
|
||||
? binding.spinnerFilterStore.getSelectedItem().toString() : "All Stores";
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterStore,
|
||||
stores.toArray(new String[0]));
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, currentSelection);
|
||||
updateStoreFilterVisibility(viewModel.getViewMode());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -224,17 +276,22 @@ public class AnalyticsFragment extends Fragment {
|
||||
}
|
||||
|
||||
// Employee Performance
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
||||
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
|
||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
|
||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
|
||||
boolean showEmployeeSection = viewModel.getViewMode().equals("store");
|
||||
View empParent = (View) binding.llEmployeePerformance.getParent();
|
||||
if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE);
|
||||
if (showEmployeeSection) {
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
||||
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
|
||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
|
||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llEmployeePerformance, "No data");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llEmployeePerformance, "No data");
|
||||
}
|
||||
|
||||
// Daily Revenue
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.LayoutInflater;
|
||||
@@ -123,14 +124,14 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
adapter,
|
||||
"appointment",
|
||||
viewModel::bulkDeleteAppointments,
|
||||
this::loadAppointmentData
|
||||
() -> loadAppointmentData(true)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadAppointmentData();
|
||||
loadAppointmentData(true);
|
||||
if (!isStaff()) viewModel.loadStores();
|
||||
}
|
||||
|
||||
@@ -143,7 +144,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
|
||||
private void setupMyAppointmentFilter() {
|
||||
binding.btnMyAppointments.setOnClickListener(v -> {
|
||||
loadAppointmentData();
|
||||
loadAppointmentData(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -177,7 +178,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
} else {
|
||||
selectedCalendarDay = null;
|
||||
}
|
||||
loadAppointmentData();
|
||||
loadAppointmentData(true);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -200,20 +201,20 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchAppointment, this::loadAppointmentData);
|
||||
UIUtils.attachSearch(binding.etSearchAppointment, () -> loadAppointmentData(true));
|
||||
}
|
||||
|
||||
private void setupStatusFilter() {
|
||||
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() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadAppointmentData);
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadAppointmentData(true));
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshAppointment.setOnRefreshListener(this::loadAppointmentData);
|
||||
binding.swipeRefreshAppointment.setOnRefreshListener(() -> loadAppointmentData(true));
|
||||
}
|
||||
|
||||
private void openAppointmentDetails(int position) {
|
||||
@@ -241,7 +242,7 @@ public class AppointmentFragment extends Fragment implements AppointmentAdapter.
|
||||
return "STAFF".equalsIgnoreCase(tokenManager.getRole());
|
||||
}
|
||||
|
||||
private void loadAppointmentData() {
|
||||
private void loadAppointmentData(boolean reset) {
|
||||
String query = binding.etSearchAppointment.getText().toString().trim();
|
||||
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;
|
||||
else status = status.toUpperCase();
|
||||
|
||||
viewModel.loadAppointments(query, status, storeId, selectedDateString, employeeId);
|
||||
viewModel.loadAppointments(reset, query, status, storeId, selectedDateString, employeeId);
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new AppointmentAdapter(appointmentList, this);
|
||||
binding.recyclerViewAppointments.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.Navigation;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.CouponAdapter;
|
||||
@@ -46,7 +47,7 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl
|
||||
setupSwipeRefresh();
|
||||
observeViewModel();
|
||||
|
||||
viewModel.loadCoupons(0, 100, null, null, null);
|
||||
applyFilters(true);
|
||||
|
||||
binding.fabAddCoupon.setOnClickListener(v -> openDetail(-1));
|
||||
binding.btnBulkDeleteCoupons.setOnClickListener(v -> confirmBulkDelete());
|
||||
@@ -74,27 +75,43 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl
|
||||
adapter = new CouponAdapter(couponList, this);
|
||||
binding.recyclerViewCoupon.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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() {
|
||||
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() {
|
||||
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() {
|
||||
UIUtils.attachSearch(binding.etSearchCoupon, this::applyFilters);
|
||||
UIUtils.attachSearch(binding.etSearchCoupon, () -> applyFilters(true));
|
||||
}
|
||||
|
||||
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 ?
|
||||
binding.spinnerStatusCoupon.getSelectedItem().toString() : "All Statuses";
|
||||
Boolean active = null;
|
||||
@@ -105,7 +122,7 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl
|
||||
binding.spinnerTypeCoupon.getSelectedItem().toString() : "All Types";
|
||||
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) {
|
||||
@@ -133,7 +150,7 @@ public class CouponFragment extends Fragment implements CouponAdapter.OnCouponCl
|
||||
viewModel.bulkDeleteCoupons(ids).observe(getViewLifecycleOwner(), resource -> {
|
||||
if (resource.status == Resource.Status.SUCCESS) {
|
||||
adapter.setSelectionMode(false);
|
||||
applyFilters();
|
||||
applyFilters(true);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
UIUtils.showToast(requireContext(), resource.message);
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.CustomerAdapter;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
@@ -44,7 +45,7 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust
|
||||
setupSwipeRefresh();
|
||||
observeViewModel();
|
||||
|
||||
viewModel.loadCustomers();
|
||||
viewModel.loadCustomers(true);
|
||||
|
||||
binding.fabAddCustomer.setOnClickListener(v -> openDetail(-1));
|
||||
|
||||
@@ -73,6 +74,22 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust
|
||||
adapter.setToken(tokenManager.getToken());
|
||||
binding.recyclerViewCustomer.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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() {
|
||||
@@ -92,7 +109,7 @@ public class CustomerFragment extends Fragment implements CustomerAdapter.OnCust
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshCustomer.setOnRefreshListener(viewModel::loadCustomers);
|
||||
binding.swipeRefreshCustomer.setOnRefreshListener(() -> viewModel.loadCustomers(true));
|
||||
}
|
||||
|
||||
private void openDetail(int position) {
|
||||
|
||||
@@ -8,11 +8,11 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
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.StoreDTO;
|
||||
import com.example.petstoremobile.utils.BulkDeleteHandler;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.PetListViewModel;
|
||||
@@ -91,7 +90,7 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
|
||||
viewModel.getSpeciesOptions().observe(getViewLifecycleOwner(), options -> {
|
||||
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,
|
||||
"pet",
|
||||
viewModel::bulkDeletePets,
|
||||
this::loadPetData
|
||||
() -> loadPetData(true)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadPetData();
|
||||
loadPetData(true);
|
||||
viewModel.loadSpecies();
|
||||
if (!isStaff()) viewModel.loadStores();
|
||||
}
|
||||
@@ -132,28 +131,28 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchPet, this::loadPetData);
|
||||
UIUtils.attachSearch(binding.etSearchPet, () -> loadPetData(true));
|
||||
}
|
||||
|
||||
private void setupStatusFilter() {
|
||||
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() {
|
||||
String[] initial = {"All Species"};
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, this::loadPetData);
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerSpecies, initial, () -> loadPetData(true));
|
||||
}
|
||||
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadPetData);
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadPetData(true));
|
||||
}
|
||||
|
||||
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 status = binding.spinnerStatus.getSelectedItem() != null ? binding.spinnerStatus.getSelectedItem().toString() : "All Statuses";
|
||||
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() {
|
||||
@@ -178,6 +177,22 @@ public class PetFragment extends Fragment implements PetAdapter.OnPetClickListen
|
||||
adapter.setToken(tokenManager.getToken());
|
||||
binding.recyclerViewPets.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -85,7 +86,7 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadProductData();
|
||||
loadProductData(true);
|
||||
viewModel.loadCategories();
|
||||
}
|
||||
|
||||
@@ -95,18 +96,18 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchProduct, this::loadProductData);
|
||||
UIUtils.attachSearch(binding.etSearchProduct, () -> loadProductData(true));
|
||||
}
|
||||
|
||||
private void setupCategoryFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, this::loadProductData);
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerCategory, () -> loadProductData(true));
|
||||
}
|
||||
|
||||
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();
|
||||
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();
|
||||
}
|
||||
|
||||
viewModel.loadProducts(query, categoryId);
|
||||
viewModel.loadProducts(reset, query, categoryId);
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
@@ -124,6 +125,22 @@ public class ProductFragment extends Fragment implements ProductAdapter.OnProduc
|
||||
adapter.setBaseUrl(baseUrl);
|
||||
binding.recyclerViewProducts.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.ProductSupplierAdapter;
|
||||
@@ -97,14 +98,14 @@ public class ProductSupplierFragment extends Fragment
|
||||
adapter,
|
||||
"relationship",
|
||||
viewModel::bulkDeleteProductSuppliers,
|
||||
this::loadData
|
||||
() -> loadData(true)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadData();
|
||||
loadData(true);
|
||||
viewModel.loadFilterData();
|
||||
}
|
||||
|
||||
@@ -123,25 +124,41 @@ public class ProductSupplierFragment extends Fragment
|
||||
adapter = new ProductSupplierAdapter(psList, this);
|
||||
binding.recyclerViewPS.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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() {
|
||||
UIUtils.attachSearch(binding.etSearchPS, this::loadData);
|
||||
UIUtils.attachSearch(binding.etSearchPS, () -> loadData(true));
|
||||
}
|
||||
|
||||
private void setupProductFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, this::loadData);
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerProduct, () -> loadData(true));
|
||||
}
|
||||
|
||||
private void setupSupplierFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, this::loadData);
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerSupplier, () -> loadData(true));
|
||||
}
|
||||
|
||||
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();
|
||||
if (query.isEmpty()) query = null;
|
||||
|
||||
@@ -157,7 +174,7 @@ public class ProductSupplierFragment extends Fragment
|
||||
supplierId = suppliers.get(binding.spinnerSupplier.getSelectedItemPosition() - 1).getSupId();
|
||||
}
|
||||
|
||||
viewModel.loadProductSuppliers(query, productId, supplierId);
|
||||
viewModel.loadProductSuppliers(reset, query, productId, supplierId);
|
||||
}
|
||||
|
||||
private void openDetail(int position) {
|
||||
|
||||
@@ -11,6 +11,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.PurchaseOrderAdapter;
|
||||
@@ -83,7 +84,7 @@ public class PurchaseOrderFragment extends Fragment
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
loadData();
|
||||
loadData(true);
|
||||
if (!isStaff()) viewModel.loadStores();
|
||||
}
|
||||
|
||||
@@ -101,24 +102,40 @@ public class PurchaseOrderFragment extends Fragment
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchPO, this::loadData);
|
||||
UIUtils.attachSearch(binding.etSearchPO, () -> loadData(true));
|
||||
}
|
||||
|
||||
private void setupStoreFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, this::loadData);
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerStore, () -> loadData(true));
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new PurchaseOrderAdapter(poList, this);
|
||||
binding.recyclerViewPO.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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() {
|
||||
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() : "";
|
||||
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) {
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.SaleAdapter;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentSaleBinding;
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
@@ -57,6 +58,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
setupStoreFilter();
|
||||
setupPaymentMethodFilter();
|
||||
setupRefundStatusFilter();
|
||||
setupCustomerFilter();
|
||||
setupSwipeRefresh();
|
||||
setupFilterToggle();
|
||||
observeViewModel();
|
||||
@@ -89,6 +91,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
StoreDTO::getStoreName, "All Stores", null, StoreDTO::getStoreId);
|
||||
});
|
||||
|
||||
viewModel.getCustomers().observe(getViewLifecycleOwner(), list -> {
|
||||
SpinnerUtils.populateWhiteSpinner(requireContext(), binding.spinnerCustomer, list,
|
||||
CustomerDTO::getFullName, "All Customers", null, CustomerDTO::getCustomerId);
|
||||
});
|
||||
|
||||
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||
binding.swipeRefreshSale.setRefreshing(loading);
|
||||
});
|
||||
@@ -98,16 +105,17 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
if (!isStaff()) viewModel.loadStores();
|
||||
viewModel.loadCustomers();
|
||||
}
|
||||
|
||||
private void setupFilterToggle() {
|
||||
if (isStaff()) {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
|
||||
binding.spinnerPaymentMethod, binding.spinnerRefundStatus);
|
||||
binding.spinnerPaymentMethod, binding.spinnerRefundStatus, binding.spinnerCustomer);
|
||||
binding.spinnerStore.setVisibility(View.GONE);
|
||||
} else {
|
||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
|
||||
binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus);
|
||||
binding.spinnerPaymentMethod, binding.spinnerStore, binding.spinnerRefundStatus, binding.spinnerCustomer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -133,6 +141,10 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRefundStatus, refundStatuses, () -> loadSales(true));
|
||||
}
|
||||
|
||||
private void setupCustomerFilter() {
|
||||
SpinnerUtils.setupFilterSpinner(binding.spinnerCustomer, () -> loadSales(true));
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SaleAdapter(saleList, this);
|
||||
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
@@ -189,7 +201,13 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
||||
isRefund = binding.spinnerRefundStatus.getSelectedItemPosition() == 2;
|
||||
}
|
||||
|
||||
viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund);
|
||||
Long customerId = null;
|
||||
List<CustomerDTO> customerList = viewModel.getCustomers().getValue();
|
||||
if (binding.spinnerCustomer.getSelectedItemPosition() > 0 && customerList != null && !customerList.isEmpty()) {
|
||||
customerId = customerList.get(binding.spinnerCustomer.getSelectedItemPosition() - 1).getCustomerId();
|
||||
}
|
||||
|
||||
viewModel.loadSales(reset, query, paymentMethod, storeId, isRefund, customerId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.adapters.EmployeeAdapter;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
@@ -47,7 +48,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
||||
setupSwipeRefresh();
|
||||
observeViewModel();
|
||||
|
||||
viewModel.loadStaff();
|
||||
viewModel.loadStaff(true);
|
||||
viewModel.loadStores();
|
||||
|
||||
binding.fabAddStaff.setOnClickListener(v -> openDetail(-1));
|
||||
@@ -82,6 +83,22 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
||||
adapter.setToken(tokenManager.getToken());
|
||||
binding.recyclerViewStaff.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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() {
|
||||
@@ -112,7 +129,7 @@ public class StaffFragment extends Fragment implements EmployeeAdapter.OnEmploye
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshStaff.setOnRefreshListener(viewModel::loadStaff);
|
||||
binding.swipeRefreshStaff.setOnRefreshListener(() -> viewModel.loadStaff(true));
|
||||
}
|
||||
|
||||
private void openDetail(int position) {
|
||||
|
||||
@@ -8,6 +8,7 @@ import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.navigation.fragment.NavHostFragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
@@ -53,7 +54,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
setupBulkDelete();
|
||||
observeViewModel();
|
||||
|
||||
loadSupplierData();
|
||||
loadSupplierData(true);
|
||||
|
||||
binding.fabAddSupplier.setOnClickListener(v -> openSupplierDetails(-1));
|
||||
|
||||
@@ -83,7 +84,7 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
adapter,
|
||||
"supplier",
|
||||
viewModel::bulkDeleteSuppliers,
|
||||
this::loadSupplierData
|
||||
() -> loadSupplierData(true)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -98,11 +99,11 @@ public class SupplierFragment extends Fragment implements SupplierAdapter.OnSupp
|
||||
}
|
||||
|
||||
private void setupSearch() {
|
||||
UIUtils.attachSearch(binding.etSearchSupplier, this::loadSupplierData);
|
||||
UIUtils.attachSearch(binding.etSearchSupplier, () -> loadSupplierData(true));
|
||||
}
|
||||
|
||||
private void setupSwipeRefresh() {
|
||||
binding.swipeRefreshSupplier.setOnRefreshListener(this::loadSupplierData);
|
||||
binding.swipeRefreshSupplier.setOnRefreshListener(() -> loadSupplierData(true));
|
||||
}
|
||||
|
||||
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() : "";
|
||||
if (query.isEmpty()) query = null;
|
||||
viewModel.loadSuppliers(query);
|
||||
viewModel.loadSuppliers(reset, query);
|
||||
}
|
||||
|
||||
private void setupRecyclerView() {
|
||||
adapter = new SupplierAdapter(supplierList, this);
|
||||
binding.recyclerViewSuppliers.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||
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);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,6 +117,15 @@ public class ProductSupplierDetailFragment extends Fragment {
|
||||
|
||||
binding.tvPSMode.setText("Edit Product Supplier");
|
||||
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 {
|
||||
binding.tvPSMode.setText("Add Product Supplier");
|
||||
binding.btnDeletePS.setVisibility(View.GONE);
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.example.petstoremobile.fragments.listfragments.detailfragments;
|
||||
|
||||
import android.graphics.Color;
|
||||
import android.os.Bundle;
|
||||
import android.view.*;
|
||||
import android.widget.Toast;
|
||||
@@ -82,26 +81,6 @@ public class PurchaseOrderDetailFragment extends Fragment {
|
||||
binding.tvPODetailSupplier.setText(po.getSupplierName());
|
||||
binding.tvPODetailStore.setText(po.getStoreName() != null ? po.getStoreName() : "N/A");
|
||||
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) {
|
||||
Toast.makeText(getContext(), "Failed to load order: " + resource.message, Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
|
||||
@@ -111,6 +111,7 @@ public class SaleDetailFragment extends Fragment {
|
||||
binding.llLoyaltyPoints.setVisibility(View.GONE);
|
||||
binding.cbUseLoyaltyPoints.setChecked(false);
|
||||
}
|
||||
updateTotal();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -420,6 +421,15 @@ public class SaleDetailFragment extends Fragment {
|
||||
}
|
||||
|
||||
binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total));
|
||||
|
||||
CustomerDTO customer = viewModel.getSelectedCustomerData().getValue();
|
||||
if (customer != null && !viewModel.isViewOnly()) {
|
||||
int pointsToEarn = total.max(BigDecimal.ZERO).intValue();
|
||||
binding.tvPointsToEarn.setText("+" + pointsToEarn + " pts");
|
||||
binding.llPointsToEarn.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.llPointsToEarn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSale() {
|
||||
|
||||
@@ -28,8 +28,6 @@ public class StaffDetailFragment extends Fragment {
|
||||
private FragmentStaffDetailBinding binding;
|
||||
private StaffDetailViewModel viewModel;
|
||||
|
||||
private final String[] ROLES = {"STAFF", "ADMIN"};
|
||||
private final String[] STAFF_ROLES = {"STORE_MANAGER", "SALES_ASSOCIATE", "GROOMER", "VETERINARIAN"};
|
||||
private final String[] STATUSES = {"Active", "Inactive"};
|
||||
|
||||
private long preselectedStoreId = -1;
|
||||
@@ -59,8 +57,6 @@ public class StaffDetailFragment extends Fragment {
|
||||
}
|
||||
|
||||
private void setupSpinners() {
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES);
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffType, STAFF_ROLES);
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES);
|
||||
}
|
||||
|
||||
@@ -112,8 +108,6 @@ public class StaffDetailFragment extends Fragment {
|
||||
binding.etStaffEmail.setText(e.getEmail());
|
||||
binding.etStaffPhone.setText(e.getPhone());
|
||||
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerStaffRole, e.getRole());
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerStaffType, e.getStaffRole());
|
||||
binding.spinnerStaffStatus.setSelection(Boolean.TRUE.equals(e.getActive()) ? 0 : 1);
|
||||
|
||||
preselectedStoreId = e.getPrimaryStoreId() != null ? e.getPrimaryStoreId() : -1;
|
||||
@@ -132,14 +126,32 @@ public class StaffDetailFragment extends Fragment {
|
||||
private void save() {
|
||||
if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return;
|
||||
|
||||
String password = binding.etStaffPassword.getText().toString().trim();
|
||||
String confirmPassword = binding.etStaffConfirmPassword.getText().toString().trim();
|
||||
|
||||
if (!viewModel.isEditing()) {
|
||||
if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return;
|
||||
String pass = binding.etStaffPassword.getText().toString();
|
||||
if (pass.length() < 6) {
|
||||
if (password.length() < 6) {
|
||||
binding.etStaffPassword.setError("At least 6 characters");
|
||||
binding.etStaffPassword.requestFocus();
|
||||
return;
|
||||
}
|
||||
if (!password.equals(confirmPassword)) {
|
||||
binding.etStaffConfirmPassword.setError("Passwords do not match");
|
||||
binding.etStaffConfirmPassword.requestFocus();
|
||||
return;
|
||||
}
|
||||
} else if (!password.isEmpty()) {
|
||||
if (password.length() < 6) {
|
||||
binding.etStaffPassword.setError("At least 6 characters");
|
||||
binding.etStaffPassword.requestFocus();
|
||||
return;
|
||||
}
|
||||
if (!password.equals(confirmPassword)) {
|
||||
binding.etStaffConfirmPassword.setError("Passwords do not match");
|
||||
binding.etStaffConfirmPassword.requestFocus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!InputValidator.isNotEmpty(binding.etStaffFirstName, "First Name")) return;
|
||||
@@ -149,13 +161,12 @@ public class StaffDetailFragment extends Fragment {
|
||||
if (!InputValidator.isSpinnerSelected(binding.spinnerStaffStore, "Primary Store")) return;
|
||||
|
||||
String username = binding.etStaffUsername.getText().toString().trim();
|
||||
String password = binding.etStaffPassword.getText().toString().trim();
|
||||
String firstName = binding.etStaffFirstName.getText().toString().trim();
|
||||
String lastName = binding.etStaffLastName.getText().toString().trim();
|
||||
String email = binding.etStaffEmail.getText().toString().trim();
|
||||
String phone = binding.etStaffPhone.getText().toString().trim();
|
||||
String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()];
|
||||
String staffRole = STAFF_ROLES[binding.spinnerStaffType.getSelectedItemPosition()];
|
||||
String role = "STAFF";
|
||||
String staffRole = null;
|
||||
boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0;
|
||||
|
||||
List<DropdownDTO> stores = viewModel.getStoreList().getValue();
|
||||
|
||||
@@ -11,6 +11,8 @@ public class Message {
|
||||
private String attachmentName;
|
||||
private String attachmentMimeType;
|
||||
private Long attachmentSizeBytes;
|
||||
private String senderRole;
|
||||
private String senderDisplayName;
|
||||
|
||||
public Message() {}
|
||||
|
||||
@@ -49,4 +51,10 @@ public class Message {
|
||||
|
||||
public Long getAttachmentSizeBytes() { return attachmentSizeBytes; }
|
||||
public void setAttachmentSizeBytes(Long attachmentSizeBytes) { this.attachmentSizeBytes = attachmentSizeBytes; }
|
||||
|
||||
public String getSenderRole() { return senderRole; }
|
||||
public void setSenderRole(String senderRole) { this.senderRole = senderRole; }
|
||||
|
||||
public String getSenderDisplayName() { return senderDisplayName; }
|
||||
public void setSenderDisplayName(String senderDisplayName) { this.senderDisplayName = senderDisplayName; }
|
||||
}
|
||||
@@ -21,7 +21,7 @@ public class ActivityLogRepository extends BaseRepository {
|
||||
this.activityLogApi = activityLogApi;
|
||||
}
|
||||
|
||||
public LiveData<Resource<List<ActivityLogDTO>>> getActivityLogs(int limit, Long storeId, String role, String search) {
|
||||
return executeCall(activityLogApi.getActivityLogs(limit, storeId, role, 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, startDate, endDate));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository {
|
||||
this.saleApi = saleApi;
|
||||
}
|
||||
|
||||
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, String sortBy) {
|
||||
return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, sortBy));
|
||||
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, String sortBy) {
|
||||
return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, customerId, sortBy));
|
||||
}
|
||||
|
||||
public LiveData<Resource<SaleDTO>> getSaleById(Long id) {
|
||||
|
||||
@@ -100,13 +100,12 @@ public class InputValidator {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Checks if the phone number is valid in (XXX) XXX-XXXX format
|
||||
// Checks if the phone number is valid: XXX-XXX-XXXX, (XXX) XXX-XXXX, or XXXXXXXXXX
|
||||
public static boolean isValidPhone(EditText field) {
|
||||
String phone = field.getText().toString().trim();
|
||||
// Matches (XXX) XXX-XXXX format
|
||||
String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$";
|
||||
String pattern = "^(\\(\\d{3}\\) \\d{3}-\\d{4}|\\d{3}-\\d{3}-\\d{4}|\\d{10})$";
|
||||
if (phone.isEmpty() || !phone.matches(pattern)) {
|
||||
field.setError("Enter a valid phone number: (XXX) XXX-XXXX");
|
||||
field.setError("Enter a valid phone number");
|
||||
field.requestFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
|
||||
@HiltViewModel
|
||||
public class ActivityLogListViewModel extends ViewModel {
|
||||
private static final int LIMIT = 2000;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
private final ActivityLogRepository repository;
|
||||
private final StoreRepository storeRepository;
|
||||
@@ -32,6 +32,11 @@ public class ActivityLogListViewModel extends ViewModel {
|
||||
private Long currentStoreId = null;
|
||||
private String currentRole = null;
|
||||
private String currentSearch = null;
|
||||
private String currentStartDate = null;
|
||||
private String currentEndDate = null;
|
||||
|
||||
private int currentLimit = PAGE_SIZE;
|
||||
private boolean isLastPage = false;
|
||||
|
||||
@Inject
|
||||
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<DropdownDTO>> getStoreOptions() { return storeOptions; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public boolean isLastPage() { return isLastPage; }
|
||||
|
||||
public void loadInitialData() {
|
||||
loadStores();
|
||||
loadLogs();
|
||||
loadLogs(true);
|
||||
}
|
||||
|
||||
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);
|
||||
observeOnce(repository.getActivityLogs(LIMIT, currentStoreId, currentRole, currentSearch), resource -> {
|
||||
observeOnce(repository.getActivityLogs(currentLimit, currentStoreId, currentRole, currentSearch, currentStartDate, currentEndDate), resource -> {
|
||||
if (resource != 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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
@@ -72,19 +90,26 @@ public class ActivityLogListViewModel extends ViewModel {
|
||||
|
||||
public void setRoleFilter(String role) {
|
||||
currentRole = "All Roles".equals(role) ? null : role;
|
||||
loadLogs();
|
||||
loadLogs(true);
|
||||
}
|
||||
|
||||
public void setStoreFilter(Long storeId) {
|
||||
currentStoreId = storeId;
|
||||
loadLogs();
|
||||
loadLogs(true);
|
||||
}
|
||||
|
||||
public void setSearchQuery(String query) {
|
||||
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) {
|
||||
liveData.observeForever(new Observer<Resource<T>>() {
|
||||
@Override
|
||||
|
||||
@@ -52,6 +52,8 @@ public class AdoptionListViewModel extends ViewModel {
|
||||
isLastPage = false;
|
||||
}
|
||||
|
||||
if (isLastPage) return;
|
||||
|
||||
if ("All Statuses".equals(status)) status = null;
|
||||
|
||||
isLoading.setValue(true);
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.repositories.SaleRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
@@ -21,6 +22,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -29,33 +31,40 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
@HiltViewModel
|
||||
public class AnalyticsViewModel extends ViewModel {
|
||||
private final SaleRepository saleRepository;
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<String>> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<String>> availableStores = new MutableLiveData<>(new ArrayList<>());
|
||||
|
||||
private List<SaleDTO> cachedSales = new ArrayList<>();
|
||||
private FilterState currentFilter = new FilterState();
|
||||
private String viewMode = "store";
|
||||
private String storeFilter = "All Stores";
|
||||
|
||||
@Inject
|
||||
public AnalyticsViewModel(SaleRepository saleRepository) {
|
||||
public AnalyticsViewModel(SaleRepository saleRepository, TokenManager tokenManager) {
|
||||
this.saleRepository = saleRepository;
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public LiveData<String> getErrorMessage() { return errorMessage; }
|
||||
public LiveData<List<String>> getAvailablePaymentMethods() { return availablePaymentMethods; }
|
||||
public LiveData<List<String>> getAvailableStores() { return availableStores; }
|
||||
|
||||
public void loadAnalytics() {
|
||||
isLoading.setValue(true);
|
||||
errorMessage.setValue(null);
|
||||
observeOnce(saleRepository.getAllSales(0, 2000, null, null, null, null, "saleDate,desc"), resource -> {
|
||||
observeOnce(saleRepository.getAllSales(0, 2000, null, null, null, null, null, "saleDate,desc"), resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
cachedSales = resource.data.getContent();
|
||||
derivePaymentMethods();
|
||||
deriveStores();
|
||||
applyCurrentFilter();
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
@@ -73,14 +82,61 @@ public class AnalyticsViewModel extends ViewModel {
|
||||
|
||||
public void resetFilter() {
|
||||
currentFilter = new FilterState();
|
||||
storeFilter = "All Stores";
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public void setViewMode(String mode) {
|
||||
viewMode = mode;
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public String getViewMode() {
|
||||
return viewMode;
|
||||
}
|
||||
|
||||
public void setStoreFilter(String store) {
|
||||
storeFilter = (store != null && !store.isEmpty()) ? store : "All Stores";
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public String getStoreFilter() {
|
||||
return storeFilter;
|
||||
}
|
||||
|
||||
private void applyCurrentFilter() {
|
||||
List<SaleDTO> filtered = filterSales(cachedSales, currentFilter);
|
||||
List<SaleDTO> salesForMode;
|
||||
if (viewMode.equals("mine")) {
|
||||
String currentUser = tokenManager.getUsername();
|
||||
salesForMode = cachedSales.stream()
|
||||
.filter(s -> currentUser != null && currentUser.equalsIgnoreCase(s.getEmployeeName() != null ? s.getEmployeeName() : ""))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
salesForMode = cachedSales;
|
||||
}
|
||||
if (!storeFilter.equals("All Stores") && !storeFilter.isEmpty()) {
|
||||
final String sf = storeFilter;
|
||||
salesForMode = salesForMode.stream()
|
||||
.filter(s -> sf.equalsIgnoreCase(s.getStoreName() != null ? s.getStoreName() : ""))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
List<SaleDTO> filtered = filterSales(salesForMode, currentFilter);
|
||||
computeAnalytics(filtered, currentFilter);
|
||||
}
|
||||
|
||||
private void deriveStores() {
|
||||
java.util.Set<String> stores = new java.util.TreeSet<>();
|
||||
for (SaleDTO s : cachedSales) {
|
||||
if (s.getStoreName() != null && !s.getStoreName().isEmpty()) {
|
||||
stores.add(s.getStoreName());
|
||||
}
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
result.add("All Stores");
|
||||
result.addAll(stores);
|
||||
availableStores.setValue(result);
|
||||
}
|
||||
|
||||
private void derivePaymentMethods() {
|
||||
java.util.Set<String> methods = new java.util.TreeSet<>();
|
||||
for (SaleDTO s : cachedSales) {
|
||||
|
||||
@@ -29,6 +29,10 @@ public class AppointmentListViewModel extends ViewModel {
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public AppointmentListViewModel(AppointmentRepository appointmentRepository, StoreRepository storeRepository) {
|
||||
this.appointmentRepository = appointmentRepository;
|
||||
@@ -38,13 +42,27 @@ public class AppointmentListViewModel extends ViewModel {
|
||||
public LiveData<List<AppointmentDTO>> getAppointments() { return appointments; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
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);
|
||||
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.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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
|
||||
@@ -133,6 +133,11 @@ public class ChatListViewModel extends ViewModel {
|
||||
|
||||
public void addMessageLocally(MessageDTO dto) {
|
||||
List<Message> current = new ArrayList<>(messageList.getValue());
|
||||
if (dto.getId() != null) {
|
||||
for (Message m : current) {
|
||||
if (dto.getId().equals(m.getId())) return;
|
||||
}
|
||||
}
|
||||
current.add(dtoToModel(dto));
|
||||
messageList.setValue(current);
|
||||
}
|
||||
@@ -168,6 +173,8 @@ public class ChatListViewModel extends ViewModel {
|
||||
m.setIsRead(dto.getIsRead());
|
||||
m.setAttachmentUrl(dto.getAttachmentUrl());
|
||||
m.setAttachmentName(dto.getAttachmentName());
|
||||
m.setSenderRole(dto.getSenderRole());
|
||||
m.setSenderDisplayName(dto.getSenderDisplayName());
|
||||
m.setAttachmentMimeType(dto.getAttachmentMimeType());
|
||||
m.setAttachmentSizeBytes(dto.getAttachmentSizeBytes());
|
||||
return m;
|
||||
|
||||
@@ -5,7 +5,6 @@ import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.CouponDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.repositories.CouponRepository;
|
||||
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<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public CouponListViewModel(CouponRepository repository) {
|
||||
this.repository = repository;
|
||||
@@ -31,13 +34,27 @@ public class CouponListViewModel extends ViewModel {
|
||||
|
||||
public LiveData<List<CouponDTO>> getCoupons() { return coupons; }
|
||||
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);
|
||||
observeOnce(repository.getAllCoupons(page, size, active, discountType, sort), resource -> {
|
||||
observeOnce(repository.getAllCoupons(currentPage, PAGE_SIZE, active, discountType, sort), resource -> {
|
||||
if (resource != 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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
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<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 lastStatus = "All Statuses";
|
||||
|
||||
@@ -35,14 +39,28 @@ public class CustomerListViewModel extends ViewModel {
|
||||
|
||||
public LiveData<List<CustomerDTO>> getFilteredCustomers() { return filteredCustomers; }
|
||||
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);
|
||||
observeOnce(repository.getAllCustomers(0, 200), resource -> {
|
||||
observeOnce(repository.getAllCustomers(currentPage, PAGE_SIZE), resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
customers.setValue(resource.data.getContent());
|
||||
filter(lastQuery, lastStatus);
|
||||
List<CustomerDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(customers.getValue());
|
||||
currentList.addAll(resource.data.getContent());
|
||||
customers.setValue(currentList);
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
applyFilter(currentList);
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
@@ -66,23 +84,25 @@ public class CustomerListViewModel extends ViewModel {
|
||||
public void filter(String query, String status) {
|
||||
this.lastQuery = query;
|
||||
this.lastStatus = status;
|
||||
applyFilter(customers.getValue());
|
||||
}
|
||||
|
||||
List<CustomerDTO> all = customers.getValue();
|
||||
private void applyFilter(List<CustomerDTO> all) {
|
||||
if (all == null) return;
|
||||
|
||||
List<CustomerDTO> filtered = new ArrayList<>();
|
||||
String lowerQuery = query.toLowerCase();
|
||||
String lowerQuery = lastQuery.toLowerCase();
|
||||
|
||||
for (CustomerDTO c : all) {
|
||||
boolean matchesQuery = query.isEmpty() ||
|
||||
boolean matchesQuery = lastQuery.isEmpty() ||
|
||||
(c.getFullName() != null && c.getFullName().toLowerCase().contains(lowerQuery)) ||
|
||||
(c.getUsername() != null && c.getUsername().toLowerCase().contains(lowerQuery)) ||
|
||||
(c.getEmail() != null && c.getEmail().toLowerCase().contains(lowerQuery)) ||
|
||||
(c.getPhone() != null && c.getPhone().toLowerCase().contains(lowerQuery));
|
||||
|
||||
boolean matchesStatus = status.equals("All Statuses") ||
|
||||
(status.equals("Active") && Boolean.TRUE.equals(c.getActive())) ||
|
||||
(status.equals("Inactive") && Boolean.FALSE.equals(c.getActive()));
|
||||
boolean matchesStatus = lastStatus.equals("All Statuses") ||
|
||||
(lastStatus.equals("Active") && Boolean.TRUE.equals(c.getActive())) ||
|
||||
(lastStatus.equals("Inactive") && Boolean.FALSE.equals(c.getActive()));
|
||||
|
||||
if (matchesQuery && matchesStatus) {
|
||||
filtered.add(c);
|
||||
|
||||
@@ -6,7 +6,6 @@ import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.BulkDeleteRequest;
|
||||
import com.example.petstoremobile.dtos.DropdownDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.PetDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
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<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public PetListViewModel(PetRepository petRepository, StoreRepository storeRepository) {
|
||||
this.petRepository = petRepository;
|
||||
@@ -42,16 +45,32 @@ public class PetListViewModel extends ViewModel {
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<List<String>> getSpeciesOptions() { return speciesOptions; }
|
||||
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 Species".equals(species)) species = null;
|
||||
|
||||
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.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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
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<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public ProductListViewModel(ProductRepository productRepository, CategoryRepository categoryRepository) {
|
||||
this.productRepository = productRepository;
|
||||
@@ -37,13 +41,27 @@ public class ProductListViewModel extends ViewModel {
|
||||
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
||||
public LiveData<List<DropdownDTO>> getCategories() { return categories; }
|
||||
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);
|
||||
observeOnce(productRepository.getAllProducts(query, categoryId, 0, 100, "prodName"), resource -> {
|
||||
observeOnce(productRepository.getAllProducts(query, categoryId, currentPage, PAGE_SIZE, "prodName"), resource -> {
|
||||
if (resource != 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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
|
||||
@@ -46,6 +46,10 @@ public class ProductSupplierDetailViewModel extends ViewModel {
|
||||
this.editSupplierId = supplierId;
|
||||
}
|
||||
|
||||
public LiveData<Resource<ProductSupplierDTO>> loadProductSupplier() {
|
||||
return psRepository.getProductSupplierById(editProductId, editSupplierId);
|
||||
}
|
||||
|
||||
public boolean isEditing() { return isEditing; }
|
||||
public long getEditProductId() { return editProductId; }
|
||||
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<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public ProductSupplierListViewModel(ProductSupplierRepository psRepository, ProductRepository productRepository, SupplierRepository supplierRepository) {
|
||||
this.psRepository = psRepository;
|
||||
@@ -44,13 +48,27 @@ public class ProductSupplierListViewModel extends ViewModel {
|
||||
public LiveData<List<ProductDTO>> getProducts() { return products; }
|
||||
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
||||
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);
|
||||
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.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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
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<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public PurchaseOrderListViewModel(PurchaseOrderRepository purchaseOrderRepository, StoreRepository storeRepository) {
|
||||
this.purchaseOrderRepository = purchaseOrderRepository;
|
||||
@@ -37,13 +41,27 @@ public class PurchaseOrderListViewModel extends ViewModel {
|
||||
public LiveData<List<PurchaseOrderDTO>> getPurchaseOrders() { return purchaseOrders; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
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);
|
||||
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.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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
|
||||
@@ -35,7 +35,7 @@ public class RefundViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
public LiveData<Resource<PageResponse<SaleDTO>>> loadAllSales() {
|
||||
return saleRepository.getAllSales(0, 1000, null, null, null, null, "saleDate,desc");
|
||||
return saleRepository.getAllSales(0, 1000, null, null, null, null, null, "saleDate,desc");
|
||||
}
|
||||
|
||||
public void setAllSales(List<SaleDTO> sales) {
|
||||
|
||||
@@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||
import com.example.petstoremobile.dtos.PageResponse;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.dtos.StoreDTO;
|
||||
import com.example.petstoremobile.repositories.CustomerRepository;
|
||||
import com.example.petstoremobile.repositories.SaleRepository;
|
||||
import com.example.petstoremobile.repositories.StoreRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
@@ -24,9 +26,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
public class SaleListViewModel extends ViewModel {
|
||||
private final SaleRepository saleRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final CustomerRepository customerRepository;
|
||||
|
||||
private final MutableLiveData<List<SaleDTO>> sales = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<CustomerDTO>> customers = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
@@ -34,17 +38,19 @@ public class SaleListViewModel extends ViewModel {
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) {
|
||||
public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository, CustomerRepository customerRepository) {
|
||||
this.saleRepository = saleRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
this.customerRepository = customerRepository;
|
||||
}
|
||||
|
||||
public LiveData<List<SaleDTO>> getSales() { return sales; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
public LiveData<List<CustomerDTO>> getCustomers() { return customers; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public boolean isLastPage() { return isLastPage; }
|
||||
|
||||
public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund) {
|
||||
public void loadSales(boolean reset, String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId) {
|
||||
if (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||
|
||||
if (reset) {
|
||||
@@ -53,7 +59,7 @@ public class SaleListViewModel extends ViewModel {
|
||||
}
|
||||
|
||||
isLoading.setValue(true);
|
||||
observeOnce(saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, "saleDate,desc"), resource -> {
|
||||
observeOnce(saleRepository.getAllSales(currentPage, PAGE_SIZE, query, paymentMethod, storeId, isRefund, customerId, "saleDate,desc"), resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());
|
||||
@@ -77,6 +83,14 @@ public class SaleListViewModel extends ViewModel {
|
||||
});
|
||||
}
|
||||
|
||||
public void loadCustomers() {
|
||||
observeOnce(customerRepository.getAllCustomers(0, 500), resource -> {
|
||||
if (resource != null && resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
customers.setValue(resource.data.getContent());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
|
||||
liveData.observeForever(new Observer<Resource<T>>() {
|
||||
@Override
|
||||
|
||||
@@ -28,6 +28,11 @@ public class StaffListViewModel extends ViewModel {
|
||||
private final MutableLiveData<List<EmployeeDTO>> filteredEmployees = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||
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 Long lastStoreId = null;
|
||||
private String lastStatus = "All Statuses";
|
||||
@@ -41,14 +46,28 @@ public class StaffListViewModel extends ViewModel {
|
||||
public LiveData<List<EmployeeDTO>> getFilteredEmployees() { return filteredEmployees; }
|
||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||
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);
|
||||
observeOnce(repository.getAllEmployees(0, 100), resource -> {
|
||||
observeOnce(repository.getAllEmployees(currentPage, PAGE_SIZE), resource -> {
|
||||
if (resource != null) {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
employees.setValue(resource.data.getContent());
|
||||
filter(lastQuery, lastStoreId, lastStatus);
|
||||
List<EmployeeDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(employees.getValue());
|
||||
currentList.addAll(resource.data.getContent());
|
||||
employees.setValue(currentList);
|
||||
isLastPage = resource.data.isLast();
|
||||
if (!isLastPage) currentPage++;
|
||||
applyFilter(currentList);
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
@@ -81,28 +100,27 @@ public class StaffListViewModel extends ViewModel {
|
||||
this.lastQuery = query;
|
||||
this.lastStoreId = storeId;
|
||||
this.lastStatus = status;
|
||||
applyFilter(employees.getValue());
|
||||
}
|
||||
|
||||
List<EmployeeDTO> all = employees.getValue();
|
||||
private void applyFilter(List<EmployeeDTO> all) {
|
||||
if (all == null) return;
|
||||
|
||||
List<EmployeeDTO> filtered = new ArrayList<>();
|
||||
String lowerQuery = query.toLowerCase();
|
||||
String lowerQuery = lastQuery.toLowerCase();
|
||||
|
||||
for (EmployeeDTO e : all) {
|
||||
// Search Query Filter
|
||||
boolean matchesQuery = query.isEmpty() ||
|
||||
boolean matchesQuery = lastQuery.isEmpty() ||
|
||||
(e.getFullName() != null && e.getFullName().toLowerCase().contains(lowerQuery)) ||
|
||||
(e.getUsername() != null && e.getUsername().toLowerCase().contains(lowerQuery)) ||
|
||||
(e.getEmail() != null && e.getEmail().toLowerCase().contains(lowerQuery)) ||
|
||||
(e.getPhone() != null && e.getPhone().toLowerCase().contains(lowerQuery));
|
||||
|
||||
// Store Filter
|
||||
boolean matchesStore = storeId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(storeId));
|
||||
boolean matchesStore = lastStoreId == null || (e.getPrimaryStoreId() != null && e.getPrimaryStoreId().equals(lastStoreId));
|
||||
|
||||
// Status Filter
|
||||
boolean matchesStatus = status.equals("All Statuses") ||
|
||||
(status.equals("Active") && Boolean.TRUE.equals(e.getActive())) ||
|
||||
(status.equals("Inactive") && Boolean.FALSE.equals(e.getActive()));
|
||||
boolean matchesStatus = lastStatus.equals("All Statuses") ||
|
||||
(lastStatus.equals("Active") && Boolean.TRUE.equals(e.getActive())) ||
|
||||
(lastStatus.equals("Inactive") && Boolean.FALSE.equals(e.getActive()));
|
||||
|
||||
if (matchesQuery && matchesStore && matchesStatus) {
|
||||
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<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
|
||||
private int currentPage = 0;
|
||||
private boolean isLastPage = false;
|
||||
private static final int PAGE_SIZE = 20;
|
||||
|
||||
@Inject
|
||||
public SupplierListViewModel(SupplierRepository repository) {
|
||||
this.repository = repository;
|
||||
@@ -32,13 +36,27 @@ public class SupplierListViewModel extends ViewModel {
|
||||
|
||||
public LiveData<List<SupplierDTO>> getSuppliers() { return suppliers; }
|
||||
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);
|
||||
observeOnce(repository.getAllSuppliers(0, 100, query, "supCompany"), resource -> {
|
||||
observeOnce(repository.getAllSuppliers(currentPage, PAGE_SIZE, query, "supCompany"), resource -> {
|
||||
if (resource != 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);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
isLoading.setValue(false);
|
||||
|
||||
@@ -95,6 +95,7 @@ public class StompChatManager {
|
||||
headers.put("Authorization", "Bearer " + authToken);
|
||||
|
||||
stompClient = Stomp.over(Stomp.ConnectionProvider.OKHTTP, webSocketUrl, headers);
|
||||
stompClient.withClientHeartbeat(0).withServerHeartbeat(0);
|
||||
|
||||
compositeDisposable.add(
|
||||
stompClient.lifecycle()
|
||||
|
||||
@@ -88,6 +88,57 @@
|
||||
|
||||
</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 -->
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
@@ -119,6 +170,7 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
|
||||
@@ -45,6 +45,40 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llViewModeToggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnMyAnalytics"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_weight="1"
|
||||
android:text="My Analytics"
|
||||
android:textSize="12sp"
|
||||
android:backgroundTint="#CBD5E1"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginEnd="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnStoreAnalytics"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Store Analytics"
|
||||
android:textSize="12sp"
|
||||
android:backgroundTint="@color/primary_medium"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginStart="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -102,6 +136,23 @@
|
||||
android:layout_marginTop="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStoreFilterLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Store"
|
||||
android:textColor="@color/text_light"
|
||||
android:textSize="11sp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerFilterStore"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -105,19 +105,6 @@
|
||||
android:textSize="15sp"
|
||||
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>
|
||||
|
||||
|
||||
@@ -151,6 +151,15 @@
|
||||
android:layout_marginStart="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerCustomer"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="44dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:background="@drawable/bg_spinner"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="8dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||
|
||||
@@ -444,6 +444,30 @@
|
||||
android:textColor="@color/accent_coral"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="12dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llPointsToEarn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="4dp"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Points to earn: "
|
||||
android:textColor="#4ECDC4"
|
||||
android:textSize="13sp"/>
|
||||
<TextView
|
||||
android:id="@+id/tvPointsToEarn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+0 pts"
|
||||
android:textColor="#4ECDC4"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -101,6 +101,23 @@
|
||||
android:inputType="textPassword"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<!-- Confirm Password -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Confirm Password"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<EditText
|
||||
android:id="@+id/etStaffConfirmPassword"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:hint="Re-enter password"
|
||||
android:inputType="textPassword"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<!-- First Name -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
@@ -169,35 +186,6 @@
|
||||
android:inputType="phone"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<!-- Role -->
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="User Role"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerStaffRole"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Staff Role"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerStaffType"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="16dp"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -13,17 +13,17 @@
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginBottom="4dp">
|
||||
android:gravity="center_vertical"
|
||||
android:layout_marginBottom="2dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLogActivity"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_weight="1"
|
||||
android:textSize="13sp"
|
||||
android:textSize="14sp"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textStyle="bold"
|
||||
android:fontFamily="monospace"/>
|
||||
android:textStyle="bold"/>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLogTimestamp"
|
||||
@@ -36,18 +36,42 @@
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvLogUser"
|
||||
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:id="@+id/tvLogTechnical"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
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>
|
||||
|
||||
@@ -14,6 +14,15 @@
|
||||
android:padding="8dp"
|
||||
android:maxWidth="300dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSenderName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="@color/text_dark"
|
||||
android:textStyle="bold"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivAttachment"
|
||||
android:layout_width="200dp"
|
||||
@@ -38,6 +47,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Received message"
|
||||
android:textColor="@color/text_dark" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTimestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#94a3b8"
|
||||
android:textSize="11sp"
|
||||
android:layout_marginTop="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -14,6 +14,16 @@
|
||||
android:padding="8dp"
|
||||
android:maxWidth="300dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSenderName"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="You"
|
||||
android:textColor="@color/white"
|
||||
android:textStyle="bold"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginBottom="4dp"/>
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/ivAttachment"
|
||||
android:layout_width="200dp"
|
||||
@@ -38,6 +48,14 @@
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Sent message"
|
||||
android:textColor="@color/white" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvTimestamp"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textColor="#dbeafe"
|
||||
android:textSize="11sp"
|
||||
android:layout_marginTop="4dp"/>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
@@ -27,19 +27,6 @@
|
||||
android:textSize="18sp"
|
||||
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>
|
||||
|
||||
<TextView
|
||||
|
||||
@@ -74,6 +74,15 @@
|
||||
android:textColor="#888888"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSaleCustomer"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textColor="#888888"
|
||||
android:textSize="14sp"
|
||||
android:visibility="gone" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvSaleDate"
|
||||
android:layout_width="wrap_content"
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
STRIPE_SECRET_KEY=sk_test_...
|
||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_test_...
|
||||
OPENROUTER_API_KEY=sk-or-v1-...
|
||||
RESEND_API_KEY=re_...
|
||||
3
backend/.gitignore
vendored
3
backend/.gitignore
vendored
@@ -44,7 +44,8 @@ build/
|
||||
.env
|
||||
|
||||
### Project Specific ###
|
||||
src/test/
|
||||
!src/test/
|
||||
!src/test/**
|
||||
tmp/
|
||||
uploads/*
|
||||
!uploads/avatars/
|
||||
|
||||
@@ -9,6 +9,5 @@ RUN mvn -q -DskipTests package
|
||||
FROM eclipse-temurin:25-jre
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/target/*.jar app.jar
|
||||
COPY uploads ./uploads
|
||||
EXPOSE 8080
|
||||
ENTRYPOINT ["java","-jar","app.jar"]
|
||||
|
||||
265
backend/log.txt
Normal file
265
backend/log.txt
Normal file
@@ -0,0 +1,265 @@
|
||||
2026-04-14T20:46:27.127-06:00 INFO 332751 --- [petshop-backend] [main] org.flywaydb.core.FlywayExecutor : Database: jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC (MySQL 8.0)
|
||||
2026-04-14T20:46:27.163-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbValidate : Successfully validated 4 migrations (execution time 00:00.014s)
|
||||
2026-04-14T20:46:27.171-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Current version of schema `Petstoredb`: 3
|
||||
2026-04-14T20:46:27.179-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Migrating schema `Petstoredb` to version "4 - drop purchase order status"
|
||||
2026-04-14T20:46:27.207-06:00 INFO 332751 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Successfully applied 1 migration to schema `Petstoredb`, now at version v4 (execution time 00:00.016s)
|
||||
2026-04-14T20:46:27.212-06:00 INFO 332751 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : Starting BackendApplication using Java 25.0.2 with PID 332751 (/home/user/threaded/main/backend/target/classes started by user in /home/user/threaded/main/backend)
|
||||
2026-04-14T20:46:27.213-06:00 INFO 332751 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : No active profile set, falling back to 1 default profile: "default"
|
||||
2026-04-14T20:46:27.648-06:00 INFO 332751 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2026-04-14T20:46:27.688-06:00 INFO 332751 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 36 ms. Found 22 JPA repository interfaces.
|
||||
2026-04-14T20:46:27.920-06:00 INFO 332751 --- [petshop-backend] [main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
|
||||
2026-04-14T20:46:27.926-06:00 INFO 332751 --- [petshop-backend] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2026-04-14T20:46:27.926-06:00 INFO 332751 --- [petshop-backend] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.18]
|
||||
2026-04-14T20:46:27.944-06:00 INFO 332751 --- [petshop-backend] [main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 711 ms
|
||||
2026-04-14T20:46:28.006-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
|
||||
2026-04-14T20:46:28.025-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.2.4.Final
|
||||
2026-04-14T20:46:28.185-06:00 INFO 332751 --- [petshop-backend] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2026-04-14T20:46:28.198-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
|
||||
2026-04-14T20:46:28.205-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@71e2ddd
|
||||
2026-04-14T20:46:28.206-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
|
||||
2026-04-14T20:46:28.232-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
|
||||
Database JDBC URL [jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC]
|
||||
Database driver: MySQL Connector/J
|
||||
Database dialect: MySQLDialect
|
||||
Database version: 8.0.45
|
||||
Default catalog/schema: Petstoredb/undefined
|
||||
Autocommit mode: undefined/unknown
|
||||
Isolation level: REPEATABLE_READ [default REPEATABLE_READ]
|
||||
JDBC fetch size: none
|
||||
Pool: DataSourceConnectionProvider
|
||||
Minimum pool size: undefined/unknown
|
||||
Maximum pool size: undefined/unknown
|
||||
2026-04-14T20:46:28.790-06:00 INFO 332751 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
|
||||
2026-04-14T20:46:28.813-06:00 INFO 332751 --- [petshop-backend] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
|
||||
2026-04-14T20:46:28.844-06:00 INFO 332751 --- [petshop-backend] [main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
|
||||
2026-04-14T20:46:29.610-06:00 ERROR 332751 --- [petshop-backend] [main] t.s.DeferredServletContainerInitializers : Error starting Tomcat context. Exception: org.springframework.beans.factory.UnsatisfiedDependencyException. Message: Error creating bean with name 'jwtAuthenticationFilter' defined in file [/home/user/threaded/main/backend/target/classes/com/petshop/backend/security/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
|
||||
2026-04-14T20:46:29.619-06:00 INFO 332751 --- [petshop-backend] [main] o.apache.catalina.core.StandardService : Stopping service [Tomcat]
|
||||
2026-04-14T20:46:29.620-06:00 WARN 332751 --- [petshop-backend] [main] o.a.c.loader.WebappClassLoaderBase : The web application [ROOT] appears to have started a thread named [HikariPool-1:housekeeper] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
|
||||
java.base/jdk.internal.misc.Unsafe.park(Native Method)
|
||||
java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:271)
|
||||
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1803)
|
||||
java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:1166)
|
||||
java.base/java.util.concurrent.ScheduledThreadPoolExecutor$DelayedWorkQueue.take(ScheduledThreadPoolExecutor.java:883)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1016)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
|
||||
java.base/java.lang.Thread.run(Thread.java:1474)
|
||||
2026-04-14T20:46:29.620-06:00 WARN 332751 --- [petshop-backend] [main] o.a.c.loader.WebappClassLoaderBase : The web application [ROOT] appears to have started a thread named [HikariPool-1:connection-adder] but has failed to stop it. This is very likely to create a memory leak. Stack trace of thread:
|
||||
java.base/jdk.internal.misc.Unsafe.park(Native Method)
|
||||
java.base/java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:271)
|
||||
java.base/java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.awaitNanos(AbstractQueuedSynchronizer.java:1803)
|
||||
java.base/java.util.concurrent.LinkedBlockingQueue.poll(LinkedBlockingQueue.java:460)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor.getTask(ThreadPoolExecutor.java:1015)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1076)
|
||||
java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:614)
|
||||
java.base/java.lang.Thread.run(Thread.java:1474)
|
||||
2026-04-14T20:46:29.621-06:00 WARN 332751 --- [petshop-backend] [main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.context.ApplicationContextException: Unable to start web server
|
||||
2026-04-14T20:46:29.621-06:00 INFO 332751 --- [petshop-backend] [main] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
|
||||
2026-04-14T20:46:29.623-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
|
||||
2026-04-14T20:46:29.624-06:00 INFO 332751 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
|
||||
2026-04-14T20:46:29.627-06:00 INFO 332751 --- [petshop-backend] [main] .s.b.a.l.ConditionEvaluationReportLogger :
|
||||
|
||||
Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
|
||||
2026-04-14T20:46:29.633-06:00 ERROR 332751 --- [petshop-backend] [main] o.s.boot.SpringApplication : Application run failed
|
||||
|
||||
org.springframework.context.ApplicationContextException: Unable to start web server
|
||||
at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:167) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
|
||||
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:615) ~[spring-context-7.0.5.jar:7.0.5]
|
||||
at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.SpringApplication.run(SpringApplication.java:321) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.builder.SpringApplicationBuilder.run(SpringApplicationBuilder.java:154) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at com.petshop.backend.BackendApplication.main(BackendApplication.java:17) ~[classes/:na]
|
||||
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat
|
||||
at org.springframework.boot.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:150) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.tomcat.TomcatWebServer.<init>(TomcatWebServer.java:110) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory.getTomcatWebServer(TomcatServletWebServerFactory.java:408) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.tomcat.servlet.TomcatServletWebServerFactory.getWebServer(TomcatServletWebServerFactory.java:166) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.createWebServer(ServletWebServerApplicationContext.java:190) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.onRefresh(ServletWebServerApplicationContext.java:164) ~[spring-boot-web-server-4.0.3.jar:4.0.3]
|
||||
... 7 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'jwtAuthenticationFilter' defined in file [/home/user/threaded/main/backend/target/classes/com/petshop/backend/security/JwtAuthenticationFilter.class]: Unsatisfied dependency expressed through constructor parameter 0: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:804) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:240) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1382) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1221) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:565) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.getOrderedBeansOfType(ServletContextInitializerBeans.java:231) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:185) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAsRegistrationBean(ServletContextInitializerBeans.java:180) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.addAdaptableBeans(ServletContextInitializerBeans.java:165) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.web.servlet.ServletContextInitializerBeans.<init>(ServletContextInitializerBeans.java:97) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.web.context.servlet.WebApplicationContextInitializer.initialize(WebApplicationContextInitializer.java:53) ~[spring-boot-4.0.3.jar:4.0.3]
|
||||
at org.springframework.boot.tomcat.servlet.DeferredServletContainerInitializers.onStartup(DeferredServletContainerInitializers.java:55) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
|
||||
at org.apache.catalina.core.StandardContext.startInternal(StandardContext.java:4416) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1162) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1158) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) ~[na:na]
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:149) ~[na:na]
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:714) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.StandardHost.startInternal(StandardHost.java:780) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1162) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.ContainerBase$StartChild.call(ContainerBase.java:1158) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:328) ~[na:na]
|
||||
at org.apache.tomcat.util.threads.InlineExecutorService.execute(InlineExecutorService.java:81) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at java.base/java.util.concurrent.AbstractExecutorService.submit(AbstractExecutorService.java:149) ~[na:na]
|
||||
at org.apache.catalina.core.ContainerBase.startInternal(ContainerBase.java:714) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.StandardEngine.startInternal(StandardEngine.java:201) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.StandardService.startInternal(StandardService.java:410) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.core.StandardServer.startInternal(StandardServer.java:864) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:164) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.apache.catalina.startup.Tomcat.start(Tomcat.java:435) ~[tomcat-embed-core-11.0.18.jar:11.0.18]
|
||||
at org.springframework.boot.tomcat.TomcatWebServer.initialize(TomcatWebServer.java:131) ~[spring-boot-tomcat-4.0.3.jar:4.0.3]
|
||||
... 12 common frames omitted
|
||||
Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jwtUtil': Injection of autowired dependencies failed
|
||||
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:499) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1446) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:602) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:525) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:333) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:371) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:331) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:201) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveBean(DefaultListableBeanFactory.java:1225) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1704) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1651) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:912) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
... 52 common frames omitted
|
||||
Caused by: org.springframework.util.PlaceholderResolutionException: Could not resolve placeholder 'JWT_SECRET' in value "${JWT_SECRET}" <-- "${jwt.secret}"
|
||||
at org.springframework.util.PlaceholderResolutionException.withValue(PlaceholderResolutionException.java:81) ~[spring-core-7.0.5.jar:7.0.5]
|
||||
at org.springframework.util.PlaceholderParser$ParsedValue.resolve(PlaceholderParser.java:296) ~[spring-core-7.0.5.jar:7.0.5]
|
||||
at org.springframework.util.PlaceholderParser.replacePlaceholders(PlaceholderParser.java:129) ~[spring-core-7.0.5.jar:7.0.5]
|
||||
at org.springframework.util.PropertyPlaceholderHelper.replacePlaceholders(PropertyPlaceholderHelper.java:96) ~[spring-core-7.0.5.jar:7.0.5]
|
||||
at org.springframework.core.env.AbstractPropertyResolver.doResolvePlaceholders(AbstractPropertyResolver.java:286) ~[spring-core-7.0.5.jar:7.0.5]
|
||||
at org.springframework.core.env.AbstractPropertyResolver.resolveRequiredPlaceholders(AbstractPropertyResolver.java:257) ~[spring-core-7.0.5.jar:7.0.5]
|
||||
at org.springframework.context.support.PropertySourcesPlaceholderConfigurer.lambda$processProperties$0(PropertySourcesPlaceholderConfigurer.java:184) ~[spring-context-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.AbstractBeanFactory.resolveEmbeddedValue(AbstractBeanFactory.java:959) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1672) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1651) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:764) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:748) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:493) ~[spring-beans-7.0.5.jar:7.0.5]
|
||||
... 64 common frames omitted
|
||||
|
||||
2026-04-14T20:47:58.809-06:00 INFO 333355 --- [petshop-backend] [main] org.flywaydb.core.FlywayExecutor : Database: jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC (MySQL 8.0)
|
||||
2026-04-14T20:47:58.842-06:00 INFO 333355 --- [petshop-backend] [main] o.f.core.internal.command.DbValidate : Successfully validated 4 migrations (execution time 00:00.012s)
|
||||
2026-04-14T20:47:58.850-06:00 INFO 333355 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Current version of schema `Petstoredb`: 4
|
||||
2026-04-14T20:47:58.851-06:00 INFO 333355 --- [petshop-backend] [main] o.f.core.internal.command.DbMigrate : Schema `Petstoredb` is up to date. No migration necessary.
|
||||
2026-04-14T20:47:58.857-06:00 INFO 333355 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : Starting BackendApplication using Java 25.0.2 with PID 333355 (/home/user/threaded/main/backend/target/classes started by user in /home/user/threaded/main/backend)
|
||||
2026-04-14T20:47:58.857-06:00 INFO 333355 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : The following 1 profile is active: "local"
|
||||
2026-04-14T20:47:59.251-06:00 INFO 333355 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2026-04-14T20:47:59.288-06:00 INFO 333355 --- [petshop-backend] [main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 33 ms. Found 22 JPA repository interfaces.
|
||||
2026-04-14T20:47:59.550-06:00 INFO 333355 --- [petshop-backend] [main] o.s.boot.tomcat.TomcatWebServer : Tomcat initialized with port 8080 (http)
|
||||
2026-04-14T20:47:59.557-06:00 INFO 333355 --- [petshop-backend] [main] o.apache.catalina.core.StandardService : Starting service [Tomcat]
|
||||
2026-04-14T20:47:59.557-06:00 INFO 333355 --- [petshop-backend] [main] o.apache.catalina.core.StandardEngine : Starting Servlet engine: [Apache Tomcat/11.0.18]
|
||||
2026-04-14T20:47:59.575-06:00 INFO 333355 --- [petshop-backend] [main] b.w.c.s.WebApplicationContextInitializer : Root WebApplicationContext: initialization completed in 699 ms
|
||||
2026-04-14T20:47:59.636-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
|
||||
2026-04-14T20:47:59.655-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.2.4.Final
|
||||
2026-04-14T20:47:59.799-06:00 INFO 333355 --- [petshop-backend] [main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2026-04-14T20:47:59.811-06:00 INFO 333355 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
|
||||
2026-04-14T20:47:59.817-06:00 INFO 333355 --- [petshop-backend] [main] com.zaxxer.hikari.pool.HikariPool : HikariPool-1 - Added connection com.mysql.cj.jdbc.ConnectionImpl@6109366a
|
||||
2026-04-14T20:47:59.818-06:00 INFO 333355 --- [petshop-backend] [main] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
|
||||
2026-04-14T20:47:59.845-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
|
||||
Database JDBC URL [jdbc:mysql://localhost:3306/Petstoredb?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC]
|
||||
Database driver: MySQL Connector/J
|
||||
Database dialect: MySQLDialect
|
||||
Database version: 8.0.45
|
||||
Default catalog/schema: Petstoredb/undefined
|
||||
Autocommit mode: undefined/unknown
|
||||
Isolation level: REPEATABLE_READ [default REPEATABLE_READ]
|
||||
JDBC fetch size: none
|
||||
Pool: DataSourceConnectionProvider
|
||||
Minimum pool size: undefined/unknown
|
||||
Maximum pool size: undefined/unknown
|
||||
2026-04-14T20:48:00.410-06:00 INFO 333355 --- [petshop-backend] [main] org.hibernate.orm.core : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
|
||||
2026-04-14T20:48:00.429-06:00 INFO 333355 --- [petshop-backend] [main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
|
||||
2026-04-14T20:48:00.460-06:00 INFO 333355 --- [petshop-backend] [main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
|
||||
2026-04-14T20:48:01.322-06:00 INFO 333355 --- [petshop-backend] [main] o.s.boot.web.servlet.RegistrationBean : Filter activityLoggingFilterRegistration was not registered (disabled)
|
||||
2026-04-14T20:48:02.531-06:00 INFO 333355 --- [petshop-backend] [main] o.s.m.s.b.SimpleBrokerMessageHandler : Starting...
|
||||
2026-04-14T20:48:02.531-06:00 INFO 333355 --- [petshop-backend] [main] o.s.m.s.b.SimpleBrokerMessageHandler : BrokerAvailabilityEvent[available=true, SimpleBrokerMessageHandler [org.springframework.messaging.simp.broker.DefaultSubscriptionRegistry@6a975fe4]]
|
||||
2026-04-14T20:48:02.532-06:00 INFO 333355 --- [petshop-backend] [main] o.s.m.s.b.SimpleBrokerMessageHandler : Started.
|
||||
2026-04-14T20:48:02.537-06:00 INFO 333355 --- [petshop-backend] [main] o.s.boot.tomcat.TomcatWebServer : Tomcat started on port 8080 (http) with context path '/'
|
||||
2026-04-14T20:48:03.256-06:00 INFO 333355 --- [petshop-backend] [main] c.p.backend.service.ActivityLogService : [ACTIVITY] CUSTOMER | ellie.washington | West Side Store | Email sent: Reminder: appointment tomorrow — Veterinary Checkup → ellie.washington@gmail.com
|
||||
2026-04-14T20:48:03.729-06:00 INFO 333355 --- [petshop-backend] [main] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | daniel.moore | West Side Store | Email sent: Reminder: appointment tomorrow — Veterinary Checkup → daniel.moore@petshop.com
|
||||
2026-04-14T20:48:03.742-06:00 INFO 333355 --- [petshop-backend] [main] com.petshop.backend.BackendApplication : Started BackendApplication in 5.357 seconds (process running for 5.532)
|
||||
2026-04-14T20:48:05.562-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring DispatcherServlet 'dispatcherServlet'
|
||||
2026-04-14T20:48:05.562-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Initializing Servlet 'dispatcherServlet'
|
||||
2026-04-14T20:48:05.563-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 1 ms
|
||||
2026-04-14T20:48:05.571-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
|
||||
2026-04-14T20:48:10.402-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
|
||||
2026-04-14T20:48:10.431-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
|
||||
2026-04-14T20:48:12.470-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
|
||||
2026-04-14T20:48:14.398-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.b.s.RestAuthenticationEntryPoint : Unauthorized request: POST /api/auth/login - Full authentication is required to access this resource
|
||||
2026-04-14T20:48:29.437-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:48:29.539-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:48:34.923-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:48:43.731-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:48:50.154-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:48:50.197-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/adoptions -> 200
|
||||
2026-04-14T20:48:53.571-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-1] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:48:53.594-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/adoptions -> 200
|
||||
2026-04-14T20:48:57.295-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:48:57.340-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/adoptions -> 200
|
||||
2026-04-14T20:49:02.530-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-2] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 4, active threads = 1, queued tasks = 2, completed tasks = 1]
|
||||
2026-04-14T20:49:04.360-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:04.503-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-9] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
|
||||
2026-04-14T20:49:04.532-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/adoptions -> 500
|
||||
2026-04-14T20:49:09.172-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:09.305-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
|
||||
2026-04-14T20:49:13.132-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:13.158-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
|
||||
2026-04-14T20:49:18.817-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:18.967-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] org.hibernate.orm.jdbc.error : HHH000247: ErrorCode: 1048, SQLState: 23000
|
||||
2026-04-14T20:49:18.967-06:00 WARN 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] org.hibernate.orm.jdbc.error : Column 'sourceStoreId' cannot be null
|
||||
2026-04-14T20:49:18.973-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/adoptions -> 400
|
||||
2026-04-14T20:49:19.013-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
|
||||
2026-04-14T20:49:23.058-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:29.750-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:29.890-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users -> 200
|
||||
2026-04-14T20:49:30.434-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] CUSTOMER | customer | Downtown Branch | Email sent: Your adoption — Milo → customer@petshop.com
|
||||
2026-04-14T20:49:30.924-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | Email sent: Adoption assigned — Milo → staff@petshop.com
|
||||
2026-04-14T20:49:30.936-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/adoptions -> 201
|
||||
2026-04-14T20:49:36.666-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:36.690-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
|
||||
2026-04-14T20:49:36.719-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-9] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | DELETE /api/v1/adoptions/20 -> 204
|
||||
2026-04-14T20:49:36.736-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-2] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets/2 -> 200
|
||||
2026-04-14T20:49:44.162-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:56.940-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:49:57.066-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
|
||||
2026-04-14T20:49:57.103-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/appointments -> 400
|
||||
2026-04-14T20:49:57.140-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/pets -> 200
|
||||
2026-04-14T20:49:57.654-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] CUSTOMER | customer | Downtown Branch | Email sent: Appointment confirmed — Pet Grooming → customer@petshop.com
|
||||
2026-04-14T20:49:58.138-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | Email sent: Appointment assigned — Pet Grooming → staff@petshop.com
|
||||
2026-04-14T20:49:58.147-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/appointments -> 201
|
||||
2026-04-14T20:50:03.962-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:50:04.077-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
|
||||
2026-04-14T20:50:07.463-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:50:07.485-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-7] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/products -> 200
|
||||
2026-04-14T20:50:14.352-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:50:14.473-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-3] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
|
||||
2026-04-14T20:50:14.509-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/sales -> 403
|
||||
2026-04-14T20:50:14.538-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
|
||||
2026-04-14T20:50:18.667-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:50:26.690-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-9] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:50:26.816-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-4] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | GET /api/v1/auth/me -> 200
|
||||
2026-04-14T20:50:26.893-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-5] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | POST /api/v1/auth/login -> 200
|
||||
2026-04-14T20:50:26.913-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-6] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
|
||||
2026-04-14T20:50:26.929-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-8] c.p.backend.service.ActivityLogService : [ACTIVITY] STAFF | staff | Downtown Branch | POST /api/v1/sales -> 500
|
||||
2026-04-14T20:50:26.965-06:00 INFO 333355 --- [petshop-backend] [http-nio-0.0.0.0-8080-exec-10] c.p.backend.service.ActivityLogService : [ACTIVITY] ADMIN | admin | Downtown Branch | GET /api/v1/users/15 -> 200
|
||||
2026-04-14T21:19:02.531-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-6] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 11, active threads = 1, queued tasks = 2, completed tasks = 8]
|
||||
2026-04-14T21:49:02.532-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-12] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 16, active threads = 1, queued tasks = 2, completed tasks = 15]
|
||||
2026-04-14T22:19:02.532-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-15] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 16, active threads = 1, queued tasks = 2, completed tasks = 22]
|
||||
2026-04-14T22:49:02.533-06:00 INFO 333355 --- [petshop-backend] [MessageBroker-1] o.s.w.s.c.WebSocketMessageBrokerStats : WebSocketSession[0 current WS(0)-HttpStream(0)-HttpPoll(0), 0 total, 0 closed abnormally (0 connect failure, 0 send limit, 0 transport error)], stompSubProtocol[processed CONNECT(0)-CONNECTED(0)-DISCONNECT(0)], stompBrokerRelay[null], inboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], outboundChannel[pool size = 0, active threads = 0, queued tasks = 0, completed tasks = 0], sockJsScheduler[pool size = 16, active threads = 1, queued tasks = 2, completed tasks = 29]
|
||||
@@ -96,6 +96,18 @@
|
||||
<version>25.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.resend</groupId>
|
||||
<artifactId>resend-java</artifactId>
|
||||
<version>3.1.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.azure</groupId>
|
||||
<artifactId>azure-storage-blob</artifactId>
|
||||
<version>12.29.0</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
|
||||
@@ -17,9 +17,9 @@ import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@Component
|
||||
@Order(Ordered.LOWEST_PRECEDENCE - 20)
|
||||
public class ActivityLoggingFilter extends OncePerRequestFilter {
|
||||
@Component
|
||||
@Order(Ordered.LOWEST_PRECEDENCE - 20)
|
||||
public class ActivityLoggingFilter extends OncePerRequestFilter {
|
||||
|
||||
private final ActivityLogService activityLogService;
|
||||
|
||||
@@ -30,13 +30,8 @@ import java.io.IOException;
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String uri = request.getRequestURI();
|
||||
if (uri == null || uri.isBlank()) {
|
||||
return true;
|
||||
}
|
||||
if (!uri.startsWith("/api/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (uri == null || uri.isBlank()) return true;
|
||||
if (!uri.startsWith("/api/")) return true;
|
||||
String lower = uri.toLowerCase(java.util.Locale.ROOT);
|
||||
return lower.startsWith("/api/v1/health")
|
||||
|| lower.startsWith("/api/v1/activity-logs")
|
||||
@@ -46,16 +41,15 @@ import java.io.IOException;
|
||||
}
|
||||
|
||||
@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);
|
||||
recordActivity(request, response);
|
||||
}
|
||||
|
||||
private void recordActivity(HttpServletRequest request, HttpServletResponse response) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
if (authentication == null || !authentication.isAuthenticated()) {
|
||||
return;
|
||||
}
|
||||
if (authentication == null || !authentication.isAuthenticated()) return;
|
||||
|
||||
Long userId = null;
|
||||
User.Role role = null;
|
||||
@@ -69,11 +63,136 @@ import java.io.IOException;
|
||||
role = appPrincipal.getRole();
|
||||
}
|
||||
|
||||
if (userId == null || role == null || role == User.Role.CUSTOMER) {
|
||||
return;
|
||||
}
|
||||
if (userId == null || role == null) 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);
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,9 @@ package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.dto.activity.ActivityLogResponse;
|
||||
import com.petshop.backend.service.ActivityLogService;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -26,8 +28,10 @@ public class ActivityLogController {
|
||||
@RequestParam(defaultValue = "2000") int limit,
|
||||
@RequestParam(required = false) Long storeId,
|
||||
@RequestParam(required = false) String role,
|
||||
@RequestParam(required = false) String search) {
|
||||
@RequestParam(required = false) String search,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate,
|
||||
@RequestParam(required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
|
||||
int safeLimit = Math.min(Math.max(1, limit), 10000);
|
||||
return ResponseEntity.ok(activityLogService.getLogs(safeLimit, storeId, role, search));
|
||||
return ResponseEntity.ok(activityLogService.getLogs(safeLimit, storeId, role, search, startDate, endDate));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.petshop.backend.repository.PetRepository;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.service.OpenRouterService;
|
||||
import com.petshop.backend.util.AuthenticationHelper;
|
||||
import com.petshop.backend.util.ContentFilter;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
@@ -49,6 +50,7 @@ public class AiChatController {
|
||||
if (request.getMessage() == null || request.getMessage().isBlank()) {
|
||||
return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty"));
|
||||
}
|
||||
ContentFilter.validate(request.getMessage());
|
||||
|
||||
User user = getCurrentUser();
|
||||
|
||||
|
||||
@@ -17,6 +17,7 @@ import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.security.JwtUtil;
|
||||
import com.petshop.backend.service.ActivityLogService;
|
||||
import com.petshop.backend.service.AvatarStorageService;
|
||||
import com.petshop.backend.service.EmailService;
|
||||
import com.petshop.backend.service.PasswordResetService;
|
||||
import com.petshop.backend.util.AuthenticationHelper;
|
||||
import com.petshop.backend.util.PhoneUtils;
|
||||
@@ -55,8 +56,9 @@ public class AuthController {
|
||||
private final AvatarStorageService avatarStorageService;
|
||||
private final ActivityLogService activityLogService;
|
||||
private final PasswordResetService passwordResetService;
|
||||
private final EmailService emailService;
|
||||
|
||||
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService) {
|
||||
public AuthController(AuthenticationManager authenticationManager, UserRepository userRepository, JwtUtil jwtUtil, PasswordEncoder passwordEncoder, AvatarStorageService avatarStorageService, ActivityLogService activityLogService, PasswordResetService passwordResetService, EmailService emailService) {
|
||||
this.authenticationManager = authenticationManager;
|
||||
this.userRepository = userRepository;
|
||||
this.jwtUtil = jwtUtil;
|
||||
@@ -64,6 +66,7 @@ public class AuthController {
|
||||
this.avatarStorageService = avatarStorageService;
|
||||
this.activityLogService = activityLogService;
|
||||
this.passwordResetService = passwordResetService;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
@PostMapping("/register")
|
||||
@@ -108,6 +111,8 @@ public class AuthController {
|
||||
|
||||
User savedUser = userRepository.save(user);
|
||||
|
||||
emailService.sendWelcome(savedUser);
|
||||
|
||||
String token = jwtUtil.generateToken(savedUser);
|
||||
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse(
|
||||
@@ -132,7 +137,7 @@ public class AuthController {
|
||||
|
||||
String token = jwtUtil.generateToken(user);
|
||||
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(
|
||||
@@ -262,7 +267,8 @@ public class AuthController {
|
||||
user.getRole().name(),
|
||||
customerId,
|
||||
primaryStore != null ? primaryStore.getStoreId() : null,
|
||||
primaryStore != null ? primaryStore.getStoreName() : null
|
||||
primaryStore != null ? primaryStore.getStoreName() : null,
|
||||
user.getLoyaltyPoints()
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -75,6 +75,24 @@ public class CartController {
|
||||
return ResponseEntity.ok(cartService.applyCoupon(userId, storeId, request.getCouponCode()));
|
||||
}
|
||||
|
||||
@PostMapping("/apply-points")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<CartResponse> applyPoints(
|
||||
@RequestParam Long storeId,
|
||||
@RequestParam Boolean useLoyaltyPoints) {
|
||||
Long userId = AuthenticationHelper.getAuthenticatedUserId();
|
||||
|
||||
return ResponseEntity.ok(cartService.applyPoints(userId, storeId, useLoyaltyPoints));
|
||||
}
|
||||
|
||||
@DeleteMapping("/coupon")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<CartResponse> removeCoupon(@RequestParam Long storeId) {
|
||||
Long userId = AuthenticationHelper.getAuthenticatedUserId();
|
||||
|
||||
return ResponseEntity.ok(cartService.removeCoupon(userId, storeId));
|
||||
}
|
||||
|
||||
@PostMapping("/checkout")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<CheckoutResponse> checkout(@Valid @RequestBody CheckoutRequest request) {
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.repository.UserRepository;
|
||||
import com.petshop.backend.service.EmailService;
|
||||
import com.petshop.backend.util.AuthenticationHelper;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/contact")
|
||||
public class ContactController {
|
||||
|
||||
private final EmailService emailService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
public ContactController(EmailService emailService, UserRepository userRepository) {
|
||||
this.emailService = emailService;
|
||||
this.userRepository = userRepository;
|
||||
}
|
||||
|
||||
public record ContactRequest(
|
||||
@NotBlank @Size(max = 150) String subject,
|
||||
@NotBlank @Size(max = 2000) String body
|
||||
) {}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<Void> sendContactEmail(@Valid @RequestBody ContactRequest req) {
|
||||
Long userId = AuthenticationHelper.getAuthenticatedUserId();
|
||||
User user = userRepository.findById(userId).orElseThrow();
|
||||
emailService.sendContactMessage(user, req.subject(), req.body());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package com.petshop.backend.controller;
|
||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||
import com.petshop.backend.dto.user.UserRequest;
|
||||
import com.petshop.backend.dto.user.UserResponse;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -32,30 +33,30 @@ public class CustomerController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> getCustomerById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getUserById(id));
|
||||
return ResponseEntity.ok(userService.getUserById(id, User.Role.CUSTOMER));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserResponse> createCustomer(@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request, User.Role.CUSTOMER));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> updateCustomer(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.ok(userService.updateUser(id, request));
|
||||
return ResponseEntity.ok(userService.updateUser(id, request, User.Role.CUSTOMER));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
|
||||
userService.deleteUser(id);
|
||||
userService.deleteUser(id, User.Role.CUSTOMER);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
|
||||
@PostMapping("/bulk-delete")
|
||||
public ResponseEntity<Void> bulkDeleteCustomers(@Valid @RequestBody BulkDeleteRequest request) {
|
||||
userService.bulkDeleteUsers(request);
|
||||
userService.bulkDeleteUsers(request, User.Role.CUSTOMER);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.petshop.backend.controller;
|
||||
|
||||
import com.petshop.backend.dto.user.UserRequest;
|
||||
import com.petshop.backend.dto.user.UserResponse;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.service.UserService;
|
||||
import jakarta.validation.Valid;
|
||||
import org.springframework.data.domain.Page;
|
||||
@@ -31,24 +32,24 @@ public class EmployeeController {
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> getEmployeeById(@PathVariable Long id) {
|
||||
return ResponseEntity.ok(userService.getUserById(id));
|
||||
return ResponseEntity.ok(userService.getUserById(id, User.Role.STAFF));
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<UserResponse> createEmployee(@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request));
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(userService.createUser(request, User.Role.STAFF));
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<UserResponse> updateEmployee(
|
||||
@PathVariable Long id,
|
||||
@Valid @RequestBody UserRequest request) {
|
||||
return ResponseEntity.ok(userService.updateUser(id, request));
|
||||
return ResponseEntity.ok(userService.updateUser(id, request, User.Role.STAFF));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public ResponseEntity<Void> deleteEmployee(@PathVariable Long id) {
|
||||
userService.deleteUser(id);
|
||||
userService.deleteUser(id, User.Role.STAFF);
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class RefundController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')")
|
||||
public ResponseEntity<RefundResponse> createRefund(@Valid @RequestBody RefundRequest request) {
|
||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||
String role = authentication.getAuthorities().stream()
|
||||
@@ -85,7 +85,7 @@ public class RefundController {
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||
@PreAuthorize("hasRole('STAFF')")
|
||||
public ResponseEntity<RefundResponse> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
|
||||
return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus()));
|
||||
}
|
||||
|
||||
@@ -28,8 +28,9 @@ public class SaleController {
|
||||
@RequestParam(required = false) String paymentMethod,
|
||||
@RequestParam(required = false) Long storeId,
|
||||
@RequestParam(required = false) Boolean isRefund,
|
||||
@RequestParam(required = false) Long customerId,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(saleService.getAllSales(q, paymentMethod, storeId, isRefund, pageable));
|
||||
return ResponseEntity.ok(saleService.getAllSales(q, paymentMethod, storeId, isRefund, customerId, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
@@ -39,7 +40,7 @@ public class SaleController {
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||
@PreAuthorize("hasRole('STAFF')")
|
||||
public ResponseEntity<SaleResponse> createSale(@Valid @RequestBody SaleRequest request) {
|
||||
return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request));
|
||||
}
|
||||
|
||||
@@ -25,8 +25,9 @@ public class ServiceController {
|
||||
@GetMapping
|
||||
public ResponseEntity<Page<ServiceResponse>> getAllServices(
|
||||
@RequestParam(required = false) String q,
|
||||
@RequestParam(required = false) String species,
|
||||
Pageable pageable) {
|
||||
return ResponseEntity.ok(serviceService.getAllServices(q, pageable));
|
||||
return ResponseEntity.ok(serviceService.getAllServices(q, species, pageable));
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
|
||||
@@ -24,7 +24,7 @@ public class UserAvatarController {
|
||||
}
|
||||
|
||||
@GetMapping("/{userId}/avatar/file")
|
||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
||||
@PreAuthorize("isAuthenticated()")
|
||||
public ResponseEntity<Resource> getUserAvatarFile(@PathVariable Long userId) {
|
||||
User user = userRepository.findById(userId).orElse(null);
|
||||
if (user == null || !avatarStorageService.hasAvatar(user)) {
|
||||
|
||||
@@ -15,11 +15,12 @@ public class UserInfoResponse {
|
||||
private Long customerId;
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private Integer loyaltyPoints;
|
||||
|
||||
public UserInfoResponse() {
|
||||
}
|
||||
|
||||
public UserInfoResponse(Long id, String username, String firstName, String lastName, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName) {
|
||||
public UserInfoResponse(Long id, String username, String firstName, String lastName, String email, String fullName, String phone, String avatarUrl, String role, Long customerId, Long storeId, String storeName, Integer loyaltyPoints) {
|
||||
this.id = id;
|
||||
this.username = username;
|
||||
this.firstName = firstName;
|
||||
@@ -32,6 +33,7 @@ public class UserInfoResponse {
|
||||
this.customerId = customerId;
|
||||
this.storeId = storeId;
|
||||
this.storeName = storeName;
|
||||
this.loyaltyPoints = loyaltyPoints;
|
||||
}
|
||||
|
||||
public Long getId() {
|
||||
@@ -131,6 +133,14 @@ public class UserInfoResponse {
|
||||
this.storeName = storeName;
|
||||
}
|
||||
|
||||
public Integer getLoyaltyPoints() {
|
||||
return loyaltyPoints;
|
||||
}
|
||||
|
||||
public void setLoyaltyPoints(Integer loyaltyPoints) {
|
||||
this.loyaltyPoints = loyaltyPoints;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) return true;
|
||||
|
||||
@@ -14,6 +14,8 @@ public class CartResponse {
|
||||
private BigDecimal pointsDiscountAmount;
|
||||
private BigDecimal totalAmount;
|
||||
private String couponCode;
|
||||
private String couponDiscountType;
|
||||
private BigDecimal couponDiscountValue;
|
||||
private Boolean pointsApplied;
|
||||
private Integer availableLoyaltyPoints;
|
||||
private Boolean checkoutPending;
|
||||
@@ -45,6 +47,12 @@ public class CartResponse {
|
||||
public String getCouponCode() { return couponCode; }
|
||||
public void setCouponCode(String couponCode) { this.couponCode = couponCode; }
|
||||
|
||||
public String getCouponDiscountType() { return couponDiscountType; }
|
||||
public void setCouponDiscountType(String couponDiscountType) { this.couponDiscountType = couponDiscountType; }
|
||||
|
||||
public BigDecimal getCouponDiscountValue() { return couponDiscountValue; }
|
||||
public void setCouponDiscountValue(BigDecimal couponDiscountValue) { this.couponDiscountValue = couponDiscountValue; }
|
||||
|
||||
public BigDecimal getPointsDiscountAmount() { return pointsDiscountAmount; }
|
||||
public void setPointsDiscountAmount(BigDecimal pointsDiscountAmount) { this.pointsDiscountAmount = pointsDiscountAmount; }
|
||||
|
||||
|
||||
@@ -11,21 +11,19 @@ public class PurchaseOrderResponse {
|
||||
private Long storeId;
|
||||
private String storeName;
|
||||
private LocalDate orderDate;
|
||||
private String status;
|
||||
private LocalDateTime createdAt;
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
public PurchaseOrderResponse() {
|
||||
}
|
||||
|
||||
public PurchaseOrderResponse(Long purchaseOrderId, Long supId, String supplierName, Long storeId, String storeName, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||
public PurchaseOrderResponse(Long purchaseOrderId, Long supId, String supplierName, Long storeId, String storeName, LocalDate orderDate, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||
this.purchaseOrderId = purchaseOrderId;
|
||||
this.supId = supId;
|
||||
this.supplierName = supplierName;
|
||||
this.storeId = storeId;
|
||||
this.storeName = storeName;
|
||||
this.orderDate = orderDate;
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
@@ -78,14 +76,6 @@ public class PurchaseOrderResponse {
|
||||
this.orderDate = orderDate;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
@@ -107,12 +97,12 @@ public class PurchaseOrderResponse {
|
||||
if (this == o) return true;
|
||||
if (o == null || getClass() != o.getClass()) return false;
|
||||
PurchaseOrderResponse that = (PurchaseOrderResponse) o;
|
||||
return Objects.equals(purchaseOrderId, that.purchaseOrderId) && Objects.equals(supId, that.supId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(orderDate, that.orderDate) && Objects.equals(status, that.status) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
|
||||
return Objects.equals(purchaseOrderId, that.purchaseOrderId) && Objects.equals(supId, that.supId) && Objects.equals(supplierName, that.supplierName) && Objects.equals(storeId, that.storeId) && Objects.equals(storeName, that.storeName) && Objects.equals(orderDate, that.orderDate) && Objects.equals(createdAt, that.createdAt) && Objects.equals(updatedAt, that.updatedAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(purchaseOrderId, supId, supplierName, storeId, storeName, orderDate, status, createdAt, updatedAt);
|
||||
return Objects.hash(purchaseOrderId, supId, supplierName, storeId, storeName, orderDate, createdAt, updatedAt);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -124,7 +114,6 @@ public class PurchaseOrderResponse {
|
||||
", storeId=" + storeId +
|
||||
", storeName='" + storeName + '\'' +
|
||||
", orderDate=" + orderDate +
|
||||
", status='" + status + '\'' +
|
||||
", createdAt=" + createdAt +
|
||||
", updatedAt=" + updatedAt +
|
||||
'}';
|
||||
|
||||
@@ -28,6 +28,8 @@ public class SaleRequest {
|
||||
|
||||
private Long cartId;
|
||||
|
||||
private Integer pointsUsed;
|
||||
|
||||
|
||||
public Long getStoreId() {
|
||||
return storeId;
|
||||
@@ -101,6 +103,14 @@ public class SaleRequest {
|
||||
this.cartId = cartId;
|
||||
}
|
||||
|
||||
public Integer getPointsUsed() {
|
||||
return pointsUsed;
|
||||
}
|
||||
|
||||
public void setPointsUsed(Integer pointsUsed) {
|
||||
this.pointsUsed = pointsUsed;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
@@ -115,12 +125,13 @@ public class SaleRequest {
|
||||
Objects.equals(customerId, that.customerId) &&
|
||||
Objects.equals(channel, that.channel) &&
|
||||
Objects.equals(couponId, that.couponId) &&
|
||||
Objects.equals(cartId, that.cartId);
|
||||
Objects.equals(cartId, that.cartId) &&
|
||||
Objects.equals(pointsUsed, that.pointsUsed);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId, channel, couponId, cartId);
|
||||
return Objects.hash(storeId, paymentMethod, items, isRefund, originalSaleId, customerId, channel, couponId, cartId, pointsUsed);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -135,6 +146,7 @@ public class SaleRequest {
|
||||
", channel='" + channel + '\'' +
|
||||
", couponId=" + couponId +
|
||||
", cartId=" + cartId +
|
||||
", pointsUsed=" + pointsUsed +
|
||||
'}';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,9 +27,6 @@ public class PurchaseOrder {
|
||||
@Column(nullable = false)
|
||||
private LocalDate orderDate;
|
||||
|
||||
@Column(nullable = false, length = 50)
|
||||
private String status;
|
||||
|
||||
@CreationTimestamp
|
||||
@Column(name = "created_at", updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
@@ -41,11 +38,10 @@ public class PurchaseOrder {
|
||||
public PurchaseOrder() {
|
||||
}
|
||||
|
||||
public PurchaseOrder(Long purchaseOrderId, Supplier supplier, LocalDate orderDate, String status, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||
public PurchaseOrder(Long purchaseOrderId, Supplier supplier, LocalDate orderDate, LocalDateTime createdAt, LocalDateTime updatedAt) {
|
||||
this.purchaseOrderId = purchaseOrderId;
|
||||
this.supplier = supplier;
|
||||
this.orderDate = orderDate;
|
||||
this.status = status;
|
||||
this.createdAt = createdAt;
|
||||
this.updatedAt = updatedAt;
|
||||
}
|
||||
@@ -82,14 +78,6 @@ public class PurchaseOrder {
|
||||
this.orderDate = orderDate;
|
||||
}
|
||||
|
||||
public String getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(String status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreatedAt() {
|
||||
return createdAt;
|
||||
}
|
||||
@@ -125,7 +113,6 @@ public class PurchaseOrder {
|
||||
"purchaseOrderId=" + purchaseOrderId +
|
||||
", supplier=" + supplier +
|
||||
", orderDate=" + orderDate +
|
||||
", status='" + status + '\'' +
|
||||
", createdAt=" + createdAt +
|
||||
", updatedAt=" + updatedAt +
|
||||
'}';
|
||||
|
||||
@@ -42,9 +42,10 @@ public class GlobalExceptionHandler {
|
||||
errors.put(fieldName, errorMessage);
|
||||
});
|
||||
|
||||
String firstMessage = errors.values().stream().findFirst().orElse("Validation failed");
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("status", HttpStatus.BAD_REQUEST.value());
|
||||
response.put("message", "Validation failed");
|
||||
response.put("message", firstMessage);
|
||||
response.put("errors", errors);
|
||||
response.put("details", buildDetails(ex));
|
||||
response.put("path", request.getRequestURI());
|
||||
|
||||
@@ -40,4 +40,6 @@ public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
|
||||
boolean existsByPet_IdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus);
|
||||
|
||||
List<Adoption> findByCustomer_IdAndAdoptionStatusIgnoreCase(Long customerId, String adoptionStatus);
|
||||
|
||||
List<Adoption> findByAdoptionDateAndAdoptionStatusIgnoreCase(LocalDate date, String status);
|
||||
}
|
||||
|
||||
@@ -52,4 +52,6 @@ public interface AppointmentRepository extends JpaRepository<Appointment, Long>
|
||||
List<Appointment> findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime);
|
||||
|
||||
List<Appointment> findByPet_Id(Long petId);
|
||||
|
||||
List<Appointment> findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status);
|
||||
}
|
||||
|
||||
@@ -11,6 +11,10 @@ import org.springframework.stereotype.Repository;
|
||||
@Repository
|
||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||
|
||||
boolean existsByProdNameIgnoreCase(String prodName);
|
||||
|
||||
boolean existsByProdNameIgnoreCaseAndProdIdNot(String prodName, Long prodId);
|
||||
|
||||
@Query("SELECT p FROM Product p WHERE " +
|
||||
"(:q IS NULL OR LOWER(p.prodName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(p.prodDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:categoryId IS NULL OR p.category.categoryId = :categoryId)")
|
||||
|
||||
@@ -22,8 +22,9 @@ public interface SaleRepository extends JpaRepository<Sale, Long> {
|
||||
")) AND " +
|
||||
"(:paymentMethod IS NULL OR LOWER(s.paymentMethod) = LOWER(:paymentMethod)) AND " +
|
||||
"(:isRefund IS NULL OR s.isRefund = :isRefund) AND " +
|
||||
"(:storeId IS NULL OR s.store.storeId = :storeId)")
|
||||
Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, @Param("isRefund") Boolean isRefund, Pageable pageable);
|
||||
"(:storeId IS NULL OR s.store.storeId = :storeId) AND " +
|
||||
"(:customerId IS NULL OR s.customer.id = :customerId)")
|
||||
Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, @Param("isRefund") Boolean isRefund, @Param("customerId") Long customerId, Pageable pageable);
|
||||
|
||||
List<Sale> findByOriginalSaleSaleId(Long originalSaleId);
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ import org.springframework.stereotype.Repository;
|
||||
@Repository
|
||||
public interface ServiceRepository extends JpaRepository<Service, Long> {
|
||||
|
||||
@Query("SELECT s FROM Service s WHERE " +
|
||||
"LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
||||
"LOWER(s.serviceDesc) LIKE LOWER(CONCAT('%', :q, '%'))")
|
||||
Page<Service> searchServices(@Param("q") String query, Pageable pageable);
|
||||
@Query("SELECT DISTINCT s FROM Service s LEFT JOIN s.species sp WHERE " +
|
||||
"(:q IS NULL OR LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(s.serviceDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||
"(:species IS NULL OR LOWER(sp) = LOWER(:species))")
|
||||
Page<Service> searchServices(@Param("q") String q, @Param("species") String species, Pageable pageable);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
package com.petshop.backend.security;
|
||||
|
||||
import com.petshop.backend.exception.ApiErrorResponder;
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.time.Duration;
|
||||
import java.util.Map;
|
||||
|
||||
@Component
|
||||
public class RateLimitFilter extends OncePerRequestFilter {
|
||||
|
||||
private static final Map<String, int[]> RULES = Map.of(
|
||||
"/api/v1/auth/login", new int[]{10, 15},
|
||||
"/api/v1/auth/register", new int[]{5, 60},
|
||||
"/api/v1/auth/forgot-password", new int[]{3, 10},
|
||||
"/api/v1/auth/reset-password", new int[]{10, 15}
|
||||
);
|
||||
|
||||
private final RateLimiterService rateLimiterService;
|
||||
private final ApiErrorResponder apiErrorResponder;
|
||||
|
||||
public RateLimitFilter(RateLimiterService rateLimiterService, ApiErrorResponder apiErrorResponder) {
|
||||
this.rateLimiterService = rateLimiterService;
|
||||
this.apiErrorResponder = apiErrorResponder;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void doFilterInternal(@NonNull HttpServletRequest request,
|
||||
@NonNull HttpServletResponse response,
|
||||
@NonNull FilterChain filterChain) throws ServletException, IOException {
|
||||
String path = request.getRequestURI();
|
||||
int[] rule = RULES.get(path);
|
||||
|
||||
if (rule != null) {
|
||||
String ip = extractIp(request);
|
||||
String key = path + ":" + ip;
|
||||
if (!rateLimiterService.isAllowed(key, rule[0], Duration.ofMinutes(rule[1]))) {
|
||||
apiErrorResponder.write(response, HttpStatus.TOO_MANY_REQUESTS,
|
||||
"Too many requests. Please try again later.", null, path);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
private String extractIp(HttpServletRequest request) {
|
||||
String forwarded = request.getHeader("X-Forwarded-For");
|
||||
if (forwarded != null && !forwarded.isBlank()) {
|
||||
return forwarded.split(",")[0].trim();
|
||||
}
|
||||
return request.getRemoteAddr();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.petshop.backend.security;
|
||||
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.Instant;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Deque;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class RateLimiterService {
|
||||
|
||||
private final Map<String, Deque<Instant>> buckets = new ConcurrentHashMap<>();
|
||||
|
||||
public boolean isAllowed(String key, int maxRequests, Duration window) {
|
||||
Instant now = Instant.now();
|
||||
Instant windowStart = now.minus(window);
|
||||
|
||||
Deque<Instant> timestamps = buckets.computeIfAbsent(key, k -> new ArrayDeque<>());
|
||||
synchronized (timestamps) {
|
||||
while (!timestamps.isEmpty() && timestamps.peekFirst().isBefore(windowStart)) {
|
||||
timestamps.pollFirst();
|
||||
}
|
||||
if (timestamps.size() >= maxRequests) {
|
||||
return false;
|
||||
}
|
||||
timestamps.addLast(now);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@Scheduled(fixedDelay = 300_000)
|
||||
public void evictStale() {
|
||||
Instant cutoff = Instant.now().minus(Duration.ofHours(2));
|
||||
buckets.entrySet().removeIf(entry -> {
|
||||
Deque<Instant> timestamps = entry.getValue();
|
||||
synchronized (timestamps) {
|
||||
return timestamps.isEmpty() || timestamps.peekLast().isBefore(cutoff);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -31,15 +31,18 @@ import java.util.List;
|
||||
public class SecurityConfig {
|
||||
|
||||
private final JwtAuthenticationFilter jwtAuthFilter;
|
||||
private final RateLimitFilter rateLimitFilter;
|
||||
private final UserDetailsService userDetailsService;
|
||||
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
|
||||
private final RestAccessDeniedHandler restAccessDeniedHandler;
|
||||
|
||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
|
||||
RateLimitFilter rateLimitFilter,
|
||||
UserDetailsService userDetailsService,
|
||||
RestAuthenticationEntryPoint restAuthenticationEntryPoint,
|
||||
RestAccessDeniedHandler restAccessDeniedHandler) {
|
||||
this.jwtAuthFilter = jwtAuthFilter;
|
||||
this.rateLimitFilter = rateLimitFilter;
|
||||
this.userDetailsService = userDetailsService;
|
||||
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
|
||||
this.restAccessDeniedHandler = restAccessDeniedHandler;
|
||||
@@ -76,6 +79,7 @@ public class SecurityConfig {
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authenticationProvider(daoAuthenticationProvider())
|
||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||
http.addFilterBefore(rateLimitFilter, JwtAuthenticationFilter.class);
|
||||
http.addFilterAfter(activityLoggingFilter, JwtAuthenticationFilter.class);
|
||||
|
||||
return http.build();
|
||||
|
||||
@@ -15,6 +15,7 @@ import org.springframework.data.jpa.domain.Specification;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@@ -52,6 +53,11 @@ public class ActivityLogService {
|
||||
entry.setStoreNameSnapshot(store != null ? store.getStoreName() : null);
|
||||
entry.setActivity(activity.trim());
|
||||
activityLogRepository.save(entry);
|
||||
log.info("[ACTIVITY] {} | {} | {} | {}",
|
||||
entry.getRoleSnapshot(),
|
||||
entry.getUsernameSnapshot(),
|
||||
entry.getStoreNameSnapshot() != null ? entry.getStoreNameSnapshot() : "no store",
|
||||
entry.getActivity());
|
||||
} catch (Exception ex) {
|
||||
log.warn("Failed to persist activity log", ex);
|
||||
}
|
||||
@@ -65,7 +71,7 @@ public class ActivityLogService {
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search) {
|
||||
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search, LocalDate startDate, LocalDate endDate) {
|
||||
Specification<ActivityLog> spec = (root, query, cb) -> {
|
||||
List<Predicate> predicates = new ArrayList<>();
|
||||
|
||||
@@ -87,6 +93,14 @@ public class ActivityLogService {
|
||||
predicates.add(searchPredicate);
|
||||
}
|
||||
|
||||
if (startDate != null) {
|
||||
predicates.add(cb.greaterThanOrEqualTo(root.get("logTimestamp"), startDate.atStartOfDay()));
|
||||
}
|
||||
|
||||
if (endDate != null) {
|
||||
predicates.add(cb.lessThan(root.get("logTimestamp"), endDate.plusDays(1).atStartOfDay()));
|
||||
}
|
||||
|
||||
return cb.and(predicates.toArray(new Predicate[0]));
|
||||
};
|
||||
|
||||
@@ -96,9 +110,14 @@ public class ActivityLogService {
|
||||
.toList();
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ActivityLogResponse> getLogs(int limit, Long storeId, String role, String search) {
|
||||
return getLogs(limit, storeId, role, search, null, null);
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
public List<ActivityLogResponse> getLogs(int limit) {
|
||||
return getLogs(limit, null, null, null);
|
||||
return getLogs(limit, null, null, null, null, null);
|
||||
}
|
||||
|
||||
private ActivityLogResponse toResponse(ActivityLog entry) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
@Service
|
||||
public class AdoptionService {
|
||||
@@ -39,13 +40,15 @@ public class AdoptionService {
|
||||
private final UserRepository userRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final SaleRepository saleRepository;
|
||||
private final EmailService emailService;
|
||||
|
||||
public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository, StoreRepository storeRepository, SaleRepository saleRepository) {
|
||||
public AdoptionService(AdoptionRepository adoptionRepository, PetRepository petRepository, UserRepository userRepository, StoreRepository storeRepository, SaleRepository saleRepository, EmailService emailService) {
|
||||
this.adoptionRepository = adoptionRepository;
|
||||
this.petRepository = petRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
this.saleRepository = saleRepository;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
public Page<AdoptionResponse> getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) {
|
||||
@@ -106,6 +109,9 @@ public class AdoptionService {
|
||||
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
|
||||
createSaleForAdoption(adoption, request.getPaymentMethod());
|
||||
}
|
||||
if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
|
||||
emailService.sendAdoptionConfirmation(adoption);
|
||||
}
|
||||
return mapToResponse(adoption);
|
||||
}
|
||||
|
||||
@@ -125,6 +131,7 @@ public class AdoptionService {
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
||||
: null;
|
||||
boolean wasCompleted = ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoption.getAdoptionStatus());
|
||||
String previousStatus = adoption.getAdoptionStatus();
|
||||
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
||||
Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null;
|
||||
validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId);
|
||||
@@ -144,6 +151,10 @@ public class AdoptionService {
|
||||
if (!wasCompleted && ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
|
||||
createSaleForAdoption(adoption, request.getPaymentMethod());
|
||||
}
|
||||
boolean statusChanged = !adoptionStatus.equalsIgnoreCase(previousStatus);
|
||||
if (statusChanged && (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus))) {
|
||||
emailService.sendAdoptionConfirmation(adoption);
|
||||
}
|
||||
return mapToResponse(adoption);
|
||||
}
|
||||
|
||||
@@ -200,15 +211,37 @@ public class AdoptionService {
|
||||
|
||||
@Transactional
|
||||
public void deleteAdoption(Long id) {
|
||||
if (!adoptionRepository.existsById(id)) {
|
||||
throw new ResourceNotFoundException("Adoption not found with id: " + id);
|
||||
}
|
||||
adoptionRepository.deleteById(id);
|
||||
Adoption adoption = adoptionRepository.findById(id)
|
||||
.orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id));
|
||||
Pet pet = adoption.getPet();
|
||||
String status = adoption.getAdoptionStatus();
|
||||
adoptionRepository.delete(adoption);
|
||||
resetPetIfPending(pet, status);
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void bulkDeleteAdoptions(BulkDeleteRequest request) {
|
||||
adoptionRepository.deleteAllById(request.getIds());
|
||||
List<Adoption> adoptions = adoptionRepository.findAllById(request.getIds());
|
||||
adoptionRepository.deleteAll(adoptions);
|
||||
for (Adoption adoption : adoptions) {
|
||||
resetPetIfPending(adoption.getPet(), adoption.getAdoptionStatus());
|
||||
}
|
||||
}
|
||||
|
||||
private void resetPetIfPending(Pet pet, String deletedAdoptionStatus) {
|
||||
if (!ADOPTION_STATUS_PENDING.equalsIgnoreCase(deletedAdoptionStatus)) {
|
||||
return;
|
||||
}
|
||||
if (!PET_STATUS_PENDING.equalsIgnoreCase(pet.getPetStatus())) {
|
||||
return;
|
||||
}
|
||||
boolean completedElsewhere = adoptionRepository.existsByPet_IdAndAdoptionStatusIgnoreCase(
|
||||
pet.getPetId(), ADOPTION_STATUS_COMPLETED);
|
||||
if (!completedElsewhere) {
|
||||
pet.setPetStatus(PET_STATUS_AVAILABLE);
|
||||
pet.setOwner(null);
|
||||
petRepository.save(pet);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
@@ -309,7 +342,8 @@ public class AdoptionService {
|
||||
sale.setPaymentMethod(paymentMethod != null && !paymentMethod.isBlank() ? paymentMethod : "Cash");
|
||||
sale.setIsRefund(false);
|
||||
sale.setChannel("ADOPTION");
|
||||
saleRepository.save(sale);
|
||||
Sale savedSale = saleRepository.save(sale);
|
||||
emailService.sendPurchaseReceipt(savedSale);
|
||||
}
|
||||
|
||||
private void syncPetStatus(Pet pet, String adoptionStatus, Long adoptionId, User customer) {
|
||||
|
||||
@@ -3,11 +3,14 @@ package com.petshop.backend.service;
|
||||
import com.petshop.backend.dto.appointment.AppointmentRequest;
|
||||
import com.petshop.backend.dto.appointment.AppointmentResponse;
|
||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||
import com.petshop.backend.entity.Adoption;
|
||||
import com.petshop.backend.entity.Appointment;
|
||||
import com.petshop.backend.entity.Pet;
|
||||
import com.petshop.backend.entity.StoreLocation;
|
||||
import com.petshop.backend.entity.User;
|
||||
import com.petshop.backend.exception.BusinessException;
|
||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||
import com.petshop.backend.repository.AdoptionRepository;
|
||||
import com.petshop.backend.repository.AppointmentRepository;
|
||||
import com.petshop.backend.repository.PetRepository;
|
||||
import com.petshop.backend.repository.ServiceRepository;
|
||||
@@ -26,6 +29,7 @@ import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@Service
|
||||
@@ -36,13 +40,17 @@ public class AppointmentService {
|
||||
private final PetRepository petRepository;
|
||||
private final StoreRepository storeRepository;
|
||||
private final UserRepository userRepository;
|
||||
private final AdoptionRepository adoptionRepository;
|
||||
private final EmailService emailService;
|
||||
|
||||
public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository) {
|
||||
public AppointmentService(AppointmentRepository appointmentRepository, ServiceRepository serviceRepository, PetRepository petRepository, StoreRepository storeRepository, UserRepository userRepository, AdoptionRepository adoptionRepository, EmailService emailService) {
|
||||
this.appointmentRepository = appointmentRepository;
|
||||
this.serviceRepository = serviceRepository;
|
||||
this.petRepository = petRepository;
|
||||
this.storeRepository = storeRepository;
|
||||
this.userRepository = userRepository;
|
||||
this.adoptionRepository = adoptionRepository;
|
||||
this.emailService = emailService;
|
||||
}
|
||||
|
||||
@Transactional(readOnly = true)
|
||||
@@ -118,6 +126,7 @@ public class AppointmentService {
|
||||
validateSpeciesServiceCompatibility(pet, service);
|
||||
|
||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||
validatePetServiceCompatibility(pet, service);
|
||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
||||
|
||||
Appointment appointment = new Appointment();
|
||||
@@ -131,6 +140,7 @@ public class AppointmentService {
|
||||
appointment.setPet(pet);
|
||||
|
||||
appointment = appointmentRepository.save(appointment);
|
||||
emailService.sendAppointmentConfirmation(appointment);
|
||||
return mapToResponse(appointment);
|
||||
}
|
||||
|
||||
@@ -156,6 +166,7 @@ public class AppointmentService {
|
||||
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
||||
|
||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||
validatePetServiceCompatibility(pet, service);
|
||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
|
||||
|
||||
appointment.setCustomer(customer);
|
||||
@@ -168,6 +179,7 @@ public class AppointmentService {
|
||||
appointment.setEmployee(employee);
|
||||
|
||||
appointment = appointmentRepository.save(appointment);
|
||||
emailService.sendAppointmentConfirmation(appointment);
|
||||
return mapToResponse(appointment);
|
||||
}
|
||||
|
||||
@@ -243,7 +255,7 @@ public class AppointmentService {
|
||||
return availableSlots;
|
||||
}
|
||||
|
||||
//Update booked status to completed at every midnight
|
||||
//Update booked status to completed at every midnight, and send 24h reminders
|
||||
@Scheduled(cron = "0 0 0 * * ?")
|
||||
@Transactional
|
||||
public void updatePastAppointmentsStatus() {
|
||||
@@ -255,6 +267,20 @@ public class AppointmentService {
|
||||
appointment.setAppointmentStatus("COMPLETED");
|
||||
appointmentRepository.save(appointment);
|
||||
}
|
||||
|
||||
LocalDate tomorrow = currentDate.plusDays(1);
|
||||
|
||||
List<Appointment> tomorrowAppointments = appointmentRepository
|
||||
.findByAppointmentDateAndAppointmentStatusIgnoreCase(tomorrow, "Booked");
|
||||
for (Appointment appointment : tomorrowAppointments) {
|
||||
emailService.sendAppointmentReminder(appointment);
|
||||
}
|
||||
|
||||
List<Adoption> tomorrowAdoptions = adoptionRepository
|
||||
.findByAdoptionDateAndAdoptionStatusIgnoreCase(tomorrow, "Pending");
|
||||
for (Adoption adoption : tomorrowAdoptions) {
|
||||
emailService.sendAdoptionReminder(adoption);
|
||||
}
|
||||
}
|
||||
|
||||
private String normalizeFilter(String value) {
|
||||
@@ -265,6 +291,17 @@ public class AppointmentService {
|
||||
return trimmed.isEmpty() ? null : trimmed;
|
||||
}
|
||||
|
||||
private void validatePetServiceCompatibility(Pet pet, com.petshop.backend.entity.Service service) {
|
||||
if (pet == null) return;
|
||||
Set<String> allowed = service.getSpecies();
|
||||
if (allowed == null || allowed.isEmpty()) return;
|
||||
boolean compatible = allowed.stream().anyMatch(s -> s.equalsIgnoreCase(pet.getPetSpecies()));
|
||||
if (!compatible) {
|
||||
throw new BusinessException(
|
||||
"Service \"" + service.getServiceName() + "\" is not available for " + pet.getPetSpecies());
|
||||
}
|
||||
}
|
||||
|
||||
private void validateAppointmentRequest(AppointmentRequest request) {
|
||||
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
|
||||
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.petshop.backend.entity.User;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.ByteArrayResource;
|
||||
import org.springframework.core.io.PathResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -22,11 +24,14 @@ import java.util.UUID;
|
||||
public class AvatarStorageService {
|
||||
|
||||
private static final String STORED_PREFIX = "/uploads/avatars/";
|
||||
private static final String OWNER_ENDPOINT = "/api/v1/auth/me/avatar/file";
|
||||
private static final String BLOB_CONTAINER = "avatars";
|
||||
|
||||
@Value("${app.upload.base-dir:uploads}")
|
||||
private String uploadBaseDir;
|
||||
|
||||
@Autowired
|
||||
private AzureBlobService blobService;
|
||||
|
||||
private Path avatarDirectory;
|
||||
|
||||
@PostConstruct
|
||||
@@ -35,18 +40,22 @@ public class AvatarStorageService {
|
||||
}
|
||||
|
||||
public String storeAvatar(MultipartFile file) throws IOException {
|
||||
Files.createDirectories(avatarDirectory);
|
||||
|
||||
String originalFilename = file.getOriginalFilename();
|
||||
String extension = resolveExtension(originalFilename);
|
||||
String extension = resolveExtension(file.getOriginalFilename());
|
||||
String filename = UUID.randomUUID() + extension;
|
||||
Path filePath = avatarDirectory.resolve(filename).normalize();
|
||||
|
||||
Files.copy(file.getInputStream(), filePath, StandardCopyOption.REPLACE_EXISTING);
|
||||
if (blobService.isEnabled()) {
|
||||
blobService.upload(BLOB_CONTAINER, filename, file.getBytes());
|
||||
} else {
|
||||
Files.createDirectories(avatarDirectory);
|
||||
Files.copy(file.getInputStream(), avatarDirectory.resolve(filename).normalize(), StandardCopyOption.REPLACE_EXISTING);
|
||||
}
|
||||
return STORED_PREFIX + filename;
|
||||
}
|
||||
|
||||
public Resource loadAvatarResource(User user) {
|
||||
String filename = extractFilename(user.getAvatarUrl());
|
||||
if (blobService.isEnabled()) {
|
||||
return new ByteArrayResource(blobService.download(BLOB_CONTAINER, filename));
|
||||
}
|
||||
Path filePath = resolveStoredAvatarPath(user.getAvatarUrl());
|
||||
if (!Files.exists(filePath) || !Files.isRegularFile(filePath)) {
|
||||
throw new IllegalArgumentException("Avatar file was not found");
|
||||
@@ -55,17 +64,23 @@ public class AvatarStorageService {
|
||||
}
|
||||
|
||||
public void deleteAvatar(User user) throws IOException {
|
||||
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
|
||||
} catch (IllegalArgumentException ignored) {
|
||||
if (user.getAvatarUrl() == null || user.getAvatarUrl().isBlank()) return;
|
||||
if (blobService.isEnabled()) {
|
||||
blobService.delete(BLOB_CONTAINER, extractFilename(user.getAvatarUrl()));
|
||||
} else {
|
||||
try {
|
||||
Files.deleteIfExists(resolveStoredAvatarPath(user.getAvatarUrl()));
|
||||
} catch (IllegalArgumentException ignored) {}
|
||||
}
|
||||
}
|
||||
|
||||
private String extractFilename(String avatarUrl) {
|
||||
if (avatarUrl == null || !avatarUrl.startsWith(STORED_PREFIX)) throw new IllegalArgumentException("Avatar file was not found");
|
||||
return avatarUrl.substring(STORED_PREFIX.length());
|
||||
}
|
||||
|
||||
public String toOwnerAvatarUrl(User user) {
|
||||
return hasAvatar(user) ? OWNER_ENDPOINT : null;
|
||||
return hasAvatar(user) ? "/api/v1/users/" + user.getId() + "/avatar/file" : null;
|
||||
}
|
||||
|
||||
public String toStoredAvatarUrl(String avatarFilenamePath) {
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.petshop.backend.service;
|
||||
|
||||
import com.azure.storage.blob.BlobContainerClient;
|
||||
import com.azure.storage.blob.BlobServiceClient;
|
||||
import com.azure.storage.blob.BlobServiceClientBuilder;
|
||||
import com.azure.core.util.BinaryData;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
@Service
|
||||
public class AzureBlobService {
|
||||
|
||||
private final BlobServiceClient client;
|
||||
private final String containerPrefix;
|
||||
|
||||
public AzureBlobService(
|
||||
@Value("${azure.storage.connection-string:}") String connectionString,
|
||||
@Value("${azure.storage.container-prefix:petshop}") String containerPrefix) {
|
||||
this.containerPrefix = containerPrefix;
|
||||
this.client = (connectionString != null && !connectionString.isBlank())
|
||||
? new BlobServiceClientBuilder().connectionString(connectionString).buildClient()
|
||||
: null;
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return client != null;
|
||||
}
|
||||
|
||||
public void upload(String container, String blobName, byte[] bytes) {
|
||||
getContainerClient(container).getBlobClient(blobName).upload(BinaryData.fromBytes(bytes), true);
|
||||
}
|
||||
|
||||
public byte[] download(String container, String blobName) {
|
||||
return getContainerClient(container).getBlobClient(blobName).downloadContent().toBytes();
|
||||
}
|
||||
|
||||
public void delete(String container, String blobName) {
|
||||
getContainerClient(container).getBlobClient(blobName).deleteIfExists();
|
||||
}
|
||||
|
||||
private BlobContainerClient getContainerClient(String container) {
|
||||
return client.createBlobContainerIfNotExists(containerPrefix + "-" + container);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user