merge main
This commit is contained in:
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"permissions": {
|
|
||||||
"allow": [
|
|
||||||
"Bash(grep -E \"\\\\.\\(tsx?|jsx?\\)$\")",
|
|
||||||
"Bash(grep -E \"\\\\.js$|^d\")"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.example.petstoremobile.activities;
|
package com.example.petstoremobile.activities;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.view.View;
|
||||||
import android.widget.Toast;
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.EdgeToEdge;
|
import androidx.activity.EdgeToEdge;
|
||||||
@@ -9,14 +10,26 @@ import androidx.core.graphics.Insets;
|
|||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.view.WindowInsetsCompat;
|
import androidx.core.view.WindowInsetsCompat;
|
||||||
|
|
||||||
|
import com.example.petstoremobile.api.auth.AuthApi;
|
||||||
import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding;
|
import com.example.petstoremobile.databinding.ActivityForgotPasswordBinding;
|
||||||
import com.example.petstoremobile.utils.InputValidator;
|
import com.example.petstoremobile.utils.InputValidator;
|
||||||
|
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
import dagger.hilt.android.AndroidEntryPoint;
|
||||||
|
import retrofit2.Call;
|
||||||
|
import retrofit2.Callback;
|
||||||
|
import retrofit2.Response;
|
||||||
|
|
||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
public class ForgotPasswordActivity extends AppCompatActivity {
|
public class ForgotPasswordActivity extends AppCompatActivity {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
AuthApi authApi;
|
||||||
|
|
||||||
private ActivityForgotPasswordBinding binding;
|
private ActivityForgotPasswordBinding binding;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -33,14 +46,39 @@ public class ForgotPasswordActivity extends AppCompatActivity {
|
|||||||
});
|
});
|
||||||
|
|
||||||
binding.btnSubmit.setOnClickListener(v -> {
|
binding.btnSubmit.setOnClickListener(v -> {
|
||||||
if (InputValidator.isValidEmail(binding.etEmail)) {
|
if (!InputValidator.isValidEmail(binding.etEmail)) return;
|
||||||
String email = binding.etEmail.getText().toString().trim();
|
String email = binding.etEmail.getText().toString().trim();
|
||||||
// TODO: Implement password reset logic here
|
sendResetLink(email);
|
||||||
Toast.makeText(this, "If this email is linked, a reset email will be sent.", Toast.LENGTH_LONG).show();
|
|
||||||
finish();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
binding.btnBackToLogin.setOnClickListener(v -> finish());
|
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();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import com.bumptech.glide.Glide;
|
|||||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||||
import com.bumptech.glide.load.model.GlideUrl;
|
import com.bumptech.glide.load.model.GlideUrl;
|
||||||
import com.bumptech.glide.load.model.LazyHeaders;
|
import com.bumptech.glide.load.model.LazyHeaders;
|
||||||
|
import com.bumptech.glide.signature.ObjectKey;
|
||||||
import com.example.petstoremobile.R;
|
import com.example.petstoremobile.R;
|
||||||
import com.example.petstoremobile.databinding.ItemMessageReceivedBinding;
|
import com.example.petstoremobile.databinding.ItemMessageReceivedBinding;
|
||||||
import com.example.petstoremobile.databinding.ItemMessageSentBinding;
|
import com.example.petstoremobile.databinding.ItemMessageSentBinding;
|
||||||
@@ -35,6 +36,13 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||||||
public MessageAdapter(List<Message> messages, Long currentUserId) {
|
public MessageAdapter(List<Message> messages, Long currentUserId) {
|
||||||
this.messages = messages;
|
this.messages = messages;
|
||||||
this.currentUserId = currentUserId;
|
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) {
|
public void setCurrentUserId(Long id) {
|
||||||
@@ -150,6 +158,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (url == null) {
|
if (url == null) {
|
||||||
|
Glide.with(iv.getContext()).clear(iv);
|
||||||
iv.setVisibility(View.GONE);
|
iv.setVisibility(View.GONE);
|
||||||
tvName.setVisibility(View.GONE);
|
tvName.setVisibility(View.GONE);
|
||||||
return;
|
return;
|
||||||
@@ -166,18 +175,25 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
|||||||
.build());
|
.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())
|
Glide.with(iv.getContext())
|
||||||
.load(loadTarget)
|
.load(loadTarget)
|
||||||
|
.signature(new ObjectKey(signatureKey))
|
||||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||||
.placeholder(R.drawable.placeholder)
|
.placeholder(R.drawable.placeholder)
|
||||||
.error(R.drawable.placeholder)
|
.error(R.drawable.placeholder)
|
||||||
.into(iv);
|
.into(iv);
|
||||||
} else {
|
} else {
|
||||||
|
Glide.with(iv.getContext()).clear(iv);
|
||||||
iv.setVisibility(View.GONE);
|
iv.setVisibility(View.GONE);
|
||||||
tvName.setVisibility(View.VISIBLE);
|
tvName.setVisibility(View.VISIBLE);
|
||||||
tvName.setText(m.getAttachmentName() != null ? m.getAttachmentName() : "Attachment");
|
tvName.setText(m.getAttachmentName() != null ? m.getAttachmentName() : "Attachment");
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
Glide.with(iv.getContext()).clear(iv);
|
||||||
iv.setVisibility(View.GONE);
|
iv.setVisibility(View.GONE);
|
||||||
tvName.setVisibility(View.GONE);
|
tvName.setVisibility(View.GONE);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,6 +48,12 @@ public class SaleAdapter extends RecyclerView.Adapter<SaleAdapter.SaleViewHolder
|
|||||||
|
|
||||||
binding.tvSaleId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : ""));
|
binding.tvSaleId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : ""));
|
||||||
binding.tvSaleEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : ""));
|
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.tvSaleDate.setText(s.getSaleDate() != null ? s.getSaleDate().substring(0, Math.min(10, s.getSaleDate().length())) : "");
|
||||||
binding.tvSalePayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : "");
|
binding.tvSalePayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : "");
|
||||||
binding.tvSaleTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : "");
|
binding.tvSaleTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : "");
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ public interface SaleApi {
|
|||||||
@Query("paymentMethod") String paymentMethod,
|
@Query("paymentMethod") String paymentMethod,
|
||||||
@Query("storeId") Long storeId,
|
@Query("storeId") Long storeId,
|
||||||
@Query("isRefund") Boolean isRefund,
|
@Query("isRefund") Boolean isRefund,
|
||||||
|
@Query("customerId") Long customerId,
|
||||||
@Query("sort") String sort);
|
@Query("sort") String sort);
|
||||||
|
|
||||||
@GET("api/v1/sales/{id}")
|
@GET("api/v1/sales/{id}")
|
||||||
|
|||||||
@@ -43,4 +43,8 @@ public interface AuthApi {
|
|||||||
@DELETE("api/v1/auth/me/avatar")
|
@DELETE("api/v1/auth/me/avatar")
|
||||||
Call<Void> deleteAvatar();
|
Call<Void> deleteAvatar();
|
||||||
|
|
||||||
|
//forgot password endpoint
|
||||||
|
@POST("api/v1/auth/forgot-password")
|
||||||
|
Call<Void> forgotPassword(@Body Map<String, String> body);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
LinearLayoutManager lm = new LinearLayoutManager(getContext());
|
LinearLayoutManager lm = new LinearLayoutManager(getContext());
|
||||||
lm.setStackFromEnd(true);
|
lm.setStackFromEnd(true);
|
||||||
binding.rvMessages.setLayoutManager(lm);
|
binding.rvMessages.setLayoutManager(lm);
|
||||||
|
binding.rvMessages.setItemAnimator(null);
|
||||||
binding.rvMessages.setAdapter(messageAdapter);
|
binding.rvMessages.setAdapter(messageAdapter);
|
||||||
setConversationActive(false, null);
|
setConversationActive(false, null);
|
||||||
}
|
}
|
||||||
@@ -285,9 +286,14 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
|||||||
});
|
});
|
||||||
|
|
||||||
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
|
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
|
||||||
|
int prevSize = messageList.size();
|
||||||
messageList.clear();
|
messageList.clear();
|
||||||
messageList.addAll(list);
|
messageList.addAll(list);
|
||||||
messageAdapter.notifyDataSetChanged();
|
if (prevSize > 0 && list.size() == prevSize + 1) {
|
||||||
|
messageAdapter.notifyItemInserted(list.size() - 1);
|
||||||
|
} else {
|
||||||
|
messageAdapter.notifyDataSetChanged();
|
||||||
|
}
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -194,7 +194,7 @@ public class AdoptionFragment extends Fragment implements AdoptionAdapter.OnAdop
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupStatusFilter() {
|
private void setupStatusFilter() {
|
||||||
String[] statuses = {"All Statuses", "Completed", "Pending", "Cancelled"};
|
String[] statuses = {"All Statuses", "Completed", "Pending", "Missed", "Cancelled"};
|
||||||
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerStatusAdoption, statuses, this::loadAdoptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,11 +7,13 @@ import android.widget.*;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
|
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
|
||||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||||
import com.example.petstoremobile.utils.UIUtils;
|
import com.example.petstoremobile.utils.UIUtils;
|
||||||
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
|
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
|
||||||
import dagger.hilt.android.AndroidEntryPoint;
|
import dagger.hilt.android.AndroidEntryPoint;
|
||||||
|
import javax.inject.Inject;
|
||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
@@ -19,6 +21,9 @@ import java.util.*;
|
|||||||
@AndroidEntryPoint
|
@AndroidEntryPoint
|
||||||
public class AnalyticsFragment extends Fragment {
|
public class AnalyticsFragment extends Fragment {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
TokenManager tokenManager;
|
||||||
|
|
||||||
private FragmentAnalyticsBinding binding;
|
private FragmentAnalyticsBinding binding;
|
||||||
private AnalyticsViewModel viewModel;
|
private AnalyticsViewModel viewModel;
|
||||||
private boolean filtersExpanded = false;
|
private boolean filtersExpanded = false;
|
||||||
@@ -33,6 +38,7 @@ public class AnalyticsFragment extends Fragment {
|
|||||||
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
|
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
|
||||||
|
|
||||||
setupFilterPanel();
|
setupFilterPanel();
|
||||||
|
setupViewModeToggle();
|
||||||
observeViewModel();
|
observeViewModel();
|
||||||
viewModel.loadAnalytics();
|
viewModel.loadAnalytics();
|
||||||
|
|
||||||
@@ -42,6 +48,39 @@ public class AnalyticsFragment extends Fragment {
|
|||||||
return binding.getRoot();
|
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
|
// Filter Panel
|
||||||
|
|
||||||
private void setupFilterPanel() {
|
private void setupFilterPanel() {
|
||||||
@@ -96,6 +135,9 @@ public class AnalyticsFragment extends Fragment {
|
|||||||
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
|
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
|
||||||
filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
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();
|
updateFilterSummary();
|
||||||
viewModel.applyFilter(filter);
|
viewModel.applyFilter(filter);
|
||||||
}
|
}
|
||||||
@@ -104,8 +146,8 @@ public class AnalyticsFragment extends Fragment {
|
|||||||
binding.etFilterStartDate.setText("");
|
binding.etFilterStartDate.setText("");
|
||||||
binding.etFilterEndDate.setText("");
|
binding.etFilterEndDate.setText("");
|
||||||
binding.spinnerTopN.setSelection(0);
|
binding.spinnerTopN.setSelection(0);
|
||||||
// Reset payment method to "All"
|
|
||||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
|
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
|
||||||
|
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, "All Stores");
|
||||||
updateFilterSummary();
|
updateFilterSummary();
|
||||||
viewModel.resetFilter();
|
viewModel.resetFilter();
|
||||||
}
|
}
|
||||||
@@ -162,6 +204,16 @@ public class AnalyticsFragment extends Fragment {
|
|||||||
methods.toArray(new String[0]));
|
methods.toArray(new String[0]));
|
||||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection);
|
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
|
@Override
|
||||||
@@ -224,17 +276,22 @@ public class AnalyticsFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Employee Performance
|
// Employee Performance
|
||||||
binding.llEmployeePerformance.removeAllViews();
|
boolean showEmployeeSection = viewModel.getViewMode().equals("store");
|
||||||
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
View empParent = (View) binding.llEmployeePerformance.getParent();
|
||||||
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
|
if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE);
|
||||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
if (showEmployeeSection) {
|
||||||
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
|
binding.llEmployeePerformance.removeAllViews();
|
||||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
||||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
|
||||||
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
|
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
|
// Daily Revenue
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.example.petstoremobile.R;
|
|||||||
import com.example.petstoremobile.adapters.SaleAdapter;
|
import com.example.petstoremobile.adapters.SaleAdapter;
|
||||||
import com.example.petstoremobile.api.auth.TokenManager;
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
import com.example.petstoremobile.databinding.FragmentSaleBinding;
|
import com.example.petstoremobile.databinding.FragmentSaleBinding;
|
||||||
|
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||||
import com.example.petstoremobile.dtos.SaleDTO;
|
import com.example.petstoremobile.dtos.SaleDTO;
|
||||||
import com.example.petstoremobile.dtos.StoreDTO;
|
import com.example.petstoremobile.dtos.StoreDTO;
|
||||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||||
@@ -57,6 +58,7 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
|||||||
setupStoreFilter();
|
setupStoreFilter();
|
||||||
setupPaymentMethodFilter();
|
setupPaymentMethodFilter();
|
||||||
setupRefundStatusFilter();
|
setupRefundStatusFilter();
|
||||||
|
setupCustomerFilter();
|
||||||
setupSwipeRefresh();
|
setupSwipeRefresh();
|
||||||
setupFilterToggle();
|
setupFilterToggle();
|
||||||
observeViewModel();
|
observeViewModel();
|
||||||
@@ -89,6 +91,11 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
|||||||
StoreDTO::getStoreName, "All Stores", null, StoreDTO::getStoreId);
|
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 -> {
|
viewModel.getIsLoading().observe(getViewLifecycleOwner(), loading -> {
|
||||||
binding.swipeRefreshSale.setRefreshing(loading);
|
binding.swipeRefreshSale.setRefreshing(loading);
|
||||||
});
|
});
|
||||||
@@ -98,16 +105,17 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
|||||||
public void onResume() {
|
public void onResume() {
|
||||||
super.onResume();
|
super.onResume();
|
||||||
if (!isStaff()) viewModel.loadStores();
|
if (!isStaff()) viewModel.loadStores();
|
||||||
|
viewModel.loadCustomers();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setupFilterToggle() {
|
private void setupFilterToggle() {
|
||||||
if (isStaff()) {
|
if (isStaff()) {
|
||||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
|
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
|
||||||
binding.spinnerPaymentMethod, binding.spinnerRefundStatus);
|
binding.spinnerPaymentMethod, binding.spinnerRefundStatus, binding.spinnerCustomer);
|
||||||
binding.spinnerStore.setVisibility(View.GONE);
|
binding.spinnerStore.setVisibility(View.GONE);
|
||||||
} else {
|
} else {
|
||||||
UIUtils.setupFilterToggle(binding.btnToggleFilter, binding.layoutFilter, binding.etSearchSale,
|
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));
|
SpinnerUtils.setupStringFilterSpinner(requireContext(), binding.spinnerRefundStatus, refundStatuses, () -> loadSales(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupCustomerFilter() {
|
||||||
|
SpinnerUtils.setupFilterSpinner(binding.spinnerCustomer, () -> loadSales(true));
|
||||||
|
}
|
||||||
|
|
||||||
private void setupRecyclerView() {
|
private void setupRecyclerView() {
|
||||||
adapter = new SaleAdapter(saleList, this);
|
adapter = new SaleAdapter(saleList, this);
|
||||||
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
|
binding.recyclerViewSales.setLayoutManager(new LinearLayoutManager(getContext()));
|
||||||
@@ -189,7 +201,13 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis
|
|||||||
isRefund = binding.spinnerRefundStatus.getSelectedItemPosition() == 2;
|
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
|
@Override
|
||||||
|
|||||||
@@ -111,6 +111,7 @@ public class SaleDetailFragment extends Fragment {
|
|||||||
binding.llLoyaltyPoints.setVisibility(View.GONE);
|
binding.llLoyaltyPoints.setVisibility(View.GONE);
|
||||||
binding.cbUseLoyaltyPoints.setChecked(false);
|
binding.cbUseLoyaltyPoints.setChecked(false);
|
||||||
}
|
}
|
||||||
|
updateTotal();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -420,6 +421,15 @@ public class SaleDetailFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total));
|
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() {
|
private void saveSale() {
|
||||||
|
|||||||
@@ -28,8 +28,6 @@ public class StaffDetailFragment extends Fragment {
|
|||||||
private FragmentStaffDetailBinding binding;
|
private FragmentStaffDetailBinding binding;
|
||||||
private StaffDetailViewModel viewModel;
|
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 final String[] STATUSES = {"Active", "Inactive"};
|
||||||
|
|
||||||
private long preselectedStoreId = -1;
|
private long preselectedStoreId = -1;
|
||||||
@@ -59,8 +57,6 @@ public class StaffDetailFragment extends Fragment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void setupSpinners() {
|
private void setupSpinners() {
|
||||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffRole, ROLES);
|
|
||||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffType, STAFF_ROLES);
|
|
||||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES);
|
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerStaffStatus, STATUSES);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +108,6 @@ public class StaffDetailFragment extends Fragment {
|
|||||||
binding.etStaffEmail.setText(e.getEmail());
|
binding.etStaffEmail.setText(e.getEmail());
|
||||||
binding.etStaffPhone.setText(e.getPhone());
|
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);
|
binding.spinnerStaffStatus.setSelection(Boolean.TRUE.equals(e.getActive()) ? 0 : 1);
|
||||||
|
|
||||||
preselectedStoreId = e.getPrimaryStoreId() != null ? e.getPrimaryStoreId() : -1;
|
preselectedStoreId = e.getPrimaryStoreId() != null ? e.getPrimaryStoreId() : -1;
|
||||||
@@ -132,14 +126,32 @@ public class StaffDetailFragment extends Fragment {
|
|||||||
private void save() {
|
private void save() {
|
||||||
if (!InputValidator.isNotEmpty(binding.etStaffUsername, "Username")) return;
|
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 (!viewModel.isEditing()) {
|
||||||
if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return;
|
if (!InputValidator.isNotEmpty(binding.etStaffPassword, "Password")) return;
|
||||||
String pass = binding.etStaffPassword.getText().toString();
|
if (password.length() < 6) {
|
||||||
if (pass.length() < 6) {
|
|
||||||
binding.etStaffPassword.setError("At least 6 characters");
|
binding.etStaffPassword.setError("At least 6 characters");
|
||||||
binding.etStaffPassword.requestFocus();
|
binding.etStaffPassword.requestFocus();
|
||||||
return;
|
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;
|
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;
|
if (!InputValidator.isSpinnerSelected(binding.spinnerStaffStore, "Primary Store")) return;
|
||||||
|
|
||||||
String username = binding.etStaffUsername.getText().toString().trim();
|
String username = binding.etStaffUsername.getText().toString().trim();
|
||||||
String password = binding.etStaffPassword.getText().toString().trim();
|
|
||||||
String firstName = binding.etStaffFirstName.getText().toString().trim();
|
String firstName = binding.etStaffFirstName.getText().toString().trim();
|
||||||
String lastName = binding.etStaffLastName.getText().toString().trim();
|
String lastName = binding.etStaffLastName.getText().toString().trim();
|
||||||
String email = binding.etStaffEmail.getText().toString().trim();
|
String email = binding.etStaffEmail.getText().toString().trim();
|
||||||
String phone = binding.etStaffPhone.getText().toString().trim();
|
String phone = binding.etStaffPhone.getText().toString().trim();
|
||||||
String role = ROLES[binding.spinnerStaffRole.getSelectedItemPosition()];
|
String role = "STAFF";
|
||||||
String staffRole = STAFF_ROLES[binding.spinnerStaffType.getSelectedItemPosition()];
|
String staffRole = null;
|
||||||
boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0;
|
boolean active = binding.spinnerStaffStatus.getSelectedItemPosition() == 0;
|
||||||
|
|
||||||
List<DropdownDTO> stores = viewModel.getStoreList().getValue();
|
List<DropdownDTO> stores = viewModel.getStoreList().getValue();
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ public class SaleRepository extends BaseRepository {
|
|||||||
this.saleApi = saleApi;
|
this.saleApi = saleApi;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Resource<PageResponse<SaleDTO>>> getAllSales(int page, int size, String query, String paymentMethod, Long storeId, Boolean isRefund, String 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, sortBy));
|
return executeCall(saleApi.getAllSales(page, size, query, paymentMethod, storeId, isRefund, customerId, sortBy));
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Resource<SaleDTO>> getSaleById(Long id) {
|
public LiveData<Resource<SaleDTO>> getSaleById(Long id) {
|
||||||
|
|||||||
@@ -100,13 +100,12 @@ public class InputValidator {
|
|||||||
return true;
|
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) {
|
public static boolean isValidPhone(EditText field) {
|
||||||
String phone = field.getText().toString().trim();
|
String phone = field.getText().toString().trim();
|
||||||
// Matches (XXX) XXX-XXXX format
|
String pattern = "^(\\(\\d{3}\\) \\d{3}-\\d{4}|\\d{3}-\\d{3}-\\d{4}|\\d{10})$";
|
||||||
String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$";
|
|
||||||
if (phone.isEmpty() || !phone.matches(pattern)) {
|
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();
|
field.requestFocus();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData;
|
|||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import com.example.petstoremobile.api.auth.TokenManager;
|
||||||
import com.example.petstoremobile.dtos.SaleDTO;
|
import com.example.petstoremobile.dtos.SaleDTO;
|
||||||
import com.example.petstoremobile.repositories.SaleRepository;
|
import com.example.petstoremobile.repositories.SaleRepository;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
import com.example.petstoremobile.utils.Resource;
|
||||||
@@ -21,6 +22,7 @@ import java.util.List;
|
|||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.TreeMap;
|
import java.util.TreeMap;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@@ -29,33 +31,40 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
|
|||||||
@HiltViewModel
|
@HiltViewModel
|
||||||
public class AnalyticsViewModel extends ViewModel {
|
public class AnalyticsViewModel extends ViewModel {
|
||||||
private final SaleRepository saleRepository;
|
private final SaleRepository saleRepository;
|
||||||
|
private final TokenManager tokenManager;
|
||||||
|
|
||||||
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
|
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
|
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
|
||||||
private final MutableLiveData<List<String>> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>());
|
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 List<SaleDTO> cachedSales = new ArrayList<>();
|
||||||
private FilterState currentFilter = new FilterState();
|
private FilterState currentFilter = new FilterState();
|
||||||
|
private String viewMode = "store";
|
||||||
|
private String storeFilter = "All Stores";
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public AnalyticsViewModel(SaleRepository saleRepository) {
|
public AnalyticsViewModel(SaleRepository saleRepository, TokenManager tokenManager) {
|
||||||
this.saleRepository = saleRepository;
|
this.saleRepository = saleRepository;
|
||||||
|
this.tokenManager = tokenManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
|
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
public LiveData<String> getErrorMessage() { return errorMessage; }
|
public LiveData<String> getErrorMessage() { return errorMessage; }
|
||||||
public LiveData<List<String>> getAvailablePaymentMethods() { return availablePaymentMethods; }
|
public LiveData<List<String>> getAvailablePaymentMethods() { return availablePaymentMethods; }
|
||||||
|
public LiveData<List<String>> getAvailableStores() { return availableStores; }
|
||||||
|
|
||||||
public void loadAnalytics() {
|
public void loadAnalytics() {
|
||||||
isLoading.setValue(true);
|
isLoading.setValue(true);
|
||||||
errorMessage.setValue(null);
|
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 != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
cachedSales = resource.data.getContent();
|
cachedSales = resource.data.getContent();
|
||||||
derivePaymentMethods();
|
derivePaymentMethods();
|
||||||
|
deriveStores();
|
||||||
applyCurrentFilter();
|
applyCurrentFilter();
|
||||||
isLoading.setValue(false);
|
isLoading.setValue(false);
|
||||||
} else if (resource.status == Resource.Status.ERROR) {
|
} else if (resource.status == Resource.Status.ERROR) {
|
||||||
@@ -73,14 +82,61 @@ public class AnalyticsViewModel extends ViewModel {
|
|||||||
|
|
||||||
public void resetFilter() {
|
public void resetFilter() {
|
||||||
currentFilter = new FilterState();
|
currentFilter = new FilterState();
|
||||||
|
storeFilter = "All Stores";
|
||||||
applyCurrentFilter();
|
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() {
|
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);
|
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() {
|
private void derivePaymentMethods() {
|
||||||
java.util.Set<String> methods = new java.util.TreeSet<>();
|
java.util.Set<String> methods = new java.util.TreeSet<>();
|
||||||
for (SaleDTO s : cachedSales) {
|
for (SaleDTO s : cachedSales) {
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ public class RefundViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<Resource<PageResponse<SaleDTO>>> loadAllSales() {
|
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) {
|
public void setAllSales(List<SaleDTO> sales) {
|
||||||
|
|||||||
@@ -4,9 +4,11 @@ import androidx.lifecycle.LiveData;
|
|||||||
import androidx.lifecycle.MutableLiveData;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.ViewModel;
|
||||||
|
|
||||||
|
import com.example.petstoremobile.dtos.CustomerDTO;
|
||||||
import com.example.petstoremobile.dtos.PageResponse;
|
import com.example.petstoremobile.dtos.PageResponse;
|
||||||
import com.example.petstoremobile.dtos.SaleDTO;
|
import com.example.petstoremobile.dtos.SaleDTO;
|
||||||
import com.example.petstoremobile.dtos.StoreDTO;
|
import com.example.petstoremobile.dtos.StoreDTO;
|
||||||
|
import com.example.petstoremobile.repositories.CustomerRepository;
|
||||||
import com.example.petstoremobile.repositories.SaleRepository;
|
import com.example.petstoremobile.repositories.SaleRepository;
|
||||||
import com.example.petstoremobile.repositories.StoreRepository;
|
import com.example.petstoremobile.repositories.StoreRepository;
|
||||||
import com.example.petstoremobile.utils.Resource;
|
import com.example.petstoremobile.utils.Resource;
|
||||||
@@ -24,9 +26,11 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
|
|||||||
public class SaleListViewModel extends ViewModel {
|
public class SaleListViewModel extends ViewModel {
|
||||||
private final SaleRepository saleRepository;
|
private final SaleRepository saleRepository;
|
||||||
private final StoreRepository storeRepository;
|
private final StoreRepository storeRepository;
|
||||||
|
private final CustomerRepository customerRepository;
|
||||||
|
|
||||||
private final MutableLiveData<List<SaleDTO>> sales = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<SaleDTO>> sales = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
private final MutableLiveData<List<StoreDTO>> stores = new MutableLiveData<>(new ArrayList<>());
|
||||||
|
private final MutableLiveData<List<CustomerDTO>> customers = new MutableLiveData<>(new ArrayList<>());
|
||||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||||
|
|
||||||
private int currentPage = 0;
|
private int currentPage = 0;
|
||||||
@@ -34,17 +38,19 @@ public class SaleListViewModel extends ViewModel {
|
|||||||
private static final int PAGE_SIZE = 20;
|
private static final int PAGE_SIZE = 20;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository) {
|
public SaleListViewModel(SaleRepository saleRepository, StoreRepository storeRepository, CustomerRepository customerRepository) {
|
||||||
this.saleRepository = saleRepository;
|
this.saleRepository = saleRepository;
|
||||||
this.storeRepository = storeRepository;
|
this.storeRepository = storeRepository;
|
||||||
|
this.customerRepository = customerRepository;
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<List<SaleDTO>> getSales() { return sales; }
|
public LiveData<List<SaleDTO>> getSales() { return sales; }
|
||||||
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
public LiveData<List<StoreDTO>> getStores() { return stores; }
|
||||||
|
public LiveData<List<CustomerDTO>> getCustomers() { return customers; }
|
||||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||||
public boolean isLastPage() { return isLastPage; }
|
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 (isLoading.getValue() != null && isLoading.getValue() && !reset) return;
|
||||||
|
|
||||||
if (reset) {
|
if (reset) {
|
||||||
@@ -53,7 +59,7 @@ public class SaleListViewModel extends ViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isLoading.setValue(true);
|
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 != null) {
|
||||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||||
List<SaleDTO> currentList = reset ? new ArrayList<>() : new ArrayList<>(sales.getValue());
|
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) {
|
private <T> void observeOnce(LiveData<Resource<T>> liveData, Observer<Resource<T>> handler) {
|
||||||
liveData.observeForever(new Observer<Resource<T>>() {
|
liveData.observeForever(new Observer<Resource<T>>() {
|
||||||
@Override
|
@Override
|
||||||
|
|||||||
@@ -45,6 +45,40 @@
|
|||||||
|
|
||||||
</LinearLayout>
|
</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
|
<LinearLayout
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
@@ -102,6 +136,23 @@
|
|||||||
android:layout_marginTop="12dp"
|
android:layout_marginTop="12dp"
|
||||||
android:visibility="gone">
|
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
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -151,6 +151,15 @@
|
|||||||
android:layout_marginStart="4dp"/>
|
android:layout_marginStart="4dp"/>
|
||||||
</LinearLayout>
|
</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>
|
</LinearLayout>
|
||||||
|
|
||||||
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
<androidx.swiperefreshlayout.widget.SwipeRefreshLayout
|
||||||
|
|||||||
@@ -444,6 +444,30 @@
|
|||||||
android:textColor="@color/accent_coral"
|
android:textColor="@color/accent_coral"
|
||||||
android:layout_gravity="end"
|
android:layout_gravity="end"
|
||||||
android:layout_marginTop="12dp"/>
|
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>
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|||||||
@@ -101,6 +101,23 @@
|
|||||||
android:inputType="textPassword"
|
android:inputType="textPassword"
|
||||||
android:layout_marginBottom="16dp"/>
|
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 -->
|
<!-- First Name -->
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
@@ -169,35 +186,6 @@
|
|||||||
android:inputType="phone"
|
android:inputType="phone"
|
||||||
android:layout_marginBottom="16dp"/>
|
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
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -74,6 +74,15 @@
|
|||||||
android:textColor="#888888"
|
android:textColor="#888888"
|
||||||
android:textSize="14sp" />
|
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
|
<TextView
|
||||||
android:id="@+id/tvSaleDate"
|
android:id="@+id/tvSaleDate"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
|
|||||||
@@ -1,2 +1,4 @@
|
|||||||
STRIPE_SECRET_KEY=sk_test_...
|
STRIPE_SECRET_KEY=sk_test_...
|
||||||
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=pk_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
|
.env
|
||||||
|
|
||||||
### Project Specific ###
|
### Project Specific ###
|
||||||
src/test/
|
!src/test/
|
||||||
|
!src/test/**
|
||||||
tmp/
|
tmp/
|
||||||
uploads/*
|
uploads/*
|
||||||
!uploads/avatars/
|
!uploads/avatars/
|
||||||
|
|||||||
@@ -96,6 +96,12 @@
|
|||||||
<version>25.3.0</version>
|
<version>25.3.0</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.resend</groupId>
|
||||||
|
<artifactId>resend-java</artifactId>
|
||||||
|
<version>3.1.0</version>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter-test</artifactId>
|
<artifactId>spring-boot-starter-test</artifactId>
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.petshop.backend.controller;
|
|||||||
|
|
||||||
import com.petshop.backend.dto.activity.ActivityLogResponse;
|
import com.petshop.backend.dto.activity.ActivityLogResponse;
|
||||||
import com.petshop.backend.service.ActivityLogService;
|
import com.petshop.backend.service.ActivityLogService;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -26,8 +28,10 @@ public class ActivityLogController {
|
|||||||
@RequestParam(defaultValue = "2000") int limit,
|
@RequestParam(defaultValue = "2000") int limit,
|
||||||
@RequestParam(required = false) Long storeId,
|
@RequestParam(required = false) Long storeId,
|
||||||
@RequestParam(required = false) String role,
|
@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);
|
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.repository.UserRepository;
|
||||||
import com.petshop.backend.service.OpenRouterService;
|
import com.petshop.backend.service.OpenRouterService;
|
||||||
import com.petshop.backend.util.AuthenticationHelper;
|
import com.petshop.backend.util.AuthenticationHelper;
|
||||||
|
import com.petshop.backend.util.ContentFilter;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
@@ -49,6 +50,7 @@ public class AiChatController {
|
|||||||
if (request.getMessage() == null || request.getMessage().isBlank()) {
|
if (request.getMessage() == null || request.getMessage().isBlank()) {
|
||||||
return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty"));
|
return ResponseEntity.badRequest().body(AiChatResponse.fail("Message cannot be empty"));
|
||||||
}
|
}
|
||||||
|
ContentFilter.validate(request.getMessage());
|
||||||
|
|
||||||
User user = getCurrentUser();
|
User user = getCurrentUser();
|
||||||
|
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import com.petshop.backend.repository.UserRepository;
|
|||||||
import com.petshop.backend.security.JwtUtil;
|
import com.petshop.backend.security.JwtUtil;
|
||||||
import com.petshop.backend.service.ActivityLogService;
|
import com.petshop.backend.service.ActivityLogService;
|
||||||
import com.petshop.backend.service.AvatarStorageService;
|
import com.petshop.backend.service.AvatarStorageService;
|
||||||
|
import com.petshop.backend.service.EmailService;
|
||||||
import com.petshop.backend.service.PasswordResetService;
|
import com.petshop.backend.service.PasswordResetService;
|
||||||
import com.petshop.backend.util.AuthenticationHelper;
|
import com.petshop.backend.util.AuthenticationHelper;
|
||||||
import com.petshop.backend.util.PhoneUtils;
|
import com.petshop.backend.util.PhoneUtils;
|
||||||
@@ -55,8 +56,9 @@ public class AuthController {
|
|||||||
private final AvatarStorageService avatarStorageService;
|
private final AvatarStorageService avatarStorageService;
|
||||||
private final ActivityLogService activityLogService;
|
private final ActivityLogService activityLogService;
|
||||||
private final PasswordResetService passwordResetService;
|
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.authenticationManager = authenticationManager;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.jwtUtil = jwtUtil;
|
this.jwtUtil = jwtUtil;
|
||||||
@@ -64,6 +66,7 @@ public class AuthController {
|
|||||||
this.avatarStorageService = avatarStorageService;
|
this.avatarStorageService = avatarStorageService;
|
||||||
this.activityLogService = activityLogService;
|
this.activityLogService = activityLogService;
|
||||||
this.passwordResetService = passwordResetService;
|
this.passwordResetService = passwordResetService;
|
||||||
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/register")
|
@PostMapping("/register")
|
||||||
@@ -108,6 +111,8 @@ public class AuthController {
|
|||||||
|
|
||||||
User savedUser = userRepository.save(user);
|
User savedUser = userRepository.save(user);
|
||||||
|
|
||||||
|
emailService.sendWelcome(savedUser);
|
||||||
|
|
||||||
String token = jwtUtil.generateToken(savedUser);
|
String token = jwtUtil.generateToken(savedUser);
|
||||||
|
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse(
|
return ResponseEntity.status(HttpStatus.CREATED).body(new RegisterResponse(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package com.petshop.backend.controller;
|
|||||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||||
import com.petshop.backend.dto.user.UserRequest;
|
import com.petshop.backend.dto.user.UserRequest;
|
||||||
import com.petshop.backend.dto.user.UserResponse;
|
import com.petshop.backend.dto.user.UserResponse;
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
import com.petshop.backend.service.UserService;
|
import com.petshop.backend.service.UserService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -32,30 +33,30 @@ public class CustomerController {
|
|||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<UserResponse> getCustomerById(@PathVariable Long id) {
|
public ResponseEntity<UserResponse> getCustomerById(@PathVariable Long id) {
|
||||||
return ResponseEntity.ok(userService.getUserById(id));
|
return ResponseEntity.ok(userService.getUserById(id, User.Role.CUSTOMER));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<UserResponse> createCustomer(@Valid @RequestBody UserRequest request) {
|
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}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<UserResponse> updateCustomer(
|
public ResponseEntity<UserResponse> updateCustomer(
|
||||||
@PathVariable Long id,
|
@PathVariable Long id,
|
||||||
@Valid @RequestBody UserRequest request) {
|
@Valid @RequestBody UserRequest request) {
|
||||||
return ResponseEntity.ok(userService.updateUser(id, request));
|
return ResponseEntity.ok(userService.updateUser(id, request, User.Role.CUSTOMER));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
|
public ResponseEntity<Void> deleteCustomer(@PathVariable Long id) {
|
||||||
userService.deleteUser(id);
|
userService.deleteUser(id, User.Role.CUSTOMER);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/bulk-delete")
|
@PostMapping("/bulk-delete")
|
||||||
public ResponseEntity<Void> bulkDeleteCustomers(@Valid @RequestBody BulkDeleteRequest request) {
|
public ResponseEntity<Void> bulkDeleteCustomers(@Valid @RequestBody BulkDeleteRequest request) {
|
||||||
userService.bulkDeleteUsers(request);
|
userService.bulkDeleteUsers(request, User.Role.CUSTOMER);
|
||||||
return ResponseEntity.noContent().build();
|
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.UserRequest;
|
||||||
import com.petshop.backend.dto.user.UserResponse;
|
import com.petshop.backend.dto.user.UserResponse;
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
import com.petshop.backend.service.UserService;
|
import com.petshop.backend.service.UserService;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
@@ -31,24 +32,24 @@ public class EmployeeController {
|
|||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
public ResponseEntity<UserResponse> getEmployeeById(@PathVariable Long id) {
|
public ResponseEntity<UserResponse> getEmployeeById(@PathVariable Long id) {
|
||||||
return ResponseEntity.ok(userService.getUserById(id));
|
return ResponseEntity.ok(userService.getUserById(id, User.Role.STAFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
public ResponseEntity<UserResponse> createEmployee(@Valid @RequestBody UserRequest request) {
|
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}")
|
@PutMapping("/{id}")
|
||||||
public ResponseEntity<UserResponse> updateEmployee(
|
public ResponseEntity<UserResponse> updateEmployee(
|
||||||
@PathVariable Long id,
|
@PathVariable Long id,
|
||||||
@Valid @RequestBody UserRequest request) {
|
@Valid @RequestBody UserRequest request) {
|
||||||
return ResponseEntity.ok(userService.updateUser(id, request));
|
return ResponseEntity.ok(userService.updateUser(id, request, User.Role.STAFF));
|
||||||
}
|
}
|
||||||
|
|
||||||
@DeleteMapping("/{id}")
|
@DeleteMapping("/{id}")
|
||||||
public ResponseEntity<Void> deleteEmployee(@PathVariable Long id) {
|
public ResponseEntity<Void> deleteEmployee(@PathVariable Long id) {
|
||||||
userService.deleteUser(id);
|
userService.deleteUser(id, User.Role.STAFF);
|
||||||
return ResponseEntity.noContent().build();
|
return ResponseEntity.noContent().build();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class RefundController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF', 'ADMIN')")
|
@PreAuthorize("hasAnyRole('CUSTOMER', 'STAFF')")
|
||||||
public ResponseEntity<RefundResponse> createRefund(@Valid @RequestBody RefundRequest request) {
|
public ResponseEntity<RefundResponse> createRefund(@Valid @RequestBody RefundRequest request) {
|
||||||
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
|
||||||
String role = authentication.getAuthorities().stream()
|
String role = authentication.getAuthorities().stream()
|
||||||
@@ -85,7 +85,7 @@ public class RefundController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PutMapping("/{id}")
|
@PutMapping("/{id}")
|
||||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
@PreAuthorize("hasRole('STAFF')")
|
||||||
public ResponseEntity<RefundResponse> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
|
public ResponseEntity<RefundResponse> updateRefund(@PathVariable Long id, @Valid @RequestBody RefundUpdateRequest request) {
|
||||||
return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus()));
|
return ResponseEntity.ok(refundService.updateRefundStatus(id, request.getStatus()));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,8 +28,9 @@ public class SaleController {
|
|||||||
@RequestParam(required = false) String paymentMethod,
|
@RequestParam(required = false) String paymentMethod,
|
||||||
@RequestParam(required = false) Long storeId,
|
@RequestParam(required = false) Long storeId,
|
||||||
@RequestParam(required = false) Boolean isRefund,
|
@RequestParam(required = false) Boolean isRefund,
|
||||||
|
@RequestParam(required = false) Long customerId,
|
||||||
Pageable pageable) {
|
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}")
|
@GetMapping("/{id}")
|
||||||
@@ -39,7 +40,7 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping
|
@PostMapping
|
||||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
@PreAuthorize("hasRole('STAFF')")
|
||||||
public ResponseEntity<SaleResponse> createSale(@Valid @RequestBody SaleRequest request) {
|
public ResponseEntity<SaleResponse> createSale(@Valid @RequestBody SaleRequest request) {
|
||||||
return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request));
|
return ResponseEntity.status(HttpStatus.CREATED).body(saleService.createSale(request));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ public class ServiceController {
|
|||||||
@GetMapping
|
@GetMapping
|
||||||
public ResponseEntity<Page<ServiceResponse>> getAllServices(
|
public ResponseEntity<Page<ServiceResponse>> getAllServices(
|
||||||
@RequestParam(required = false) String q,
|
@RequestParam(required = false) String q,
|
||||||
|
@RequestParam(required = false) String species,
|
||||||
Pageable pageable) {
|
Pageable pageable) {
|
||||||
return ResponseEntity.ok(serviceService.getAllServices(q, pageable));
|
return ResponseEntity.ok(serviceService.getAllServices(q, species, pageable));
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/{id}")
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ public class UserAvatarController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/{userId}/avatar/file")
|
@GetMapping("/{userId}/avatar/file")
|
||||||
@PreAuthorize("hasAnyRole('STAFF', 'ADMIN')")
|
@PreAuthorize("isAuthenticated()")
|
||||||
public ResponseEntity<Resource> getUserAvatarFile(@PathVariable Long userId) {
|
public ResponseEntity<Resource> getUserAvatarFile(@PathVariable Long userId) {
|
||||||
User user = userRepository.findById(userId).orElse(null);
|
User user = userRepository.findById(userId).orElse(null);
|
||||||
if (user == null || !avatarStorageService.hasAvatar(user)) {
|
if (user == null || !avatarStorageService.hasAvatar(user)) {
|
||||||
|
|||||||
@@ -11,21 +11,19 @@ public class PurchaseOrderResponse {
|
|||||||
private Long storeId;
|
private Long storeId;
|
||||||
private String storeName;
|
private String storeName;
|
||||||
private LocalDate orderDate;
|
private LocalDate orderDate;
|
||||||
private String status;
|
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
private LocalDateTime updatedAt;
|
private LocalDateTime updatedAt;
|
||||||
|
|
||||||
public PurchaseOrderResponse() {
|
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.purchaseOrderId = purchaseOrderId;
|
||||||
this.supId = supId;
|
this.supId = supId;
|
||||||
this.supplierName = supplierName;
|
this.supplierName = supplierName;
|
||||||
this.storeId = storeId;
|
this.storeId = storeId;
|
||||||
this.storeName = storeName;
|
this.storeName = storeName;
|
||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
this.status = status;
|
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -78,14 +76,6 @@ public class PurchaseOrderResponse {
|
|||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -107,12 +97,12 @@ public class PurchaseOrderResponse {
|
|||||||
if (this == o) return true;
|
if (this == o) return true;
|
||||||
if (o == null || getClass() != o.getClass()) return false;
|
if (o == null || getClass() != o.getClass()) return false;
|
||||||
PurchaseOrderResponse that = (PurchaseOrderResponse) o;
|
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
|
@Override
|
||||||
public int hashCode() {
|
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
|
@Override
|
||||||
@@ -124,7 +114,6 @@ public class PurchaseOrderResponse {
|
|||||||
", storeId=" + storeId +
|
", storeId=" + storeId +
|
||||||
", storeName='" + storeName + '\'' +
|
", storeName='" + storeName + '\'' +
|
||||||
", orderDate=" + orderDate +
|
", orderDate=" + orderDate +
|
||||||
", status='" + status + '\'' +
|
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ public class SaleRequest {
|
|||||||
|
|
||||||
private Long cartId;
|
private Long cartId;
|
||||||
|
|
||||||
|
private Integer pointsUsed;
|
||||||
|
|
||||||
|
|
||||||
public Long getStoreId() {
|
public Long getStoreId() {
|
||||||
return storeId;
|
return storeId;
|
||||||
@@ -101,6 +103,14 @@ public class SaleRequest {
|
|||||||
this.cartId = cartId;
|
this.cartId = cartId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getPointsUsed() {
|
||||||
|
return pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPointsUsed(Integer pointsUsed) {
|
||||||
|
this.pointsUsed = pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
@@ -115,12 +125,13 @@ public class SaleRequest {
|
|||||||
Objects.equals(customerId, that.customerId) &&
|
Objects.equals(customerId, that.customerId) &&
|
||||||
Objects.equals(channel, that.channel) &&
|
Objects.equals(channel, that.channel) &&
|
||||||
Objects.equals(couponId, that.couponId) &&
|
Objects.equals(couponId, that.couponId) &&
|
||||||
Objects.equals(cartId, that.cartId);
|
Objects.equals(cartId, that.cartId) &&
|
||||||
|
Objects.equals(pointsUsed, that.pointsUsed);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
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
|
@Override
|
||||||
@@ -135,6 +146,7 @@ public class SaleRequest {
|
|||||||
", channel='" + channel + '\'' +
|
", channel='" + channel + '\'' +
|
||||||
", couponId=" + couponId +
|
", couponId=" + couponId +
|
||||||
", cartId=" + cartId +
|
", cartId=" + cartId +
|
||||||
|
", pointsUsed=" + pointsUsed +
|
||||||
'}';
|
'}';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,9 +27,6 @@ public class PurchaseOrder {
|
|||||||
@Column(nullable = false)
|
@Column(nullable = false)
|
||||||
private LocalDate orderDate;
|
private LocalDate orderDate;
|
||||||
|
|
||||||
@Column(nullable = false, length = 50)
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
@CreationTimestamp
|
@CreationTimestamp
|
||||||
@Column(name = "created_at", updatable = false)
|
@Column(name = "created_at", updatable = false)
|
||||||
private LocalDateTime createdAt;
|
private LocalDateTime createdAt;
|
||||||
@@ -41,11 +38,10 @@ public class PurchaseOrder {
|
|||||||
public 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.purchaseOrderId = purchaseOrderId;
|
||||||
this.supplier = supplier;
|
this.supplier = supplier;
|
||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
this.status = status;
|
|
||||||
this.createdAt = createdAt;
|
this.createdAt = createdAt;
|
||||||
this.updatedAt = updatedAt;
|
this.updatedAt = updatedAt;
|
||||||
}
|
}
|
||||||
@@ -82,14 +78,6 @@ public class PurchaseOrder {
|
|||||||
this.orderDate = orderDate;
|
this.orderDate = orderDate;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getStatus() {
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setStatus(String status) {
|
|
||||||
this.status = status;
|
|
||||||
}
|
|
||||||
|
|
||||||
public LocalDateTime getCreatedAt() {
|
public LocalDateTime getCreatedAt() {
|
||||||
return createdAt;
|
return createdAt;
|
||||||
}
|
}
|
||||||
@@ -125,7 +113,6 @@ public class PurchaseOrder {
|
|||||||
"purchaseOrderId=" + purchaseOrderId +
|
"purchaseOrderId=" + purchaseOrderId +
|
||||||
", supplier=" + supplier +
|
", supplier=" + supplier +
|
||||||
", orderDate=" + orderDate +
|
", orderDate=" + orderDate +
|
||||||
", status='" + status + '\'' +
|
|
||||||
", createdAt=" + createdAt +
|
", createdAt=" + createdAt +
|
||||||
", updatedAt=" + updatedAt +
|
", updatedAt=" + updatedAt +
|
||||||
'}';
|
'}';
|
||||||
|
|||||||
@@ -42,9 +42,10 @@ public class GlobalExceptionHandler {
|
|||||||
errors.put(fieldName, errorMessage);
|
errors.put(fieldName, errorMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
String firstMessage = errors.values().stream().findFirst().orElse("Validation failed");
|
||||||
Map<String, Object> response = new HashMap<>();
|
Map<String, Object> response = new HashMap<>();
|
||||||
response.put("status", HttpStatus.BAD_REQUEST.value());
|
response.put("status", HttpStatus.BAD_REQUEST.value());
|
||||||
response.put("message", "Validation failed");
|
response.put("message", firstMessage);
|
||||||
response.put("errors", errors);
|
response.put("errors", errors);
|
||||||
response.put("details", buildDetails(ex));
|
response.put("details", buildDetails(ex));
|
||||||
response.put("path", request.getRequestURI());
|
response.put("path", request.getRequestURI());
|
||||||
|
|||||||
@@ -40,4 +40,6 @@ public interface AdoptionRepository extends JpaRepository<Adoption, Long> {
|
|||||||
boolean existsByPet_IdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus);
|
boolean existsByPet_IdAndAdoptionStatusIgnoreCase(Long petId, String adoptionStatus);
|
||||||
|
|
||||||
List<Adoption> findByCustomer_IdAndAdoptionStatusIgnoreCase(Long customerId, 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> findPastBookedAppointments(@Param("currentDate") LocalDate currentDate, @Param("currentTime") LocalTime currentTime);
|
||||||
|
|
||||||
List<Appointment> findByPet_Id(Long petId);
|
List<Appointment> findByPet_Id(Long petId);
|
||||||
|
|
||||||
|
List<Appointment> findByAppointmentDateAndAppointmentStatusIgnoreCase(LocalDate date, String status);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import org.springframework.stereotype.Repository;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface ProductRepository extends JpaRepository<Product, Long> {
|
public interface ProductRepository extends JpaRepository<Product, Long> {
|
||||||
|
|
||||||
|
boolean existsByProdNameIgnoreCase(String prodName);
|
||||||
|
|
||||||
|
boolean existsByProdNameIgnoreCaseAndProdIdNot(String prodName, Long prodId);
|
||||||
|
|
||||||
@Query("SELECT p FROM Product p WHERE " +
|
@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 " +
|
"(: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)")
|
"(:categoryId IS NULL OR p.category.categoryId = :categoryId)")
|
||||||
|
|||||||
@@ -22,8 +22,9 @@ public interface SaleRepository extends JpaRepository<Sale, Long> {
|
|||||||
")) AND " +
|
")) AND " +
|
||||||
"(:paymentMethod IS NULL OR LOWER(s.paymentMethod) = LOWER(:paymentMethod)) AND " +
|
"(:paymentMethod IS NULL OR LOWER(s.paymentMethod) = LOWER(:paymentMethod)) AND " +
|
||||||
"(:isRefund IS NULL OR s.isRefund = :isRefund) AND " +
|
"(:isRefund IS NULL OR s.isRefund = :isRefund) AND " +
|
||||||
"(:storeId IS NULL OR s.store.storeId = :storeId)")
|
"(:storeId IS NULL OR s.store.storeId = :storeId) AND " +
|
||||||
Page<Sale> searchSales(@Param("q") String query, @Param("paymentMethod") String paymentMethod, @Param("storeId") Long storeId, @Param("isRefund") Boolean isRefund, Pageable pageable);
|
"(: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);
|
List<Sale> findByOriginalSaleSaleId(Long originalSaleId);
|
||||||
|
|
||||||
|
|||||||
@@ -11,8 +11,8 @@ import org.springframework.stereotype.Repository;
|
|||||||
@Repository
|
@Repository
|
||||||
public interface ServiceRepository extends JpaRepository<Service, Long> {
|
public interface ServiceRepository extends JpaRepository<Service, Long> {
|
||||||
|
|
||||||
@Query("SELECT s FROM Service s WHERE " +
|
@Query("SELECT DISTINCT s FROM Service s LEFT JOIN s.species sp WHERE " +
|
||||||
"LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR " +
|
"(:q IS NULL OR LOWER(s.serviceName) LIKE LOWER(CONCAT('%', :q, '%')) OR LOWER(COALESCE(s.serviceDesc, '')) LIKE LOWER(CONCAT('%', :q, '%'))) AND " +
|
||||||
"LOWER(s.serviceDesc) LIKE LOWER(CONCAT('%', :q, '%'))")
|
"(:species IS NULL OR LOWER(sp) = LOWER(:species))")
|
||||||
Page<Service> searchServices(@Param("q") String query, Pageable pageable);
|
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 {
|
public class SecurityConfig {
|
||||||
|
|
||||||
private final JwtAuthenticationFilter jwtAuthFilter;
|
private final JwtAuthenticationFilter jwtAuthFilter;
|
||||||
|
private final RateLimitFilter rateLimitFilter;
|
||||||
private final UserDetailsService userDetailsService;
|
private final UserDetailsService userDetailsService;
|
||||||
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
|
private final RestAuthenticationEntryPoint restAuthenticationEntryPoint;
|
||||||
private final RestAccessDeniedHandler restAccessDeniedHandler;
|
private final RestAccessDeniedHandler restAccessDeniedHandler;
|
||||||
|
|
||||||
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
|
public SecurityConfig(JwtAuthenticationFilter jwtAuthFilter,
|
||||||
|
RateLimitFilter rateLimitFilter,
|
||||||
UserDetailsService userDetailsService,
|
UserDetailsService userDetailsService,
|
||||||
RestAuthenticationEntryPoint restAuthenticationEntryPoint,
|
RestAuthenticationEntryPoint restAuthenticationEntryPoint,
|
||||||
RestAccessDeniedHandler restAccessDeniedHandler) {
|
RestAccessDeniedHandler restAccessDeniedHandler) {
|
||||||
this.jwtAuthFilter = jwtAuthFilter;
|
this.jwtAuthFilter = jwtAuthFilter;
|
||||||
|
this.rateLimitFilter = rateLimitFilter;
|
||||||
this.userDetailsService = userDetailsService;
|
this.userDetailsService = userDetailsService;
|
||||||
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
|
this.restAuthenticationEntryPoint = restAuthenticationEntryPoint;
|
||||||
this.restAccessDeniedHandler = restAccessDeniedHandler;
|
this.restAccessDeniedHandler = restAccessDeniedHandler;
|
||||||
@@ -76,6 +79,7 @@ public class SecurityConfig {
|
|||||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||||
.authenticationProvider(daoAuthenticationProvider())
|
.authenticationProvider(daoAuthenticationProvider())
|
||||||
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
.addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class);
|
||||||
|
http.addFilterBefore(rateLimitFilter, JwtAuthenticationFilter.class);
|
||||||
http.addFilterAfter(activityLoggingFilter, JwtAuthenticationFilter.class);
|
http.addFilterAfter(activityLoggingFilter, JwtAuthenticationFilter.class);
|
||||||
|
|
||||||
return http.build();
|
return http.build();
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import org.springframework.data.jpa.domain.Specification;
|
|||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -52,6 +53,11 @@ public class ActivityLogService {
|
|||||||
entry.setStoreNameSnapshot(store != null ? store.getStoreName() : null);
|
entry.setStoreNameSnapshot(store != null ? store.getStoreName() : null);
|
||||||
entry.setActivity(activity.trim());
|
entry.setActivity(activity.trim());
|
||||||
activityLogRepository.save(entry);
|
activityLogRepository.save(entry);
|
||||||
|
log.info("[ACTIVITY] {} | {} | {} | {}",
|
||||||
|
entry.getRoleSnapshot(),
|
||||||
|
entry.getUsernameSnapshot(),
|
||||||
|
entry.getStoreNameSnapshot() != null ? entry.getStoreNameSnapshot() : "no store",
|
||||||
|
entry.getActivity());
|
||||||
} catch (Exception ex) {
|
} catch (Exception ex) {
|
||||||
log.warn("Failed to persist activity log", ex);
|
log.warn("Failed to persist activity log", ex);
|
||||||
}
|
}
|
||||||
@@ -65,7 +71,7 @@ public class ActivityLogService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@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) -> {
|
Specification<ActivityLog> spec = (root, query, cb) -> {
|
||||||
List<Predicate> predicates = new ArrayList<>();
|
List<Predicate> predicates = new ArrayList<>();
|
||||||
|
|
||||||
@@ -87,6 +93,14 @@ public class ActivityLogService {
|
|||||||
predicates.add(searchPredicate);
|
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]));
|
return cb.and(predicates.toArray(new Predicate[0]));
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -96,9 +110,14 @@ public class ActivityLogService {
|
|||||||
.toList();
|
.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)
|
@Transactional(readOnly = true)
|
||||||
public List<ActivityLogResponse> getLogs(int limit) {
|
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) {
|
private ActivityLogResponse toResponse(ActivityLog entry) {
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|||||||
import java.math.BigDecimal;
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class AdoptionService {
|
public class AdoptionService {
|
||||||
@@ -39,13 +40,15 @@ public class AdoptionService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final StoreRepository storeRepository;
|
private final StoreRepository storeRepository;
|
||||||
private final SaleRepository saleRepository;
|
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.adoptionRepository = adoptionRepository;
|
||||||
this.petRepository = petRepository;
|
this.petRepository = petRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.storeRepository = storeRepository;
|
this.storeRepository = storeRepository;
|
||||||
this.saleRepository = saleRepository;
|
this.saleRepository = saleRepository;
|
||||||
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Page<AdoptionResponse> getAllAdoptions(String query, Long customerId, String status, Long storeId, LocalDate date, Pageable pageable) {
|
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)) {
|
if (ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
|
||||||
createSaleForAdoption(adoption, request.getPaymentMethod());
|
createSaleForAdoption(adoption, request.getPaymentMethod());
|
||||||
}
|
}
|
||||||
|
if (ADOPTION_STATUS_PENDING.equalsIgnoreCase(adoptionStatus) || ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
|
||||||
|
emailService.sendAdoptionConfirmation(adoption);
|
||||||
|
}
|
||||||
return mapToResponse(adoption);
|
return mapToResponse(adoption);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -125,6 +131,7 @@ public class AdoptionService {
|
|||||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + request.getSourceStoreId()))
|
||||||
: null;
|
: null;
|
||||||
boolean wasCompleted = ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoption.getAdoptionStatus());
|
boolean wasCompleted = ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoption.getAdoptionStatus());
|
||||||
|
String previousStatus = adoption.getAdoptionStatus();
|
||||||
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
String adoptionStatus = normalizeAdoptionStatus(request.getAdoptionStatus());
|
||||||
Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null;
|
Long currentPetId = adoption.getPet() != null ? adoption.getPet().getPetId() : null;
|
||||||
validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId);
|
validatePetAvailability(pet, adoption.getAdoptionId(), currentPetId);
|
||||||
@@ -144,6 +151,10 @@ public class AdoptionService {
|
|||||||
if (!wasCompleted && ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
|
if (!wasCompleted && ADOPTION_STATUS_COMPLETED.equalsIgnoreCase(adoptionStatus)) {
|
||||||
createSaleForAdoption(adoption, request.getPaymentMethod());
|
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);
|
return mapToResponse(adoption);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -200,15 +211,37 @@ public class AdoptionService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteAdoption(Long id) {
|
public void deleteAdoption(Long id) {
|
||||||
if (!adoptionRepository.existsById(id)) {
|
Adoption adoption = adoptionRepository.findById(id)
|
||||||
throw new ResourceNotFoundException("Adoption not found with id: " + id);
|
.orElseThrow(() -> new ResourceNotFoundException("Adoption not found with id: " + id));
|
||||||
}
|
Pet pet = adoption.getPet();
|
||||||
adoptionRepository.deleteById(id);
|
String status = adoption.getAdoptionStatus();
|
||||||
|
adoptionRepository.delete(adoption);
|
||||||
|
resetPetIfPending(pet, status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void bulkDeleteAdoptions(BulkDeleteRequest request) {
|
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) {
|
private String normalizeFilter(String value) {
|
||||||
@@ -309,7 +342,8 @@ public class AdoptionService {
|
|||||||
sale.setPaymentMethod(paymentMethod != null && !paymentMethod.isBlank() ? paymentMethod : "Cash");
|
sale.setPaymentMethod(paymentMethod != null && !paymentMethod.isBlank() ? paymentMethod : "Cash");
|
||||||
sale.setIsRefund(false);
|
sale.setIsRefund(false);
|
||||||
sale.setChannel("ADOPTION");
|
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) {
|
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.AppointmentRequest;
|
||||||
import com.petshop.backend.dto.appointment.AppointmentResponse;
|
import com.petshop.backend.dto.appointment.AppointmentResponse;
|
||||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||||
|
import com.petshop.backend.entity.Adoption;
|
||||||
import com.petshop.backend.entity.Appointment;
|
import com.petshop.backend.entity.Appointment;
|
||||||
import com.petshop.backend.entity.Pet;
|
import com.petshop.backend.entity.Pet;
|
||||||
import com.petshop.backend.entity.StoreLocation;
|
import com.petshop.backend.entity.StoreLocation;
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.petshop.backend.exception.BusinessException;
|
||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
|
import com.petshop.backend.repository.AdoptionRepository;
|
||||||
import com.petshop.backend.repository.AppointmentRepository;
|
import com.petshop.backend.repository.AppointmentRepository;
|
||||||
import com.petshop.backend.repository.PetRepository;
|
import com.petshop.backend.repository.PetRepository;
|
||||||
import com.petshop.backend.repository.ServiceRepository;
|
import com.petshop.backend.repository.ServiceRepository;
|
||||||
@@ -26,6 +29,7 @@ import java.time.LocalDateTime;
|
|||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
@@ -36,13 +40,17 @@ public class AppointmentService {
|
|||||||
private final PetRepository petRepository;
|
private final PetRepository petRepository;
|
||||||
private final StoreRepository storeRepository;
|
private final StoreRepository storeRepository;
|
||||||
private final UserRepository userRepository;
|
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.appointmentRepository = appointmentRepository;
|
||||||
this.serviceRepository = serviceRepository;
|
this.serviceRepository = serviceRepository;
|
||||||
this.petRepository = petRepository;
|
this.petRepository = petRepository;
|
||||||
this.storeRepository = storeRepository;
|
this.storeRepository = storeRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
|
this.adoptionRepository = adoptionRepository;
|
||||||
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
@@ -118,6 +126,7 @@ public class AppointmentService {
|
|||||||
validateSpeciesServiceCompatibility(pet, service);
|
validateSpeciesServiceCompatibility(pet, service);
|
||||||
|
|
||||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||||
|
validatePetServiceCompatibility(pet, service);
|
||||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), null);
|
||||||
|
|
||||||
Appointment appointment = new Appointment();
|
Appointment appointment = new Appointment();
|
||||||
@@ -131,6 +140,7 @@ public class AppointmentService {
|
|||||||
appointment.setPet(pet);
|
appointment.setPet(pet);
|
||||||
|
|
||||||
appointment = appointmentRepository.save(appointment);
|
appointment = appointmentRepository.save(appointment);
|
||||||
|
emailService.sendAppointmentConfirmation(appointment);
|
||||||
return mapToResponse(appointment);
|
return mapToResponse(appointment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -156,6 +166,7 @@ public class AppointmentService {
|
|||||||
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
User employee = resolveAppointmentEmployee(request.getEmployeeId(), store.getStoreId());
|
||||||
|
|
||||||
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
validateStoreAccess(store.getStoreId(), authenticatedUser);
|
||||||
|
validatePetServiceCompatibility(pet, service);
|
||||||
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
|
validateAvailability(employee, service, request.getAppointmentDate(), request.getAppointmentTime(), id);
|
||||||
|
|
||||||
appointment.setCustomer(customer);
|
appointment.setCustomer(customer);
|
||||||
@@ -168,6 +179,7 @@ public class AppointmentService {
|
|||||||
appointment.setEmployee(employee);
|
appointment.setEmployee(employee);
|
||||||
|
|
||||||
appointment = appointmentRepository.save(appointment);
|
appointment = appointmentRepository.save(appointment);
|
||||||
|
emailService.sendAppointmentConfirmation(appointment);
|
||||||
return mapToResponse(appointment);
|
return mapToResponse(appointment);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -243,7 +255,7 @@ public class AppointmentService {
|
|||||||
return availableSlots;
|
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 * * ?")
|
@Scheduled(cron = "0 0 0 * * ?")
|
||||||
@Transactional
|
@Transactional
|
||||||
public void updatePastAppointmentsStatus() {
|
public void updatePastAppointmentsStatus() {
|
||||||
@@ -255,6 +267,20 @@ public class AppointmentService {
|
|||||||
appointment.setAppointmentStatus("COMPLETED");
|
appointment.setAppointmentStatus("COMPLETED");
|
||||||
appointmentRepository.save(appointment);
|
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) {
|
private String normalizeFilter(String value) {
|
||||||
@@ -265,6 +291,17 @@ public class AppointmentService {
|
|||||||
return trimmed.isEmpty() ? null : trimmed;
|
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) {
|
private void validateAppointmentRequest(AppointmentRequest request) {
|
||||||
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
|
if ("Booked".equalsIgnoreCase(request.getAppointmentStatus())) {
|
||||||
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
|
LocalDateTime appointmentDateTime = LocalDateTime.of(request.getAppointmentDate(), request.getAppointmentTime());
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import java.util.UUID;
|
|||||||
public class AvatarStorageService {
|
public class AvatarStorageService {
|
||||||
|
|
||||||
private static final String STORED_PREFIX = "/uploads/avatars/";
|
private static final String STORED_PREFIX = "/uploads/avatars/";
|
||||||
private static final String OWNER_ENDPOINT = "/api/v1/auth/me/avatar/file";
|
|
||||||
|
|
||||||
@Value("${app.upload.base-dir:uploads}")
|
@Value("${app.upload.base-dir:uploads}")
|
||||||
private String uploadBaseDir;
|
private String uploadBaseDir;
|
||||||
@@ -65,7 +64,7 @@ public class AvatarStorageService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String toOwnerAvatarUrl(User user) {
|
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) {
|
public String toStoredAvatarUrl(String avatarFilenamePath) {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.petshop.backend.entity.Conversation;
|
|||||||
import com.petshop.backend.entity.Message;
|
import com.petshop.backend.entity.Message;
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
|
import com.petshop.backend.util.ContentFilter;
|
||||||
import com.petshop.backend.repository.ConversationRepository;
|
import com.petshop.backend.repository.ConversationRepository;
|
||||||
import com.petshop.backend.repository.MessageRepository;
|
import com.petshop.backend.repository.MessageRepository;
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
@@ -31,17 +32,20 @@ public class ChatService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final AvatarStorageService avatarStorageService;
|
private final AvatarStorageService avatarStorageService;
|
||||||
private final ChatAttachmentStorageService attachmentStorageService;
|
private final ChatAttachmentStorageService attachmentStorageService;
|
||||||
|
private final EmailService emailService;
|
||||||
|
|
||||||
public ChatService(ConversationRepository conversationRepository,
|
public ChatService(ConversationRepository conversationRepository,
|
||||||
MessageRepository messageRepository,
|
MessageRepository messageRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
AvatarStorageService avatarStorageService,
|
AvatarStorageService avatarStorageService,
|
||||||
ChatAttachmentStorageService attachmentStorageService) {
|
ChatAttachmentStorageService attachmentStorageService,
|
||||||
|
EmailService emailService) {
|
||||||
this.conversationRepository = conversationRepository;
|
this.conversationRepository = conversationRepository;
|
||||||
this.messageRepository = messageRepository;
|
this.messageRepository = messageRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.avatarStorageService = avatarStorageService;
|
this.avatarStorageService = avatarStorageService;
|
||||||
this.attachmentStorageService = attachmentStorageService;
|
this.attachmentStorageService = attachmentStorageService;
|
||||||
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -135,6 +139,8 @@ public class ChatService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ContentFilter.validate(request.getContent());
|
||||||
|
|
||||||
Message message = new Message();
|
Message message = new Message();
|
||||||
message.setConversationId(conversationId);
|
message.setConversationId(conversationId);
|
||||||
message.setSenderId(userId);
|
message.setSenderId(userId);
|
||||||
@@ -261,13 +267,19 @@ public class ChatService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
conversation.setStatus(Conversation.ConversationStatus.valueOf(request.getStatus()));
|
conversation.setStatus(Conversation.ConversationStatus.valueOf(request.getStatus()));
|
||||||
conversation = conversationRepository.save(conversation);
|
Conversation savedConversation = conversationRepository.save(conversation);
|
||||||
|
|
||||||
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
List<Message> messages = messageRepository.findByConversationIdOrderByTimestampAsc(conversationId);
|
||||||
|
|
||||||
|
if (Conversation.ConversationStatus.CLOSED.name().equals(request.getStatus())) {
|
||||||
|
userRepository.findById(savedConversation.getCustomerId()).ifPresent(customer ->
|
||||||
|
emailService.sendChatTranscript(savedConversation, messages, customer));
|
||||||
|
}
|
||||||
|
|
||||||
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
Message last = messages.isEmpty() ? null : messages.get(messages.size() - 1);
|
||||||
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
String lastMessage = last != null && last.getContent() != null ? last.getContent() : "";
|
||||||
Long lastSenderId = last != null ? last.getSenderId() : null;
|
Long lastSenderId = last != null ? last.getSenderId() : null;
|
||||||
return ConversationResponse.fromEntity(conversation, lastMessage, lastSenderId);
|
return ConversationResponse.fromEntity(savedConversation, lastMessage, lastSenderId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
|
public List<MessageResponse> getMessages(Long conversationId, Long userId, User.Role role) {
|
||||||
|
|||||||
@@ -0,0 +1,308 @@
|
|||||||
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
|
import com.petshop.backend.entity.Adoption;
|
||||||
|
import com.petshop.backend.entity.Appointment;
|
||||||
|
import com.petshop.backend.entity.Conversation;
|
||||||
|
import com.petshop.backend.entity.Message;
|
||||||
|
import com.petshop.backend.entity.Sale;
|
||||||
|
import com.petshop.backend.entity.SaleItem;
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.resend.Resend;
|
||||||
|
import com.resend.services.emails.model.CreateEmailOptions;
|
||||||
|
import org.slf4j.Logger;
|
||||||
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class EmailService {
|
||||||
|
|
||||||
|
private static final Logger log = LoggerFactory.getLogger(EmailService.class);
|
||||||
|
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("MMMM d, yyyy");
|
||||||
|
private static final DateTimeFormatter TIME_FMT = DateTimeFormatter.ofPattern("h:mm a");
|
||||||
|
|
||||||
|
private final Resend resend;
|
||||||
|
private final String from;
|
||||||
|
private final String frontendUrl;
|
||||||
|
private final ActivityLogService activityLogService;
|
||||||
|
|
||||||
|
public EmailService(
|
||||||
|
@Value("${resend.api-key}") String apiKey,
|
||||||
|
@Value("${resend.from}") String from,
|
||||||
|
@Value("${app.frontend-url}") String frontendUrl,
|
||||||
|
ActivityLogService activityLogService) {
|
||||||
|
this.resend = new Resend(apiKey);
|
||||||
|
this.from = from;
|
||||||
|
this.frontendUrl = frontendUrl;
|
||||||
|
this.activityLogService = activityLogService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendWelcome(User user) {
|
||||||
|
if (user.getEmail() == null || user.getEmail().isBlank()) return;
|
||||||
|
String subject = "Welcome to PetShop!";
|
||||||
|
String html = """
|
||||||
|
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||||
|
<h2>Welcome to PetShop, %s!</h2>
|
||||||
|
<p>Your account has been created successfully.</p>
|
||||||
|
<p>You can now log in and start exploring our pets, products, and services.</p>
|
||||||
|
<p>Thank you for joining us!</p>
|
||||||
|
</div>""".formatted(firstName(user));
|
||||||
|
send(user.getId(), user.getEmail(), subject, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPasswordResetLink(User user, String rawToken) {
|
||||||
|
if (user.getEmail() == null || user.getEmail().isBlank()) return;
|
||||||
|
String link = frontendUrl + "/reset-password?token=" + rawToken;
|
||||||
|
String subject = "Reset your PetShop password";
|
||||||
|
String html = """
|
||||||
|
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||||
|
<h2>Password Reset Request</h2>
|
||||||
|
<p>Hi %s,</p>
|
||||||
|
<p>We received a request to reset your password. Click the button below to proceed.</p>
|
||||||
|
<p><a href="%s" style="background:#2563eb;color:#fff;padding:10px 20px;border-radius:4px;text-decoration:none;display:inline-block">Reset Password</a></p>
|
||||||
|
<p>This link expires in <strong>30 minutes</strong>. If you did not request a reset, you can safely ignore this email.</p>
|
||||||
|
</div>""".formatted(firstName(user), link);
|
||||||
|
send(user.getId(), user.getEmail(), subject, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendPurchaseReceipt(Sale sale) {
|
||||||
|
User customer = sale.getCustomer();
|
||||||
|
if (customer == null || customer.getEmail() == null || customer.getEmail().isBlank()) return;
|
||||||
|
String subject = "Your PetShop receipt";
|
||||||
|
String html = buildReceiptHtml(sale);
|
||||||
|
send(customer.getId(), customer.getEmail(), subject, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendAdoptionConfirmation(Adoption adoption) {
|
||||||
|
User customer = adoption.getCustomer();
|
||||||
|
if (customer != null && customer.getEmail() != null && !customer.getEmail().isBlank()) {
|
||||||
|
String subject = "Your adoption — " + adoption.getPet().getPetName();
|
||||||
|
send(customer.getId(), customer.getEmail(), subject, buildAdoptionHtml(adoption, customer));
|
||||||
|
}
|
||||||
|
User employee = adoption.getEmployee();
|
||||||
|
if (employee != null && employee.getEmail() != null && !employee.getEmail().isBlank()) {
|
||||||
|
String subject = "Adoption assigned — " + adoption.getPet().getPetName();
|
||||||
|
send(employee.getId(), employee.getEmail(), subject, buildAdoptionHtml(adoption, customer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendAdoptionReminder(Adoption adoption) {
|
||||||
|
User customer = adoption.getCustomer();
|
||||||
|
if (customer != null && customer.getEmail() != null && !customer.getEmail().isBlank()) {
|
||||||
|
String subject = "Reminder: adoption tomorrow — " + adoption.getPet().getPetName();
|
||||||
|
send(customer.getId(), customer.getEmail(), subject, buildAdoptionReminderHtml(adoption));
|
||||||
|
}
|
||||||
|
User employee = adoption.getEmployee();
|
||||||
|
if (employee != null && employee.getEmail() != null && !employee.getEmail().isBlank()) {
|
||||||
|
String subject = "Reminder: adoption tomorrow — " + adoption.getPet().getPetName();
|
||||||
|
send(employee.getId(), employee.getEmail(), subject, buildAdoptionReminderHtml(adoption));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendAppointmentConfirmation(Appointment appointment) {
|
||||||
|
User customer = appointment.getCustomer();
|
||||||
|
if (customer != null && customer.getEmail() != null && !customer.getEmail().isBlank()) {
|
||||||
|
String subject = "Appointment confirmed — " + appointment.getService().getServiceName();
|
||||||
|
send(customer.getId(), customer.getEmail(), subject, buildAppointmentHtml(appointment));
|
||||||
|
}
|
||||||
|
User employee = appointment.getEmployee();
|
||||||
|
if (employee != null && employee.getEmail() != null && !employee.getEmail().isBlank()) {
|
||||||
|
String subject = "Appointment assigned — " + appointment.getService().getServiceName();
|
||||||
|
send(employee.getId(), employee.getEmail(), subject, buildAppointmentHtml(appointment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendAppointmentReminder(Appointment appointment) {
|
||||||
|
User customer = appointment.getCustomer();
|
||||||
|
if (customer != null && customer.getEmail() != null && !customer.getEmail().isBlank()) {
|
||||||
|
String subject = "Reminder: appointment tomorrow — " + appointment.getService().getServiceName();
|
||||||
|
send(customer.getId(), customer.getEmail(), subject, buildAppointmentReminderHtml(appointment));
|
||||||
|
}
|
||||||
|
User employee = appointment.getEmployee();
|
||||||
|
if (employee != null && employee.getEmail() != null && !employee.getEmail().isBlank()) {
|
||||||
|
String subject = "Reminder: appointment tomorrow — " + appointment.getService().getServiceName();
|
||||||
|
send(employee.getId(), employee.getEmail(), subject, buildAppointmentReminderHtml(appointment));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void sendChatTranscript(Conversation conversation, List<Message> messages, User customer) {
|
||||||
|
if (customer == null || customer.getEmail() == null || customer.getEmail().isBlank()) return;
|
||||||
|
String subject = "Your PetShop support transcript";
|
||||||
|
String html = buildTranscriptHtml(conversation, messages, customer);
|
||||||
|
send(customer.getId(), customer.getEmail(), subject, html);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void send(Long recipientUserId, String to, String subject, String html) {
|
||||||
|
try {
|
||||||
|
CreateEmailOptions options = CreateEmailOptions.builder()
|
||||||
|
.from(from)
|
||||||
|
.to(List.of(to))
|
||||||
|
.subject(subject)
|
||||||
|
.html(html)
|
||||||
|
.build();
|
||||||
|
resend.emails().send(options);
|
||||||
|
activityLogService.record(recipientUserId, "Email sent: " + subject + " → " + to);
|
||||||
|
} catch (Exception ex) {
|
||||||
|
log.error("Failed to send email '{}' to {}: {}", subject, to, ex.getMessage());
|
||||||
|
activityLogService.record(recipientUserId, "Email failed: " + subject + " → " + to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildReceiptHtml(Sale sale) {
|
||||||
|
StringBuilder rows = new StringBuilder();
|
||||||
|
List<SaleItem> items = sale.getItems();
|
||||||
|
if (items != null) {
|
||||||
|
for (SaleItem item : items) {
|
||||||
|
String name = item.getProduct() != null ? item.getProduct().getProdName() : "—";
|
||||||
|
rows.append("<tr><td style='padding:6px 8px'>").append(esc(name)).append("</td>")
|
||||||
|
.append("<td style='padding:6px 8px;text-align:center'>").append(item.getQuantity()).append("</td>")
|
||||||
|
.append("<td style='padding:6px 8px;text-align:right'>$").append(fmt(item.getUnitPrice())).append("</td>")
|
||||||
|
.append("<td style='padding:6px 8px;text-align:right'>$").append(fmt(item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity())))).append("</td></tr>");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String date = sale.getSaleDate() != null ? sale.getSaleDate().format(DateTimeFormatter.ofPattern("MMMM d, yyyy h:mm a")) : "—";
|
||||||
|
return """
|
||||||
|
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||||
|
<h2>Thank you for your purchase!</h2>
|
||||||
|
<p><strong>Date:</strong> %s</p>
|
||||||
|
<p><strong>Payment method:</strong> %s</p>
|
||||||
|
<table style="width:100%%;border-collapse:collapse;margin:16px 0">
|
||||||
|
<thead><tr style="background:#f3f4f6">
|
||||||
|
<th style="padding:6px 8px;text-align:left">Item</th>
|
||||||
|
<th style="padding:6px 8px;text-align:center">Qty</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right">Unit price</th>
|
||||||
|
<th style="padding:6px 8px;text-align:right">Total</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>%s</tbody>
|
||||||
|
</table>
|
||||||
|
<p><strong>Subtotal:</strong> $%s</p>
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
%s
|
||||||
|
<p style="font-size:1.1em"><strong>Total:</strong> $%s</p>
|
||||||
|
%s
|
||||||
|
</div>""".formatted(
|
||||||
|
date,
|
||||||
|
esc(sale.getPaymentMethod()),
|
||||||
|
rows.toString(),
|
||||||
|
fmt(sale.getSubtotalAmount()),
|
||||||
|
discountLine("Coupon discount", sale.getCouponDiscountAmount()),
|
||||||
|
discountLine("Employee discount", sale.getEmployeeDiscountAmount()),
|
||||||
|
discountLine("Loyalty discount", sale.getLoyaltyDiscountAmount()),
|
||||||
|
fmt(sale.getTotalAmount()),
|
||||||
|
sale.getPointsEarned() != null && sale.getPointsEarned() > 0
|
||||||
|
? "<p>You earned <strong>" + sale.getPointsEarned() + " loyalty points</strong> on this order.</p>"
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAdoptionHtml(Adoption adoption, User customer) {
|
||||||
|
String petName = adoption.getPet() != null ? adoption.getPet().getPetName() : "—";
|
||||||
|
String storeName = adoption.getSourceStore() != null ? adoption.getSourceStore().getStoreName() : "—";
|
||||||
|
String date = adoption.getAdoptionDate() != null ? adoption.getAdoptionDate().format(DATE_FMT) : "—";
|
||||||
|
String customerName = customer != null ? firstName(customer) : "—";
|
||||||
|
return """
|
||||||
|
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||||
|
<h2>Adoption update — %s</h2>
|
||||||
|
<p><strong>Customer:</strong> %s</p>
|
||||||
|
<p><strong>Pet:</strong> %s</p>
|
||||||
|
<p><strong>Date:</strong> %s</p>
|
||||||
|
<p><strong>Store:</strong> %s</p>
|
||||||
|
<p><strong>Status:</strong> %s</p>
|
||||||
|
</div>""".formatted(esc(petName), esc(customerName), esc(petName), date, esc(storeName), esc(adoption.getAdoptionStatus()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAdoptionReminderHtml(Adoption adoption) {
|
||||||
|
String petName = adoption.getPet() != null ? adoption.getPet().getPetName() : "—";
|
||||||
|
String storeName = adoption.getSourceStore() != null ? adoption.getSourceStore().getStoreName() : "—";
|
||||||
|
String date = adoption.getAdoptionDate() != null ? adoption.getAdoptionDate().format(DATE_FMT) : "—";
|
||||||
|
return """
|
||||||
|
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||||
|
<h2>Reminder: adoption tomorrow</h2>
|
||||||
|
<p>This is a reminder that the adoption of <strong>%s</strong> is scheduled for <strong>%s</strong> at <strong>%s</strong>.</p>
|
||||||
|
</div>""".formatted(esc(petName), date, esc(storeName));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAppointmentHtml(Appointment appointment) {
|
||||||
|
String service = appointment.getService() != null ? appointment.getService().getServiceName() : "—";
|
||||||
|
String store = appointment.getStore() != null ? appointment.getStore().getStoreName() : "—";
|
||||||
|
String date = appointment.getAppointmentDate() != null ? appointment.getAppointmentDate().format(DATE_FMT) : "—";
|
||||||
|
String time = appointment.getAppointmentTime() != null ? appointment.getAppointmentTime().format(TIME_FMT) : "—";
|
||||||
|
String employee = appointment.getEmployee() != null
|
||||||
|
? appointment.getEmployee().getFirstName() + " " + appointment.getEmployee().getLastName() : "—";
|
||||||
|
String pet = appointment.getPet() != null ? appointment.getPet().getPetName() : null;
|
||||||
|
return """
|
||||||
|
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||||
|
<h2>Appointment confirmed</h2>
|
||||||
|
<p><strong>Service:</strong> %s</p>
|
||||||
|
<p><strong>Date:</strong> %s</p>
|
||||||
|
<p><strong>Time:</strong> %s</p>
|
||||||
|
<p><strong>Location:</strong> %s</p>
|
||||||
|
<p><strong>Staff:</strong> %s</p>
|
||||||
|
%s
|
||||||
|
</div>""".formatted(esc(service), date, time, esc(store), esc(employee),
|
||||||
|
pet != null ? "<p><strong>Pet:</strong> " + esc(pet) + "</p>" : "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildAppointmentReminderHtml(Appointment appointment) {
|
||||||
|
String service = appointment.getService() != null ? appointment.getService().getServiceName() : "—";
|
||||||
|
String store = appointment.getStore() != null ? appointment.getStore().getStoreName() : "—";
|
||||||
|
String time = appointment.getAppointmentTime() != null ? appointment.getAppointmentTime().format(TIME_FMT) : "—";
|
||||||
|
return """
|
||||||
|
<div style="font-family:sans-serif;max-width:600px;margin:auto">
|
||||||
|
<h2>Reminder: appointment tomorrow</h2>
|
||||||
|
<p>Your <strong>%s</strong> appointment is scheduled for tomorrow at <strong>%s</strong> at <strong>%s</strong>.</p>
|
||||||
|
</div>""".formatted(esc(service), time, esc(store));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildTranscriptHtml(Conversation conversation, List<Message> messages, User customer) {
|
||||||
|
StringBuilder rows = new StringBuilder();
|
||||||
|
for (Message msg : messages) {
|
||||||
|
String sender = msg.getSenderId() != null && msg.getSenderId().equals(customer.getId())
|
||||||
|
? firstName(customer) : "Support";
|
||||||
|
String ts = msg.getTimestamp() != null
|
||||||
|
? msg.getTimestamp().format(DateTimeFormatter.ofPattern("MMM d, h:mm a")) : "";
|
||||||
|
String content = msg.getContent() != null ? esc(msg.getContent()) : "";
|
||||||
|
rows.append("<tr><td style='padding:6px 8px;white-space:nowrap'>").append(ts).append("</td>")
|
||||||
|
.append("<td style='padding:6px 8px;white-space:nowrap'>").append(esc(sender)).append("</td>")
|
||||||
|
.append("<td style='padding:6px 8px'>").append(content).append("</td></tr>");
|
||||||
|
}
|
||||||
|
return """
|
||||||
|
<div style="font-family:sans-serif;max-width:700px;margin:auto">
|
||||||
|
<h2>Support chat transcript</h2>
|
||||||
|
<p>Hi %s, here is the transcript of your support conversation.</p>
|
||||||
|
<table style="width:100%%;border-collapse:collapse;margin:16px 0;font-size:0.9em">
|
||||||
|
<thead><tr style="background:#f3f4f6">
|
||||||
|
<th style="padding:6px 8px;text-align:left">Time</th>
|
||||||
|
<th style="padding:6px 8px;text-align:left">Sender</th>
|
||||||
|
<th style="padding:6px 8px;text-align:left">Message</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>%s</tbody>
|
||||||
|
</table>
|
||||||
|
</div>""".formatted(firstName(customer), rows.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String firstName(User user) {
|
||||||
|
if (user.getFirstName() != null && !user.getFirstName().isBlank()) return esc(user.getFirstName());
|
||||||
|
if (user.getFullName() != null && !user.getFullName().isBlank()) return esc(user.getFullName().split("\\s+")[0]);
|
||||||
|
return esc(user.getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fmt(BigDecimal value) {
|
||||||
|
return value != null ? String.format("%.2f", value) : "0.00";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String discountLine(String label, BigDecimal value) {
|
||||||
|
if (value == null || value.compareTo(BigDecimal.ZERO) == 0) return "";
|
||||||
|
return "<p><strong>" + label + ":</strong> -$" + fmt(value) + "</p>";
|
||||||
|
}
|
||||||
|
|
||||||
|
private String esc(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("&", "&").replace("<", "<").replace(">", ">").replace("\"", """);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,14 +28,17 @@ public class PasswordResetService {
|
|||||||
private final PasswordResetTokenRepository passwordResetTokenRepository;
|
private final PasswordResetTokenRepository passwordResetTokenRepository;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final PasswordEncoder passwordEncoder;
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final EmailService emailService;
|
||||||
private final SecureRandom secureRandom = new SecureRandom();
|
private final SecureRandom secureRandom = new SecureRandom();
|
||||||
|
|
||||||
public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository,
|
public PasswordResetService(PasswordResetTokenRepository passwordResetTokenRepository,
|
||||||
UserRepository userRepository,
|
UserRepository userRepository,
|
||||||
PasswordEncoder passwordEncoder) {
|
PasswordEncoder passwordEncoder,
|
||||||
|
EmailService emailService) {
|
||||||
this.passwordResetTokenRepository = passwordResetTokenRepository;
|
this.passwordResetTokenRepository = passwordResetTokenRepository;
|
||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.passwordEncoder = passwordEncoder;
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
@@ -66,9 +69,11 @@ public class PasswordResetService {
|
|||||||
resetToken.setExpiresAt(now.plusMinutes(RESET_TOKEN_MINUTES));
|
resetToken.setExpiresAt(now.plusMinutes(RESET_TOKEN_MINUTES));
|
||||||
passwordResetTokenRepository.save(resetToken);
|
passwordResetTokenRepository.save(resetToken);
|
||||||
|
|
||||||
|
emailService.sendPasswordResetLink(managedUser, rawToken);
|
||||||
|
|
||||||
return new ForgotPasswordResponse(
|
return new ForgotPasswordResponse(
|
||||||
"If an account matches that username or email, a reset token has been generated.",
|
"If an account matches that username or email, a reset link has been sent.",
|
||||||
rawToken
|
null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import com.petshop.backend.dto.product.ProductRequest;
|
|||||||
import com.petshop.backend.dto.product.ProductResponse;
|
import com.petshop.backend.dto.product.ProductResponse;
|
||||||
import com.petshop.backend.entity.Category;
|
import com.petshop.backend.entity.Category;
|
||||||
import com.petshop.backend.entity.Product;
|
import com.petshop.backend.entity.Product;
|
||||||
|
import com.petshop.backend.exception.BusinessException;
|
||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
import com.petshop.backend.repository.CategoryRepository;
|
import com.petshop.backend.repository.CategoryRepository;
|
||||||
import com.petshop.backend.repository.ProductRepository;
|
import com.petshop.backend.repository.ProductRepository;
|
||||||
@@ -45,6 +46,10 @@ public class ProductService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public ProductResponse createProduct(ProductRequest request) {
|
public ProductResponse createProduct(ProductRequest request) {
|
||||||
|
if (productRepository.existsByProdNameIgnoreCase(request.getProdName())) {
|
||||||
|
throw new BusinessException("A product with this name already exists");
|
||||||
|
}
|
||||||
|
|
||||||
Category category = categoryRepository.findById(request.getCategoryId())
|
Category category = categoryRepository.findById(request.getCategoryId())
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId()));
|
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId()));
|
||||||
|
|
||||||
@@ -63,6 +68,10 @@ public class ProductService {
|
|||||||
Product product = productRepository.findById(id)
|
Product product = productRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id));
|
.orElseThrow(() -> new ResourceNotFoundException("Product not found with id: " + id));
|
||||||
|
|
||||||
|
if (productRepository.existsByProdNameIgnoreCaseAndProdIdNot(request.getProdName(), id)) {
|
||||||
|
throw new BusinessException("A product with this name already exists");
|
||||||
|
}
|
||||||
|
|
||||||
Category category = categoryRepository.findById(request.getCategoryId())
|
Category category = categoryRepository.findById(request.getCategoryId())
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId()));
|
.orElseThrow(() -> new ResourceNotFoundException("Category not found with id: " + request.getCategoryId()));
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ public class PurchaseOrderService {
|
|||||||
store != null ? store.getStoreId() : null,
|
store != null ? store.getStoreId() : null,
|
||||||
store != null ? store.getStoreName() : null,
|
store != null ? store.getStoreName() : null,
|
||||||
purchaseOrder.getOrderDate(),
|
purchaseOrder.getOrderDate(),
|
||||||
purchaseOrder.getStatus(),
|
|
||||||
purchaseOrder.getCreatedAt(),
|
purchaseOrder.getCreatedAt(),
|
||||||
purchaseOrder.getUpdatedAt()
|
purchaseOrder.getUpdatedAt()
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -31,8 +31,9 @@ public class SaleService {
|
|||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final CouponRepository couponRepository;
|
private final CouponRepository couponRepository;
|
||||||
private final CartRepository cartRepository;
|
private final CartRepository cartRepository;
|
||||||
|
private final EmailService emailService;
|
||||||
|
|
||||||
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository) {
|
public SaleService(SaleRepository saleRepository, ProductRepository productRepository, StoreRepository storeRepository, InventoryRepository inventoryRepository, UserRepository userRepository, CouponRepository couponRepository, CartRepository cartRepository, EmailService emailService) {
|
||||||
this.saleRepository = saleRepository;
|
this.saleRepository = saleRepository;
|
||||||
this.productRepository = productRepository;
|
this.productRepository = productRepository;
|
||||||
this.storeRepository = storeRepository;
|
this.storeRepository = storeRepository;
|
||||||
@@ -40,11 +41,12 @@ public class SaleService {
|
|||||||
this.userRepository = userRepository;
|
this.userRepository = userRepository;
|
||||||
this.couponRepository = couponRepository;
|
this.couponRepository = couponRepository;
|
||||||
this.cartRepository = cartRepository;
|
this.cartRepository = cartRepository;
|
||||||
|
this.emailService = emailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<SaleResponse> getAllSales(String query, String paymentMethod, Long storeId, Boolean isRefund, Pageable pageable) {
|
public Page<SaleResponse> getAllSales(String query, String paymentMethod, Long storeId, Boolean isRefund, Long customerId, Pageable pageable) {
|
||||||
Page<Sale> sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, isRefund, pageable);
|
Page<Sale> sales = saleRepository.searchSales(normalizeFilter(query), normalizeFilter(paymentMethod), storeId, isRefund, customerId, pageable);
|
||||||
return sales.map(this::mapToResponse);
|
return sales.map(this::mapToResponse);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -162,10 +164,41 @@ public class SaleService {
|
|||||||
}
|
}
|
||||||
subtotalAmount = subtotalAmount.negate();
|
subtotalAmount = subtotalAmount.negate();
|
||||||
sale.setSubtotalAmount(subtotalAmount);
|
sale.setSubtotalAmount(subtotalAmount);
|
||||||
sale.setTotalAmount(subtotalAmount);
|
|
||||||
sale.setCouponDiscountAmount(BigDecimal.ZERO);
|
Sale originalSale = sale.getOriginalSale();
|
||||||
|
BigDecimal originalSubtotal = originalSale.getSubtotalAmount();
|
||||||
|
BigDecimal loyaltyDiscountRefunded = BigDecimal.ZERO;
|
||||||
|
BigDecimal couponDiscountRefunded = BigDecimal.ZERO;
|
||||||
|
BigDecimal refundTotal;
|
||||||
|
|
||||||
|
if (originalSubtotal != null && originalSubtotal.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
BigDecimal ratio = subtotalAmount.divide(originalSubtotal, 10, RoundingMode.HALF_UP);
|
||||||
|
refundTotal = originalSale.getTotalAmount().abs().multiply(ratio).negate().setScale(2, RoundingMode.HALF_UP);
|
||||||
|
if (originalSale.getLoyaltyDiscountAmount() != null) {
|
||||||
|
loyaltyDiscountRefunded = originalSale.getLoyaltyDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
if (originalSale.getCouponDiscountAmount() != null) {
|
||||||
|
couponDiscountRefunded = originalSale.getCouponDiscountAmount().multiply(ratio).setScale(2, RoundingMode.HALF_UP);
|
||||||
|
}
|
||||||
|
User refundCustomer = originalSale.getCustomer();
|
||||||
|
if (refundCustomer != null) {
|
||||||
|
sale.setCustomer(refundCustomer);
|
||||||
|
int pointsToRestore = toPointsUsed(loyaltyDiscountRefunded);
|
||||||
|
int pointsEarnedToReverse = originalSale.getPointsEarned() != null
|
||||||
|
? ratio.multiply(BigDecimal.valueOf(originalSale.getPointsEarned())).setScale(0, RoundingMode.FLOOR).intValue()
|
||||||
|
: 0;
|
||||||
|
int currentPoints = refundCustomer.getLoyaltyPoints() != null ? refundCustomer.getLoyaltyPoints() : 0;
|
||||||
|
refundCustomer.setLoyaltyPoints(Math.max(0, currentPoints + pointsToRestore - pointsEarnedToReverse));
|
||||||
|
userRepository.save(refundCustomer);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
refundTotal = subtotalAmount.negate();
|
||||||
|
}
|
||||||
|
|
||||||
|
sale.setTotalAmount(refundTotal);
|
||||||
|
sale.setCouponDiscountAmount(couponDiscountRefunded);
|
||||||
sale.setEmployeeDiscountAmount(BigDecimal.ZERO);
|
sale.setEmployeeDiscountAmount(BigDecimal.ZERO);
|
||||||
sale.setLoyaltyDiscountAmount(BigDecimal.ZERO);
|
sale.setLoyaltyDiscountAmount(loyaltyDiscountRefunded);
|
||||||
sale.setPointsEarned(0);
|
sale.setPointsEarned(0);
|
||||||
} else {
|
} else {
|
||||||
if (request.getItems() == null || request.getItems().isEmpty()) {
|
if (request.getItems() == null || request.getItems().isEmpty()) {
|
||||||
@@ -206,18 +239,29 @@ public class SaleService {
|
|||||||
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
|
BigDecimal employeeDiscount = calculateEmployeeDiscount(customer, subtotalAmount.subtract(couponDiscount));
|
||||||
sale.setEmployeeDiscountAmount(employeeDiscount);
|
sale.setEmployeeDiscountAmount(employeeDiscount);
|
||||||
|
|
||||||
boolean useLoyaltyPoints = sale.getCart() != null && Boolean.TRUE.equals(sale.getCart().getPointsApplied());
|
BigDecimal remainingAfterDiscounts = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount);
|
||||||
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(customer, subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount), useLoyaltyPoints);
|
BigDecimal loyaltyDiscount;
|
||||||
|
int pointsDeducted;
|
||||||
|
if (request.getPointsUsed() != null && request.getPointsUsed() > 0) {
|
||||||
|
loyaltyDiscount = BigDecimal.valueOf(request.getPointsUsed())
|
||||||
|
.divide(BigDecimal.valueOf(LOYALTY_POINTS_PER_DOLLAR), 2, RoundingMode.HALF_UP)
|
||||||
|
.min(remainingAfterDiscounts.max(BigDecimal.ZERO))
|
||||||
|
.setScale(2, RoundingMode.HALF_UP);
|
||||||
|
pointsDeducted = request.getPointsUsed();
|
||||||
|
} else {
|
||||||
|
boolean useLoyaltyPoints = sale.getCart() != null && Boolean.TRUE.equals(sale.getCart().getPointsApplied());
|
||||||
|
loyaltyDiscount = calculateLoyaltyDiscount(customer, remainingAfterDiscounts, useLoyaltyPoints);
|
||||||
|
pointsDeducted = toPointsUsed(loyaltyDiscount);
|
||||||
|
}
|
||||||
sale.setLoyaltyDiscountAmount(loyaltyDiscount);
|
sale.setLoyaltyDiscountAmount(loyaltyDiscount);
|
||||||
|
|
||||||
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
|
BigDecimal finalTotal = subtotalAmount.subtract(couponDiscount).subtract(employeeDiscount).subtract(loyaltyDiscount);
|
||||||
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
|
sale.setTotalAmount(finalTotal.max(BigDecimal.ZERO));
|
||||||
|
|
||||||
int pointsUsed = toPointsUsed(loyaltyDiscount);
|
|
||||||
sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
|
sale.setPointsEarned(sale.getTotalAmount().setScale(0, RoundingMode.FLOOR).intValue());
|
||||||
if (customer != null) {
|
if (customer != null) {
|
||||||
int currentPoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0;
|
int currentPoints = customer.getLoyaltyPoints() != null ? customer.getLoyaltyPoints() : 0;
|
||||||
int updatedPoints = currentPoints - pointsUsed + sale.getPointsEarned();
|
int updatedPoints = currentPoints - pointsDeducted + sale.getPointsEarned();
|
||||||
customer.setLoyaltyPoints(Math.max(updatedPoints, 0));
|
customer.setLoyaltyPoints(Math.max(updatedPoints, 0));
|
||||||
userRepository.save(customer);
|
userRepository.save(customer);
|
||||||
}
|
}
|
||||||
@@ -226,6 +270,11 @@ public class SaleService {
|
|||||||
sale.setItems(saleItems);
|
sale.setItems(saleItems);
|
||||||
|
|
||||||
Sale savedSale = saleRepository.save(sale);
|
Sale savedSale = saleRepository.save(sale);
|
||||||
|
|
||||||
|
if (!Boolean.TRUE.equals(savedSale.getIsRefund()) && savedSale.getCustomer() != null) {
|
||||||
|
emailService.sendPurchaseReceipt(savedSale);
|
||||||
|
}
|
||||||
|
|
||||||
return mapToResponse(savedSale);
|
return mapToResponse(savedSale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -20,14 +20,10 @@ public class ServiceService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
public Page<ServiceResponse> getAllServices(String query, Pageable pageable) {
|
public Page<ServiceResponse> getAllServices(String query, String species, Pageable pageable) {
|
||||||
Page<com.petshop.backend.entity.Service> services;
|
String q = (query != null && !query.trim().isEmpty()) ? query.trim() : null;
|
||||||
if (query != null && !query.trim().isEmpty()) {
|
String sp = (species != null && !species.trim().isEmpty()) ? species.trim() : null;
|
||||||
services = serviceRepository.searchServices(query, pageable);
|
return serviceRepository.searchServices(q, sp, pageable).map(this::mapToResponse);
|
||||||
} else {
|
|
||||||
services = serviceRepository.findAll(pageable);
|
|
||||||
}
|
|
||||||
return services.map(this::mapToResponse);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
|
|||||||
@@ -3,21 +3,27 @@ package com.petshop.backend.service;
|
|||||||
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
import com.petshop.backend.dto.common.BulkDeleteRequest;
|
||||||
import com.petshop.backend.dto.user.UserRequest;
|
import com.petshop.backend.dto.user.UserRequest;
|
||||||
import com.petshop.backend.dto.user.UserResponse;
|
import com.petshop.backend.dto.user.UserResponse;
|
||||||
import com.petshop.backend.repository.ActivityLogRepository;
|
|
||||||
import com.petshop.backend.entity.StoreLocation;
|
import com.petshop.backend.entity.StoreLocation;
|
||||||
import com.petshop.backend.entity.User;
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.petshop.backend.repository.ActivityLogRepository;
|
||||||
import com.petshop.backend.exception.ResourceNotFoundException;
|
import com.petshop.backend.exception.ResourceNotFoundException;
|
||||||
import com.petshop.backend.repository.StoreRepository;
|
import com.petshop.backend.repository.StoreRepository;
|
||||||
import com.petshop.backend.repository.UserRepository;
|
import com.petshop.backend.repository.UserRepository;
|
||||||
|
import com.petshop.backend.util.AuthenticationHelper;
|
||||||
import org.springframework.data.domain.Page;
|
import org.springframework.data.domain.Page;
|
||||||
import org.springframework.data.domain.Pageable;
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.web.server.ResponseStatusException;
|
import org.springframework.web.server.ResponseStatusException;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
import static org.springframework.http.HttpStatus.BAD_REQUEST;
|
||||||
import static org.springframework.http.HttpStatus.CONFLICT;
|
import static org.springframework.http.HttpStatus.CONFLICT;
|
||||||
@@ -54,13 +60,26 @@ public class UserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public UserResponse getUserById(Long id) {
|
public UserResponse getUserById(Long id) {
|
||||||
|
return getUserById(id, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional(readOnly = true)
|
||||||
|
public UserResponse getUserById(Long id, User.Role requiredRole) {
|
||||||
User user = userRepository.findById(id)
|
User user = userRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
||||||
|
requireRoleOrNotFound(user, requiredRole, id);
|
||||||
return mapToResponse(user);
|
return mapToResponse(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public UserResponse createUser(UserRequest request) {
|
public UserResponse createUser(UserRequest request) {
|
||||||
|
return createUser(request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse createUser(UserRequest request, User.Role requiredRole) {
|
||||||
|
requireRequestedRole(request.getRole(), requiredRole);
|
||||||
|
|
||||||
User user = new User();
|
User user = new User();
|
||||||
user.setUsername(trimToNull(request.getUsername()));
|
user.setUsername(trimToNull(request.getUsername()));
|
||||||
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
|
if (request.getPassword() != null && !request.getPassword().trim().isEmpty()) {
|
||||||
@@ -88,9 +107,18 @@ public class UserService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public UserResponse updateUser(Long id, UserRequest request) {
|
public UserResponse updateUser(Long id, UserRequest request) {
|
||||||
|
return updateUser(id, request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public UserResponse updateUser(Long id, UserRequest request, User.Role requiredRole) {
|
||||||
User user = userRepository.findById(id)
|
User user = userRepository.findById(id)
|
||||||
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
||||||
|
|
||||||
|
requireRoleOrNotFound(user, requiredRole, id);
|
||||||
|
requireRequestedRole(request.getRole(), requiredRole);
|
||||||
|
requireAdminMutationAllowed(user, request.getRole());
|
||||||
|
|
||||||
boolean invalidateToken =
|
boolean invalidateToken =
|
||||||
!Objects.equals(user.getUsername(), request.getUsername())
|
!Objects.equals(user.getUsername(), request.getUsername())
|
||||||
|| user.getRole() != request.getRole()
|
|| user.getRole() != request.getRole()
|
||||||
@@ -127,9 +155,17 @@ public class UserService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void deleteUser(Long id) {
|
public void deleteUser(Long id) {
|
||||||
if (!userRepository.existsById(id)) {
|
deleteUser(id, null);
|
||||||
throw new ResourceNotFoundException("User not found with id: " + id);
|
}
|
||||||
}
|
|
||||||
|
@Transactional
|
||||||
|
public void deleteUser(Long id, User.Role requiredRole) {
|
||||||
|
User user = userRepository.findById(id)
|
||||||
|
.orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
|
||||||
|
|
||||||
|
requireRoleOrNotFound(user, requiredRole, id);
|
||||||
|
requireAdminDeletionAllowed(user);
|
||||||
|
|
||||||
if (activityLogRepository.existsByUser_Id(id)) {
|
if (activityLogRepository.existsByUser_Id(id)) {
|
||||||
throw new ResponseStatusException(CONFLICT, "User cannot be deleted because activity logs exist");
|
throw new ResponseStatusException(CONFLICT, "User cannot be deleted because activity logs exist");
|
||||||
}
|
}
|
||||||
@@ -138,7 +174,27 @@ public class UserService {
|
|||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void bulkDeleteUsers(BulkDeleteRequest request) {
|
public void bulkDeleteUsers(BulkDeleteRequest request) {
|
||||||
if (request.getIds() != null && request.getIds().stream().anyMatch(activityLogRepository::existsByUser_Id)) {
|
bulkDeleteUsers(request, null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public void bulkDeleteUsers(BulkDeleteRequest request, User.Role requiredRole) {
|
||||||
|
if (request.getIds() == null || request.getIds().isEmpty()) {
|
||||||
|
throw new ResponseStatusException(BAD_REQUEST, "IDs list cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
Set<Long> requestedIds = new HashSet<>(request.getIds());
|
||||||
|
ArrayList<User> usersToDelete = new ArrayList<>();
|
||||||
|
userRepository.findAllById(requestedIds).forEach(usersToDelete::add);
|
||||||
|
|
||||||
|
if (usersToDelete.size() != requestedIds.size()) {
|
||||||
|
throw new ResourceNotFoundException("One or more users not found for bulk delete");
|
||||||
|
}
|
||||||
|
|
||||||
|
requireRoleOrNotFound(usersToDelete, requiredRole);
|
||||||
|
requireAdminDeletionAllowed(usersToDelete);
|
||||||
|
|
||||||
|
if (request.getIds().stream().anyMatch(activityLogRepository::existsByUser_Id)) {
|
||||||
throw new ResponseStatusException(CONFLICT, "One or more users cannot be deleted because activity logs exist");
|
throw new ResponseStatusException(CONFLICT, "One or more users cannot be deleted because activity logs exist");
|
||||||
}
|
}
|
||||||
userRepository.deleteAllById(request.getIds());
|
userRepository.deleteAllById(request.getIds());
|
||||||
@@ -169,6 +225,51 @@ public class UserService {
|
|||||||
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));
|
.orElseThrow(() -> new ResourceNotFoundException("Store not found with id: " + storeId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void requireRoleOrNotFound(User user, User.Role requiredRole, Long id) {
|
||||||
|
if (requiredRole != null && user.getRole() != requiredRole) {
|
||||||
|
throw new ResourceNotFoundException("User not found with id: " + id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireRoleOrNotFound(Collection<User> users, User.Role requiredRole) {
|
||||||
|
if (requiredRole == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (User user : users) {
|
||||||
|
if (user.getRole() != requiredRole) {
|
||||||
|
throw new ResourceNotFoundException("User not found with id: " + user.getId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireRequestedRole(User.Role requestedRole, User.Role requiredRole) {
|
||||||
|
if (requiredRole != null && requestedRole != requiredRole) {
|
||||||
|
throw new AccessDeniedException("Target user must have role " + requiredRole.name());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireAdminMutationAllowed(User target, User.Role requestedRole) {
|
||||||
|
User actor = AuthenticationHelper.getAuthenticatedUser(userRepository);
|
||||||
|
if ((target.getRole() == User.Role.ADMIN && !actor.getId().equals(target.getId()))
|
||||||
|
|| (requestedRole == User.Role.ADMIN && !actor.getId().equals(target.getId()))) {
|
||||||
|
throw new AccessDeniedException("Admins cannot modify other admin accounts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireAdminDeletionAllowed(User target) {
|
||||||
|
if (target.getRole() == User.Role.ADMIN) {
|
||||||
|
throw new AccessDeniedException("Admins cannot delete admin accounts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireAdminDeletionAllowed(Collection<User> targets) {
|
||||||
|
for (User target : targets) {
|
||||||
|
if (target.getRole() == User.Role.ADMIN) {
|
||||||
|
throw new AccessDeniedException("Admins cannot delete admin accounts");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void validateUniquePhone(String phone, Long currentUserId) {
|
private void validateUniquePhone(String phone, Long currentUserId) {
|
||||||
if (phone == null || phone.isBlank()) {
|
if (phone == null || phone.isBlank()) {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.petshop.backend.util;
|
||||||
|
|
||||||
|
import com.petshop.backend.exception.BusinessException;
|
||||||
|
|
||||||
|
import java.util.Locale;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.regex.Pattern;
|
||||||
|
|
||||||
|
public class ContentFilter {
|
||||||
|
|
||||||
|
private static final Pattern SCRIPT_PATTERN = Pattern.compile(
|
||||||
|
"<script|javascript:|on\\w+\\s*=", Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
private static final Set<String> PROFANITY = Set.of(
|
||||||
|
"profanityOne", "profanityTwo", "profanityThree"
|
||||||
|
);
|
||||||
|
|
||||||
|
public static void validate(String input) {
|
||||||
|
if (input == null || input.isBlank()) return;
|
||||||
|
if (SCRIPT_PATTERN.matcher(input).find()) {
|
||||||
|
throw new BusinessException("Message contains prohibited content");
|
||||||
|
}
|
||||||
|
String lower = input.toLowerCase(Locale.ROOT);
|
||||||
|
for (String word : PROFANITY) {
|
||||||
|
if (lower.contains(word)) {
|
||||||
|
throw new BusinessException("Message contains prohibited language");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,11 @@ springdoc:
|
|||||||
app:
|
app:
|
||||||
upload:
|
upload:
|
||||||
base-dir: ${UPLOAD_BASE_DIR:uploads}
|
base-dir: ${UPLOAD_BASE_DIR:uploads}
|
||||||
|
frontend-url: ${FRONTEND_URL:http://localhost:3000}
|
||||||
|
|
||||||
|
resend:
|
||||||
|
api-key: ${RESEND_API_KEY:}
|
||||||
|
from: ${RESEND_FROM:PetShop <onboarding@resend.dev>}
|
||||||
|
|
||||||
jwt:
|
jwt:
|
||||||
secret: ${JWT_SECRET}
|
secret: ${JWT_SECRET}
|
||||||
@@ -60,9 +65,11 @@ stripe:
|
|||||||
|
|
||||||
openrouter:
|
openrouter:
|
||||||
api-key: ${OPENROUTER_API_KEY:}
|
api-key: ${OPENROUTER_API_KEY:}
|
||||||
model: ${OPENROUTER_MODEL:openai/gpt-oss-120b:free}
|
model: ${OPENROUTER_MODEL:openrouter/free}
|
||||||
|
|
||||||
logging:
|
logging:
|
||||||
|
file:
|
||||||
|
name: ${LOG_FILE:logs/app.log}
|
||||||
level:
|
level:
|
||||||
com.petshop: ${LOG_LEVEL:INFO}
|
com.petshop: ${LOG_LEVEL:INFO}
|
||||||
org.springframework.security: ${LOG_LEVEL_SECURITY:WARN}
|
org.springframework.security: ${LOG_LEVEL_SECURITY:WARN}
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
ALTER TABLE appointment MODIFY petId BIGINT NULL;
|
||||||
|
ALTER TABLE appointment DROP FOREIGN KEY fk_appointment_pet;
|
||||||
|
ALTER TABLE appointment ADD CONSTRAINT fk_appointment_pet FOREIGN KEY (petId) REFERENCES pet(petId) ON DELETE SET NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE purchaseOrder DROP COLUMN status;
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package com.petshop.backend.service;
|
||||||
|
|
||||||
|
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.exception.ResourceNotFoundException;
|
||||||
|
import com.petshop.backend.entity.User;
|
||||||
|
import com.petshop.backend.repository.ActivityLogRepository;
|
||||||
|
import com.petshop.backend.repository.StoreRepository;
|
||||||
|
import com.petshop.backend.repository.UserRepository;
|
||||||
|
import com.petshop.backend.security.AppPrincipal;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
import org.springframework.security.access.AccessDeniedException;
|
||||||
|
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||||
|
import org.springframework.security.core.context.SecurityContextHolder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.ArgumentMatchers.anyLong;
|
||||||
|
import static org.mockito.ArgumentMatchers.eq;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class UserServiceTest {
|
||||||
|
|
||||||
|
@Mock private UserRepository userRepository;
|
||||||
|
@Mock private ActivityLogRepository activityLogRepository;
|
||||||
|
@Mock private PasswordEncoder passwordEncoder;
|
||||||
|
@Mock private StoreRepository storeRepository;
|
||||||
|
|
||||||
|
private UserService userService;
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
userService = new UserService(userRepository, activityLogRepository, passwordEncoder, storeRepository);
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void tearDown() {
|
||||||
|
SecurityContextHolder.clearContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateUserDeniesEditingAnotherAdmin() {
|
||||||
|
User actor = user(1L, User.Role.ADMIN);
|
||||||
|
User target = user(2L, User.Role.ADMIN);
|
||||||
|
authenticate(actor);
|
||||||
|
|
||||||
|
when(userRepository.findById(2L)).thenReturn(Optional.of(target));
|
||||||
|
when(userRepository.findById(1L)).thenReturn(Optional.of(actor));
|
||||||
|
|
||||||
|
UserRequest request = request(User.Role.ADMIN);
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> userService.updateUser(2L, request));
|
||||||
|
verify(userRepository, never()).save(any(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateUserTreatsWrongScopedRoleAsNotFound() {
|
||||||
|
User actor = user(1L, User.Role.ADMIN);
|
||||||
|
User target = user(2L, User.Role.ADMIN);
|
||||||
|
authenticate(actor);
|
||||||
|
|
||||||
|
when(userRepository.findById(2L)).thenReturn(Optional.of(target));
|
||||||
|
|
||||||
|
UserRequest request = request(User.Role.CUSTOMER);
|
||||||
|
|
||||||
|
assertThrows(ResourceNotFoundException.class, () -> userService.updateUser(2L, request, User.Role.CUSTOMER));
|
||||||
|
verify(userRepository, never()).save(any(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteUserDeniesDeletingAnotherAdmin() {
|
||||||
|
User actor = user(1L, User.Role.ADMIN);
|
||||||
|
User target = user(2L, User.Role.ADMIN);
|
||||||
|
authenticate(actor);
|
||||||
|
|
||||||
|
when(userRepository.findById(2L)).thenReturn(Optional.of(target));
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> userService.deleteUser(2L));
|
||||||
|
verify(userRepository, never()).deleteById(anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void deleteUserDeniesDeletingSelfAdminAccount() {
|
||||||
|
User actor = user(1L, User.Role.ADMIN);
|
||||||
|
authenticate(actor);
|
||||||
|
|
||||||
|
when(userRepository.findById(1L)).thenReturn(Optional.of(actor));
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> userService.deleteUser(1L));
|
||||||
|
verify(userRepository, never()).deleteById(anyLong());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bulkDeleteUsersDeniesMixedAdminTargets() {
|
||||||
|
User targetAdmin = user(2L, User.Role.ADMIN);
|
||||||
|
User customer = user(3L, User.Role.CUSTOMER);
|
||||||
|
|
||||||
|
when(userRepository.findAllById(eq(new HashSet<>(List.of(2L, 3L))))).thenReturn(List.of(targetAdmin, customer));
|
||||||
|
|
||||||
|
BulkDeleteRequest request = new BulkDeleteRequest();
|
||||||
|
request.setIds(List.of(2L, 3L));
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> userService.bulkDeleteUsers(request));
|
||||||
|
verify(userRepository, never()).deleteAllById(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void bulkDeleteUsersThrowsWhenAnyIdMissing() {
|
||||||
|
User customer = user(2L, User.Role.CUSTOMER);
|
||||||
|
|
||||||
|
when(userRepository.findAllById(any())).thenReturn(List.of(customer));
|
||||||
|
|
||||||
|
BulkDeleteRequest request = new BulkDeleteRequest();
|
||||||
|
request.setIds(List.of(2L, 3L));
|
||||||
|
|
||||||
|
assertThrows(ResourceNotFoundException.class, () -> userService.bulkDeleteUsers(request, User.Role.CUSTOMER));
|
||||||
|
verify(userRepository, never()).deleteAllById(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void createUserDeniesWrongRoleForScopedRoute() {
|
||||||
|
UserRequest request = request(User.Role.ADMIN);
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> userService.createUser(request, User.Role.CUSTOMER));
|
||||||
|
verify(userRepository, never()).save(any(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateUserAllowsEditingOwnAdminAccount() {
|
||||||
|
User actor = user(1L, User.Role.ADMIN);
|
||||||
|
authenticate(actor);
|
||||||
|
|
||||||
|
when(userRepository.findById(1L)).thenReturn(Optional.of(actor));
|
||||||
|
when(userRepository.save(any(User.class))).thenAnswer(invocation -> invocation.getArgument(0));
|
||||||
|
|
||||||
|
UserRequest request = request(User.Role.ADMIN);
|
||||||
|
request.setFirstName("Updated");
|
||||||
|
|
||||||
|
UserResponse response = userService.updateUser(1L, request);
|
||||||
|
assertEquals("Updated", response.getFirstName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateUserDeniesPromotingAnotherUserToAdmin() {
|
||||||
|
User actor = user(1L, User.Role.ADMIN);
|
||||||
|
User target = user(2L, User.Role.CUSTOMER);
|
||||||
|
authenticate(actor);
|
||||||
|
|
||||||
|
when(userRepository.findById(2L)).thenReturn(Optional.of(target));
|
||||||
|
when(userRepository.findById(1L)).thenReturn(Optional.of(actor));
|
||||||
|
|
||||||
|
UserRequest request = request(User.Role.ADMIN);
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> userService.updateUser(2L, request));
|
||||||
|
verify(userRepository, never()).save(any(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void scopedUpdateDeniesRoleEscalation() {
|
||||||
|
User actor = user(1L, User.Role.ADMIN);
|
||||||
|
User target = user(2L, User.Role.CUSTOMER);
|
||||||
|
authenticate(actor);
|
||||||
|
|
||||||
|
when(userRepository.findById(2L)).thenReturn(Optional.of(target));
|
||||||
|
|
||||||
|
UserRequest request = request(User.Role.ADMIN);
|
||||||
|
|
||||||
|
assertThrows(AccessDeniedException.class, () -> userService.updateUser(2L, request, User.Role.CUSTOMER));
|
||||||
|
verify(userRepository, never()).save(any(User.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void authenticate(User user) {
|
||||||
|
AppPrincipal principal = new AppPrincipal(user.getId(), user.getUsername(), user.getRole(), user.getTokenVersion());
|
||||||
|
var context = SecurityContextHolder.createEmptyContext();
|
||||||
|
context.setAuthentication(new UsernamePasswordAuthenticationToken(principal, "n/a", principal.getAuthorities()));
|
||||||
|
SecurityContextHolder.setContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
private User user(Long id, User.Role role) {
|
||||||
|
User user = new User();
|
||||||
|
user.setId(id);
|
||||||
|
user.setUsername("user" + id);
|
||||||
|
user.setFirstName("First");
|
||||||
|
user.setLastName("Last");
|
||||||
|
user.setEmail("user" + id + "@example.com");
|
||||||
|
user.setRole(role);
|
||||||
|
user.setActive(true);
|
||||||
|
user.setTokenVersion(0);
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
private UserRequest request(User.Role role) {
|
||||||
|
UserRequest request = new UserRequest();
|
||||||
|
request.setUsername("updated-user");
|
||||||
|
request.setFirstName("First");
|
||||||
|
request.setLastName("Last");
|
||||||
|
request.setEmail("updated@example.com");
|
||||||
|
request.setRole(role);
|
||||||
|
request.setActive(true);
|
||||||
|
return request;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -22,6 +22,7 @@ public class AppointmentDTO {
|
|||||||
private SimpleStringProperty appointmentTime;
|
private SimpleStringProperty appointmentTime;
|
||||||
private SimpleStringProperty appointmentStatus;
|
private SimpleStringProperty appointmentStatus;
|
||||||
private SimpleStringProperty storeName;
|
private SimpleStringProperty storeName;
|
||||||
|
private Long storeId;
|
||||||
|
|
||||||
public AppointmentDTO(int appointmentId,
|
public AppointmentDTO(int appointmentId,
|
||||||
int customerId, String customerName,
|
int customerId, String customerName,
|
||||||
@@ -32,7 +33,8 @@ public class AppointmentDTO {
|
|||||||
String appointmentDate,
|
String appointmentDate,
|
||||||
String appointmentTime,
|
String appointmentTime,
|
||||||
String appointmentStatus,
|
String appointmentStatus,
|
||||||
String storeName) {
|
String storeName,
|
||||||
|
Long storeId) {
|
||||||
|
|
||||||
this.appointmentId = new SimpleIntegerProperty(appointmentId);
|
this.appointmentId = new SimpleIntegerProperty(appointmentId);
|
||||||
this.customerId = new SimpleIntegerProperty(customerId);
|
this.customerId = new SimpleIntegerProperty(customerId);
|
||||||
@@ -47,6 +49,7 @@ public class AppointmentDTO {
|
|||||||
this.appointmentTime = new SimpleStringProperty(appointmentTime);
|
this.appointmentTime = new SimpleStringProperty(appointmentTime);
|
||||||
this.appointmentStatus = new SimpleStringProperty(appointmentStatus);
|
this.appointmentStatus = new SimpleStringProperty(appointmentStatus);
|
||||||
this.storeName = new SimpleStringProperty(storeName != null ? storeName : "");
|
this.storeName = new SimpleStringProperty(storeName != null ? storeName : "");
|
||||||
|
this.storeId = storeId;
|
||||||
}
|
}
|
||||||
|
|
||||||
public int getAppointmentId() { return appointmentId.get(); }
|
public int getAppointmentId() { return appointmentId.get(); }
|
||||||
@@ -66,4 +69,6 @@ public class AppointmentDTO {
|
|||||||
public String getAppointmentTime() { return appointmentTime.get(); }
|
public String getAppointmentTime() { return appointmentTime.get(); }
|
||||||
public String getAppointmentStatus() { return appointmentStatus.get(); }
|
public String getAppointmentStatus() { return appointmentStatus.get(); }
|
||||||
public String getStoreName() { return storeName.get(); }
|
public String getStoreName() { return storeName.get(); }
|
||||||
|
|
||||||
|
public Long getStoreId() { return storeId; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,17 +3,33 @@ package org.example.petshopdesktop;
|
|||||||
import javafx.application.Application;
|
import javafx.application.Application;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.image.Image;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.example.petshopdesktop.util.DesktopNotificationService;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
public class PetShopApplication extends Application {
|
public class PetShopApplication extends Application {
|
||||||
@Override
|
@Override
|
||||||
public void start(Stage stage) throws IOException {
|
public void start(Stage stage) throws IOException {
|
||||||
FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("login-view.fxml"));
|
FXMLLoader fxmlLoader = new FXMLLoader(PetShopApplication.class.getResource("login-view.fxml"));
|
||||||
Scene scene = new Scene(fxmlLoader.load());
|
Scene scene = new Scene(fxmlLoader.load());
|
||||||
stage.setTitle("Pet Shop Manager - Login");
|
stage.setTitle("Leon's Pet Store - Login");
|
||||||
|
|
||||||
|
try {
|
||||||
|
stage.getIcons().add(new Image(Objects.requireNonNull(
|
||||||
|
getClass().getResourceAsStream("/org/example/petshopdesktop/images/leons-pet-store-badge.png")
|
||||||
|
)));
|
||||||
|
} catch (Exception e) {
|
||||||
|
System.err.println("Could not load application icon: " + e.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
stage.setScene(scene);
|
stage.setScene(scene);
|
||||||
stage.show();
|
stage.show();
|
||||||
|
|
||||||
|
DesktopNotificationService.getInstance().init(
|
||||||
|
stage.getIcons().isEmpty() ? null : stage.getIcons().get(0)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.databind.ObjectMapper;
|
|||||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
|
|
||||||
|
import java.io.ByteArrayOutputStream;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
import java.net.http.HttpRequest;
|
import java.net.http.HttpRequest;
|
||||||
@@ -142,6 +143,38 @@ public class ApiClient {
|
|||||||
return handleResponse(response, responseClass);
|
return handleResponse(response, responseClass);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <T> T postMultipartWithText(String path, String filePartName, Path filePath,
|
||||||
|
String textPartName, String textContent,
|
||||||
|
Class<T> responseClass) throws Exception {
|
||||||
|
String boundary = "----PetShopDesktop" + UUID.randomUUID();
|
||||||
|
String mimeType = Files.probeContentType(filePath);
|
||||||
|
if (mimeType == null || mimeType.isBlank()) mimeType = "application/octet-stream";
|
||||||
|
|
||||||
|
byte[] fileBytes = Files.readAllBytes(filePath);
|
||||||
|
String fileName = filePath.getFileName().toString();
|
||||||
|
|
||||||
|
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||||
|
out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + filePartName
|
||||||
|
+ "\"; filename=\"" + fileName + "\"\r\nContent-Type: " + mimeType + "\r\n\r\n")
|
||||||
|
.getBytes(StandardCharsets.UTF_8));
|
||||||
|
out.write(fileBytes);
|
||||||
|
out.write("\r\n".getBytes(StandardCharsets.UTF_8));
|
||||||
|
if (textContent != null && !textContent.isBlank()) {
|
||||||
|
out.write(("--" + boundary + "\r\nContent-Disposition: form-data; name=\"" + textPartName
|
||||||
|
+ "\"\r\n\r\n" + textContent + "\r\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
}
|
||||||
|
out.write(("--" + boundary + "--\r\n").getBytes(StandardCharsets.UTF_8));
|
||||||
|
|
||||||
|
HttpRequest.Builder builder = HttpRequest.newBuilder()
|
||||||
|
.uri(URI.create(baseUrl + path))
|
||||||
|
.header("Content-Type", "multipart/form-data; boundary=" + boundary)
|
||||||
|
.POST(HttpRequest.BodyPublishers.ofByteArray(out.toByteArray()))
|
||||||
|
.timeout(Duration.ofSeconds(30));
|
||||||
|
addAuthHeader(builder);
|
||||||
|
HttpResponse<String> response = httpClient.send(builder.build(), HttpResponse.BodyHandlers.ofString());
|
||||||
|
return handleResponse(response, responseClass);
|
||||||
|
}
|
||||||
|
|
||||||
public <T> T put(String path, Object requestBody, Class<T> responseClass) throws Exception {
|
public <T> T put(String path, Object requestBody, Class<T> responseClass) throws Exception {
|
||||||
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
String jsonBody = objectMapper.writeValueAsString(requestBody);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
|||||||
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
||||||
import org.example.petshopdesktop.api.dto.chat.MessageResponse;
|
import org.example.petshopdesktop.api.dto.chat.MessageResponse;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
|
import org.example.petshopdesktop.util.DesktopNotificationService;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.http.HttpClient;
|
import java.net.http.HttpClient;
|
||||||
@@ -72,6 +73,16 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
updateNotificationState();
|
updateNotificationState();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void markConversationReplied(Long conversationId, Long senderId) {
|
||||||
|
synchronized (lock) {
|
||||||
|
ConversationResponse conv = globalConversations.get(conversationId);
|
||||||
|
if (conv != null) {
|
||||||
|
conv.setLastSenderId(senderId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updateNotificationState();
|
||||||
|
}
|
||||||
|
|
||||||
public boolean hasActionableChats() {
|
public boolean hasActionableChats() {
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
UserSession session = UserSession.getInstance();
|
UserSession session = UserSession.getInstance();
|
||||||
@@ -334,6 +345,12 @@ public class ChatRealtimeClient implements WebSocket.Listener {
|
|||||||
messageListener.accept(message);
|
messageListener.accept(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Long currentUserId = UserSession.getInstance().getUserId();
|
||||||
|
if (message.getSenderId() != null && !message.getSenderId().equals(currentUserId)) {
|
||||||
|
DesktopNotificationService.getInstance()
|
||||||
|
.notifyNewMessage(message.getSenderDisplayName(), message.getContent());
|
||||||
|
}
|
||||||
|
|
||||||
// Also update globalConversation last sender if this is the active conversation
|
// Also update globalConversation last sender if this is the active conversation
|
||||||
synchronized (lock) {
|
synchronized (lock) {
|
||||||
ConversationResponse conv = globalConversations.get(message.getConversationId());
|
ConversationResponse conv = globalConversations.get(message.getConversationId());
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.example.petshopdesktop.api.dto.adoption;
|
package org.example.petshopdesktop.api.dto.adoption;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
|
|
||||||
public class AdoptionRequest {
|
public class AdoptionRequest {
|
||||||
@@ -9,7 +10,7 @@ public class AdoptionRequest {
|
|||||||
private Long sourceStoreId;
|
private Long sourceStoreId;
|
||||||
private LocalDate adoptionDate;
|
private LocalDate adoptionDate;
|
||||||
private String adoptionStatus;
|
private String adoptionStatus;
|
||||||
private String paymentMethod;
|
private BigDecimal adoptionFee;
|
||||||
|
|
||||||
public AdoptionRequest() {
|
public AdoptionRequest() {
|
||||||
}
|
}
|
||||||
@@ -62,11 +63,11 @@ public class AdoptionRequest {
|
|||||||
this.adoptionStatus = adoptionStatus;
|
this.adoptionStatus = adoptionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
public String getPaymentMethod() {
|
public BigDecimal getAdoptionFee() {
|
||||||
return paymentMethod;
|
return adoptionFee;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setPaymentMethod(String paymentMethod) {
|
public void setAdoptionFee(BigDecimal adoptionFee) {
|
||||||
this.paymentMethod = paymentMethod;
|
this.adoptionFee = adoptionFee;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ public class AdoptionResponse {
|
|||||||
private LocalDate adoptionDate;
|
private LocalDate adoptionDate;
|
||||||
private java.math.BigDecimal adoptionFee;
|
private java.math.BigDecimal adoptionFee;
|
||||||
private String adoptionStatus;
|
private String adoptionStatus;
|
||||||
|
private Long sourceStoreId;
|
||||||
private String sourceStoreName;
|
private String sourceStoreName;
|
||||||
|
|
||||||
public AdoptionResponse() {
|
public AdoptionResponse() {
|
||||||
@@ -98,6 +99,14 @@ public class AdoptionResponse {
|
|||||||
this.adoptionStatus = adoptionStatus;
|
this.adoptionStatus = adoptionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getSourceStoreId() {
|
||||||
|
return sourceStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSourceStoreId(Long sourceStoreId) {
|
||||||
|
this.sourceStoreId = sourceStoreId;
|
||||||
|
}
|
||||||
|
|
||||||
public String getSourceStoreName() {
|
public String getSourceStoreName() {
|
||||||
return sourceStoreName;
|
return sourceStoreName;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ public class MessageResponse {
|
|||||||
private String content;
|
private String content;
|
||||||
private LocalDateTime timestamp;
|
private LocalDateTime timestamp;
|
||||||
private Boolean isRead;
|
private Boolean isRead;
|
||||||
|
private String attachmentName;
|
||||||
|
private String attachmentUrl;
|
||||||
|
|
||||||
public MessageResponse() {
|
public MessageResponse() {
|
||||||
}
|
}
|
||||||
@@ -87,4 +89,20 @@ public class MessageResponse {
|
|||||||
public void setIsRead(Boolean isRead) {
|
public void setIsRead(Boolean isRead) {
|
||||||
this.isRead = isRead;
|
this.isRead = isRead;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getAttachmentName() {
|
||||||
|
return attachmentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttachmentName(String attachmentName) {
|
||||||
|
this.attachmentName = attachmentName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getAttachmentUrl() {
|
||||||
|
return attachmentUrl;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAttachmentUrl(String attachmentUrl) {
|
||||||
|
this.attachmentUrl = attachmentUrl;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,9 @@ public class SaleRequest {
|
|||||||
private List<SaleItemRequest> items;
|
private List<SaleItemRequest> items;
|
||||||
private Boolean isRefund;
|
private Boolean isRefund;
|
||||||
private Long originalSaleId;
|
private Long originalSaleId;
|
||||||
|
private Long customerId;
|
||||||
|
private Long couponId;
|
||||||
|
private Integer pointsUsed;
|
||||||
|
|
||||||
public SaleRequest() {
|
public SaleRequest() {
|
||||||
}
|
}
|
||||||
@@ -51,4 +54,28 @@ public class SaleRequest {
|
|||||||
public void setOriginalSaleId(Long originalSaleId) {
|
public void setOriginalSaleId(Long originalSaleId) {
|
||||||
this.originalSaleId = originalSaleId;
|
this.originalSaleId = originalSaleId;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerId(Long customerId) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getCouponId() {
|
||||||
|
return couponId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCouponId(Long couponId) {
|
||||||
|
this.couponId = couponId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPointsUsed() {
|
||||||
|
return pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPointsUsed(Integer pointsUsed) {
|
||||||
|
this.pointsUsed = pointsUsed;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,13 @@ public class SaleResponse {
|
|||||||
private Boolean isRefund;
|
private Boolean isRefund;
|
||||||
private Long originalSaleId;
|
private Long originalSaleId;
|
||||||
private List<SaleItemResponse> items;
|
private List<SaleItemResponse> items;
|
||||||
|
private Long customerId;
|
||||||
|
private String customerName;
|
||||||
|
private BigDecimal couponDiscountAmount;
|
||||||
|
private BigDecimal loyaltyDiscountAmount;
|
||||||
|
private Integer pointsUsed;
|
||||||
|
private Integer pointsEarned;
|
||||||
|
private BigDecimal subtotalAmount;
|
||||||
|
|
||||||
public SaleResponse() {
|
public SaleResponse() {
|
||||||
}
|
}
|
||||||
@@ -89,4 +96,60 @@ public class SaleResponse {
|
|||||||
public void setItems(List<SaleItemResponse> items) {
|
public void setItems(List<SaleItemResponse> items) {
|
||||||
this.items = items;
|
this.items = items;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Long getCustomerId() {
|
||||||
|
return customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerId(Long customerId) {
|
||||||
|
this.customerId = customerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getCustomerName() {
|
||||||
|
return customerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomerName(String customerName) {
|
||||||
|
this.customerName = customerName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getCouponDiscountAmount() {
|
||||||
|
return couponDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCouponDiscountAmount(BigDecimal couponDiscountAmount) {
|
||||||
|
this.couponDiscountAmount = couponDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getLoyaltyDiscountAmount() {
|
||||||
|
return loyaltyDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoyaltyDiscountAmount(BigDecimal loyaltyDiscountAmount) {
|
||||||
|
this.loyaltyDiscountAmount = loyaltyDiscountAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPointsUsed() {
|
||||||
|
return pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPointsUsed(Integer pointsUsed) {
|
||||||
|
this.pointsUsed = pointsUsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getPointsEarned() {
|
||||||
|
return pointsEarned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPointsEarned(Integer pointsEarned) {
|
||||||
|
this.pointsEarned = pointsEarned;
|
||||||
|
}
|
||||||
|
|
||||||
|
public BigDecimal getSubtotalAmount() {
|
||||||
|
return subtotalAmount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSubtotalAmount(BigDecimal subtotalAmount) {
|
||||||
|
this.subtotalAmount = subtotalAmount;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,14 @@ package org.example.petshopdesktop.api.dto.user;
|
|||||||
public class UserRequest {
|
public class UserRequest {
|
||||||
private String username;
|
private String username;
|
||||||
private String password;
|
private String password;
|
||||||
|
private String firstName;
|
||||||
|
private String lastName;
|
||||||
private String fullName;
|
private String fullName;
|
||||||
private String email;
|
private String email;
|
||||||
private String phone;
|
private String phone;
|
||||||
private String role;
|
private String role;
|
||||||
private Boolean active;
|
private Boolean active;
|
||||||
|
private Integer loyaltyPoints;
|
||||||
|
|
||||||
public UserRequest() {
|
public UserRequest() {
|
||||||
}
|
}
|
||||||
@@ -28,6 +31,22 @@ public class UserRequest {
|
|||||||
this.password = password;
|
this.password = password;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public String getFirstName() {
|
||||||
|
return firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFirstName(String firstName) {
|
||||||
|
this.firstName = firstName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getLastName() {
|
||||||
|
return lastName;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLastName(String lastName) {
|
||||||
|
this.lastName = lastName;
|
||||||
|
}
|
||||||
|
|
||||||
public String getFullName() {
|
public String getFullName() {
|
||||||
return fullName;
|
return fullName;
|
||||||
}
|
}
|
||||||
@@ -67,4 +86,12 @@ public class UserRequest {
|
|||||||
public void setActive(Boolean active) {
|
public void setActive(Boolean active) {
|
||||||
this.active = active;
|
this.active = active;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Integer getLoyaltyPoints() {
|
||||||
|
return loyaltyPoints;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setLoyaltyPoints(Integer loyaltyPoints) {
|
||||||
|
this.loyaltyPoints = loyaltyPoints;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import com.fasterxml.jackson.core.type.TypeReference;
|
|||||||
import org.example.petshopdesktop.api.ApiClient;
|
import org.example.petshopdesktop.api.ApiClient;
|
||||||
import org.example.petshopdesktop.api.dto.activity.ActivityLogResponse;
|
import org.example.petshopdesktop.api.dto.activity.ActivityLogResponse;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ActivityLogApi {
|
public class ActivityLogApi {
|
||||||
@@ -18,12 +19,18 @@ public class ActivityLogApi {
|
|||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<ActivityLogResponse> getActivityLogs(int limit) throws Exception {
|
public List<ActivityLogResponse> getActivityLogs(int limit, LocalDate startDate, LocalDate endDate) throws Exception {
|
||||||
String path = "/api/v1/activity-logs?limit=" + limit;
|
StringBuilder path = new StringBuilder("/api/v1/activity-logs?limit=").append(limit);
|
||||||
String response = apiClient.getRawResponse(path);
|
if (startDate != null) path.append("&startDate=").append(startDate);
|
||||||
|
if (endDate != null) path.append("&endDate=").append(endDate);
|
||||||
|
String response = apiClient.getRawResponse(path.toString());
|
||||||
return apiClient.getObjectMapper().readValue(
|
return apiClient.getObjectMapper().readValue(
|
||||||
response,
|
response,
|
||||||
new TypeReference<List<ActivityLogResponse>>() {}
|
new TypeReference<List<ActivityLogResponse>>() {}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<ActivityLogResponse> getActivityLogs(int limit) throws Exception {
|
||||||
|
return getActivityLogs(limit, null, null);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ public class AppointmentApi {
|
|||||||
return INSTANCE;
|
return INSTANCE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<AppointmentResponse> listAppointments(String query, Long storeId) throws Exception {
|
public List<AppointmentResponse> listAppointments(String query, Long storeId, Long employeeId) throws Exception {
|
||||||
String path = "/api/v1/appointments?page=0&size=1000";
|
String path = "/api/v1/appointments?page=0&size=1000";
|
||||||
if (query != null && !query.isEmpty()) {
|
if (query != null && !query.isEmpty()) {
|
||||||
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
|
path += "&q=" + URLEncoder.encode(query, StandardCharsets.UTF_8);
|
||||||
@@ -31,6 +31,9 @@ public class AppointmentApi {
|
|||||||
if (storeId != null) {
|
if (storeId != null) {
|
||||||
path += "&storeId=" + storeId;
|
path += "&storeId=" + storeId;
|
||||||
}
|
}
|
||||||
|
if (employeeId != null) {
|
||||||
|
path += "&employeeId=" + employeeId;
|
||||||
|
}
|
||||||
String response = apiClient.getRawResponse(path);
|
String response = apiClient.getRawResponse(path);
|
||||||
PageResponse<AppointmentResponse> pageResponse = apiClient.getObjectMapper().readValue(
|
PageResponse<AppointmentResponse> pageResponse = apiClient.getObjectMapper().readValue(
|
||||||
response,
|
response,
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import org.example.petshopdesktop.api.dto.auth.AvatarUploadResponse;
|
|||||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||||
|
|
||||||
import java.nio.file.Path;
|
import java.nio.file.Path;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
public class AuthApi {
|
public class AuthApi {
|
||||||
private static final AuthApi INSTANCE = new AuthApi();
|
private static final AuthApi INSTANCE = new AuthApi();
|
||||||
@@ -33,4 +35,10 @@ public class AuthApi {
|
|||||||
public void deleteAvatar() throws Exception {
|
public void deleteAvatar() throws Exception {
|
||||||
apiClient.delete("/api/v1/auth/me/avatar");
|
apiClient.delete("/api/v1/auth/me/avatar");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void forgotPassword(String usernameOrEmail) throws Exception {
|
||||||
|
Map<String, String> body = new HashMap<>();
|
||||||
|
body.put("usernameOrEmail", usernameOrEmail);
|
||||||
|
apiClient.post("/api/v1/auth/forgot-password", body, Object.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import org.example.petshopdesktop.api.dto.chat.ConversationResponse;
|
|||||||
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
import org.example.petshopdesktop.api.dto.chat.MessageRequest;
|
||||||
import org.example.petshopdesktop.api.dto.chat.MessageResponse;
|
import org.example.petshopdesktop.api.dto.chat.MessageResponse;
|
||||||
import org.example.petshopdesktop.api.dto.chat.UpdateConversationRequest;
|
import org.example.petshopdesktop.api.dto.chat.UpdateConversationRequest;
|
||||||
|
import java.io.File;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
public class ChatApi {
|
public class ChatApi {
|
||||||
@@ -43,6 +44,11 @@ public class ChatApi {
|
|||||||
return apiClient.post("/api/v1/chat/conversations/" + conversationId + "/messages", request, MessageResponse.class);
|
return apiClient.post("/api/v1/chat/conversations/" + conversationId + "/messages", request, MessageResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MessageResponse sendMessageWithAttachment(Long conversationId, File file, String content) throws Exception {
|
||||||
|
String path = "/api/v1/chat/conversations/" + conversationId + "/attachments";
|
||||||
|
return apiClient.postMultipartWithText(path, "file", file.toPath(), "content", content, MessageResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
public ConversationResponse updateConversation(Long id, UpdateConversationRequest request) throws Exception {
|
public ConversationResponse updateConversation(Long id, UpdateConversationRequest request) throws Exception {
|
||||||
return apiClient.put("/api/v1/chat/conversations/" + id, request, ConversationResponse.class);
|
return apiClient.put("/api/v1/chat/conversations/" + id, request, ConversationResponse.class);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,4 +39,8 @@ public class CouponApi {
|
|||||||
public void deleteCoupon(Long id) throws Exception {
|
public void deleteCoupon(Long id) throws Exception {
|
||||||
apiClient.delete("/api/v1/coupons/" + id);
|
apiClient.delete("/api/v1/coupons/" + id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CouponResponse getCouponByCode(String code) throws Exception {
|
||||||
|
return apiClient.get("/api/v1/coupons/code/" + code, CouponResponse.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,4 +45,8 @@ public class CustomerApi {
|
|||||||
public UserResponse createCustomer(UserRequest request) throws Exception {
|
public UserResponse createCustomer(UserRequest request) throws Exception {
|
||||||
return apiClient.post("/api/v1/customers", request, UserResponse.class);
|
return apiClient.post("/api/v1/customers", request, UserResponse.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public UserResponse getCustomerById(Long id) throws Exception {
|
||||||
|
return apiClient.get("/api/v1/customers/" + id, UserResponse.class);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -107,6 +107,14 @@ public class DropdownApi {
|
|||||||
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
|
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<DropdownOption> getAdoptionPets(Long storeId) throws Exception {
|
||||||
|
String response = apiClient.getRawResponse("/api/v1/dropdowns/adoption-pets?storeId=" + storeId);
|
||||||
|
if (response == null || response.isEmpty()) {
|
||||||
|
throw new IllegalStateException("Empty response from adoption pets endpoint");
|
||||||
|
}
|
||||||
|
return apiClient.getObjectMapper().readValue(response, new TypeReference<List<DropdownOption>>() {});
|
||||||
|
}
|
||||||
|
|
||||||
public List<DropdownOption> getCustomerPets(Long customerId) throws Exception {
|
public List<DropdownOption> getCustomerPets(Long customerId) throws Exception {
|
||||||
String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets");
|
String response = apiClient.getRawResponse("/api/v1/dropdowns/customers/" + customerId + "/pets");
|
||||||
if (response == null || response.isEmpty()) {
|
if (response == null || response.isEmpty()) {
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ public class PetApi {
|
|||||||
return listPets(query, null, null, null);
|
return listPets(query, null, null, null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public PetResponse getPetById(Long id) throws Exception {
|
||||||
|
return apiClient.get("/api/v1/pets/" + id, PetResponse.class);
|
||||||
|
}
|
||||||
|
|
||||||
public PetResponse createPet(PetRequest request) throws Exception {
|
public PetResponse createPet(PetRequest request) throws Exception {
|
||||||
return apiClient.post("/api/v1/pets", request, PetResponse.class);
|
return apiClient.post("/api/v1/pets", request, PetResponse.class);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,9 +3,12 @@ package org.example.petshopdesktop.controllers;
|
|||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.collections.transformation.FilteredList;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
|
import javafx.scene.control.DatePicker;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.TableColumn;
|
import javafx.scene.control.TableColumn;
|
||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
@@ -14,6 +17,7 @@ import org.example.petshopdesktop.api.endpoints.ActivityLogApi;
|
|||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
import org.example.petshopdesktop.util.TableViewSupport;
|
import org.example.petshopdesktop.util.TableViewSupport;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@@ -40,6 +44,15 @@ public class ActivityLogController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Button btnRefresh;
|
private Button btnRefresh;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private DatePicker dpStart;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private DatePicker dpEnd;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private CheckBox chkHideViewOnly;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label lblStatus;
|
private Label lblStatus;
|
||||||
|
|
||||||
@@ -47,6 +60,7 @@ public class ActivityLogController {
|
|||||||
private Label lblError;
|
private Label lblError;
|
||||||
|
|
||||||
private final ObservableList<ActivityLogResponse> activityLogs = FXCollections.observableArrayList();
|
private final ObservableList<ActivityLogResponse> activityLogs = FXCollections.observableArrayList();
|
||||||
|
private FilteredList<ActivityLogResponse> filteredLogs;
|
||||||
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
||||||
private static final int DEFAULT_LIMIT = 2000;
|
private static final int DEFAULT_LIMIT = 2000;
|
||||||
|
|
||||||
@@ -71,7 +85,10 @@ public class ActivityLogController {
|
|||||||
colStore.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayStore(data.getValue())));
|
colStore.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(displayStore(data.getValue())));
|
||||||
colActivity.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(nullToBlank(data.getValue().getActivity())));
|
colActivity.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(nullToBlank(data.getValue().getActivity())));
|
||||||
|
|
||||||
TableViewSupport.bindSortedItems(tvActivityLogs, new javafx.collections.transformation.FilteredList<>(activityLogs, a -> true));
|
filteredLogs = new FilteredList<>(activityLogs, a -> true);
|
||||||
|
TableViewSupport.bindSortedItems(tvActivityLogs, filteredLogs);
|
||||||
|
dpStart.setValue(LocalDate.now().minusDays(30));
|
||||||
|
dpEnd.setValue(LocalDate.now());
|
||||||
loadLogs();
|
loadLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,18 +97,46 @@ public class ActivityLogController {
|
|||||||
loadLogs();
|
loadLogs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void onDateChanged(ActionEvent event) {
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void onHideViewOnlyChanged(ActionEvent event) {
|
||||||
|
applyViewOnlyFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyViewOnlyFilter() {
|
||||||
|
boolean hide = chkHideViewOnly != null && chkHideViewOnly.isSelected();
|
||||||
|
if (filteredLogs != null) {
|
||||||
|
filteredLogs.setPredicate(hide
|
||||||
|
? a -> {
|
||||||
|
String act = a.getActivity();
|
||||||
|
if (act == null) return true;
|
||||||
|
String trimmed = act.stripLeading();
|
||||||
|
return !trimmed.startsWith("View");
|
||||||
|
}
|
||||||
|
: a -> true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void loadLogs() {
|
private void loadLogs() {
|
||||||
lblError.setText("");
|
lblError.setText("");
|
||||||
tvActivityLogs.setDisable(true);
|
tvActivityLogs.setDisable(true);
|
||||||
btnRefresh.setDisable(true);
|
btnRefresh.setDisable(true);
|
||||||
|
|
||||||
|
java.time.LocalDate startDate = dpStart != null ? dpStart.getValue() : null;
|
||||||
|
java.time.LocalDate endDate = dpEnd != null ? dpEnd.getValue() : null;
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
List<ActivityLogResponse> content = ActivityLogApi.getInstance().getActivityLogs(DEFAULT_LIMIT);
|
List<ActivityLogResponse> content = ActivityLogApi.getInstance().getActivityLogs(DEFAULT_LIMIT, startDate, endDate);
|
||||||
List<ActivityLogResponse> safeContent = content != null ? content : List.of();
|
List<ActivityLogResponse> safeContent = content != null ? content : List.of();
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
activityLogs.setAll(safeContent);
|
activityLogs.setAll(safeContent);
|
||||||
|
applyViewOnlyFilter();
|
||||||
tvActivityLogs.setDisable(false);
|
tvActivityLogs.setDisable(false);
|
||||||
btnRefresh.setDisable(false);
|
btnRefresh.setDisable(false);
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import javafx.application.Platform;
|
|||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
|
import javafx.collections.transformation.FilteredList;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
@@ -13,16 +14,21 @@ import javafx.scene.control.cell.PropertyValueFactory;
|
|||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.dto.adoption.AdoptionResponse;
|
import org.example.petshopdesktop.api.dto.adoption.AdoptionResponse;
|
||||||
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.endpoints.AdoptionApi;
|
import org.example.petshopdesktop.api.endpoints.AdoptionApi;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
import org.example.petshopdesktop.controllers.dialogcontrollers.AdoptionDialogController;
|
import org.example.petshopdesktop.controllers.dialogcontrollers.AdoptionDialogController;
|
||||||
import org.example.petshopdesktop.models.Adoption;
|
import org.example.petshopdesktop.models.Adoption;
|
||||||
|
import org.example.petshopdesktop.ui.CalendarPane;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
import org.example.petshopdesktop.util.TableViewSupport;
|
import org.example.petshopdesktop.util.TableViewSupport;
|
||||||
|
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
public class AdoptionController {
|
public class AdoptionController {
|
||||||
@@ -72,6 +78,19 @@ public class AdoptionController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TextField txtSearch;
|
private TextField txtSearch;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbStatusFilter;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<DropdownOption> cbStoreFilter;
|
||||||
|
|
||||||
|
private final java.util.ArrayList<DropdownOption> storeOptions = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private CalendarPane calendarPane;
|
||||||
|
private LocalDate selectedCalendarDate = null;
|
||||||
|
private FilteredList<Adoption> filteredAdoptions;
|
||||||
|
|
||||||
private ObservableList<Adoption> data = FXCollections.observableArrayList();
|
private ObservableList<Adoption> data = FXCollections.observableArrayList();
|
||||||
private String mode = null;
|
private String mode = null;
|
||||||
|
|
||||||
@@ -92,6 +111,20 @@ public class AdoptionController {
|
|||||||
colStoreName.setCellValueFactory(new PropertyValueFactory<>("storeName"));
|
colStoreName.setCellValueFactory(new PropertyValueFactory<>("storeName"));
|
||||||
TableViewSupport.applyCurrencyColumn(colAdoptionFee);
|
TableViewSupport.applyCurrencyColumn(colAdoptionFee);
|
||||||
|
|
||||||
|
filteredAdoptions = new FilteredList<>(data, a -> true);
|
||||||
|
TableViewSupport.bindSortedItems(tvAdoptions, filteredAdoptions);
|
||||||
|
|
||||||
|
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Pending", "Completed", "Cancelled"));
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStatusFilter.valueProperty().addListener((obs, o, n) -> applyFilterPredicate());
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
cbStoreFilter.setVisible(true);
|
||||||
|
cbStoreFilter.setManaged(true);
|
||||||
|
loadStoreFilter();
|
||||||
|
}
|
||||||
|
cbStoreFilter.valueProperty().addListener((obs, o, n) -> displayAdoptions());
|
||||||
|
|
||||||
displayAdoptions();
|
displayAdoptions();
|
||||||
TableViewSupport.installDoubleClickAction(tvAdoptions, selected -> openDialog(selected, "Edit"));
|
TableViewSupport.installDoubleClickAction(tvAdoptions, selected -> openDialog(selected, "Edit"));
|
||||||
|
|
||||||
@@ -106,6 +139,11 @@ public class AdoptionController {
|
|||||||
displayFilteredAdoptions(newValue);
|
displayFilteredAdoptions(newValue);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
calendarPane.setOnDateSelected(date -> {
|
||||||
|
selectedCalendarDate = date;
|
||||||
|
applyFilterPredicate();
|
||||||
|
});
|
||||||
|
|
||||||
tvAdoptions.setOnKeyPressed(event -> {
|
tvAdoptions.setOnKeyPressed(event -> {
|
||||||
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
|
if (event.getCode() == javafx.scene.input.KeyCode.DELETE) {
|
||||||
if (tvAdoptions.getSelectionModel().getSelectedItem() != null) {
|
if (tvAdoptions.getSelectionModel().getSelectedItem() != null) {
|
||||||
@@ -118,6 +156,10 @@ public class AdoptionController {
|
|||||||
@FXML
|
@FXML
|
||||||
void btnRefresh(ActionEvent event) {
|
void btnRefresh(ActionEvent event) {
|
||||||
txtSearch.clear();
|
txtSearch.clear();
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
selectedCalendarDate = null;
|
||||||
|
if (filteredAdoptions != null) filteredAdoptions.setPredicate(a -> true);
|
||||||
tvAdoptions.getSortOrder().clear();
|
tvAdoptions.getSortOrder().clear();
|
||||||
displayAdoptions();
|
displayAdoptions();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -189,7 +231,7 @@ public class AdoptionController {
|
|||||||
} else {
|
} else {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Long storeId = selectedStoreId();
|
||||||
List<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(filter, storeId);
|
List<AdoptionResponse> adoptions = AdoptionApi.getInstance().listAdoptions(filter, storeId);
|
||||||
List<Adoption> adoptionList = adoptions.stream()
|
List<Adoption> adoptionList = adoptions.stream()
|
||||||
.map(this::mapToAdoption)
|
.map(this::mapToAdoption)
|
||||||
@@ -198,7 +240,13 @@ public class AdoptionController {
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
data.setAll(adoptionList);
|
data.setAll(adoptionList);
|
||||||
tvAdoptions.setItems(data);
|
Set<LocalDate> dates = adoptionList.stream()
|
||||||
|
.filter(a -> a.getAdoptionDate() != null && !a.getAdoptionDate().isEmpty())
|
||||||
|
.map(a -> { try { return LocalDate.parse(a.getAdoptionDate()); } catch (Exception ex) { return null; } })
|
||||||
|
.filter(d -> d != null)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
calendarPane.setEventDates(dates);
|
||||||
|
applyFilterPredicate();
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -225,7 +273,15 @@ public class AdoptionController {
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
data.setAll(adoptionList);
|
data.setAll(adoptionList);
|
||||||
tvAdoptions.setItems(data);
|
Set<LocalDate> dates = adoptionList.stream()
|
||||||
|
.filter(a -> a.getAdoptionDate() != null && !a.getAdoptionDate().isEmpty())
|
||||||
|
.map(a -> { try { return LocalDate.parse(a.getAdoptionDate()); } catch (Exception ex) { return null; } })
|
||||||
|
.filter(d -> d != null)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
calendarPane.setEventDates(dates);
|
||||||
|
if (selectedCalendarDate != null) {
|
||||||
|
filteredAdoptions.setPredicate(a -> a.getAdoptionDate().equals(selectedCalendarDate.toString()));
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -272,6 +328,44 @@ public class AdoptionController {
|
|||||||
txtSearch.setText("");
|
txtSearch.setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadStoreFilter() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
DropdownOption allStores = new DropdownOption();
|
||||||
|
allStores.setLabel("All Stores");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
storeOptions.clear();
|
||||||
|
storeOptions.addAll(stores);
|
||||||
|
java.util.List<DropdownOption> items = new java.util.ArrayList<>();
|
||||||
|
items.add(allStores);
|
||||||
|
items.addAll(stores);
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("AdoptionController.loadStoreFilter", e, "Loading store filter");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long selectedStoreId() {
|
||||||
|
if (!UserSession.getInstance().isAdmin()) return UserSession.getInstance().getStoreId();
|
||||||
|
DropdownOption selected = cbStoreFilter.getValue();
|
||||||
|
return (selected != null && selected.getId() != null) ? selected.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFilterPredicate() {
|
||||||
|
String selectedStatus = cbStatusFilter.getValue();
|
||||||
|
filteredAdoptions.setPredicate(a -> {
|
||||||
|
boolean dateMatch = selectedCalendarDate == null
|
||||||
|
|| a.getAdoptionDate().equals(selectedCalendarDate.toString());
|
||||||
|
boolean statusMatch = selectedStatus == null || selectedStatus.equals("All Statuses")
|
||||||
|
|| a.getAdoptionStatus().equalsIgnoreCase(selectedStatus);
|
||||||
|
return dateMatch && statusMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private Adoption mapToAdoption(AdoptionResponse response) {
|
private Adoption mapToAdoption(AdoptionResponse response) {
|
||||||
return new Adoption(
|
return new Adoption(
|
||||||
response.getAdoptionId().intValue(),
|
response.getAdoptionId().intValue(),
|
||||||
@@ -284,7 +378,8 @@ public class AdoptionController {
|
|||||||
response.getAdoptionDate() != null ? response.getAdoptionDate().toString() : "",
|
response.getAdoptionDate() != null ? response.getAdoptionDate().toString() : "",
|
||||||
response.getAdoptionFee() != null ? response.getAdoptionFee().doubleValue() : 0.0,
|
response.getAdoptionFee() != null ? response.getAdoptionFee().doubleValue() : 0.0,
|
||||||
response.getAdoptionStatus(),
|
response.getAdoptionStatus(),
|
||||||
response.getSourceStoreName()
|
response.getSourceStoreName(),
|
||||||
|
response.getSourceStoreId()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,19 +1,25 @@
|
|||||||
package org.example.petshopdesktop.controllers;
|
package org.example.petshopdesktop.controllers;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.chart.*;
|
import javafx.scene.chart.*;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.DatePicker;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.Tab;
|
import javafx.scene.control.Tab;
|
||||||
import javafx.scene.control.TabPane;
|
import javafx.scene.control.TabPane;
|
||||||
|
import javafx.scene.control.ToggleButton;
|
||||||
|
import javafx.scene.control.ToggleGroup;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import org.example.petshopdesktop.api.dto.analytics.DailySales;
|
import org.example.petshopdesktop.api.dto.analytics.DailySales;
|
||||||
import org.example.petshopdesktop.api.dto.analytics.DashboardResponse;
|
import org.example.petshopdesktop.api.dto.analytics.DashboardResponse;
|
||||||
import org.example.petshopdesktop.api.dto.analytics.TopProduct;
|
import org.example.petshopdesktop.api.dto.analytics.TopProduct;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
|
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.AnalyticsApi;
|
|
||||||
import org.example.petshopdesktop.api.endpoints.SaleApi;
|
import org.example.petshopdesktop.api.endpoints.SaleApi;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
@@ -74,6 +80,46 @@ public class AnalyticsController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TabPane tabPane;
|
private TabPane tabPane;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnToggleFilters;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblFilterSummary;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private VBox vbFilterContent;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private DatePicker dpStartDate;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private DatePicker dpEndDate;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbPaymentFilter;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbTopN;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbStoreFilter;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox hbViewToggle;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleButton tbnMyAnalytics;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ToggleButton tbnStoreAnalytics;
|
||||||
|
|
||||||
|
private String viewMode = "store";
|
||||||
|
|
||||||
|
private List<SaleResponse> cachedSales = new ArrayList<>();
|
||||||
|
private FilterState currentFilter = new FilterState();
|
||||||
|
|
||||||
|
private static final int[] TOP_N_VALUES = {5, 10, 15, 20};
|
||||||
|
|
||||||
private static final String SALES_COLOR = "#ff6b35";
|
private static final String SALES_COLOR = "#ff6b35";
|
||||||
private static final String REVENUE_COLOR = "#4ecdc4";
|
private static final String REVENUE_COLOR = "#4ecdc4";
|
||||||
private static final String QUANTITY_COLOR = "#ff9f1c";
|
private static final String QUANTITY_COLOR = "#ff9f1c";
|
||||||
@@ -88,13 +134,36 @@ public class AnalyticsController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
configureCharts();
|
configureCharts();
|
||||||
if (tabPane != null) {
|
|
||||||
tabPane.getSelectionModel().selectedItemProperty().addListener((obs, oldTab, newTab) -> {
|
cbTopN.setItems(FXCollections.observableArrayList("Top 5", "Top 10", "Top 15", "Top 20"));
|
||||||
if (oldTab != newTab && newTab != null) {
|
cbTopN.getSelectionModel().selectFirst();
|
||||||
loadAnalyticsData();
|
|
||||||
}
|
cbPaymentFilter.setItems(FXCollections.observableArrayList("All"));
|
||||||
});
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
}
|
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList("All Stores"));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
|
lblFilterSummary.setText("All time");
|
||||||
|
|
||||||
|
ToggleGroup tgViewMode = new ToggleGroup();
|
||||||
|
tbnMyAnalytics.setToggleGroup(tgViewMode);
|
||||||
|
tbnStoreAnalytics.setToggleGroup(tgViewMode);
|
||||||
|
tbnStoreAnalytics.setSelected(true);
|
||||||
|
tgViewMode.selectedToggleProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
if (newVal == null) {
|
||||||
|
(viewMode.equals("mine") ? tbnMyAnalytics : tbnStoreAnalytics).setSelected(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
viewMode = (newVal == tbnMyAnalytics) ? "mine" : "store";
|
||||||
|
updateViewModeStyles();
|
||||||
|
updateStoreFilterVisibility();
|
||||||
|
applyCurrentFilter();
|
||||||
|
});
|
||||||
|
|
||||||
|
hbViewToggle.setVisible(true);
|
||||||
|
hbViewToggle.setManaged(true);
|
||||||
|
|
||||||
loadAnalyticsData();
|
loadAnalyticsData();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,64 +211,161 @@ public class AnalyticsController {
|
|||||||
|
|
||||||
private void loadAnalyticsData() {
|
private void loadAnalyticsData() {
|
||||||
lblError.setVisible(false);
|
lblError.setVisible(false);
|
||||||
if (lblStatus != null) {
|
lblStatus.setVisible(false);
|
||||||
lblStatus.setVisible(false);
|
btnRefresh.setDisable(true);
|
||||||
}
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
DashboardResponse dashboard = AnalyticsApi.getInstance().getDashboard(30, 10);
|
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
||||||
|
List<SaleResponse> sales = SaleApi.getInstance().listAllSales(null, storeId);
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
try {
|
cachedSales = sales;
|
||||||
boolean isAdmin = UserSession.getInstance().isAdmin();
|
derivePaymentMethods();
|
||||||
loadSummaryData(dashboard);
|
deriveStores();
|
||||||
loadSalesOverTime(dashboard);
|
updateStoreFilterVisibility();
|
||||||
loadTopProductsByRevenue(dashboard);
|
applyCurrentFilter();
|
||||||
loadTopProductsByQuantity(dashboard);
|
btnRefresh.setDisable(false);
|
||||||
loadPaymentMethodDistribution(dashboard);
|
|
||||||
loadEmployeePerformance(dashboard, isAdmin);
|
|
||||||
applyRoleVisibility(isAdmin);
|
|
||||||
} catch (Exception e) {
|
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data");
|
|
||||||
lblError.setText("Error loading analytics data. Please try again.");
|
|
||||||
lblError.setVisible(true);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
if (UserSession.getInstance().isStaff()) {
|
|
||||||
try {
|
|
||||||
DashboardResponse fallback = buildStaffFallbackDashboard();
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
try {
|
|
||||||
loadSummaryData(fallback);
|
|
||||||
loadSalesOverTime(fallback);
|
|
||||||
loadTopProductsByRevenue(fallback);
|
|
||||||
loadTopProductsByQuantity(fallback);
|
|
||||||
loadPaymentMethodDistribution(fallback);
|
|
||||||
loadEmployeePerformance(fallback, false);
|
|
||||||
applyRoleVisibility(false);
|
|
||||||
lblError.setVisible(false);
|
|
||||||
} catch (Exception inner) {
|
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", inner, "Rendering fallback analytics data");
|
|
||||||
lblError.setText("Error loading analytics data. Please try again.");
|
|
||||||
lblError.setVisible(true);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} catch (Exception fallbackError) {
|
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", fallbackError, "Loading fallback analytics data");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading analytics data");
|
ActivityLogger.getInstance().logException("AnalyticsController.loadAnalyticsData", e, "Loading sales data for analytics");
|
||||||
lblError.setText("Error loading analytics data. Please try again.");
|
lblError.setText("Error loading analytics data. Please try again.");
|
||||||
lblError.setVisible(true);
|
lblError.setVisible(true);
|
||||||
|
btnRefresh.setDisable(false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void updateViewModeStyles() {
|
||||||
|
String selectedStyle = "-fx-background-color: #4ECDC4; -fx-text-fill: white; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;";
|
||||||
|
String unselectedStyle = "-fx-background-color: #e2e8f0; -fx-text-fill: #475569; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;";
|
||||||
|
if (viewMode.equals("mine")) {
|
||||||
|
tbnMyAnalytics.setStyle(selectedStyle + " -fx-background-radius: 6 0 0 6;");
|
||||||
|
tbnStoreAnalytics.setStyle(unselectedStyle + " -fx-background-radius: 0 6 6 0;");
|
||||||
|
} else {
|
||||||
|
tbnMyAnalytics.setStyle(unselectedStyle + " -fx-background-radius: 6 0 0 6;");
|
||||||
|
tbnStoreAnalytics.setStyle(selectedStyle + " -fx-background-radius: 0 6 6 0;");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyCurrentFilter() {
|
||||||
|
try {
|
||||||
|
List<SaleResponse> salesForMode;
|
||||||
|
if (viewMode.equals("mine")) {
|
||||||
|
String myName = UserSession.getInstance().getEmployeeName();
|
||||||
|
salesForMode = cachedSales.stream()
|
||||||
|
.filter(s -> myName != null && myName.equalsIgnoreCase(s.getEmployeeName() != null ? s.getEmployeeName() : ""))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
} else {
|
||||||
|
salesForMode = cachedSales;
|
||||||
|
}
|
||||||
|
String storeFilter = currentFilter.storeFilter;
|
||||||
|
if (!storeFilter.equals("All Stores") && !storeFilter.isBlank()) {
|
||||||
|
salesForMode = salesForMode.stream()
|
||||||
|
.filter(s -> storeFilter.equalsIgnoreCase(s.getStoreName() != null ? s.getStoreName() : ""))
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
List<SaleResponse> filtered = filterSales(salesForMode, currentFilter);
|
||||||
|
String start = currentFilter.startDate.isEmpty() ? LocalDate.now().minusDays(6).toString() : currentFilter.startDate;
|
||||||
|
String end = currentFilter.endDate.isEmpty() ? LocalDate.now().toString() : currentFilter.endDate;
|
||||||
|
|
||||||
|
DashboardResponse dashboard = new DashboardResponse();
|
||||||
|
dashboard.setSalesSummary(buildSalesSummary(filtered));
|
||||||
|
dashboard.setDailySales(buildDailySalesBetween(filtered, start, end));
|
||||||
|
dashboard.setTopProducts(buildTopProducts(filtered, currentFilter.topN > 0 ? currentFilter.topN : 5));
|
||||||
|
dashboard.setPaymentMethods(buildPaymentMethods(filtered));
|
||||||
|
dashboard.setEmployeePerformance(buildEmployeePerformance(filtered));
|
||||||
|
|
||||||
|
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||||
|
loadSummaryData(dashboard);
|
||||||
|
loadSalesOverTime(dashboard);
|
||||||
|
loadTopProductsByRevenue(dashboard);
|
||||||
|
loadTopProductsByQuantity(dashboard);
|
||||||
|
loadPaymentMethodDistribution(dashboard);
|
||||||
|
loadEmployeePerformance(dashboard, isAdmin);
|
||||||
|
applyRoleVisibility(isAdmin);
|
||||||
|
updateFilterSummary();
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("AnalyticsController.applyCurrentFilter", e, "Computing analytics from filtered sales");
|
||||||
|
lblError.setText("Error computing analytics. Please try again.");
|
||||||
|
lblError.setVisible(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<SaleResponse> filterSales(List<SaleResponse> sales, FilterState filter) {
|
||||||
|
return sales.stream().filter(sale -> {
|
||||||
|
String date = sale.getSaleDate() != null ? sale.getSaleDate().toLocalDate().toString() : "";
|
||||||
|
if (!filter.startDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.startDate) < 0) return false;
|
||||||
|
if (!filter.endDate.isEmpty() && !date.isEmpty() && date.compareTo(filter.endDate) > 0) return false;
|
||||||
|
if (!filter.paymentMethod.equals("All") && !filter.paymentMethod.isEmpty()) {
|
||||||
|
if (!filter.paymentMethod.equalsIgnoreCase(sale.getPaymentMethod())) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}).collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void derivePaymentMethods() {
|
||||||
|
Set<String> methods = new TreeSet<>();
|
||||||
|
for (SaleResponse s : cachedSales) {
|
||||||
|
if (s.getPaymentMethod() != null && !s.getPaymentMethod().isEmpty()) {
|
||||||
|
methods.add(s.getPaymentMethod());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> items = new ArrayList<>();
|
||||||
|
items.add("All");
|
||||||
|
items.addAll(methods);
|
||||||
|
String current = cbPaymentFilter.getValue();
|
||||||
|
cbPaymentFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
if (current != null && items.contains(current)) {
|
||||||
|
cbPaymentFilter.setValue(current);
|
||||||
|
} else {
|
||||||
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void deriveStores() {
|
||||||
|
Set<String> stores = new TreeSet<>();
|
||||||
|
for (SaleResponse s : cachedSales) {
|
||||||
|
if (s.getStoreName() != null && !s.getStoreName().isBlank()) {
|
||||||
|
stores.add(s.getStoreName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<String> items = new ArrayList<>();
|
||||||
|
items.add("All Stores");
|
||||||
|
items.addAll(stores);
|
||||||
|
String current = cbStoreFilter.getValue();
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
if (current != null && items.contains(current)) {
|
||||||
|
cbStoreFilter.setValue(current);
|
||||||
|
} else {
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateStoreFilterVisibility() {
|
||||||
|
boolean show = UserSession.getInstance().isAdmin() && viewMode.equals("store");
|
||||||
|
cbStoreFilter.setVisible(show);
|
||||||
|
cbStoreFilter.setManaged(show);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void updateFilterSummary() {
|
||||||
|
String start = currentFilter.startDate;
|
||||||
|
String end = currentFilter.endDate;
|
||||||
|
if (start.isEmpty() && end.isEmpty()) {
|
||||||
|
lblFilterSummary.setText("All time");
|
||||||
|
} else if (start.isEmpty()) {
|
||||||
|
lblFilterSummary.setText("Up to " + shortDate(end));
|
||||||
|
} else if (end.isEmpty()) {
|
||||||
|
lblFilterSummary.setText("From " + shortDate(start));
|
||||||
|
} else {
|
||||||
|
lblFilterSummary.setText(shortDate(start) + " – " + shortDate(end));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String shortDate(String date) {
|
||||||
|
return (date != null && date.length() >= 10) ? date.substring(5) : date;
|
||||||
|
}
|
||||||
|
|
||||||
private void loadSummaryData(DashboardResponse dashboard) throws Exception {
|
private void loadSummaryData(DashboardResponse dashboard) throws Exception {
|
||||||
if (dashboard != null) {
|
if (dashboard != null) {
|
||||||
BigDecimal totalRevenue = BigDecimal.ZERO;
|
BigDecimal totalRevenue = BigDecimal.ZERO;
|
||||||
@@ -318,34 +484,32 @@ public class AnalyticsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void applyRoleVisibility(boolean isAdmin) {
|
private void applyRoleVisibility(boolean isAdmin) {
|
||||||
chartEmployeePerformance.setVisible(isAdmin);
|
boolean showEmpChart = isAdmin && viewMode.equals("store");
|
||||||
chartEmployeePerformance.setManaged(isAdmin);
|
chartEmployeePerformance.setVisible(showEmpChart);
|
||||||
|
chartEmployeePerformance.setManaged(showEmpChart);
|
||||||
if (chartEmployeePerformance.getParent() != null) {
|
if (chartEmployeePerformance.getParent() != null) {
|
||||||
chartEmployeePerformance.getParent().setVisible(isAdmin);
|
chartEmployeePerformance.getParent().setVisible(showEmpChart);
|
||||||
chartEmployeePerformance.getParent().setManaged(isAdmin);
|
chartEmployeePerformance.getParent().setManaged(showEmpChart);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private DashboardResponse buildStaffFallbackDashboard() throws Exception {
|
private List<DashboardResponse.EmployeePerformanceData> buildEmployeePerformance(List<SaleResponse> sales) {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Map<String, BigDecimal> empRevenue = new LinkedHashMap<>();
|
||||||
List<SaleResponse> sales = SaleApi.getInstance().listSales(0, Integer.MAX_VALUE, null, storeId);
|
for (SaleResponse sale : sales) {
|
||||||
String employeeName = UserSession.getInstance().getEmployeeName();
|
if (Boolean.TRUE.equals(sale.getIsRefund())) continue;
|
||||||
if (employeeName == null || employeeName.isBlank()) {
|
String emp = sale.getEmployeeName() != null ? sale.getEmployeeName() : "Unknown";
|
||||||
employeeName = UserSession.getInstance().getUsername();
|
BigDecimal amount = sale.getTotalAmount() != null ? sale.getTotalAmount() : BigDecimal.ZERO;
|
||||||
|
empRevenue.merge(emp, amount, BigDecimal::add);
|
||||||
}
|
}
|
||||||
final String employeeNameFilter = employeeName;
|
return empRevenue.entrySet().stream()
|
||||||
|
.sorted((a, b) -> b.getValue().compareTo(a.getValue()))
|
||||||
List<SaleResponse> personalSales = sales.stream()
|
.map(e -> {
|
||||||
.filter(sale -> sale.getEmployeeName() != null && sale.getEmployeeName().equalsIgnoreCase(employeeNameFilter))
|
DashboardResponse.EmployeePerformanceData d = new DashboardResponse.EmployeePerformanceData();
|
||||||
.toList();
|
d.setEmployeeName(e.getKey());
|
||||||
|
d.setRevenue(e.getValue());
|
||||||
DashboardResponse dashboard = new DashboardResponse();
|
return d;
|
||||||
dashboard.setSalesSummary(buildSalesSummary(personalSales));
|
})
|
||||||
dashboard.setDailySales(buildDailySales(personalSales, 30));
|
.collect(Collectors.toList());
|
||||||
dashboard.setTopProducts(buildTopProducts(personalSales, 10));
|
|
||||||
dashboard.setPaymentMethods(buildPaymentMethods(personalSales));
|
|
||||||
dashboard.setEmployeePerformance(List.of(new DashboardResponse.EmployeePerformanceData()));
|
|
||||||
return dashboard;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DashboardResponse.SalesSummary buildSalesSummary(List<SaleResponse> sales) {
|
private DashboardResponse.SalesSummary buildSalesSummary(List<SaleResponse> sales) {
|
||||||
@@ -379,21 +543,26 @@ public class AnalyticsController {
|
|||||||
return summary;
|
return summary;
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<DailySales> buildDailySales(List<SaleResponse> sales, int days) {
|
private List<DailySales> buildDailySalesBetween(List<SaleResponse> sales, String startStr, String endStr) {
|
||||||
Map<LocalDate, DailySales> daily = new LinkedHashMap<>();
|
Map<LocalDate, DailySales> daily = new LinkedHashMap<>();
|
||||||
LocalDate start = LocalDate.now().minusDays(days - 1L);
|
try {
|
||||||
for (int i = 0; i < days; i++) {
|
LocalDate start = LocalDate.parse(startStr);
|
||||||
LocalDate date = start.plusDays(i);
|
LocalDate end = LocalDate.parse(endStr);
|
||||||
DailySales row = new DailySales();
|
if (start.plusDays(60).isBefore(end)) {
|
||||||
row.setDate(date.toString());
|
start = end.minusDays(59);
|
||||||
row.setRevenue(BigDecimal.ZERO);
|
}
|
||||||
row.setSalesCount(0L);
|
for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
|
||||||
daily.put(date, row);
|
DailySales row = new DailySales();
|
||||||
|
row.setDate(date.toString());
|
||||||
|
row.setRevenue(BigDecimal.ZERO);
|
||||||
|
row.setSalesCount(0L);
|
||||||
|
daily.put(date, row);
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
return new ArrayList<>();
|
||||||
}
|
}
|
||||||
for (SaleResponse sale : sales) {
|
for (SaleResponse sale : sales) {
|
||||||
if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) {
|
if (Boolean.TRUE.equals(sale.getIsRefund()) || sale.getSaleDate() == null) continue;
|
||||||
continue;
|
|
||||||
}
|
|
||||||
LocalDate date = sale.getSaleDate().toLocalDate();
|
LocalDate date = sale.getSaleDate().toLocalDate();
|
||||||
DailySales row = daily.get(date);
|
DailySales row = daily.get(date);
|
||||||
if (row != null) {
|
if (row != null) {
|
||||||
@@ -494,4 +663,77 @@ public class AnalyticsController {
|
|||||||
loadAnalyticsData();
|
loadAnalyticsData();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnToggleFilters(ActionEvent event) {
|
||||||
|
boolean expanded = vbFilterContent.isVisible();
|
||||||
|
vbFilterContent.setVisible(!expanded);
|
||||||
|
vbFilterContent.setManaged(!expanded);
|
||||||
|
btnToggleFilters.setText(expanded ? "▼ Filters" : "▲ Filters");
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPresetToday(ActionEvent event) { applyPreset(0, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset7D(ActionEvent event) { applyPreset(-6, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset30D(ActionEvent event) { applyPreset(-29, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset3M(ActionEvent event) { applyPreset(-89, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPreset1Y(ActionEvent event) { applyPreset(-364, 0); }
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnPresetAll(ActionEvent event) {
|
||||||
|
dpStartDate.setValue(null);
|
||||||
|
dpEndDate.setValue(null);
|
||||||
|
applyFiltersFromUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnApplyFilter(ActionEvent event) {
|
||||||
|
applyFiltersFromUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnResetFilter(ActionEvent event) {
|
||||||
|
dpStartDate.setValue(null);
|
||||||
|
dpEndDate.setValue(null);
|
||||||
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
|
cbTopN.getSelectionModel().selectFirst();
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
currentFilter = new FilterState();
|
||||||
|
applyCurrentFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPreset(int startOffset, int endOffset) {
|
||||||
|
dpStartDate.setValue(LocalDate.now().plusDays(startOffset));
|
||||||
|
dpEndDate.setValue(LocalDate.now().plusDays(endOffset));
|
||||||
|
applyFiltersFromUI();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFiltersFromUI() {
|
||||||
|
currentFilter = new FilterState();
|
||||||
|
currentFilter.startDate = dpStartDate.getValue() != null ? dpStartDate.getValue().toString() : "";
|
||||||
|
currentFilter.endDate = dpEndDate.getValue() != null ? dpEndDate.getValue().toString() : "";
|
||||||
|
String pm = cbPaymentFilter.getValue();
|
||||||
|
currentFilter.paymentMethod = pm != null ? pm : "All";
|
||||||
|
int topNPos = cbTopN.getSelectionModel().getSelectedIndex();
|
||||||
|
currentFilter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
||||||
|
String sf = cbStoreFilter.getValue();
|
||||||
|
currentFilter.storeFilter = (sf != null && !sf.isBlank()) ? sf : "All Stores";
|
||||||
|
applyCurrentFilter();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class FilterState {
|
||||||
|
String startDate = "";
|
||||||
|
String endDate = "";
|
||||||
|
String paymentMethod = "All";
|
||||||
|
int topN = 5;
|
||||||
|
String storeFilter = "All Stores";
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,12 +16,17 @@ import javafx.stage.Stage;
|
|||||||
|
|
||||||
import org.example.petshopdesktop.DTOs.AppointmentDTO;
|
import org.example.petshopdesktop.DTOs.AppointmentDTO;
|
||||||
import org.example.petshopdesktop.api.dto.appointment.AppointmentResponse;
|
import org.example.petshopdesktop.api.dto.appointment.AppointmentResponse;
|
||||||
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.endpoints.AppointmentApi;
|
import org.example.petshopdesktop.api.endpoints.AppointmentApi;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialogController;
|
import org.example.petshopdesktop.controllers.dialogcontrollers.AppointmentDialogController;
|
||||||
|
import org.example.petshopdesktop.ui.CalendarPane;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
import org.example.petshopdesktop.util.TableViewSupport;
|
import org.example.petshopdesktop.util.TableViewSupport;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Set;
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
@@ -43,11 +48,20 @@ public class AppointmentController {
|
|||||||
@FXML private Button btnEdit;
|
@FXML private Button btnEdit;
|
||||||
@FXML private Button btnDelete;
|
@FXML private Button btnDelete;
|
||||||
@FXML private Button btnRefresh;
|
@FXML private Button btnRefresh;
|
||||||
|
@FXML private javafx.scene.control.ToggleButton btnMyAppointments;
|
||||||
|
|
||||||
@FXML private Label lblStatus;
|
@FXML private Label lblStatus;
|
||||||
|
|
||||||
@FXML private TextField txtSearch;
|
@FXML private TextField txtSearch;
|
||||||
|
|
||||||
|
@FXML private ComboBox<String> cbStatusFilter;
|
||||||
|
@FXML private ComboBox<DropdownOption> cbStoreFilter;
|
||||||
|
|
||||||
|
private final java.util.ArrayList<DropdownOption> storeOptions = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
@FXML private CalendarPane calendarPane;
|
||||||
|
private LocalDate selectedCalendarDate = null;
|
||||||
|
|
||||||
private final ObservableList<AppointmentDTO> appointments = FXCollections.observableArrayList();
|
private final ObservableList<AppointmentDTO> appointments = FXCollections.observableArrayList();
|
||||||
private FilteredList<AppointmentDTO> filtered;
|
private FilteredList<AppointmentDTO> filtered;
|
||||||
|
|
||||||
@@ -71,6 +85,22 @@ public class AppointmentController {
|
|||||||
TableViewSupport.bindSortedItems(tvAppointments, filtered);
|
TableViewSupport.bindSortedItems(tvAppointments, filtered);
|
||||||
TableViewSupport.installDoubleClickAction(tvAppointments, selected -> openDialog(selected, "Edit"));
|
TableViewSupport.installDoubleClickAction(tvAppointments, selected -> openDialog(selected, "Edit"));
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isStaff()) {
|
||||||
|
btnMyAppointments.setVisible(true);
|
||||||
|
btnMyAppointments.setManaged(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Booked", "Completed", "Missed", "Cancelled"));
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStatusFilter.valueProperty().addListener((obs, o, n) -> applyFilterPredicate());
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
cbStoreFilter.setVisible(true);
|
||||||
|
cbStoreFilter.setManaged(true);
|
||||||
|
loadStoreFilter();
|
||||||
|
}
|
||||||
|
cbStoreFilter.valueProperty().addListener((obs, o, n) -> loadAppointments());
|
||||||
|
|
||||||
if (txtSearch != null) {
|
if (txtSearch != null) {
|
||||||
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
|
txtSearch.textProperty().addListener((obs, o, n) -> applyFilter(n));
|
||||||
}
|
}
|
||||||
@@ -90,13 +120,24 @@ public class AppointmentController {
|
|||||||
});
|
});
|
||||||
|
|
||||||
loadAppointments();
|
loadAppointments();
|
||||||
|
|
||||||
|
calendarPane.setOnDateSelected(date -> {
|
||||||
|
selectedCalendarDate = date;
|
||||||
|
applyFilterPredicate();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnMyAppointmentsToggled(javafx.event.ActionEvent event) {
|
||||||
|
loadAppointments();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadAppointments(){
|
private void loadAppointments(){
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try{
|
try{
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Long storeId = selectedStoreId();
|
||||||
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(null, storeId);
|
Long employeeId = btnMyAppointments.isSelected() ? UserSession.getInstance().getEmployeeId() : null;
|
||||||
|
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(null, storeId, employeeId);
|
||||||
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
||||||
.map(this::mapToAppointmentDTO)
|
.map(this::mapToAppointmentDTO)
|
||||||
.sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed())
|
.sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed())
|
||||||
@@ -104,6 +145,13 @@ public class AppointmentController {
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
appointments.setAll(appointmentDTOs);
|
appointments.setAll(appointmentDTOs);
|
||||||
|
Set<LocalDate> dates = appointmentDTOs.stream()
|
||||||
|
.filter(a -> a.getAppointmentDate() != null && !a.getAppointmentDate().isEmpty())
|
||||||
|
.map(a -> { try { return LocalDate.parse(a.getAppointmentDate()); } catch (Exception ex) { return null; } })
|
||||||
|
.filter(d -> d != null)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
calendarPane.setEventDates(dates);
|
||||||
|
applyFilterPredicate();
|
||||||
});
|
});
|
||||||
}catch(Exception e){
|
}catch(Exception e){
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -121,8 +169,9 @@ public class AppointmentController {
|
|||||||
String query = text == null || text.trim().isEmpty() ? null : text.trim();
|
String query = text == null || text.trim().isEmpty() ? null : text.trim();
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Long storeId = selectedStoreId();
|
||||||
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(query, storeId);
|
Long employeeId = btnMyAppointments.isSelected() ? UserSession.getInstance().getEmployeeId() : null;
|
||||||
|
List<AppointmentResponse> responses = AppointmentApi.getInstance().listAppointments(query, storeId, employeeId);
|
||||||
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
List<AppointmentDTO> appointmentDTOs = responses.stream()
|
||||||
.map(this::mapToAppointmentDTO)
|
.map(this::mapToAppointmentDTO)
|
||||||
.sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed())
|
.sorted(Comparator.comparing((AppointmentDTO a) -> a.getAppointmentDate() + "T" + a.getAppointmentTime()).reversed())
|
||||||
@@ -130,6 +179,13 @@ public class AppointmentController {
|
|||||||
|
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
appointments.setAll(appointmentDTOs);
|
appointments.setAll(appointmentDTOs);
|
||||||
|
Set<LocalDate> dates = appointmentDTOs.stream()
|
||||||
|
.filter(a -> a.getAppointmentDate() != null && !a.getAppointmentDate().isEmpty())
|
||||||
|
.map(a -> { try { return LocalDate.parse(a.getAppointmentDate()); } catch (Exception ex) { return null; } })
|
||||||
|
.filter(d -> d != null)
|
||||||
|
.collect(java.util.stream.Collectors.toSet());
|
||||||
|
calendarPane.setEventDates(dates);
|
||||||
|
applyFilterPredicate();
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -146,6 +202,10 @@ public class AppointmentController {
|
|||||||
@FXML
|
@FXML
|
||||||
void btnRefresh(ActionEvent event) {
|
void btnRefresh(ActionEvent event) {
|
||||||
txtSearch.clear();
|
txtSearch.clear();
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
selectedCalendarDate = null;
|
||||||
|
filtered.setPredicate(a -> true);
|
||||||
tvAppointments.getSortOrder().clear();
|
tvAppointments.getSortOrder().clear();
|
||||||
loadAppointments();
|
loadAppointments();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -247,6 +307,44 @@ public class AppointmentController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadStoreFilter() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
DropdownOption allStores = new DropdownOption();
|
||||||
|
allStores.setLabel("All Stores");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
storeOptions.clear();
|
||||||
|
storeOptions.addAll(stores);
|
||||||
|
java.util.List<DropdownOption> items = new java.util.ArrayList<>();
|
||||||
|
items.add(allStores);
|
||||||
|
items.addAll(stores);
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("AppointmentController.loadStoreFilter", e, "Loading store filter");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long selectedStoreId() {
|
||||||
|
if (!UserSession.getInstance().isAdmin()) return UserSession.getInstance().getStoreId();
|
||||||
|
DropdownOption selected = cbStoreFilter.getValue();
|
||||||
|
return (selected != null && selected.getId() != null) ? selected.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyFilterPredicate() {
|
||||||
|
String selectedStatus = cbStatusFilter.getValue();
|
||||||
|
filtered.setPredicate(apt -> {
|
||||||
|
boolean dateMatch = selectedCalendarDate == null
|
||||||
|
|| apt.getAppointmentDate().equals(selectedCalendarDate.toString());
|
||||||
|
boolean statusMatch = selectedStatus == null || selectedStatus.equals("All Statuses")
|
||||||
|
|| apt.getAppointmentStatus().equalsIgnoreCase(selectedStatus);
|
||||||
|
return dateMatch && statusMatch;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void showAlert(String title, String msg){
|
private void showAlert(String title, String msg){
|
||||||
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||||
alert.setTitle(title);
|
alert.setTitle(title);
|
||||||
@@ -269,7 +367,8 @@ public class AppointmentController {
|
|||||||
response.getAppointmentDate() != null ? response.getAppointmentDate().toString() : "",
|
response.getAppointmentDate() != null ? response.getAppointmentDate().toString() : "",
|
||||||
response.getAppointmentTime() != null ? response.getAppointmentTime().toString() : "",
|
response.getAppointmentTime() != null ? response.getAppointmentTime().toString() : "",
|
||||||
normalizeAppointmentStatus(response.getAppointmentStatus()),
|
normalizeAppointmentStatus(response.getAppointmentStatus()),
|
||||||
response.getStoreName()
|
response.getStoreName(),
|
||||||
|
response.getStoreId()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import org.example.petshopdesktop.api.dto.chat.UpdateConversationRequest;
|
|||||||
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.endpoints.ChatApi;
|
import org.example.petshopdesktop.api.endpoints.ChatApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
|
import org.example.petshopdesktop.api.ApiClient;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
@@ -176,33 +177,33 @@ public class ChatController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim();
|
String content = txtMessage.getText() == null ? "" : txtMessage.getText().trim();
|
||||||
if (content.isEmpty()) {
|
if (content.isEmpty() && selectedAttachmentFile == null) {
|
||||||
if (selectedAttachmentFile != null) {
|
|
||||||
lblChatStatus.setText("Attachments are not available yet");
|
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Long convId = selectedConversation.getId();
|
Long convId = selectedConversation.getId();
|
||||||
|
File attachmentFile = selectedAttachmentFile;
|
||||||
|
|
||||||
txtMessage.clear();
|
txtMessage.clear();
|
||||||
btnSend.setDisable(true);
|
btnSend.setDisable(true);
|
||||||
|
|
||||||
lblChatStatus.setText("Sending message...");
|
lblChatStatus.setText("Sending message...");
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
MessageRequest request = new MessageRequest(content);
|
MessageResponse response;
|
||||||
MessageResponse response = ChatApi.getInstance().sendMessage(convId, request);
|
if (attachmentFile != null) {
|
||||||
|
response = ChatApi.getInstance().sendMessageWithAttachment(convId, attachmentFile, content);
|
||||||
|
} else {
|
||||||
|
response = ChatApi.getInstance().sendMessage(convId, new MessageRequest(content));
|
||||||
|
}
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
btnSend.setDisable(false);
|
btnSend.setDisable(false);
|
||||||
|
realtimeClient.markConversationReplied(convId, UserSession.getInstance().getUserId());
|
||||||
appendMessageIfSelected(response);
|
appendMessageIfSelected(response);
|
||||||
if (selectedAttachmentFile != null) {
|
if (attachmentFile != null) {
|
||||||
clearLocalAttachment();
|
clearLocalAttachment();
|
||||||
lblChatStatus.setText("Message sent without attachment");
|
|
||||||
} else {
|
|
||||||
lblChatStatus.setText("Message sent");
|
|
||||||
}
|
}
|
||||||
|
lblChatStatus.setText("Message sent");
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
@@ -380,6 +381,7 @@ public class ChatController {
|
|||||||
List<ConversationResponse> response = ChatApi.getInstance().listConversations();
|
List<ConversationResponse> response = ChatApi.getInstance().listConversations();
|
||||||
response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
response.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
|
realtimeClient.initializeState(response);
|
||||||
conversations.setAll(response);
|
conversations.setAll(response);
|
||||||
refreshSections();
|
refreshSections();
|
||||||
restoreSelection();
|
restoreSelection();
|
||||||
@@ -471,6 +473,7 @@ public class ChatController {
|
|||||||
.findFirst()
|
.findFirst()
|
||||||
.ifPresent(conversation -> {
|
.ifPresent(conversation -> {
|
||||||
conversation.setLastMessage(message.getContent());
|
conversation.setLastMessage(message.getContent());
|
||||||
|
conversation.setLastSenderId(message.getSenderId());
|
||||||
conversation.setUpdatedAt(message.getTimestamp());
|
conversation.setUpdatedAt(message.getTimestamp());
|
||||||
conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
conversations.sort(Comparator.comparing(ChatController::conversationSortTime, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
refreshSections();
|
refreshSections();
|
||||||
@@ -504,14 +507,16 @@ public class ChatController {
|
|||||||
Circle avatar = new Circle(16);
|
Circle avatar = new Circle(16);
|
||||||
avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1"));
|
avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1"));
|
||||||
if (message.getSenderAvatarUrl() != null && !message.getSenderAvatarUrl().isBlank()) {
|
if (message.getSenderAvatarUrl() != null && !message.getSenderAvatarUrl().isBlank()) {
|
||||||
try {
|
String relativeUrl = message.getSenderAvatarUrl();
|
||||||
String fullUrl = org.example.petshopdesktop.api.ApiConfig.getInstance().getBaseUrl() + message.getSenderAvatarUrl();
|
new Thread(() -> {
|
||||||
Image img = new Image(fullUrl, true);
|
try {
|
||||||
img.errorProperty().addListener((obs, old, err) -> {
|
byte[] bytes = ApiClient.getInstance().getBytes(relativeUrl);
|
||||||
if (err) avatar.setFill(javafx.scene.paint.Color.web(mine ? "#0f766e" : "#cbd5e1"));
|
Image img = new Image(new java.io.ByteArrayInputStream(bytes));
|
||||||
});
|
if (!img.isError()) {
|
||||||
avatar.setFill(new ImagePattern(img));
|
Platform.runLater(() -> avatar.setFill(new ImagePattern(img)));
|
||||||
} catch (Exception ignored) {}
|
}
|
||||||
|
} catch (Exception ignored) {}
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
Label author = new Label(resolveAuthorLabel(message));
|
Label author = new Label(resolveAuthorLabel(message));
|
||||||
@@ -529,6 +534,12 @@ public class ChatController {
|
|||||||
content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";");
|
content.setStyle("-fx-text-fill: " + (mine ? "#ffffff" : "#1f2937") + ";");
|
||||||
bubble.getChildren().add(content);
|
bubble.getChildren().add(content);
|
||||||
}
|
}
|
||||||
|
if (message.getAttachmentName() != null && !message.getAttachmentName().isBlank()) {
|
||||||
|
Label attachmentLabel = new Label("\uD83D\uDCCE " + message.getAttachmentName());
|
||||||
|
attachmentLabel.setStyle("-fx-text-fill: " + (mine ? "#dbeafe" : "#475569") + "; -fx-font-size: 12px;");
|
||||||
|
attachmentLabel.setWrapText(true);
|
||||||
|
bubble.getChildren().add(attachmentLabel);
|
||||||
|
}
|
||||||
|
|
||||||
bubble.getChildren().add(timestamp);
|
bubble.getChildren().add(timestamp);
|
||||||
bubble.setMaxWidth(420);
|
bubble.setMaxWidth(420);
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ import javafx.collections.transformation.FilteredList;
|
|||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||||
import org.example.petshopdesktop.util.TableViewSupport;
|
import org.example.petshopdesktop.util.TableViewSupport;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -25,6 +29,9 @@ public class CustomerAccountsController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TableView<UserResponse> tvCustomers;
|
private TableView<UserResponse> tvCustomers;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableColumn<UserResponse, String> colCustomerAvatar;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TableColumn<UserResponse, String> colCustomerUsername;
|
private TableColumn<UserResponse, String> colCustomerUsername;
|
||||||
|
|
||||||
@@ -49,6 +56,9 @@ public class CustomerAccountsController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TextField txtSearchCustomer;
|
private TextField txtSearchCustomer;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbStatusFilter;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Button btnEditCustomer;
|
private Button btnEditCustomer;
|
||||||
|
|
||||||
@@ -66,6 +76,13 @@ public class CustomerAccountsController {
|
|||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
colCustomerAvatar.setCellValueFactory(data -> {
|
||||||
|
Long id = data.getValue().getId();
|
||||||
|
if (id == null) return new javafx.beans.property.SimpleStringProperty("");
|
||||||
|
return new javafx.beans.property.SimpleStringProperty("/api/v1/users/" + id + "/avatar/file");
|
||||||
|
});
|
||||||
|
configureAvatarColumn(colCustomerAvatar);
|
||||||
|
|
||||||
colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername()));
|
colCustomerUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername()));
|
||||||
colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName()));
|
colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName()));
|
||||||
colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail()));
|
colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail()));
|
||||||
@@ -82,14 +99,40 @@ public class CustomerAccountsController {
|
|||||||
btnEditCustomer.setDisable(newVal == null));
|
btnEditCustomer.setDisable(newVal == null));
|
||||||
btnEditCustomer.setDisable(true);
|
btnEditCustomer.setDisable(true);
|
||||||
|
|
||||||
|
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Active", "Inactive"));
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStatusFilter.valueProperty().addListener((obs, o, n) -> applyCustomerFilter(txtSearchCustomer.getText()));
|
||||||
|
|
||||||
txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n));
|
txtSearchCustomer.textProperty().addListener((obs, o, n) -> applyCustomerFilter(n));
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void configureAvatarColumn(TableColumn<UserResponse, String> column) {
|
||||||
|
column.setCellFactory(col -> new TableCell<>() {
|
||||||
|
private final ImageView imageView = new ImageView();
|
||||||
|
private final StackPane container = new StackPane(imageView);
|
||||||
|
{
|
||||||
|
container.setAlignment(Pos.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null || item.isBlank()) {
|
||||||
|
setGraphic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DesktopImageSupport.loadImageInto(imageView, item, 48, 48);
|
||||||
|
setGraphic(container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnRefreshClicked(ActionEvent event) {
|
void btnRefreshClicked(ActionEvent event) {
|
||||||
txtSearchCustomer.clear();
|
txtSearchCustomer.clear();
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
TableViewSupport.clearSort(tvCustomers);
|
TableViewSupport.clearSort(tvCustomers);
|
||||||
refresh();
|
refresh();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -108,15 +151,15 @@ public class CustomerAccountsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/staff-edit-dialog-view.fxml"));
|
FXMLLoader loader = new FXMLLoader(getClass().getResource("/org/example/petshopdesktop/dialogviews/customer-edit-dialog-view.fxml"));
|
||||||
Stage dialog = new Stage();
|
Stage dialog = new Stage();
|
||||||
dialog.initOwner(tvCustomers.getScene().getWindow());
|
dialog.initOwner(tvCustomers.getScene().getWindow());
|
||||||
dialog.initModality(Modality.APPLICATION_MODAL);
|
dialog.initModality(Modality.APPLICATION_MODAL);
|
||||||
dialog.setTitle("Edit Customer Account");
|
dialog.setTitle("Edit Customer Account");
|
||||||
dialog.setScene(new Scene(loader.load()));
|
dialog.setScene(new Scene(loader.load()));
|
||||||
dialog.setResizable(false);
|
dialog.setResizable(false);
|
||||||
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
|
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.CustomerEditDialogController) loader.getController();
|
||||||
controller.setUser(selected);
|
controller.setCustomer(selected);
|
||||||
dialog.showAndWait();
|
dialog.showAndWait();
|
||||||
refresh();
|
refresh();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
@@ -154,16 +197,19 @@ public class CustomerAccountsController {
|
|||||||
|
|
||||||
private void applyCustomerFilter(String text) {
|
private void applyCustomerFilter(String text) {
|
||||||
String q = text == null ? "" : text.trim().toLowerCase();
|
String q = text == null ? "" : text.trim().toLowerCase();
|
||||||
if (q.isEmpty()) {
|
String selectedStatus = cbStatusFilter.getValue();
|
||||||
filteredCustomers.setPredicate(a -> true);
|
filteredCustomers.setPredicate(a -> {
|
||||||
return;
|
boolean textMatch = q.isEmpty()
|
||||||
}
|
|| safe(a.getUsername()).contains(q)
|
||||||
filteredCustomers.setPredicate(a ->
|
|| safe(a.getFullName()).contains(q)
|
||||||
safe(a.getUsername()).contains(q)
|
|| safe(a.getEmail()).contains(q)
|
||||||
|| safe(a.getFullName()).contains(q)
|
|| safe(a.getPhone()).contains(q);
|
||||||
|| safe(a.getEmail()).contains(q)
|
boolean active = Boolean.TRUE.equals(a.getActive());
|
||||||
|| safe(a.getPhone()).contains(q)
|
boolean statusMatch = selectedStatus == null || selectedStatus.equals("All Statuses")
|
||||||
);
|
|| (selectedStatus.equals("Active") && active)
|
||||||
|
|| (selectedStatus.equals("Inactive") && !active);
|
||||||
|
return textMatch && statusMatch;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String safe(String v) {
|
private static String safe(String v) {
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import javafx.scene.control.cell.PropertyValueFactory;
|
|||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.dto.inventory.InventoryResponse;
|
import org.example.petshopdesktop.api.dto.inventory.InventoryResponse;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.InventoryApi;
|
import org.example.petshopdesktop.api.endpoints.InventoryApi;
|
||||||
import org.example.petshopdesktop.controllers.dialogcontrollers.InventoryDialogController;
|
import org.example.petshopdesktop.controllers.dialogcontrollers.InventoryDialogController;
|
||||||
import org.example.petshopdesktop.models.Inventory;
|
import org.example.petshopdesktop.models.Inventory;
|
||||||
@@ -63,6 +65,11 @@ public class InventoryController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TextField txtSearch;
|
private TextField txtSearch;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<DropdownOption> cbStoreFilter;
|
||||||
|
|
||||||
|
private final java.util.ArrayList<DropdownOption> storeOptions = new java.util.ArrayList<>();
|
||||||
|
|
||||||
private ObservableList<Inventory> data = FXCollections.observableArrayList();
|
private ObservableList<Inventory> data = FXCollections.observableArrayList();
|
||||||
|
|
||||||
//Determines if in add/edit mode
|
//Determines if in add/edit mode
|
||||||
@@ -81,6 +88,13 @@ public class InventoryController {
|
|||||||
colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
colQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
||||||
colStoreName.setCellValueFactory(new PropertyValueFactory<>("storeName"));
|
colStoreName.setCellValueFactory(new PropertyValueFactory<>("storeName"));
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
cbStoreFilter.setVisible(true);
|
||||||
|
cbStoreFilter.setManaged(true);
|
||||||
|
loadStoreFilter();
|
||||||
|
}
|
||||||
|
cbStoreFilter.valueProperty().addListener((obs, o, n) -> displayInventory());
|
||||||
|
|
||||||
displayInventory();
|
displayInventory();
|
||||||
TableViewSupport.installDoubleClickAction(tvInventory, selected -> openDialog(selected, "Edit"));
|
TableViewSupport.installDoubleClickAction(tvInventory, selected -> openDialog(selected, "Edit"));
|
||||||
|
|
||||||
@@ -108,6 +122,7 @@ public class InventoryController {
|
|||||||
@FXML
|
@FXML
|
||||||
void btnRefresh(ActionEvent event) {
|
void btnRefresh(ActionEvent event) {
|
||||||
txtSearch.clear();
|
txtSearch.clear();
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
tvInventory.getSortOrder().clear();
|
tvInventory.getSortOrder().clear();
|
||||||
displayInventory();
|
displayInventory();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -172,7 +187,7 @@ public class InventoryController {
|
|||||||
} else {
|
} else {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Long storeId = selectedStoreId();
|
||||||
List<InventoryResponse> inventories = InventoryApi.getInstance().listInventory(filter, storeId);
|
List<InventoryResponse> inventories = InventoryApi.getInstance().listInventory(filter, storeId);
|
||||||
List<Inventory> inventoryList = inventories.stream()
|
List<Inventory> inventoryList = inventories.stream()
|
||||||
.map(this::mapToInventory)
|
.map(this::mapToInventory)
|
||||||
@@ -198,7 +213,7 @@ public class InventoryController {
|
|||||||
private void displayInventory() {
|
private void displayInventory() {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Long storeId = selectedStoreId();
|
||||||
List<InventoryResponse> inventories = InventoryApi.getInstance().listInventory(null, storeId);
|
List<InventoryResponse> inventories = InventoryApi.getInstance().listInventory(null, storeId);
|
||||||
List<Inventory> inventoryList = inventories.stream()
|
List<Inventory> inventoryList = inventories.stream()
|
||||||
.map(this::mapToInventory)
|
.map(this::mapToInventory)
|
||||||
@@ -253,6 +268,33 @@ public class InventoryController {
|
|||||||
txtSearch.setText("");
|
txtSearch.setText("");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadStoreFilter() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
DropdownOption allStores = new DropdownOption();
|
||||||
|
allStores.setLabel("All Stores");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
storeOptions.clear();
|
||||||
|
storeOptions.addAll(stores);
|
||||||
|
java.util.List<DropdownOption> items = new java.util.ArrayList<>();
|
||||||
|
items.add(allStores);
|
||||||
|
items.addAll(stores);
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("InventoryController.loadStoreFilter", e, "Loading store filter");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long selectedStoreId() {
|
||||||
|
if (!UserSession.getInstance().isAdmin()) return UserSession.getInstance().getStoreId();
|
||||||
|
DropdownOption selected = cbStoreFilter.getValue();
|
||||||
|
return (selected != null && selected.getId() != null) ? selected.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
private Inventory mapToInventory(InventoryResponse response) {
|
private Inventory mapToInventory(InventoryResponse response) {
|
||||||
return new Inventory(
|
return new Inventory(
|
||||||
response.getInventoryId().intValue(),
|
response.getInventoryId().intValue(),
|
||||||
|
|||||||
@@ -1,12 +1,15 @@
|
|||||||
package org.example.petshopdesktop.controllers;
|
package org.example.petshopdesktop.controllers;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
|
import javafx.scene.control.TextInputDialog;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.layout.StackPane;
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
@@ -14,6 +17,7 @@ import org.example.petshopdesktop.api.ApiClient;
|
|||||||
import org.example.petshopdesktop.api.dto.auth.LoginRequest;
|
import org.example.petshopdesktop.api.dto.auth.LoginRequest;
|
||||||
import org.example.petshopdesktop.api.dto.auth.LoginResponse;
|
import org.example.petshopdesktop.api.dto.auth.LoginResponse;
|
||||||
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
import org.example.petshopdesktop.api.dto.auth.UserInfoResponse;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.AuthApi;
|
||||||
import org.example.petshopdesktop.auth.Role;
|
import org.example.petshopdesktop.auth.Role;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
import org.example.petshopdesktop.ui.SvgWebViewFactory;
|
||||||
@@ -105,6 +109,42 @@ public class LoginController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void lnkForgotPasswordClicked(ActionEvent event) {
|
||||||
|
TextInputDialog dialog = new TextInputDialog();
|
||||||
|
dialog.setTitle("Forgot Password");
|
||||||
|
dialog.setHeaderText("Reset your password");
|
||||||
|
dialog.setContentText("Enter your username or email:");
|
||||||
|
|
||||||
|
dialog.showAndWait().ifPresent(input -> {
|
||||||
|
if (input.trim().isEmpty()) return;
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
AuthApi.getInstance().forgotPassword(input.trim());
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.INFORMATION);
|
||||||
|
alert.setTitle("Reset Link Sent");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText("If this account exists, a password reset link has been sent to the associated email.");
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"LoginController.lnkForgotPasswordClicked",
|
||||||
|
e,
|
||||||
|
"Forgot password request for: " + input.trim());
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
|
alert.setTitle("Error");
|
||||||
|
alert.setHeaderText(null);
|
||||||
|
alert.setContentText("Could not send reset link. Please try again.");
|
||||||
|
alert.showAndWait();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
private void openMainLayout() {
|
private void openMainLayout() {
|
||||||
try {
|
try {
|
||||||
FXMLLoader loader = new FXMLLoader(
|
FXMLLoader loader = new FXMLLoader(
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ public class MainLayoutController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Button btnRemoveAvatar;
|
private Button btnRemoveAvatar;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnRefreshAvatar;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label lblUsername;
|
private Label lblUsername;
|
||||||
|
|
||||||
@@ -253,6 +256,29 @@ public class MainLayoutController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnRefreshAvatarClicked(ActionEvent event) {
|
||||||
|
btnRefreshAvatar.setDisable(true);
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
UserInfoResponse userInfo = AuthApi.getInstance().getCurrentUser();
|
||||||
|
String displayName = userInfo.getFullName() == null || userInfo.getFullName().isBlank()
|
||||||
|
? UserSession.getInstance().getUsername()
|
||||||
|
: userInfo.getFullName();
|
||||||
|
Image avatarImage = loadAvatarImage(userInfo.getAvatarUrl());
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
UserSession.getInstance().setAvatarUrl(userInfo.getAvatarUrl());
|
||||||
|
renderAvatar(displayName, avatarImage);
|
||||||
|
btnRemoveAvatar.setDisable(userInfo.getAvatarUrl() == null || userInfo.getAvatarUrl().isBlank());
|
||||||
|
btnRefreshAvatar.setDisable(false);
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> btnRefreshAvatar.setDisable(false));
|
||||||
|
ActivityLogger.getInstance().logException("MainLayoutController.btnRefreshAvatarClicked", e, "Refreshing avatar");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnRemoveAvatarClicked(ActionEvent event) {
|
void btnRemoveAvatarClicked(ActionEvent event) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -86,18 +86,20 @@ public class PetController {
|
|||||||
@FXML
|
@FXML
|
||||||
private ComboBox<String> cbStatusFilter;
|
private ComboBox<String> cbStatusFilter;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<DropdownOption> cbStoreFilter;
|
||||||
|
|
||||||
|
private final java.util.ArrayList<DropdownOption> storeOptions = new java.util.ArrayList<>();
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TextField txtSearch;
|
private TextField txtSearch;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnRefresh(ActionEvent event) {
|
void btnRefresh(ActionEvent event) {
|
||||||
txtSearch.clear();
|
txtSearch.clear();
|
||||||
if (cbSpeciesFilter != null) {
|
if (cbSpeciesFilter != null) cbSpeciesFilter.getSelectionModel().selectFirst();
|
||||||
cbSpeciesFilter.getSelectionModel().selectFirst();
|
if (cbStatusFilter != null) cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
}
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
if (cbStatusFilter != null) {
|
|
||||||
cbStatusFilter.getSelectionModel().selectFirst();
|
|
||||||
}
|
|
||||||
tvPets.getSortOrder().clear();
|
tvPets.getSortOrder().clear();
|
||||||
displayPets();
|
displayPets();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -194,6 +196,13 @@ public class PetController {
|
|||||||
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Owned", "Pending"));
|
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Available", "Adopted", "Owned", "Pending"));
|
||||||
cbStatusFilter.getSelectionModel().selectFirst();
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
cbStoreFilter.setVisible(true);
|
||||||
|
cbStoreFilter.setManaged(true);
|
||||||
|
loadStoreFilter();
|
||||||
|
}
|
||||||
|
cbStoreFilter.valueProperty().addListener((obs, o, n) -> applyFilters());
|
||||||
|
|
||||||
displayPets();
|
displayPets();
|
||||||
TableViewSupport.installDoubleClickAction(tvPets, selected -> openDialog(selected, "Edit"));
|
TableViewSupport.installDoubleClickAction(tvPets, selected -> openDialog(selected, "Edit"));
|
||||||
|
|
||||||
@@ -229,7 +238,7 @@ public class PetController {
|
|||||||
} else {
|
} else {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Long storeId = selectedStoreId();
|
||||||
List<PetResponse> pets = PetApi.getInstance().listPets(filter, species, status, storeId);
|
List<PetResponse> pets = PetApi.getInstance().listPets(filter, species, status, storeId);
|
||||||
List<Pet> petList = pets.stream()
|
List<Pet> petList = pets.stream()
|
||||||
.map(this::mapToPet)
|
.map(this::mapToPet)
|
||||||
@@ -255,7 +264,7 @@ public class PetController {
|
|||||||
private void displayPets() {
|
private void displayPets() {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
Long storeId = selectedStoreId();
|
||||||
List<PetResponse> pets = PetApi.getInstance().listPets(null, selectedSpecies(), selectedStatus(), storeId);
|
List<PetResponse> pets = PetApi.getInstance().listPets(null, selectedSpecies(), selectedStatus(), storeId);
|
||||||
List<Pet> petList = pets.stream()
|
List<Pet> petList = pets.stream()
|
||||||
.map(this::mapToPet)
|
.map(this::mapToPet)
|
||||||
@@ -281,6 +290,33 @@ public class PetController {
|
|||||||
displayFilteredPet(txtSearch.getText());
|
displayFilteredPet(txtSearch.getText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadStoreFilter() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
DropdownOption allStores = new DropdownOption();
|
||||||
|
allStores.setLabel("All Stores");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
storeOptions.clear();
|
||||||
|
storeOptions.addAll(stores);
|
||||||
|
java.util.List<DropdownOption> items = new java.util.ArrayList<>();
|
||||||
|
items.add(allStores);
|
||||||
|
items.addAll(stores);
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("PetController.loadStoreFilter", e, "Loading store filter");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long selectedStoreId() {
|
||||||
|
if (!UserSession.getInstance().isAdmin()) return UserSession.getInstance().getStoreId();
|
||||||
|
DropdownOption selected = cbStoreFilter.getValue();
|
||||||
|
return (selected != null && selected.getId() != null) ? selected.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
private void loadSpeciesFilter() {
|
private void loadSpeciesFilter() {
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import javafx.application.Platform;
|
|||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.CheckBox;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.SelectionMode;
|
import javafx.scene.control.SelectionMode;
|
||||||
@@ -19,18 +20,25 @@ import javafx.scene.control.TableColumn;
|
|||||||
import javafx.scene.control.TableView;
|
import javafx.scene.control.TableView;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import javafx.concurrent.Task;
|
import javafx.concurrent.Task;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.SaleApi;
|
import org.example.petshopdesktop.api.endpoints.SaleApi;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.CouponApi;
|
||||||
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
|
import org.example.petshopdesktop.api.dto.coupon.CouponResponse;
|
||||||
import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
|
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
import org.example.petshopdesktop.api.dto.sale.SaleItemResponse;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleRequest;
|
import org.example.petshopdesktop.api.dto.sale.SaleRequest;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
|
import org.example.petshopdesktop.api.dto.sale.SaleResponse;
|
||||||
|
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
||||||
import org.example.petshopdesktop.models.Product;
|
import org.example.petshopdesktop.models.Product;
|
||||||
import org.example.petshopdesktop.models.SaleCartItem;
|
import org.example.petshopdesktop.models.SaleCartItem;
|
||||||
import org.example.petshopdesktop.models.SaleDetail;
|
import org.example.petshopdesktop.models.SaleDetail;
|
||||||
@@ -111,6 +119,9 @@ public class SaleController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TableColumn<SaleLineItem, String> colEmployeeName;
|
private TableColumn<SaleLineItem, String> colEmployeeName;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TableColumn<SaleLineItem, String> colCustomerName;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private TableColumn<SaleLineItem, String> colServiceProduct;
|
private TableColumn<SaleLineItem, String> colServiceProduct;
|
||||||
|
|
||||||
@@ -135,11 +146,73 @@ public class SaleController {
|
|||||||
@FXML
|
@FXML
|
||||||
private TextField txtSearch;
|
private TextField txtSearch;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbFilterPayment;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<String> cbFilterRefundStatus;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<DropdownOption> cbFilterCustomer;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<DropdownOption> cbStoreFilter;
|
||||||
|
|
||||||
|
private final java.util.ArrayList<DropdownOption> storeOptions = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private ComboBox<DropdownOption> cbCustomer;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private CheckBox chkUseLoyaltyPoints;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblLoyaltyPoints;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private TextField txtCouponCode;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnApplyCoupon;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Button btnClearCoupon;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblCouponStatus;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblSubtotal;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox hbCouponDiscount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblCouponDiscount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox hbLoyaltyDiscount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblLoyaltyDiscountLabel;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblLoyaltyDiscount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox hbPointsToEarn;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblPointsToEarn;
|
||||||
|
|
||||||
private final ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
|
private final ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
|
||||||
private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList();
|
private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList();
|
||||||
private FilteredList<SaleLineItem> filteredSales;
|
private FilteredList<SaleLineItem> filteredSales;
|
||||||
private boolean saleSaveInProgress;
|
private boolean saleSaveInProgress;
|
||||||
|
|
||||||
|
private CouponResponse appliedCoupon = null;
|
||||||
|
private UserResponse selectedCustomerData = null;
|
||||||
|
|
||||||
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
|
private final NumberFormat currency = NumberFormat.getCurrencyInstance(Locale.CANADA);
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm");
|
||||||
|
|
||||||
@@ -173,6 +246,8 @@ public class SaleController {
|
|||||||
colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId"));
|
colSaleId.setCellValueFactory(new PropertyValueFactory<>("saleId"));
|
||||||
colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate"));
|
colSaleDate.setCellValueFactory(new PropertyValueFactory<>("saleDate"));
|
||||||
colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName"));
|
colEmployeeName.setCellValueFactory(new PropertyValueFactory<>("employeeName"));
|
||||||
|
colCustomerName.setCellValueFactory(new PropertyValueFactory<>("customerName"));
|
||||||
|
colCustomerName.setMinWidth(120);
|
||||||
colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName"));
|
colServiceProduct.setCellValueFactory(new PropertyValueFactory<>("itemName"));
|
||||||
colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
colSaleQuantity.setCellValueFactory(new PropertyValueFactory<>("quantity"));
|
||||||
colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
|
colSaleUnitPrice.setCellValueFactory(new PropertyValueFactory<>("unitPrice"));
|
||||||
@@ -194,6 +269,24 @@ public class SaleController {
|
|||||||
TableViewSupport.bindSortedItems(tvSales, filteredSales);
|
TableViewSupport.bindSortedItems(tvSales, filteredSales);
|
||||||
TableViewSupport.installDoubleClickAction(tvSales, selected -> openSaleDetailDialog(selected.getSaleId()));
|
TableViewSupport.installDoubleClickAction(tvSales, selected -> openSaleDetailDialog(selected.getSaleId()));
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
cbStoreFilter.setVisible(true);
|
||||||
|
cbStoreFilter.setManaged(true);
|
||||||
|
loadStoreFilter();
|
||||||
|
}
|
||||||
|
cbStoreFilter.valueProperty().addListener((obs, o, n) -> refreshSales(false));
|
||||||
|
|
||||||
|
cbFilterPayment.setItems(FXCollections.observableArrayList("All Payments", "Cash", "Card"));
|
||||||
|
cbFilterPayment.getSelectionModel().selectFirst();
|
||||||
|
cbFilterPayment.valueProperty().addListener((obs, o, n) -> applySalesFilter(txtSearch.getText()));
|
||||||
|
|
||||||
|
cbFilterRefundStatus.setItems(FXCollections.observableArrayList("All Status", "Sales Only", "Refunds Only"));
|
||||||
|
cbFilterRefundStatus.getSelectionModel().selectFirst();
|
||||||
|
cbFilterRefundStatus.valueProperty().addListener((obs, o, n) -> applySalesFilter(txtSearch.getText()));
|
||||||
|
|
||||||
|
loadCustomerFilter();
|
||||||
|
cbFilterCustomer.valueProperty().addListener((obs, o, n) -> applySalesFilter(txtSearch.getText()));
|
||||||
|
|
||||||
txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal));
|
txtSearch.textProperty().addListener((obs, oldVal, newVal) -> applySalesFilter(newVal));
|
||||||
tvSales.widthProperty().addListener((obs, oldWidth, newWidth) -> updateSalesColumnWidths(newWidth.doubleValue()));
|
tvSales.widthProperty().addListener((obs, oldWidth, newWidth) -> updateSalesColumnWidths(newWidth.doubleValue()));
|
||||||
tvCart.widthProperty().addListener((obs, oldWidth, newWidth) -> updateCartColumnWidths(newWidth.doubleValue()));
|
tvCart.widthProperty().addListener((obs, oldWidth, newWidth) -> updateCartColumnWidths(newWidth.doubleValue()));
|
||||||
@@ -214,7 +307,23 @@ public class SaleController {
|
|||||||
|
|
||||||
setCreateSaleControlsDisabled(true);
|
setCreateSaleControlsDisabled(true);
|
||||||
|
|
||||||
Task<ObservableList<Product>> task = new Task<>() {
|
cbCustomer.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
if (newVal != null) {
|
||||||
|
loadCustomerDetails(newVal.getId());
|
||||||
|
} else {
|
||||||
|
selectedCustomerData = null;
|
||||||
|
lblLoyaltyPoints.setVisible(false);
|
||||||
|
lblLoyaltyPoints.setManaged(false);
|
||||||
|
chkUseLoyaltyPoints.setVisible(false);
|
||||||
|
chkUseLoyaltyPoints.setManaged(false);
|
||||||
|
chkUseLoyaltyPoints.setSelected(false);
|
||||||
|
updateCartTotal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
chkUseLoyaltyPoints.selectedProperty().addListener((obs, oldVal, newVal) -> updateCartTotal());
|
||||||
|
|
||||||
|
Task<ObservableList<Product>> productsTask = new Task<>() {
|
||||||
@Override
|
@Override
|
||||||
protected ObservableList<Product> call() throws Exception {
|
protected ObservableList<Product> call() throws Exception {
|
||||||
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
|
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
|
||||||
@@ -232,13 +341,13 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
task.setOnSucceeded(event -> {
|
productsTask.setOnSucceeded(event -> {
|
||||||
cbProduct.setItems(task.getValue());
|
cbProduct.setItems(productsTask.getValue());
|
||||||
setCreateSaleControlsDisabled(false);
|
setCreateSaleControlsDisabled(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
task.setOnFailed(event -> {
|
productsTask.setOnFailed(event -> {
|
||||||
Throwable e = task.getException();
|
Throwable e = productsTask.getException();
|
||||||
ActivityLogger.getInstance().logException(
|
ActivityLogger.getInstance().logException(
|
||||||
"SaleController.setupCreateSale",
|
"SaleController.setupCreateSale",
|
||||||
e instanceof Exception ? (Exception) e : new RuntimeException(e),
|
e instanceof Exception ? (Exception) e : new RuntimeException(e),
|
||||||
@@ -248,6 +357,73 @@ public class SaleController {
|
|||||||
showError("Sales", "Could not load products. Check the backend connection and refresh the view.");
|
showError("Sales", "Could not load products. Check the backend connection and refresh the view.");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
new Thread(productsTask).start();
|
||||||
|
|
||||||
|
Task<List<DropdownOption>> customersTask = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected List<DropdownOption> call() throws Exception {
|
||||||
|
return DropdownApi.getInstance().getCustomers();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
customersTask.setOnSucceeded(event -> {
|
||||||
|
List<DropdownOption> customers = customersTask.getValue();
|
||||||
|
ObservableList<DropdownOption> customerOptions = FXCollections.observableArrayList();
|
||||||
|
customerOptions.addAll(customers);
|
||||||
|
cbCustomer.setItems(customerOptions);
|
||||||
|
});
|
||||||
|
|
||||||
|
customersTask.setOnFailed(event -> {
|
||||||
|
Throwable e = customersTask.getException();
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"SaleController.setupCreateSale",
|
||||||
|
e instanceof Exception ? (Exception) e : new RuntimeException(e),
|
||||||
|
"Loading customers"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
new Thread(customersTask).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadCustomerDetails(Long customerId) {
|
||||||
|
Task<UserResponse> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected UserResponse call() throws Exception {
|
||||||
|
return CustomerApi.getInstance().getCustomerById(customerId);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(event -> {
|
||||||
|
selectedCustomerData = task.getValue();
|
||||||
|
if (selectedCustomerData != null) {
|
||||||
|
int pts = selectedCustomerData.getLoyaltyPoints() != null ? selectedCustomerData.getLoyaltyPoints() : 0;
|
||||||
|
lblLoyaltyPoints.setText(pts + " pts available");
|
||||||
|
lblLoyaltyPoints.setVisible(true);
|
||||||
|
lblLoyaltyPoints.setManaged(true);
|
||||||
|
boolean canRedeem = pts >= 20;
|
||||||
|
chkUseLoyaltyPoints.setVisible(canRedeem);
|
||||||
|
chkUseLoyaltyPoints.setManaged(canRedeem);
|
||||||
|
if (!canRedeem) chkUseLoyaltyPoints.setSelected(false);
|
||||||
|
} else {
|
||||||
|
lblLoyaltyPoints.setVisible(false);
|
||||||
|
lblLoyaltyPoints.setManaged(false);
|
||||||
|
chkUseLoyaltyPoints.setVisible(false);
|
||||||
|
chkUseLoyaltyPoints.setManaged(false);
|
||||||
|
chkUseLoyaltyPoints.setSelected(false);
|
||||||
|
}
|
||||||
|
updateCartTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(event -> {
|
||||||
|
selectedCustomerData = null;
|
||||||
|
Throwable e = task.getException();
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"SaleController.loadCustomerDetails",
|
||||||
|
e instanceof Exception ? (Exception) e : new RuntimeException(e),
|
||||||
|
"Loading customer details"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
new Thread(task).start();
|
new Thread(task).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -269,8 +445,7 @@ public class SaleController {
|
|||||||
Task<List<SaleLineItem>> task = new Task<List<SaleLineItem>>() {
|
Task<List<SaleLineItem>> task = new Task<List<SaleLineItem>>() {
|
||||||
@Override
|
@Override
|
||||||
protected List<SaleLineItem> call() throws Exception {
|
protected List<SaleLineItem> call() throws Exception {
|
||||||
Long storeId = UserSession.getInstance().isAdmin() ? null : UserSession.getInstance().getStoreId();
|
List<SaleResponse> sales = SaleApi.getInstance().listAllSales(null, selectedStoreId());
|
||||||
List<SaleResponse> sales = SaleApi.getInstance().listAllSales(null, storeId);
|
|
||||||
sales.sort(Comparator.comparing(SaleResponse::getSaleDate, Comparator.nullsLast(Comparator.reverseOrder()))
|
sales.sort(Comparator.comparing(SaleResponse::getSaleDate, Comparator.nullsLast(Comparator.reverseOrder()))
|
||||||
.thenComparing(SaleResponse::getSaleId, Comparator.nullsLast(Comparator.reverseOrder())));
|
.thenComparing(SaleResponse::getSaleId, Comparator.nullsLast(Comparator.reverseOrder())));
|
||||||
List<SaleLineItem> lineItems = new ArrayList<>();
|
List<SaleLineItem> lineItems = new ArrayList<>();
|
||||||
@@ -281,6 +456,9 @@ public class SaleController {
|
|||||||
: "";
|
: "";
|
||||||
|
|
||||||
if (sale.getItems() != null && !sale.getItems().isEmpty()) {
|
if (sale.getItems() != null && !sale.getItems().isEmpty()) {
|
||||||
|
double saleSubtotal = sale.getSubtotalAmount() != null ? Math.abs(sale.getSubtotalAmount().doubleValue()) : 0;
|
||||||
|
double saleActualTotal = sale.getTotalAmount() != null ? Math.abs(sale.getTotalAmount().doubleValue()) : 0;
|
||||||
|
double discountRatio = saleSubtotal > 0 ? saleActualTotal / saleSubtotal : 1.0;
|
||||||
for (SaleItemResponse item : sale.getItems()) {
|
for (SaleItemResponse item : sale.getItems()) {
|
||||||
boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund();
|
boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund();
|
||||||
double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0;
|
double unitPrice = item.getUnitPrice() != null ? item.getUnitPrice().doubleValue() : 0.0;
|
||||||
@@ -288,7 +466,7 @@ public class SaleController {
|
|||||||
if (isRefund && quantity > 0) {
|
if (isRefund && quantity > 0) {
|
||||||
quantity = -quantity;
|
quantity = -quantity;
|
||||||
}
|
}
|
||||||
double lineTotal = unitPrice * quantity;
|
double lineTotal = unitPrice * quantity * discountRatio;
|
||||||
lineItems.add(new SaleLineItem(
|
lineItems.add(new SaleLineItem(
|
||||||
sale.getSaleId().intValue(),
|
sale.getSaleId().intValue(),
|
||||||
saleDate,
|
saleDate,
|
||||||
@@ -299,7 +477,8 @@ public class SaleController {
|
|||||||
lineTotal,
|
lineTotal,
|
||||||
sale.getPaymentMethod(),
|
sale.getPaymentMethod(),
|
||||||
isRefund,
|
isRefund,
|
||||||
sale.getStoreName()
|
sale.getStoreName(),
|
||||||
|
sale.getCustomerName()
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -326,6 +505,10 @@ public class SaleController {
|
|||||||
@FXML
|
@FXML
|
||||||
void btnRefresh(ActionEvent event) {
|
void btnRefresh(ActionEvent event) {
|
||||||
txtSearch.clear();
|
txtSearch.clear();
|
||||||
|
cbFilterPayment.getSelectionModel().selectFirst();
|
||||||
|
cbFilterRefundStatus.getSelectionModel().selectFirst();
|
||||||
|
cbFilterCustomer.getSelectionModel().selectFirst();
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
TableViewSupport.clearSort(tvSales);
|
TableViewSupport.clearSort(tvSales);
|
||||||
refreshSales(false);
|
refreshSales(false);
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -379,6 +562,73 @@ public class SaleController {
|
|||||||
updateCartTotal();
|
updateCartTotal();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnApplyCoupon(ActionEvent event) {
|
||||||
|
String code = txtCouponCode.getText().trim();
|
||||||
|
if (code.isEmpty()) {
|
||||||
|
lblCouponStatus.setText("Enter a coupon code.");
|
||||||
|
lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
BigDecimal subtotal = BigDecimal.valueOf(cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum());
|
||||||
|
|
||||||
|
Task<CouponResponse> task = new Task<>() {
|
||||||
|
@Override
|
||||||
|
protected CouponResponse call() throws Exception {
|
||||||
|
return CouponApi.getInstance().getCouponByCode(code);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
task.setOnSucceeded(evt -> {
|
||||||
|
CouponResponse coupon = task.getValue();
|
||||||
|
if (coupon == null) {
|
||||||
|
lblCouponStatus.setText("Coupon not found.");
|
||||||
|
lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!Boolean.TRUE.equals(coupon.getActive())) {
|
||||||
|
lblCouponStatus.setText("Coupon is not active.");
|
||||||
|
lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (coupon.getMinOrderAmount() != null && subtotal.compareTo(coupon.getMinOrderAmount()) < 0) {
|
||||||
|
lblCouponStatus.setText("Order minimum not met ($" + coupon.getMinOrderAmount() + ").");
|
||||||
|
lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
appliedCoupon = coupon;
|
||||||
|
txtCouponCode.setDisable(true);
|
||||||
|
lblCouponStatus.setText("Coupon applied: " + coupon.getCouponCode());
|
||||||
|
lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#27ae60"));
|
||||||
|
updateCartTotal();
|
||||||
|
});
|
||||||
|
|
||||||
|
task.setOnFailed(evt -> {
|
||||||
|
Throwable e = task.getException();
|
||||||
|
lblCouponStatus.setText("Invalid or not found.");
|
||||||
|
lblCouponStatus.setTextFill(javafx.scene.paint.Color.web("#e74c3c"));
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"SaleController.btnApplyCoupon",
|
||||||
|
e instanceof Exception ? (Exception) e : new RuntimeException(e),
|
||||||
|
"Applying coupon"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
new Thread(task).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnClearCoupon(ActionEvent event) {
|
||||||
|
appliedCoupon = null;
|
||||||
|
txtCouponCode.clear();
|
||||||
|
txtCouponCode.setDisable(false);
|
||||||
|
lblCouponStatus.setText("");
|
||||||
|
hbCouponDiscount.setVisible(false);
|
||||||
|
hbCouponDiscount.setManaged(false);
|
||||||
|
updateCartTotal();
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnSaveSale(ActionEvent event) {
|
void btnSaveSale(ActionEvent event) {
|
||||||
if (saleSaveInProgress) {
|
if (saleSaveInProgress) {
|
||||||
@@ -409,6 +659,7 @@ public class SaleController {
|
|||||||
SaleRequest request = new SaleRequest();
|
SaleRequest request = new SaleRequest();
|
||||||
request.setStoreId(storeId);
|
request.setStoreId(storeId);
|
||||||
request.setPaymentMethod(payment);
|
request.setPaymentMethod(payment);
|
||||||
|
request.setIsRefund(false);
|
||||||
|
|
||||||
List<SaleItemRequest> itemRequests = new ArrayList<>();
|
List<SaleItemRequest> itemRequests = new ArrayList<>();
|
||||||
for (SaleCartItem cartItem : cartItems) {
|
for (SaleCartItem cartItem : cartItems) {
|
||||||
@@ -419,6 +670,20 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
request.setItems(itemRequests);
|
request.setItems(itemRequests);
|
||||||
|
|
||||||
|
DropdownOption customerOption = cbCustomer.getValue();
|
||||||
|
if (customerOption != null) {
|
||||||
|
request.setCustomerId(customerOption.getId());
|
||||||
|
}
|
||||||
|
if (appliedCoupon != null) {
|
||||||
|
request.setCouponId(appliedCoupon.getCouponId());
|
||||||
|
}
|
||||||
|
if (chkUseLoyaltyPoints.isSelected() && selectedCustomerData != null) {
|
||||||
|
BigDecimal subtotal = BigDecimal.valueOf(cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum());
|
||||||
|
BigDecimal couponDiscount = calculateCouponDiscount(subtotal);
|
||||||
|
BigDecimal subtotalAfterCoupon = subtotal.subtract(couponDiscount);
|
||||||
|
request.setPointsUsed(calculatePointsToUse(subtotalAfterCoupon));
|
||||||
|
}
|
||||||
|
|
||||||
saleSaveInProgress = true;
|
saleSaveInProgress = true;
|
||||||
setCreateSaleControlsDisabled(true);
|
setCreateSaleControlsDisabled(true);
|
||||||
btnRefund.setDisable(true);
|
btnRefund.setDisable(true);
|
||||||
@@ -437,6 +702,23 @@ public class SaleController {
|
|||||||
SaleResponse response = task.getValue();
|
SaleResponse response = task.getValue();
|
||||||
showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created.");
|
showInfo("Sale saved", "Sale ID " + response.getSaleId() + " was created.");
|
||||||
cartItems.clear();
|
cartItems.clear();
|
||||||
|
appliedCoupon = null;
|
||||||
|
txtCouponCode.clear();
|
||||||
|
txtCouponCode.setDisable(false);
|
||||||
|
lblCouponStatus.setText("");
|
||||||
|
hbCouponDiscount.setVisible(false);
|
||||||
|
hbCouponDiscount.setManaged(false);
|
||||||
|
hbLoyaltyDiscount.setVisible(false);
|
||||||
|
hbLoyaltyDiscount.setManaged(false);
|
||||||
|
hbPointsToEarn.setVisible(false);
|
||||||
|
hbPointsToEarn.setManaged(false);
|
||||||
|
cbCustomer.setValue(null);
|
||||||
|
selectedCustomerData = null;
|
||||||
|
lblLoyaltyPoints.setVisible(false);
|
||||||
|
lblLoyaltyPoints.setManaged(false);
|
||||||
|
chkUseLoyaltyPoints.setVisible(false);
|
||||||
|
chkUseLoyaltyPoints.setManaged(false);
|
||||||
|
chkUseLoyaltyPoints.setSelected(false);
|
||||||
updateCartTotal();
|
updateCartTotal();
|
||||||
refreshSales(true);
|
refreshSales(true);
|
||||||
});
|
});
|
||||||
@@ -536,6 +818,9 @@ public class SaleController {
|
|||||||
|
|
||||||
private SaleDetail mapToSaleDetail(SaleResponse sale) {
|
private SaleDetail mapToSaleDetail(SaleResponse sale) {
|
||||||
ObservableList<SaleDetail.SaleDetailItem> items = FXCollections.observableArrayList();
|
ObservableList<SaleDetail.SaleDetailItem> items = FXCollections.observableArrayList();
|
||||||
|
double saleSubtotal = sale.getSubtotalAmount() != null ? Math.abs(sale.getSubtotalAmount().doubleValue()) : 0;
|
||||||
|
double saleActualTotal = sale.getTotalAmount() != null ? Math.abs(sale.getTotalAmount().doubleValue()) : 0;
|
||||||
|
double discountRatio = saleSubtotal > 0 ? saleActualTotal / saleSubtotal : 1.0;
|
||||||
if (sale.getItems() != null) {
|
if (sale.getItems() != null) {
|
||||||
boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund();
|
boolean isRefund = sale.getIsRefund() != null && sale.getIsRefund();
|
||||||
for (SaleItemResponse item : sale.getItems()) {
|
for (SaleItemResponse item : sale.getItems()) {
|
||||||
@@ -549,10 +834,13 @@ public class SaleController {
|
|||||||
item.getProductName(),
|
item.getProductName(),
|
||||||
quantity,
|
quantity,
|
||||||
unitPrice,
|
unitPrice,
|
||||||
unitPrice * quantity
|
unitPrice * quantity * discountRatio
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
double subtotal = sale.getSubtotalAmount() != null ? sale.getSubtotalAmount().doubleValue() : 0.0;
|
||||||
|
double couponDiscount = sale.getCouponDiscountAmount() != null ? sale.getCouponDiscountAmount().doubleValue() : 0.0;
|
||||||
|
double loyaltyDiscount = sale.getLoyaltyDiscountAmount() != null ? sale.getLoyaltyDiscountAmount().doubleValue() : 0.0;
|
||||||
return new SaleDetail(
|
return new SaleDetail(
|
||||||
sale.getSaleId().intValue(),
|
sale.getSaleId().intValue(),
|
||||||
sale.getSaleDate(),
|
sale.getSaleDate(),
|
||||||
@@ -560,18 +848,86 @@ public class SaleController {
|
|||||||
sale.getPaymentMethod(),
|
sale.getPaymentMethod(),
|
||||||
sale.getEmployeeName(),
|
sale.getEmployeeName(),
|
||||||
Boolean.TRUE.equals(sale.getIsRefund()),
|
Boolean.TRUE.equals(sale.getIsRefund()),
|
||||||
items
|
items,
|
||||||
|
sale.getCustomerName(),
|
||||||
|
subtotal,
|
||||||
|
couponDiscount,
|
||||||
|
loyaltyDiscount
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCartTotal() {
|
private void updateCartTotal() {
|
||||||
double total = cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum();
|
BigDecimal subtotal = BigDecimal.valueOf(cartItems.stream().mapToDouble(SaleCartItem::getTotal).sum());
|
||||||
lblCartTotal.setText(currency.format(total));
|
BigDecimal couponDiscount = calculateCouponDiscount(subtotal);
|
||||||
|
BigDecimal subtotalAfterCoupon = subtotal.subtract(couponDiscount);
|
||||||
|
BigDecimal loyaltyDiscount = calculateLoyaltyDiscount(subtotalAfterCoupon);
|
||||||
|
BigDecimal total = subtotalAfterCoupon.subtract(loyaltyDiscount);
|
||||||
|
|
||||||
|
lblSubtotal.setText(currency.format(subtotal.doubleValue()));
|
||||||
|
|
||||||
|
if (couponDiscount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
lblCouponDiscount.setText("-" + currency.format(couponDiscount.doubleValue()));
|
||||||
|
hbCouponDiscount.setVisible(true);
|
||||||
|
hbCouponDiscount.setManaged(true);
|
||||||
|
} else {
|
||||||
|
hbCouponDiscount.setVisible(false);
|
||||||
|
hbCouponDiscount.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loyaltyDiscount.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
|
int pointsToUse = calculatePointsToUse(subtotalAfterCoupon);
|
||||||
|
lblLoyaltyDiscountLabel.setText("Loyalty Discount (" + pointsToUse + " pts):");
|
||||||
|
lblLoyaltyDiscount.setText("-" + currency.format(loyaltyDiscount.doubleValue()));
|
||||||
|
hbLoyaltyDiscount.setVisible(true);
|
||||||
|
hbLoyaltyDiscount.setManaged(true);
|
||||||
|
} else {
|
||||||
|
hbLoyaltyDiscount.setVisible(false);
|
||||||
|
hbLoyaltyDiscount.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
lblCartTotal.setText(currency.format(Math.max(0, total.doubleValue())));
|
||||||
|
|
||||||
|
if (selectedCustomerData != null) {
|
||||||
|
int pointsToEarn = (int) Math.max(0, total.doubleValue());
|
||||||
|
lblPointsToEarn.setText("+" + pointsToEarn + " pts");
|
||||||
|
hbPointsToEarn.setVisible(true);
|
||||||
|
hbPointsToEarn.setManaged(true);
|
||||||
|
} else {
|
||||||
|
hbPointsToEarn.setVisible(false);
|
||||||
|
hbPointsToEarn.setManaged(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal calculateCouponDiscount(BigDecimal subtotal) {
|
||||||
|
if (appliedCoupon == null) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
if ("PERCENTAGE".equalsIgnoreCase(appliedCoupon.getDiscountType())) {
|
||||||
|
return subtotal.multiply(appliedCoupon.getDiscountValue()).divide(BigDecimal.valueOf(100));
|
||||||
|
} else {
|
||||||
|
return appliedCoupon.getDiscountValue().min(subtotal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal calculateLoyaltyDiscount(BigDecimal subtotalAfterCoupon) {
|
||||||
|
if (!chkUseLoyaltyPoints.isSelected() || selectedCustomerData == null) {
|
||||||
|
return BigDecimal.ZERO;
|
||||||
|
}
|
||||||
|
int pointsToUse = calculatePointsToUse(subtotalAfterCoupon);
|
||||||
|
return BigDecimal.valueOf(pointsToUse).multiply(BigDecimal.valueOf(0.05));
|
||||||
|
}
|
||||||
|
|
||||||
|
private int calculatePointsToUse(BigDecimal subtotalAfterCoupon) {
|
||||||
|
if (selectedCustomerData == null || selectedCustomerData.getLoyaltyPoints() == null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
int maxPointsNeeded = subtotalAfterCoupon.multiply(BigDecimal.valueOf(20)).intValue();
|
||||||
|
return Math.min(selectedCustomerData.getLoyaltyPoints(), maxPointsNeeded);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateSalesColumnWidths(double tableWidth) {
|
private void updateSalesColumnWidths(double tableWidth) {
|
||||||
double available = Math.max(tableWidth - 28.0, 0.0);
|
double available = Math.max(tableWidth - 28.0, 0.0);
|
||||||
double baseWidth = 1255.0;
|
double baseWidth = 1395.0;
|
||||||
if (available <= 0) {
|
if (available <= 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -580,6 +936,7 @@ public class SaleController {
|
|||||||
colSaleId.setPrefWidth(60.0);
|
colSaleId.setPrefWidth(60.0);
|
||||||
colSaleDate.setPrefWidth(170.0);
|
colSaleDate.setPrefWidth(170.0);
|
||||||
colEmployeeName.setPrefWidth(160.0);
|
colEmployeeName.setPrefWidth(160.0);
|
||||||
|
colCustomerName.setPrefWidth(140.0);
|
||||||
colServiceProduct.setPrefWidth(320.0);
|
colServiceProduct.setPrefWidth(320.0);
|
||||||
colSaleQuantity.setPrefWidth(70.0);
|
colSaleQuantity.setPrefWidth(70.0);
|
||||||
colSaleUnitPrice.setPrefWidth(115.0);
|
colSaleUnitPrice.setPrefWidth(115.0);
|
||||||
@@ -591,14 +948,15 @@ public class SaleController {
|
|||||||
|
|
||||||
double extra = available - baseWidth;
|
double extra = available - baseWidth;
|
||||||
colSaleId.setPrefWidth(60.0);
|
colSaleId.setPrefWidth(60.0);
|
||||||
colSaleDate.setPrefWidth(170.0 + extra * 0.16);
|
colSaleDate.setPrefWidth(170.0 + extra * 0.14);
|
||||||
colEmployeeName.setPrefWidth(160.0 + extra * 0.16);
|
colEmployeeName.setPrefWidth(160.0 + extra * 0.14);
|
||||||
colServiceProduct.setPrefWidth(320.0 + extra * 0.38);
|
colCustomerName.setPrefWidth(140.0 + extra * 0.12);
|
||||||
|
colServiceProduct.setPrefWidth(320.0 + extra * 0.34);
|
||||||
colSaleQuantity.setPrefWidth(70.0);
|
colSaleQuantity.setPrefWidth(70.0);
|
||||||
colSaleUnitPrice.setPrefWidth(115.0 + extra * 0.08);
|
colSaleUnitPrice.setPrefWidth(115.0 + extra * 0.08);
|
||||||
colSaleTotal.setPrefWidth(120.0 + extra * 0.08);
|
colSaleTotal.setPrefWidth(120.0 + extra * 0.08);
|
||||||
colSalePaymentType.setPrefWidth(110.0 + extra * 0.06);
|
colSalePaymentType.setPrefWidth(110.0 + extra * 0.06);
|
||||||
colStoreName.setPrefWidth(130.0 + extra * 0.08);
|
colStoreName.setPrefWidth(130.0 + extra * 0.04);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void updateCartColumnWidths(double tableWidth) {
|
private void updateCartColumnWidths(double tableWidth) {
|
||||||
@@ -631,28 +989,92 @@ public class SaleController {
|
|||||||
cbPaymentMethod.setDisable(disabled);
|
cbPaymentMethod.setDisable(disabled);
|
||||||
btnClearCart.setDisable(disabled);
|
btnClearCart.setDisable(disabled);
|
||||||
btnSaveSale.setDisable(disabled);
|
btnSaveSale.setDisable(disabled);
|
||||||
|
cbCustomer.setDisable(disabled);
|
||||||
|
txtCouponCode.setDisable(disabled);
|
||||||
|
btnApplyCoupon.setDisable(disabled);
|
||||||
|
btnClearCoupon.setDisable(disabled);
|
||||||
|
chkUseLoyaltyPoints.setDisable(disabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySalesFilter(String filter) {
|
private void applySalesFilter(String filter) {
|
||||||
String f = filter == null ? "" : filter.trim().toLowerCase();
|
String f = filter == null ? "" : filter.trim().toLowerCase();
|
||||||
if (f.isEmpty()) {
|
String selectedPayment = cbFilterPayment.getValue();
|
||||||
filteredSales.setPredicate(s -> true);
|
String selectedStatus = cbFilterRefundStatus.getValue();
|
||||||
return;
|
DropdownOption selectedCustomer = cbFilterCustomer.getValue();
|
||||||
}
|
|
||||||
|
|
||||||
filteredSales.setPredicate(s ->
|
filteredSales.setPredicate(s -> {
|
||||||
String.valueOf(s.getSaleId()).contains(f)
|
boolean textMatch = f.isEmpty()
|
||||||
|
|| String.valueOf(s.getSaleId()).contains(f)
|
||||||
|| safe(s.getSaleDate()).contains(f)
|
|| safe(s.getSaleDate()).contains(f)
|
||||||
|| safe(s.getEmployeeName()).contains(f)
|
|| safe(s.getEmployeeName()).contains(f)
|
||||||
|
|| safe(s.getCustomerName()).contains(f)
|
||||||
|| safe(s.getItemName()).contains(f)
|
|| safe(s.getItemName()).contains(f)
|
||||||
|| safe(s.getPaymentMethod()).contains(f)
|
|| safe(s.getPaymentMethod()).contains(f);
|
||||||
);
|
|
||||||
|
boolean paymentMatch = selectedPayment == null || selectedPayment.equals("All Payments")
|
||||||
|
|| safe(s.getPaymentMethod()).equals(selectedPayment.toLowerCase());
|
||||||
|
|
||||||
|
boolean refundMatch = selectedStatus == null || selectedStatus.equals("All Status")
|
||||||
|
|| (selectedStatus.equals("Sales Only") && !s.isRefund())
|
||||||
|
|| (selectedStatus.equals("Refunds Only") && s.isRefund());
|
||||||
|
|
||||||
|
boolean customerMatch = selectedCustomer == null || selectedCustomer.getId() == null
|
||||||
|
|| safe(s.getCustomerName()).equals(safe(selectedCustomer.getLabel()));
|
||||||
|
|
||||||
|
return textMatch && paymentMatch && refundMatch && customerMatch;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String safe(String v) {
|
private static String safe(String v) {
|
||||||
return v == null ? "" : v.toLowerCase();
|
return v == null ? "" : v.toLowerCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadCustomerFilter() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
|
||||||
|
DropdownOption allCustomers = new DropdownOption();
|
||||||
|
allCustomers.setLabel("All Customers");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
java.util.List<DropdownOption> items = new java.util.ArrayList<>();
|
||||||
|
items.add(allCustomers);
|
||||||
|
items.addAll(customers);
|
||||||
|
cbFilterCustomer.setItems(FXCollections.observableArrayList(items));
|
||||||
|
cbFilterCustomer.getSelectionModel().selectFirst();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("SaleController.loadCustomerFilter", e, "Loading customer filter");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadStoreFilter() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
DropdownOption allStores = new DropdownOption();
|
||||||
|
allStores.setLabel("All Stores");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
storeOptions.clear();
|
||||||
|
storeOptions.addAll(stores);
|
||||||
|
java.util.List<DropdownOption> items = new java.util.ArrayList<>();
|
||||||
|
items.add(allStores);
|
||||||
|
items.addAll(stores);
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList(items));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("SaleController.loadStoreFilter", e, "Loading store filter");
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private Long selectedStoreId() {
|
||||||
|
if (!UserSession.getInstance().isAdmin()) return UserSession.getInstance().getStoreId();
|
||||||
|
DropdownOption selected = cbStoreFilter.getValue();
|
||||||
|
return (selected != null && selected.getId() != null) ? selected.getId() : null;
|
||||||
|
}
|
||||||
|
|
||||||
private void showError(String title, String message) {
|
private void showError(String title, String message) {
|
||||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
alert.setTitle(title);
|
alert.setTitle(title);
|
||||||
|
|||||||
@@ -7,15 +7,19 @@ import javafx.collections.transformation.FilteredList;
|
|||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.fxml.FXMLLoader;
|
import javafx.fxml.FXMLLoader;
|
||||||
|
import javafx.geometry.Pos;
|
||||||
import javafx.scene.Scene;
|
import javafx.scene.Scene;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
|
import javafx.scene.image.ImageView;
|
||||||
|
import javafx.scene.layout.StackPane;
|
||||||
import javafx.scene.layout.VBox;
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Modality;
|
import javafx.stage.Modality;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
import org.example.petshopdesktop.api.dto.employee.EmployeeResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.UserApi;
|
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||||
import org.example.petshopdesktop.util.TableViewSupport;
|
import org.example.petshopdesktop.util.TableViewSupport;
|
||||||
|
|
||||||
import java.util.Comparator;
|
import java.util.Comparator;
|
||||||
@@ -24,62 +28,48 @@ import java.util.stream.Collectors;
|
|||||||
|
|
||||||
public class StaffAccountsController {
|
public class StaffAccountsController {
|
||||||
|
|
||||||
@FXML
|
@FXML private VBox staffSection;
|
||||||
private VBox staffSection;
|
@FXML private TableView<EmployeeResponse> tvStaff;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colStaffAvatar;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colUsername;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colName;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colEmail;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colPhone;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colRole;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colStatus;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, Object> colCreated;
|
||||||
|
@FXML private TextField txtSearch;
|
||||||
|
@FXML private ComboBox<String> cbRoleFilter;
|
||||||
|
@FXML private ComboBox<String> cbStatusFilter;
|
||||||
|
@FXML private Button btnRefresh;
|
||||||
|
@FXML private Button btnCreateAccount;
|
||||||
|
@FXML private Button btnEditAccount;
|
||||||
|
@FXML private Label lblError;
|
||||||
|
@FXML private Label lblStatus;
|
||||||
|
|
||||||
@FXML
|
private final ObservableList<EmployeeResponse> staffAccounts = FXCollections.observableArrayList();
|
||||||
private TableView<UserResponse> tvStaff;
|
private FilteredList<EmployeeResponse> filteredStaff;
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TableColumn<UserResponse, String> colUsername;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TableColumn<UserResponse, String> colName;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TableColumn<UserResponse, String> colEmail;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TableColumn<UserResponse, String> colPhone;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TableColumn<UserResponse, String> colRole;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TableColumn<UserResponse, String> colStatus;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TableColumn<UserResponse, Object> colCreated;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField txtSearch;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button btnRefresh;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button btnCreateAccount;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button btnEditAccount;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblError;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblStatus;
|
|
||||||
|
|
||||||
private final ObservableList<UserResponse> staffAccounts = FXCollections.observableArrayList();
|
|
||||||
private FilteredList<UserResponse> filteredStaff;
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
|
colStaffAvatar.setCellValueFactory(data -> {
|
||||||
|
Long id = data.getValue().getId();
|
||||||
|
if (id == null) return new javafx.beans.property.SimpleStringProperty("");
|
||||||
|
return new javafx.beans.property.SimpleStringProperty("/api/v1/users/" + id + "/avatar/file");
|
||||||
|
});
|
||||||
|
configureAvatarColumn(colStaffAvatar);
|
||||||
|
|
||||||
colUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername()));
|
colUsername.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getUsername()));
|
||||||
colName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName()));
|
colName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName()));
|
||||||
colEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail()));
|
colEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail()));
|
||||||
colPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone()));
|
colPhone.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getPhone()));
|
||||||
colRole.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getRole()));
|
colRole.setCellValueFactory(data -> {
|
||||||
colStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getActive() != null && data.getValue().getActive() ? "Active" : "Inactive"));
|
String role = data.getValue().getRole() != null ? data.getValue().getRole() : "";
|
||||||
|
String staffRole = data.getValue().getStaffRole() != null ? " (" + data.getValue().getStaffRole() + ")" : "";
|
||||||
|
return new javafx.beans.property.SimpleStringProperty(role + staffRole);
|
||||||
|
});
|
||||||
|
colStatus.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(
|
||||||
|
Boolean.TRUE.equals(data.getValue().getActive()) ? "Active" : "Inactive"));
|
||||||
colCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt()));
|
colCreated.setCellValueFactory(data -> new javafx.beans.property.SimpleObjectProperty<>(data.getValue().getCreatedAt()));
|
||||||
|
|
||||||
filteredStaff = new FilteredList<>(staffAccounts, a -> true);
|
filteredStaff = new FilteredList<>(staffAccounts, a -> true);
|
||||||
@@ -90,14 +80,45 @@ public class StaffAccountsController {
|
|||||||
btnEditAccount.setDisable(newVal == null));
|
btnEditAccount.setDisable(newVal == null));
|
||||||
btnEditAccount.setDisable(true);
|
btnEditAccount.setDisable(true);
|
||||||
|
|
||||||
|
cbRoleFilter.setItems(FXCollections.observableArrayList("All Roles", "Admin", "Staff"));
|
||||||
|
cbRoleFilter.getSelectionModel().selectFirst();
|
||||||
|
cbRoleFilter.valueProperty().addListener((obs, o, n) -> applyStaffFilter(txtSearch.getText()));
|
||||||
|
|
||||||
|
cbStatusFilter.setItems(FXCollections.observableArrayList("All Statuses", "Active", "Inactive"));
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStatusFilter.valueProperty().addListener((obs, o, n) -> applyStaffFilter(txtSearch.getText()));
|
||||||
|
|
||||||
txtSearch.textProperty().addListener((obs, o, n) -> applyStaffFilter(n));
|
txtSearch.textProperty().addListener((obs, o, n) -> applyStaffFilter(n));
|
||||||
|
|
||||||
refresh();
|
refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void configureAvatarColumn(TableColumn<EmployeeResponse, String> column) {
|
||||||
|
column.setCellFactory(col -> new TableCell<>() {
|
||||||
|
private final ImageView imageView = new ImageView();
|
||||||
|
private final StackPane container = new StackPane(imageView);
|
||||||
|
{
|
||||||
|
container.setAlignment(Pos.CENTER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void updateItem(String item, boolean empty) {
|
||||||
|
super.updateItem(item, empty);
|
||||||
|
if (empty || item == null || item.isBlank()) {
|
||||||
|
setGraphic(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
DesktopImageSupport.loadImageInto(imageView, item, 48, 48);
|
||||||
|
setGraphic(container);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnRefreshClicked(ActionEvent event) {
|
void btnRefreshClicked(ActionEvent event) {
|
||||||
txtSearch.clear();
|
txtSearch.clear();
|
||||||
|
cbRoleFilter.getSelectionModel().selectFirst();
|
||||||
|
cbStatusFilter.getSelectionModel().selectFirst();
|
||||||
TableViewSupport.clearSort(tvStaff);
|
TableViewSupport.clearSort(tvStaff);
|
||||||
refresh();
|
refresh();
|
||||||
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
TableViewSupport.flashStatus(lblStatus, "Refreshed");
|
||||||
@@ -128,7 +149,7 @@ public class StaffAccountsController {
|
|||||||
openEditDialog(tvStaff.getSelectionModel().getSelectedItem());
|
openEditDialog(tvStaff.getSelectionModel().getSelectedItem());
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openEditDialog(UserResponse selected) {
|
private void openEditDialog(EmployeeResponse selected) {
|
||||||
if (selected == null) {
|
if (selected == null) {
|
||||||
lblError.setText("Select a user account to edit.");
|
lblError.setText("Select a user account to edit.");
|
||||||
return;
|
return;
|
||||||
@@ -152,12 +173,12 @@ public class StaffAccountsController {
|
|||||||
dialog.setScene(new Scene(loader.load()));
|
dialog.setScene(new Scene(loader.load()));
|
||||||
dialog.setResizable(false);
|
dialog.setResizable(false);
|
||||||
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
|
var controller = (org.example.petshopdesktop.controllers.dialogcontrollers.StaffEditDialogController) loader.getController();
|
||||||
controller.setUser(selected);
|
controller.setEmployee(selected);
|
||||||
dialog.showAndWait();
|
dialog.showAndWait();
|
||||||
refresh();
|
refresh();
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ActivityLogger.getInstance().logException("StaffAccountsController.openEditDialog", e, "Opening user edit dialog");
|
ActivityLogger.getInstance().logException("StaffAccountsController.openEditDialog", e, "Opening employee edit dialog");
|
||||||
lblError.setText("Could not open user account editor.");
|
lblError.setText("Could not open employee account editor.");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -167,11 +188,10 @@ public class StaffAccountsController {
|
|||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
Comparator<UserResponse> byCreated = Comparator.comparing(
|
Comparator<EmployeeResponse> byCreated = Comparator.comparing(
|
||||||
UserResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
|
EmployeeResponse::getCreatedAt, Comparator.nullsLast(Comparator.reverseOrder()));
|
||||||
|
|
||||||
List<UserResponse> staff = UserApi.getInstance().listUsers(null).stream()
|
List<EmployeeResponse> staff = EmployeeApi.getInstance().listEmployees(null).stream()
|
||||||
.filter(u -> !"CUSTOMER".equalsIgnoreCase(u.getRole()))
|
|
||||||
.sorted(byCreated)
|
.sorted(byCreated)
|
||||||
.collect(Collectors.toList());
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
@@ -180,7 +200,7 @@ public class StaffAccountsController {
|
|||||||
tvStaff.setDisable(false);
|
tvStaff.setDisable(false);
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading staff accounts");
|
ActivityLogger.getInstance().logException("StaffAccountsController.refresh", e, "Loading employee accounts");
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
lblError.setText("Could not load staff accounts.");
|
lblError.setText("Could not load staff accounts.");
|
||||||
tvStaff.setDisable(false);
|
tvStaff.setDisable(false);
|
||||||
@@ -191,17 +211,24 @@ public class StaffAccountsController {
|
|||||||
|
|
||||||
private void applyStaffFilter(String text) {
|
private void applyStaffFilter(String text) {
|
||||||
String q = text == null ? "" : text.trim().toLowerCase();
|
String q = text == null ? "" : text.trim().toLowerCase();
|
||||||
if (q.isEmpty()) {
|
String selectedRole = cbRoleFilter.getValue();
|
||||||
filteredStaff.setPredicate(a -> true);
|
String selectedStatus = cbStatusFilter.getValue();
|
||||||
return;
|
filteredStaff.setPredicate(a -> {
|
||||||
}
|
boolean textMatch = q.isEmpty()
|
||||||
filteredStaff.setPredicate(a ->
|
|| safe(a.getUsername()).contains(q)
|
||||||
safe(a.getUsername()).contains(q)
|
|| safe(a.getFullName()).contains(q)
|
||||||
|| safe(a.getFullName()).contains(q)
|
|| safe(a.getEmail()).contains(q)
|
||||||
|| safe(a.getEmail()).contains(q)
|
|| safe(a.getPhone()).contains(q)
|
||||||
|| safe(a.getPhone()).contains(q)
|
|| safe(a.getRole()).contains(q)
|
||||||
|| safe(a.getRole()).contains(q)
|
|| safe(a.getStaffRole()).contains(q);
|
||||||
);
|
boolean active = Boolean.TRUE.equals(a.getActive());
|
||||||
|
boolean statusMatch = selectedStatus == null || selectedStatus.equals("All Statuses")
|
||||||
|
|| (selectedStatus.equals("Active") && active)
|
||||||
|
|| (selectedStatus.equals("Inactive") && !active);
|
||||||
|
boolean roleMatch = selectedRole == null || selectedRole.equals("All Roles")
|
||||||
|
|| safe(a.getRole()).equals(selectedRole.toLowerCase());
|
||||||
|
return textMatch && statusMatch && roleMatch;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private static String safe(String v) {
|
private static String safe(String v) {
|
||||||
|
|||||||
@@ -8,164 +8,98 @@ import javafx.fxml.FXML;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.Alert;
|
import javafx.scene.control.Alert;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ChoiceDialog;
|
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.DatePicker;
|
import javafx.scene.control.DatePicker;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest;
|
import org.example.petshopdesktop.api.dto.adoption.AdoptionRequest;
|
||||||
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.endpoints.AdoptionApi;
|
import org.example.petshopdesktop.api.endpoints.AdoptionApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.PetApi;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.models.Adoption;
|
import org.example.petshopdesktop.models.Adoption;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
public class AdoptionDialogController {
|
public class AdoptionDialogController {
|
||||||
|
|
||||||
@FXML
|
@FXML private Button btnCancel;
|
||||||
private Button btnCancel;
|
@FXML private Button btnSave;
|
||||||
|
@FXML private ComboBox<String> cbAdoptionStatus;
|
||||||
@FXML
|
@FXML private ComboBox<DropdownOption> cbCustomer;
|
||||||
private Button btnSave;
|
@FXML private ComboBox<DropdownOption> cbEmployee;
|
||||||
|
@FXML private ComboBox<DropdownOption> cbPet;
|
||||||
@FXML
|
@FXML private ComboBox<DropdownOption> cbStore;
|
||||||
private ComboBox<String> cbAdoptionStatus;
|
@FXML private VBox vbStore;
|
||||||
|
@FXML private DatePicker dpAdoptionDate;
|
||||||
@FXML
|
@FXML private TextField txtAdoptionFee;
|
||||||
private ComboBox<DropdownOption> cbCustomer;
|
@FXML private Label lblAdoptionId;
|
||||||
|
@FXML private Label lblMode;
|
||||||
@FXML
|
|
||||||
private ComboBox<DropdownOption> cbEmployee;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ComboBox<DropdownOption> cbPet;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private DatePicker dpAdoptionDate;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblAdoptionId;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblMode;
|
|
||||||
|
|
||||||
private String mode = null;
|
private String mode = null;
|
||||||
private Adoption selectedAdoption = null;
|
private Adoption selectedAdoption = null;
|
||||||
private String selectedPaymentMethod = null;
|
private boolean suppressStatusListener = false;
|
||||||
private boolean suppressPaymentDialog = false;
|
private Long pendingStoreId = null;
|
||||||
|
private boolean isEditing = false;
|
||||||
|
|
||||||
private ObservableList<String> statusList = FXCollections.observableArrayList(
|
private final ObservableList<String> statusList = FXCollections.observableArrayList(
|
||||||
"Pending", "Completed", "Missed", "Cancelled"
|
"Pending", "Completed", "Missed", "Cancelled"
|
||||||
);
|
);
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void initialize() {
|
void initialize() {
|
||||||
|
|
||||||
cbAdoptionStatus.setItems(statusList);
|
cbAdoptionStatus.setItems(statusList);
|
||||||
cbAdoptionStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
|
cbAdoptionStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
if ("Completed".equals(newVal) && !"Completed".equals(oldVal) && !suppressPaymentDialog) {
|
if (!suppressStatusListener && newVal != null) {
|
||||||
ChoiceDialog<String> dialog = new ChoiceDialog<>("Cash", "Cash", "Credit Card", "Debit Card", "E-Transfer");
|
applyStatusFieldRules(newVal);
|
||||||
dialog.setTitle("Payment Method");
|
|
||||||
dialog.setHeaderText("Confirm payment received");
|
|
||||||
dialog.setContentText("Select payment method:");
|
|
||||||
dialog.showAndWait().ifPresentOrElse(
|
|
||||||
method -> selectedPaymentMethod = method,
|
|
||||||
() -> {
|
|
||||||
selectedPaymentMethod = null;
|
|
||||||
cbAdoptionStatus.setValue(oldVal);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else if (!"Completed".equals(newVal)) {
|
|
||||||
selectedPaymentMethod = null;
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cbEmployee.setPromptText("Select an employee");
|
|
||||||
|
|
||||||
new Thread(() -> {
|
txtAdoptionFee.setDisable(true);
|
||||||
try {
|
|
||||||
List<DropdownOption> pets = DropdownApi.getInstance().getAdoptionPets();
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
if (pets != null) {
|
|
||||||
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
|
|
||||||
ensureSelectedPetOption(petsObs);
|
|
||||||
cbPet.setItems(petsObs);
|
|
||||||
applySelectedPet();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
ActivityLogger.getInstance().logException(
|
|
||||||
"AdoptionDialogController.initialize",
|
|
||||||
e,
|
|
||||||
"Loading pets for combo box");
|
|
||||||
System.out.println("Error loading pets: " + e.getMessage());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
new Thread(() -> {
|
LocalDate today = LocalDate.now();
|
||||||
try {
|
dpAdoptionDate.setDayCellFactory(picker -> new javafx.scene.control.DateCell() {
|
||||||
List<DropdownOption> employees = DropdownApi.getInstance().getEmployees();
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
ObservableList<DropdownOption> employeesObs = FXCollections.observableArrayList(employees);
|
|
||||||
ensureSelectedEmployeeOption(employeesObs);
|
|
||||||
cbEmployee.setItems(employeesObs);
|
|
||||||
applySelectedEmployee();
|
|
||||||
});
|
|
||||||
} catch (Exception e) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
ActivityLogger.getInstance().logException(
|
|
||||||
"AdoptionDialogController.initialize",
|
|
||||||
e,
|
|
||||||
"Loading employees for combo box");
|
|
||||||
cbEmployee.setDisable(true);
|
|
||||||
cbEmployee.setPromptText("Unable to load employees");
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}).start();
|
|
||||||
|
|
||||||
cbEmployee.setCellFactory(param -> new ListCell<>() {
|
|
||||||
@Override
|
@Override
|
||||||
protected void updateItem(DropdownOption option, boolean empty) {
|
public void updateItem(LocalDate item, boolean empty) {
|
||||||
super.updateItem(option, empty);
|
super.updateItem(item, empty);
|
||||||
setText(empty || option == null ? null : option.getLabel());
|
setDisable(empty || item.isBefore(today));
|
||||||
}
|
|
||||||
});
|
|
||||||
cbEmployee.setButtonCell(new ListCell<>() {
|
|
||||||
@Override
|
|
||||||
protected void updateItem(DropdownOption option, boolean empty) {
|
|
||||||
super.updateItem(option, empty);
|
|
||||||
setText(empty || option == null ? null : option.getLabel());
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
new Thread(() -> {
|
setupDropdownCellFactory(cbEmployee);
|
||||||
try {
|
setupDropdownCellFactory(cbPet);
|
||||||
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
|
setupDropdownCellFactory(cbCustomer);
|
||||||
Platform.runLater(() -> {
|
setupDropdownCellFactory(cbStore);
|
||||||
if (customers != null) {
|
|
||||||
ObservableList<DropdownOption> customersObs = FXCollections.observableArrayList(customers);
|
cbPet.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
cbCustomer.setItems(customersObs);
|
if (newVal != null) {
|
||||||
applySelectedCustomer();
|
loadPetPrice(newVal.getId());
|
||||||
}
|
} else {
|
||||||
});
|
txtAdoptionFee.setText("0");
|
||||||
} catch (Exception e) {
|
|
||||||
Platform.runLater(() -> {
|
|
||||||
ActivityLogger.getInstance().logException(
|
|
||||||
"AdoptionDialogController.initialize",
|
|
||||||
e,
|
|
||||||
"Loading customers for combo box");
|
|
||||||
System.out.println("Error loading customers: " + e.getMessage());
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}).start();
|
});
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
vbStore.setVisible(true);
|
||||||
|
vbStore.setManaged(true);
|
||||||
|
cbPet.setDisable(true);
|
||||||
|
cbPet.setPromptText("Select a store first");
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbEmployee.setPromptText("Select a store first");
|
||||||
|
} else {
|
||||||
|
cbEmployee.setPromptText("Select an employee");
|
||||||
|
}
|
||||||
|
|
||||||
|
loadDropdownsAsync();
|
||||||
|
|
||||||
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
||||||
@Override
|
@Override
|
||||||
@@ -182,32 +116,236 @@ public class AdoptionDialogController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setupDropdownCellFactory(ComboBox<DropdownOption> cb) {
|
||||||
|
cb.setCellFactory(param -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(DropdownOption o, boolean empty) {
|
||||||
|
super.updateItem(o, empty);
|
||||||
|
setText(empty || o == null ? null : o.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cb.setButtonCell(new ListCell<>() {
|
||||||
|
@Override protected void updateItem(DropdownOption o, boolean empty) {
|
||||||
|
super.updateItem(o, empty);
|
||||||
|
setText(empty || o == null ? null : o.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadDropdownsAsync() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> customers = DropdownApi.getInstance().getCustomers();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (customers != null) {
|
||||||
|
cbCustomer.setItems(FXCollections.observableArrayList(customers));
|
||||||
|
applySelectedCustomer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.loadDropdownsAsync", e, "Loading customers"));
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (stores != null) {
|
||||||
|
cbStore.setItems(FXCollections.observableArrayList(stores));
|
||||||
|
cbStore.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
Long sid = newVal != null ? newVal.getId() : null;
|
||||||
|
cbEmployee.setValue(null);
|
||||||
|
cbEmployee.setItems(FXCollections.observableArrayList());
|
||||||
|
if (sid != null) {
|
||||||
|
String status = cbAdoptionStatus.getValue();
|
||||||
|
boolean locked = "Cancelled".equalsIgnoreCase(status)
|
||||||
|
|| "Completed".equalsIgnoreCase(status)
|
||||||
|
|| "Missed".equalsIgnoreCase(status);
|
||||||
|
if (!locked) {
|
||||||
|
cbEmployee.setDisable(false);
|
||||||
|
cbEmployee.setPromptText("Select an employee");
|
||||||
|
}
|
||||||
|
loadEmployeesForStore(sid);
|
||||||
|
if (!isEditing) {
|
||||||
|
cbPet.setValue(null);
|
||||||
|
cbPet.setItems(FXCollections.observableArrayList());
|
||||||
|
cbPet.setDisable(true);
|
||||||
|
cbPet.setPromptText("Select a pet");
|
||||||
|
loadAdoptionPetsForStore(sid);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
String status = cbAdoptionStatus.getValue();
|
||||||
|
boolean locked = "Cancelled".equalsIgnoreCase(status)
|
||||||
|
|| "Completed".equalsIgnoreCase(status)
|
||||||
|
|| "Missed".equalsIgnoreCase(status);
|
||||||
|
if (!locked) {
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbEmployee.setPromptText("Select a store first");
|
||||||
|
}
|
||||||
|
if (!isEditing) {
|
||||||
|
cbPet.setValue(null);
|
||||||
|
cbPet.setItems(FXCollections.observableArrayList());
|
||||||
|
cbPet.setDisable(true);
|
||||||
|
cbPet.setPromptText("Select a store first");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pendingStoreId != null) {
|
||||||
|
for (DropdownOption store : cbStore.getItems()) {
|
||||||
|
if (pendingStoreId.equals(store.getId())) {
|
||||||
|
cbStore.setValue(store);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingStoreId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.loadDropdownsAsync", e, "Loading stores"));
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
} else {
|
||||||
|
Long storeId = UserSession.getInstance().getStoreId();
|
||||||
|
if (storeId != null && storeId > 0) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> employees = DropdownApi.getInstance().getStoreEmployees(storeId);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ObservableList<DropdownOption> employeesObs = FXCollections.observableArrayList(employees != null ? employees : List.of());
|
||||||
|
ensureSelectedEmployeeOption(employeesObs);
|
||||||
|
cbEmployee.setItems(employeesObs);
|
||||||
|
applySelectedEmployee();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.loadDropdownsAsync", e, "Loading employees");
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbEmployee.setPromptText("Unable to load employees");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> pets = DropdownApi.getInstance().getAdoptionPets(storeId);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (!isEditing && pets != null) {
|
||||||
|
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
|
||||||
|
cbPet.setItems(petsObs);
|
||||||
|
if (petsObs.isEmpty()) {
|
||||||
|
cbPet.setDisable(true);
|
||||||
|
cbPet.setPromptText("No available pets for this store");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.loadDropdownsAsync", e, "Loading pets"));
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadEmployeesForStore(Long storeId) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> employees = DropdownApi.getInstance().getStoreEmployees(storeId);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ObservableList<DropdownOption> employeesObs = FXCollections.observableArrayList(employees != null ? employees : List.of());
|
||||||
|
ensureSelectedEmployeeOption(employeesObs);
|
||||||
|
cbEmployee.setItems(employeesObs);
|
||||||
|
applySelectedEmployee();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.loadEmployeesForStore", e, "Loading employees for store");
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbEmployee.setPromptText("Unable to load employees");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void loadAdoptionPetsForStore(Long storeId) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> pets = DropdownApi.getInstance().getAdoptionPets(storeId);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (pets != null) {
|
||||||
|
ObservableList<DropdownOption> petsObs = FXCollections.observableArrayList(pets);
|
||||||
|
cbPet.setItems(petsObs);
|
||||||
|
if (petsObs.isEmpty()) {
|
||||||
|
cbPet.setDisable(true);
|
||||||
|
cbPet.setPromptText("No available pets for this store");
|
||||||
|
} else {
|
||||||
|
cbPet.setDisable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.loadAdoptionPetsForStore", e, "Loading adoption pets for store");
|
||||||
|
cbPet.setDisable(true);
|
||||||
|
cbPet.setPromptText("Unable to load pets");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyStatusFieldRules(String status) {
|
||||||
|
if ("Cancelled".equalsIgnoreCase(status) || "Completed".equalsIgnoreCase(status) || "Missed".equalsIgnoreCase(status)) {
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
dpAdoptionDate.setDisable(true);
|
||||||
|
cbStore.setDisable(true);
|
||||||
|
} else {
|
||||||
|
cbEmployee.setDisable(false);
|
||||||
|
dpAdoptionDate.setDisable(false);
|
||||||
|
if (UserSession.getInstance().isAdmin() && !isEditing) cbStore.setDisable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void buttonSaveClicked(MouseEvent mouseEvent) {
|
private void buttonSaveClicked(MouseEvent mouseEvent) {
|
||||||
String errorMsg = "";
|
String errorMsg = "";
|
||||||
|
|
||||||
if (cbPet.getSelectionModel().getSelectedItem() == null) {
|
if (cbPet.getSelectionModel().getSelectedItem() == null) errorMsg += "Pet is required.\n";
|
||||||
errorMsg += "Pet is required.\n";
|
if (cbCustomer.getSelectionModel().getSelectedItem() == null) errorMsg += "Customer is required.\n";
|
||||||
}
|
if (cbEmployee.getSelectionModel().getSelectedItem() == null) errorMsg += "Employee is required.\n";
|
||||||
|
if (dpAdoptionDate.getValue() == null) errorMsg += "Adoption Date is required.\n";
|
||||||
|
if (cbAdoptionStatus.getSelectionModel().getSelectedItem() == null) errorMsg += "Status is required.\n";
|
||||||
|
|
||||||
if (cbCustomer.getSelectionModel().getSelectedItem() == null) {
|
BigDecimal adoptionFee = BigDecimal.ZERO;
|
||||||
errorMsg += "Customer is required.\n";
|
String feeText = txtAdoptionFee.getText() == null ? "" : txtAdoptionFee.getText().trim();
|
||||||
}
|
if (!feeText.isEmpty()) {
|
||||||
|
try {
|
||||||
if (cbEmployee.getSelectionModel().getSelectedItem() == null) {
|
adoptionFee = new BigDecimal(feeText);
|
||||||
errorMsg += "Employee is required.\n";
|
} catch (NumberFormatException e) {
|
||||||
}
|
adoptionFee = BigDecimal.ZERO;
|
||||||
|
}
|
||||||
if (dpAdoptionDate.getValue() == null) {
|
|
||||||
errorMsg += "Adoption Date is required.\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cbAdoptionStatus.getSelectionModel().getSelectedItem() == null) {
|
|
||||||
errorMsg += "Status is required.\n";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (errorMsg.isEmpty()) {
|
if (errorMsg.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
Long storeId = UserSession.getInstance().getStoreId();
|
Long storeId;
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
if (cbStore.getSelectionModel().getSelectedItem() == null) {
|
||||||
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
|
alert.setHeaderText("Input Error");
|
||||||
|
alert.setContentText("Store is required.");
|
||||||
|
alert.showAndWait();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storeId = cbStore.getSelectionModel().getSelectedItem().getId();
|
||||||
|
} else {
|
||||||
|
storeId = UserSession.getInstance().getStoreId();
|
||||||
|
}
|
||||||
if (storeId == null || storeId <= 0) {
|
if (storeId == null || storeId <= 0) {
|
||||||
throw new IllegalStateException("Store is not set for this account");
|
throw new IllegalStateException("Store is not set for this account");
|
||||||
}
|
}
|
||||||
@@ -219,15 +357,13 @@ public class AdoptionDialogController {
|
|||||||
request.setSourceStoreId(storeId);
|
request.setSourceStoreId(storeId);
|
||||||
request.setAdoptionDate(dpAdoptionDate.getValue());
|
request.setAdoptionDate(dpAdoptionDate.getValue());
|
||||||
request.setAdoptionStatus(cbAdoptionStatus.getValue());
|
request.setAdoptionStatus(cbAdoptionStatus.getValue());
|
||||||
request.setPaymentMethod(selectedPaymentMethod);
|
request.setAdoptionFee(adoptionFee);
|
||||||
|
|
||||||
if (mode.equals("Add")) {
|
if (mode.equals("Add")) {
|
||||||
AdoptionApi.getInstance().createAdoption(request);
|
AdoptionApi.getInstance().createAdoption(request);
|
||||||
} else {
|
} else {
|
||||||
String[] parts = lblAdoptionId.getText().split(": ");
|
String[] parts = lblAdoptionId.getText().split(": ");
|
||||||
if (parts.length < 2) {
|
if (parts.length < 2) throw new IllegalStateException("Invalid adoption ID format");
|
||||||
throw new IllegalStateException("Invalid adoption ID format");
|
|
||||||
}
|
|
||||||
Long adoptionId = Long.parseLong(parts[1]);
|
Long adoptionId = Long.parseLong(parts[1]);
|
||||||
AdoptionApi.getInstance().updateAdoption(adoptionId, request);
|
AdoptionApi.getInstance().updateAdoption(adoptionId, request);
|
||||||
}
|
}
|
||||||
@@ -239,9 +375,7 @@ public class AdoptionDialogController {
|
|||||||
closeStage(mouseEvent);
|
closeStage(mouseEvent);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ActivityLogger.getInstance().logException(
|
ActivityLogger.getInstance().logException(
|
||||||
"AdoptionDialogController.buttonSaveClicked",
|
"AdoptionDialogController.buttonSaveClicked", e, mode + " adoption");
|
||||||
e,
|
|
||||||
mode + " adoption");
|
|
||||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
alert.setHeaderText("Database Operation Error");
|
alert.setHeaderText("Database Operation Error");
|
||||||
alert.setContentText(e.getMessage());
|
alert.setContentText(e.getMessage());
|
||||||
@@ -262,36 +396,46 @@ public class AdoptionDialogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void displayAdoptionDetails(Adoption adoption) {
|
public void displayAdoptionDetails(Adoption adoption) {
|
||||||
if (adoption != null) {
|
if (adoption == null) return;
|
||||||
selectedAdoption = adoption;
|
selectedAdoption = adoption;
|
||||||
lblAdoptionId.setText("ID: " + adoption.getAdoptionId());
|
isEditing = true;
|
||||||
ensureSelectedEmployeeOption(cbEmployee.getItems());
|
lblAdoptionId.setText("ID: " + adoption.getAdoptionId());
|
||||||
applySelectedPet();
|
pendingStoreId = adoption.getStoreId();
|
||||||
applySelectedCustomer();
|
|
||||||
applySelectedEmployee();
|
|
||||||
|
|
||||||
if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) {
|
if (adoption.getPetId() > 0) {
|
||||||
try {
|
DropdownOption petOption = new DropdownOption();
|
||||||
dpAdoptionDate.setValue(LocalDate.parse(adoption.getAdoptionDate()));
|
petOption.setId((long) adoption.getPetId());
|
||||||
} catch (Exception e) {
|
petOption.setLabel(adoption.getPetName() != null && !adoption.getPetName().isBlank()
|
||||||
ActivityLogger.getInstance().logException(
|
? adoption.getPetName() : "Pet #" + adoption.getPetId());
|
||||||
"AdoptionDialogController.displayAdoptionDetails",
|
cbPet.setItems(FXCollections.observableArrayList(petOption));
|
||||||
e,
|
cbPet.setValue(petOption);
|
||||||
"Parsing adoption date");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
suppressPaymentDialog = true;
|
|
||||||
cbAdoptionStatus.setItems(statusList);
|
|
||||||
for (String status : cbAdoptionStatus.getItems()) {
|
|
||||||
if (status.equals(adoption.getAdoptionStatus())) {
|
|
||||||
cbAdoptionStatus.getSelectionModel().select(status);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
suppressPaymentDialog = false;
|
|
||||||
applyEditModeLock();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
applySelectedCustomer();
|
||||||
|
applySelectedEmployee();
|
||||||
|
|
||||||
|
if (adoption.getAdoptionDate() != null && !adoption.getAdoptionDate().isEmpty()) {
|
||||||
|
try {
|
||||||
|
dpAdoptionDate.setValue(LocalDate.parse(adoption.getAdoptionDate()));
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.displayAdoptionDetails", e, "Parsing adoption date");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
txtAdoptionFee.setText(adoption.getAdoptionFee() > 0
|
||||||
|
? String.format("%.2f", adoption.getAdoptionFee()) : "0.00");
|
||||||
|
|
||||||
|
suppressStatusListener = true;
|
||||||
|
cbAdoptionStatus.setItems(statusList);
|
||||||
|
for (String status : cbAdoptionStatus.getItems()) {
|
||||||
|
if (status.equals(adoption.getAdoptionStatus())) {
|
||||||
|
cbAdoptionStatus.getSelectionModel().select(status);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
suppressStatusListener = false;
|
||||||
|
applyEditModeLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applyEditModeLock() {
|
private void applyEditModeLock() {
|
||||||
@@ -304,6 +448,7 @@ public class AdoptionDialogController {
|
|||||||
dpAdoptionDate.setDisable(true);
|
dpAdoptionDate.setDisable(true);
|
||||||
cbAdoptionStatus.setDisable(true);
|
cbAdoptionStatus.setDisable(true);
|
||||||
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Cancelled"));
|
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Cancelled"));
|
||||||
|
cbStore.setDisable(true);
|
||||||
btnSave.setDisable(true);
|
btnSave.setDisable(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -311,23 +456,33 @@ public class AdoptionDialogController {
|
|||||||
LocalDate adoptionDate = dpAdoptionDate.getValue();
|
LocalDate adoptionDate = dpAdoptionDate.getValue();
|
||||||
boolean isPast = adoptionDate != null && adoptionDate.isBefore(LocalDate.now());
|
boolean isPast = adoptionDate != null && adoptionDate.isBefore(LocalDate.now());
|
||||||
|
|
||||||
cbPet.setDisable(true);
|
|
||||||
cbCustomer.setDisable(true);
|
|
||||||
dpAdoptionDate.setDisable(false);
|
|
||||||
cbEmployee.setDisable(false);
|
|
||||||
cbAdoptionStatus.setDisable(false);
|
|
||||||
|
|
||||||
suppressPaymentDialog = true;
|
|
||||||
if (isPast) {
|
if (isPast) {
|
||||||
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Completed", "Missed"));
|
cbPet.setDisable(true);
|
||||||
|
cbCustomer.setDisable(true);
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
dpAdoptionDate.setDisable(true);
|
dpAdoptionDate.setDisable(true);
|
||||||
|
cbStore.setDisable(true);
|
||||||
|
cbAdoptionStatus.setDisable(false);
|
||||||
|
suppressStatusListener = true;
|
||||||
|
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Completed", "Missed"));
|
||||||
|
if (!cbAdoptionStatus.getItems().contains(cbAdoptionStatus.getValue())) {
|
||||||
|
cbAdoptionStatus.getSelectionModel().selectFirst();
|
||||||
|
}
|
||||||
|
suppressStatusListener = false;
|
||||||
} else {
|
} else {
|
||||||
|
cbPet.setDisable(true);
|
||||||
|
cbCustomer.setDisable(true);
|
||||||
|
dpAdoptionDate.setDisable(false);
|
||||||
|
cbEmployee.setDisable(false);
|
||||||
|
cbAdoptionStatus.setDisable(false);
|
||||||
|
cbStore.setDisable(true);
|
||||||
|
suppressStatusListener = true;
|
||||||
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending", "Cancelled"));
|
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending", "Cancelled"));
|
||||||
|
if (!cbAdoptionStatus.getItems().contains(cbAdoptionStatus.getValue())) {
|
||||||
|
cbAdoptionStatus.getSelectionModel().selectFirst();
|
||||||
|
}
|
||||||
|
suppressStatusListener = false;
|
||||||
}
|
}
|
||||||
if (!cbAdoptionStatus.getItems().contains(cbAdoptionStatus.getValue())) {
|
|
||||||
cbAdoptionStatus.getSelectionModel().selectFirst();
|
|
||||||
}
|
|
||||||
suppressPaymentDialog = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setMode(String mode) {
|
public void setMode(String mode) {
|
||||||
@@ -335,60 +490,64 @@ public class AdoptionDialogController {
|
|||||||
lblMode.setText(mode + " Adoption");
|
lblMode.setText(mode + " Adoption");
|
||||||
lblAdoptionId.setVisible(mode.equals("Edit"));
|
lblAdoptionId.setVisible(mode.equals("Edit"));
|
||||||
if (mode.equals("Add")) {
|
if (mode.equals("Add")) {
|
||||||
suppressPaymentDialog = true;
|
suppressStatusListener = true;
|
||||||
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending"));
|
cbAdoptionStatus.setItems(FXCollections.observableArrayList("Pending"));
|
||||||
cbAdoptionStatus.setValue("Pending");
|
cbAdoptionStatus.setValue("Pending");
|
||||||
cbAdoptionStatus.setDisable(true);
|
cbAdoptionStatus.setDisable(true);
|
||||||
suppressPaymentDialog = false;
|
suppressStatusListener = false;
|
||||||
|
txtAdoptionFee.setText("");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySelectedPet() {
|
private void applySelectedPet() {
|
||||||
if (selectedAdoption == null || selectedAdoption.getPetId() <= 0) {
|
if (selectedAdoption == null || selectedAdoption.getPetId() <= 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
DropdownOption selected = findOptionById(cbPet.getItems(), (long) selectedAdoption.getPetId());
|
DropdownOption selected = findOptionById(cbPet.getItems(), (long) selectedAdoption.getPetId());
|
||||||
if (selected != null && !Objects.equals(cbPet.getValue(), selected)) {
|
if (selected != null && !Objects.equals(cbPet.getValue(), selected)) cbPet.setValue(selected);
|
||||||
cbPet.setValue(selected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySelectedCustomer() {
|
private void applySelectedCustomer() {
|
||||||
if (selectedAdoption == null || selectedAdoption.getCustomerId() <= 0) {
|
if (selectedAdoption == null || selectedAdoption.getCustomerId() <= 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAdoption.getCustomerId());
|
DropdownOption selected = findOptionById(cbCustomer.getItems(), (long) selectedAdoption.getCustomerId());
|
||||||
if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) {
|
if (selected != null && !Objects.equals(cbCustomer.getValue(), selected)) cbCustomer.setValue(selected);
|
||||||
cbCustomer.setValue(selected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void applySelectedEmployee() {
|
private void applySelectedEmployee() {
|
||||||
if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0) {
|
if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAdoption.getEmployeeId());
|
DropdownOption selected = findOptionById(cbEmployee.getItems(), (long) selectedAdoption.getEmployeeId());
|
||||||
if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) {
|
if (selected != null && !Objects.equals(cbEmployee.getValue(), selected)) cbEmployee.setValue(selected);
|
||||||
cbEmployee.setValue(selected);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private DropdownOption findOptionById(List<DropdownOption> options, Long id) {
|
private DropdownOption findOptionById(List<DropdownOption> options, Long id) {
|
||||||
if (id == null || options == null) {
|
if (id == null || options == null) return null;
|
||||||
return null;
|
|
||||||
}
|
|
||||||
for (DropdownOption option : options) {
|
for (DropdownOption option : options) {
|
||||||
if (option.getId() != null && option.getId().equals(id)) {
|
if (option.getId() != null && option.getId().equals(id)) return option;
|
||||||
return option;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void loadPetPrice(Long petId) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
var pet = PetApi.getInstance().getPetById(petId);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
if (pet != null && pet.getPetPrice() != null) {
|
||||||
|
txtAdoptionFee.setText(String.format("%.2f", pet.getPetPrice()));
|
||||||
|
} else {
|
||||||
|
txtAdoptionFee.setText("0.00");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
txtAdoptionFee.setText("0.00");
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"AdoptionDialogController.loadPetPrice", e, "Loading pet price");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
private void ensureSelectedEmployeeOption(ObservableList<DropdownOption> options) {
|
private void ensureSelectedEmployeeOption(ObservableList<DropdownOption> options) {
|
||||||
if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0 || options == null) {
|
if (selectedAdoption == null || selectedAdoption.getEmployeeId() <= 0 || options == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
DropdownOption existing = findOptionById(options, (long) selectedAdoption.getEmployeeId());
|
DropdownOption existing = findOptionById(options, (long) selectedAdoption.getEmployeeId());
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
DropdownOption option = new DropdownOption();
|
DropdownOption option = new DropdownOption();
|
||||||
@@ -399,9 +558,7 @@ public class AdoptionDialogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void ensureSelectedPetOption(ObservableList<DropdownOption> options) {
|
private void ensureSelectedPetOption(ObservableList<DropdownOption> options) {
|
||||||
if (selectedAdoption == null || selectedAdoption.getPetId() <= 0 || options == null) {
|
if (selectedAdoption == null || selectedAdoption.getPetId() <= 0 || options == null) return;
|
||||||
return;
|
|
||||||
}
|
|
||||||
DropdownOption existing = findOptionById(options, (long) selectedAdoption.getPetId());
|
DropdownOption existing = findOptionById(options, (long) selectedAdoption.getPetId());
|
||||||
if (existing == null) {
|
if (existing == null) {
|
||||||
DropdownOption option = new DropdownOption();
|
DropdownOption option = new DropdownOption();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import javafx.fxml.FXML;
|
|||||||
import javafx.scene.Node;
|
import javafx.scene.Node;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.scene.control.ListCell;
|
import javafx.scene.control.ListCell;
|
||||||
|
|
||||||
@@ -18,7 +19,6 @@ import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
|||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
import java.time.DayOfWeek;
|
|
||||||
import java.time.LocalTime;
|
import java.time.LocalTime;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -33,6 +33,8 @@ public class AppointmentDialogController {
|
|||||||
@FXML private ComboBox<DropdownOption> cbCustomer;
|
@FXML private ComboBox<DropdownOption> cbCustomer;
|
||||||
@FXML private ComboBox<DropdownOption> cbPet;
|
@FXML private ComboBox<DropdownOption> cbPet;
|
||||||
@FXML private ComboBox<DropdownOption> cbEmployee;
|
@FXML private ComboBox<DropdownOption> cbEmployee;
|
||||||
|
@FXML private ComboBox<DropdownOption> cbStore;
|
||||||
|
@FXML private VBox vbStore;
|
||||||
|
|
||||||
@FXML private ComboBox<Integer> cbHour;
|
@FXML private ComboBox<Integer> cbHour;
|
||||||
@FXML private ComboBox<Integer> cbMinute;
|
@FXML private ComboBox<Integer> cbMinute;
|
||||||
@@ -46,8 +48,10 @@ public class AppointmentDialogController {
|
|||||||
private String mode = null;
|
private String mode = null;
|
||||||
private AppointmentDTO selectedAppointment = null;
|
private AppointmentDTO selectedAppointment = null;
|
||||||
private Long pendingPetSelectionId = null;
|
private Long pendingPetSelectionId = null;
|
||||||
|
private Long pendingStoreId = null;
|
||||||
private boolean isOriginallyCancel = false;
|
private boolean isOriginallyCancel = false;
|
||||||
private boolean isOriginallyCompletedOrMissed = false;
|
private boolean isOriginallyCompletedOrMissed = false;
|
||||||
|
private boolean isEditing = false;
|
||||||
|
|
||||||
public void setMode(String mode) {
|
public void setMode(String mode) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
@@ -63,18 +67,39 @@ public class AppointmentDialogController {
|
|||||||
@FXML
|
@FXML
|
||||||
public void initialize() {
|
public void initialize() {
|
||||||
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Booked", "Completed", "Missed", "Cancelled"));
|
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Booked", "Completed", "Missed", "Cancelled"));
|
||||||
|
cbAppointmentStatus.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
if (newVal == null) return;
|
||||||
|
boolean lockAll = "Cancelled".equalsIgnoreCase(newVal)
|
||||||
|
|| "Completed".equalsIgnoreCase(newVal)
|
||||||
|
|| "Missed".equalsIgnoreCase(newVal);
|
||||||
|
if (lockAll) {
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbHour.setDisable(true);
|
||||||
|
cbMinute.setDisable(true);
|
||||||
|
dpAppointmentDate.setDisable(true);
|
||||||
|
cbStore.setDisable(true);
|
||||||
|
} else {
|
||||||
|
if (!isOriginallyCancel && !isOriginallyCompletedOrMissed) {
|
||||||
|
cbEmployee.setDisable(false);
|
||||||
|
cbHour.setDisable(false);
|
||||||
|
cbMinute.setDisable(false);
|
||||||
|
dpAppointmentDate.setDisable(false);
|
||||||
|
if (UserSession.getInstance().isAdmin() && !isEditing) cbStore.setDisable(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
cbPet.setDisable(true);
|
cbPet.setDisable(true);
|
||||||
cbEmployee.setPromptText("Select an employee");
|
cbEmployee.setPromptText("Select an employee");
|
||||||
cbPet.setPromptText("Select a customer first");
|
cbPet.setPromptText("Select a customer first");
|
||||||
cbCustomer.setPromptText("Select a customer");
|
cbCustomer.setPromptText("Select a customer");
|
||||||
cbService.setPromptText("Select a service");
|
cbService.setPromptText("Select a service");
|
||||||
LocalDate minDate = minAppointmentDate();
|
LocalDate today = LocalDate.now();
|
||||||
dpAppointmentDate.setValue(minDate);
|
dpAppointmentDate.setValue(today);
|
||||||
dpAppointmentDate.setDayCellFactory(picker -> new javafx.scene.control.DateCell() {
|
dpAppointmentDate.setDayCellFactory(picker -> new javafx.scene.control.DateCell() {
|
||||||
@Override
|
@Override
|
||||||
public void updateItem(LocalDate item, boolean empty) {
|
public void updateItem(LocalDate item, boolean empty) {
|
||||||
super.updateItem(item, empty);
|
super.updateItem(item, empty);
|
||||||
setDisable(empty || item.isBefore(minDate));
|
setDisable(empty || item.isBefore(today));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
cbAppointmentStatus.setValue("Booked");
|
cbAppointmentStatus.setValue("Booked");
|
||||||
@@ -179,7 +204,7 @@ public class AppointmentDialogController {
|
|||||||
Long customerId = newValue != null ? newValue.getId() : null;
|
Long customerId = newValue != null ? newValue.getId() : null;
|
||||||
cbPet.setValue(null);
|
cbPet.setValue(null);
|
||||||
cbPet.setItems(FXCollections.observableArrayList());
|
cbPet.setItems(FXCollections.observableArrayList());
|
||||||
cbPet.setDisable(customerId == null);
|
cbPet.setDisable(customerId == null || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing);
|
||||||
if (customerId != null) {
|
if (customerId != null) {
|
||||||
cbPet.setPromptText("Loading customer pets...");
|
cbPet.setPromptText("Loading customer pets...");
|
||||||
loadCustomerPets(customerId);
|
loadCustomerPets(customerId);
|
||||||
@@ -189,19 +214,47 @@ public class AppointmentDialogController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
cbStore.setCellFactory(param -> new ListCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(DropdownOption option, boolean empty) {
|
||||||
|
super.updateItem(option, empty);
|
||||||
|
setText(empty || option == null ? null : option.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cbStore.setButtonCell(new ListCell<>() {
|
||||||
|
@Override
|
||||||
|
protected void updateItem(DropdownOption option, boolean empty) {
|
||||||
|
super.updateItem(option, empty);
|
||||||
|
setText(empty || option == null ? null : option.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
vbStore.setVisible(true);
|
||||||
|
vbStore.setManaged(true);
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbEmployee.setPromptText("Select a store first");
|
||||||
|
}
|
||||||
|
|
||||||
btnSave.setOnMouseClicked(this::buttonSaveClicked);
|
btnSave.setOnMouseClicked(this::buttonSaveClicked);
|
||||||
btnCancel.setOnMouseClicked(this::closeStage);
|
btnCancel.setOnMouseClicked(this::closeStage);
|
||||||
|
|
||||||
loadServices();
|
loadServices();
|
||||||
loadAppointmentCustomers();
|
loadAppointmentCustomers();
|
||||||
loadEmployees();
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
loadStores();
|
||||||
|
} else {
|
||||||
|
loadEmployees();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void displayAppointmentDetails(AppointmentDTO appt) {
|
public void displayAppointmentDetails(AppointmentDTO appt) {
|
||||||
|
|
||||||
selectedAppointment = appt;
|
selectedAppointment = appt;
|
||||||
|
isEditing = true;
|
||||||
lblAppointmentId.setText("ID: " + appt.getAppointmentId());
|
lblAppointmentId.setText("ID: " + appt.getAppointmentId());
|
||||||
pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null;
|
pendingPetSelectionId = appt.getPetId() > 0 ? (long) appt.getPetId() : null;
|
||||||
|
pendingStoreId = appt.getStoreId();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
dpAppointmentDate.setValue(
|
dpAppointmentDate.setValue(
|
||||||
@@ -248,6 +301,7 @@ public class AppointmentDialogController {
|
|||||||
dpAppointmentDate.setDisable(true);
|
dpAppointmentDate.setDisable(true);
|
||||||
cbAppointmentStatus.setDisable(true);
|
cbAppointmentStatus.setDisable(true);
|
||||||
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Cancelled"));
|
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Cancelled"));
|
||||||
|
cbStore.setDisable(true);
|
||||||
btnSave.setDisable(true);
|
btnSave.setDisable(true);
|
||||||
} else if (isOriginallyCompletedOrMissed) {
|
} else if (isOriginallyCompletedOrMissed) {
|
||||||
cbService.setDisable(true);
|
cbService.setDisable(true);
|
||||||
@@ -257,12 +311,14 @@ public class AppointmentDialogController {
|
|||||||
cbHour.setDisable(true);
|
cbHour.setDisable(true);
|
||||||
cbMinute.setDisable(true);
|
cbMinute.setDisable(true);
|
||||||
dpAppointmentDate.setDisable(true);
|
dpAppointmentDate.setDisable(true);
|
||||||
|
cbStore.setDisable(true);
|
||||||
cbAppointmentStatus.setDisable(false);
|
cbAppointmentStatus.setDisable(false);
|
||||||
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Completed", "Missed"));
|
cbAppointmentStatus.setItems(FXCollections.observableArrayList("Completed", "Missed"));
|
||||||
} else {
|
} else {
|
||||||
cbService.setDisable(true);
|
cbService.setDisable(true);
|
||||||
cbCustomer.setDisable(true);
|
cbCustomer.setDisable(true);
|
||||||
cbPet.setDisable(true);
|
cbPet.setDisable(true);
|
||||||
|
cbStore.setDisable(true);
|
||||||
cbEmployee.setDisable(false);
|
cbEmployee.setDisable(false);
|
||||||
cbHour.setDisable(false);
|
cbHour.setDisable(false);
|
||||||
cbMinute.setDisable(false);
|
cbMinute.setDisable(false);
|
||||||
@@ -288,7 +344,16 @@ public class AppointmentDialogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
LocalTime appointmentTime = LocalTime.of(cbHour.getValue(), cbMinute.getValue());
|
LocalTime appointmentTime = LocalTime.of(cbHour.getValue(), cbMinute.getValue());
|
||||||
Long storeId = UserSession.getInstance().getStoreId();
|
Long storeId;
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
if (cbStore.getSelectionModel().getSelectedItem() == null) {
|
||||||
|
showError("Store is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
storeId = cbStore.getSelectionModel().getSelectedItem().getId();
|
||||||
|
} else {
|
||||||
|
storeId = UserSession.getInstance().getStoreId();
|
||||||
|
}
|
||||||
if (storeId == null || storeId <= 0) {
|
if (storeId == null || storeId <= 0) {
|
||||||
showError("Store is not set for this account");
|
showError("Store is not set for this account");
|
||||||
return;
|
return;
|
||||||
@@ -408,7 +473,7 @@ public class AppointmentDialogController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
cbPet.setItems(petOptions);
|
cbPet.setItems(petOptions);
|
||||||
cbPet.setDisable(petOptions.isEmpty());
|
cbPet.setDisable(petOptions.isEmpty() || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing);
|
||||||
cbPet.setPromptText(petOptions.isEmpty() ? "No pets for selected customer" : "Select a pet");
|
cbPet.setPromptText(petOptions.isEmpty() ? "No pets for selected customer" : "Select a pet");
|
||||||
if (pendingPetSelectionId != null) {
|
if (pendingPetSelectionId != null) {
|
||||||
for (DropdownOption pet : cbPet.getItems()) {
|
for (DropdownOption pet : cbPet.getItems()) {
|
||||||
@@ -462,7 +527,7 @@ public class AppointmentDialogController {
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
cbCustomer.setItems(FXCollections.observableArrayList(customers));
|
cbCustomer.setItems(FXCollections.observableArrayList(customers));
|
||||||
boolean hasCustomers = customers != null && !customers.isEmpty();
|
boolean hasCustomers = customers != null && !customers.isEmpty();
|
||||||
cbCustomer.setDisable(!hasCustomers);
|
cbCustomer.setDisable(!hasCustomers || isOriginallyCancel || isOriginallyCompletedOrMissed || isEditing);
|
||||||
cbPet.setDisable(true);
|
cbPet.setDisable(true);
|
||||||
cbPet.setItems(FXCollections.observableArrayList());
|
cbPet.setItems(FXCollections.observableArrayList());
|
||||||
cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet");
|
cbCustomer.setPromptText(hasCustomers ? "Select a customer" : "No customers with pets yet");
|
||||||
@@ -484,17 +549,70 @@ public class AppointmentDialogController {
|
|||||||
}).start();
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private LocalDate minAppointmentDate() {
|
|
||||||
LocalDate date = LocalDate.now();
|
private void loadStores() {
|
||||||
int businessDaysAdded = 0;
|
new Thread(() -> {
|
||||||
while (businessDaysAdded < 2) {
|
try {
|
||||||
date = date.plusDays(1);
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
DayOfWeek dow = date.getDayOfWeek();
|
Platform.runLater(() -> {
|
||||||
if (dow != DayOfWeek.SATURDAY && dow != DayOfWeek.SUNDAY) {
|
if (stores != null) {
|
||||||
businessDaysAdded++;
|
cbStore.setItems(FXCollections.observableArrayList(stores));
|
||||||
|
cbStore.valueProperty().addListener((obs, oldVal, newVal) -> {
|
||||||
|
Long sid = newVal != null ? newVal.getId() : null;
|
||||||
|
cbEmployee.setValue(null);
|
||||||
|
cbEmployee.setItems(FXCollections.observableArrayList());
|
||||||
|
if (sid != null) {
|
||||||
|
if (!isOriginallyCancel && !isOriginallyCompletedOrMissed) {
|
||||||
|
cbEmployee.setDisable(false);
|
||||||
|
cbEmployee.setPromptText("Select an employee");
|
||||||
|
}
|
||||||
|
loadEmployeesForStore(sid);
|
||||||
|
} else {
|
||||||
|
if (!isOriginallyCancel && !isOriginallyCompletedOrMissed) {
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbEmployee.setPromptText("Select a store first");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (pendingStoreId != null) {
|
||||||
|
for (DropdownOption store : cbStore.getItems()) {
|
||||||
|
if (pendingStoreId.equals(store.getId())) {
|
||||||
|
cbStore.setValue(store);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pendingStoreId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> ActivityLogger.getInstance().logException(
|
||||||
|
"AppointmentDialogController.loadStores",
|
||||||
|
e,
|
||||||
|
"Loading stores for appointment dialog"));
|
||||||
}
|
}
|
||||||
}
|
}).start();
|
||||||
return date;
|
}
|
||||||
|
|
||||||
|
private void loadEmployeesForStore(Long storeId) {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> employees = DropdownApi.getInstance().getStoreEmployees(storeId);
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
cbEmployee.setItems(FXCollections.observableArrayList(employees));
|
||||||
|
applySelectedEmployee();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
ActivityLogger.getInstance().logException(
|
||||||
|
"AppointmentDialogController.loadEmployeesForStore",
|
||||||
|
e,
|
||||||
|
"Loading employees for store");
|
||||||
|
cbEmployee.setDisable(true);
|
||||||
|
cbEmployee.setPromptText("Unable to load employees");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void loadEmployees() {
|
private void loadEmployees() {
|
||||||
|
|||||||
@@ -0,0 +1,177 @@
|
|||||||
|
package org.example.petshopdesktop.controllers.dialogcontrollers;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
|
import javafx.event.ActionEvent;
|
||||||
|
import javafx.fxml.FXML;
|
||||||
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.PasswordField;
|
||||||
|
import javafx.scene.control.TextField;
|
||||||
|
import javafx.stage.Stage;
|
||||||
|
import org.example.petshopdesktop.Validator;
|
||||||
|
import org.example.petshopdesktop.api.dto.user.UserRequest;
|
||||||
|
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
||||||
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
import org.example.petshopdesktop.util.TextFieldFormatSupport;
|
||||||
|
|
||||||
|
public class CustomerEditDialogController {
|
||||||
|
|
||||||
|
@FXML private TextField txtFirstName;
|
||||||
|
@FXML private TextField txtLastName;
|
||||||
|
@FXML private TextField txtEmail;
|
||||||
|
@FXML private TextField txtPhone;
|
||||||
|
@FXML private TextField txtUsername;
|
||||||
|
@FXML private PasswordField txtPassword;
|
||||||
|
@FXML private PasswordField txtPasswordConfirm;
|
||||||
|
@FXML private ComboBox<String> cbActive;
|
||||||
|
@FXML private TextField txtLoyaltyPoints;
|
||||||
|
@FXML private Label lblError;
|
||||||
|
@FXML private Button btnSave;
|
||||||
|
|
||||||
|
private UserResponse customer;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void initialize() {
|
||||||
|
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
|
||||||
|
cbActive.setItems(FXCollections.observableArrayList("Active", "Inactive"));
|
||||||
|
|
||||||
|
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||||
|
txtLoyaltyPoints.setDisable(!isAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCustomer(UserResponse user) {
|
||||||
|
this.customer = user;
|
||||||
|
String fullName = user.getFullName() == null ? "" : user.getFullName();
|
||||||
|
String[] names = splitFullName(fullName);
|
||||||
|
txtFirstName.setText(names[0]);
|
||||||
|
txtLastName.setText(names[1]);
|
||||||
|
txtEmail.setText(user.getEmail());
|
||||||
|
txtPhone.setText(user.getPhone());
|
||||||
|
txtUsername.setText(user.getUsername());
|
||||||
|
cbActive.setValue(Boolean.TRUE.equals(user.getActive()) ? "Active" : "Inactive");
|
||||||
|
int pts = user.getLoyaltyPoints() != null ? user.getLoyaltyPoints() : 0;
|
||||||
|
txtLoyaltyPoints.setText(String.valueOf(pts));
|
||||||
|
}
|
||||||
|
|
||||||
|
private String[] splitFullName(String fullName) {
|
||||||
|
if (fullName == null || fullName.trim().isEmpty()) return new String[]{"", ""};
|
||||||
|
String[] parts = fullName.trim().split("\\s+", 2);
|
||||||
|
return new String[]{parts.length > 0 ? parts[0] : "", parts.length > 1 ? parts[1] : ""};
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnSaveClicked(ActionEvent event) {
|
||||||
|
lblError.setText("");
|
||||||
|
if (customer == null) {
|
||||||
|
lblError.setText("No customer selected.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String firstName = value(txtFirstName);
|
||||||
|
String lastName = value(txtLastName);
|
||||||
|
String email = value(txtEmail);
|
||||||
|
String phone = value(txtPhone);
|
||||||
|
String username = value(txtUsername);
|
||||||
|
String password = txtPassword.getText() == null ? "" : txtPassword.getText().trim();
|
||||||
|
String confirm = txtPasswordConfirm.getText() == null ? "" : txtPasswordConfirm.getText().trim();
|
||||||
|
String loyaltyPointsText = value(txtLoyaltyPoints);
|
||||||
|
|
||||||
|
if (firstName.isBlank() || lastName.isBlank()) {
|
||||||
|
lblError.setText("First name and last name are required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (email.isBlank()) {
|
||||||
|
lblError.setText("Email is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (phone.isBlank()) {
|
||||||
|
lblError.setText("Phone is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String phoneError = Validator.isValidPhoneNumber(phone, "Phone");
|
||||||
|
if (!phoneError.isEmpty()) {
|
||||||
|
lblError.setText(phoneError.trim());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (username.isBlank()) {
|
||||||
|
lblError.setText("Username is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password.isEmpty() && password.length() < 6) {
|
||||||
|
lblError.setText("Password must be at least 6 characters.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!password.equals(confirm)) {
|
||||||
|
lblError.setText("Passwords do not match.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Integer loyaltyPoints = null;
|
||||||
|
if (UserSession.getInstance().isAdmin()) {
|
||||||
|
if (loyaltyPointsText.isBlank()) {
|
||||||
|
lblError.setText("Loyalty points is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
loyaltyPoints = Integer.parseInt(loyaltyPointsText);
|
||||||
|
if (loyaltyPoints < 0) {
|
||||||
|
lblError.setText("Loyalty points cannot be negative.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
lblError.setText("Loyalty points must be a valid integer.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
btnSave.setDisable(true);
|
||||||
|
|
||||||
|
boolean active = "Active".equals(cbActive.getValue());
|
||||||
|
Integer finalLoyaltyPoints = loyaltyPoints;
|
||||||
|
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
UserRequest request = new UserRequest();
|
||||||
|
request.setUsername(username);
|
||||||
|
request.setPassword(password.isEmpty() ? null : password);
|
||||||
|
request.setFirstName(firstName);
|
||||||
|
request.setLastName(lastName);
|
||||||
|
request.setFullName(firstName + " " + lastName);
|
||||||
|
request.setEmail(email);
|
||||||
|
request.setPhone(phone);
|
||||||
|
request.setRole(customer.getRole());
|
||||||
|
request.setActive(active);
|
||||||
|
if (finalLoyaltyPoints != null) request.setLoyaltyPoints(finalLoyaltyPoints);
|
||||||
|
|
||||||
|
CustomerApi.getInstance().updateCustomer(customer.getId(), request);
|
||||||
|
|
||||||
|
Platform.runLater(this::close);
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("CustomerEditDialogController.btnSaveClicked", e, "Updating customer");
|
||||||
|
String msg = e.getMessage() == null ? "Could not update customer." : e.getMessage();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
lblError.setText(msg);
|
||||||
|
btnSave.setDisable(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
void btnCancelClicked(ActionEvent event) {
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void close() {
|
||||||
|
Stage stage = (Stage) btnSave.getScene().getWindow();
|
||||||
|
stage.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static String value(TextField tf) {
|
||||||
|
return tf.getText() == null ? "" : tf.getText().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package org.example.petshopdesktop.controllers.dialogcontrollers;
|
package org.example.petshopdesktop.controllers.dialogcontrollers;
|
||||||
|
|
||||||
|
import javafx.application.Platform;
|
||||||
import javafx.collections.FXCollections;
|
import javafx.collections.FXCollections;
|
||||||
import javafx.collections.ObservableList;
|
import javafx.collections.ObservableList;
|
||||||
import javafx.event.EventHandler;
|
import javafx.event.EventHandler;
|
||||||
@@ -9,66 +10,65 @@ import javafx.scene.control.Alert;
|
|||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
import javafx.scene.control.ComboBox;
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.scene.input.MouseEvent;
|
import javafx.scene.input.MouseEvent;
|
||||||
|
import javafx.scene.layout.VBox;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import javafx.util.StringConverter;
|
import javafx.util.StringConverter;
|
||||||
import org.example.petshopdesktop.models.Inventory;
|
import org.example.petshopdesktop.models.Inventory;
|
||||||
import org.example.petshopdesktop.Validator;
|
import org.example.petshopdesktop.Validator;
|
||||||
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.dto.inventory.InventoryRequest;
|
import org.example.petshopdesktop.api.dto.inventory.InventoryRequest;
|
||||||
import org.example.petshopdesktop.api.dto.inventory.InventoryResponse;
|
|
||||||
import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
import org.example.petshopdesktop.api.dto.product.ProductResponse;
|
||||||
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.InventoryApi;
|
import org.example.petshopdesktop.api.endpoints.InventoryApi;
|
||||||
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
import org.example.petshopdesktop.api.endpoints.ProductApi;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.auth.UserSession;
|
||||||
import org.example.petshopdesktop.models.Product;
|
import org.example.petshopdesktop.models.Product;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
|
||||||
|
|
||||||
public class InventoryDialogController {
|
public class InventoryDialogController {
|
||||||
|
|
||||||
//FXML elements
|
@FXML private Button btnCancel;
|
||||||
@FXML
|
@FXML private Button btnSave;
|
||||||
private Button btnCancel;
|
@FXML private ComboBox<Product> cbProduct;
|
||||||
|
@FXML private ComboBox<DropdownOption> cbStore;
|
||||||
|
@FXML private VBox vbStoreField;
|
||||||
|
@FXML private Label lblInventoryId;
|
||||||
|
@FXML private Label lblMode;
|
||||||
|
@FXML private TextField txtQuantity;
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button btnSave;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private ComboBox<Product> cbProduct;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblInventoryId;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblMode;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField txtQuantity;
|
|
||||||
|
|
||||||
//Determines if the mode is add or edit
|
|
||||||
private String mode = null;
|
private String mode = null;
|
||||||
|
private Long pendingStoreId = null;
|
||||||
|
|
||||||
//Loads upon .FXML boot
|
|
||||||
@FXML
|
@FXML
|
||||||
void initialize() {
|
void initialize() {
|
||||||
cbProduct.setConverter(new StringConverter<Product>() {
|
cbProduct.setConverter(new StringConverter<Product>() {
|
||||||
|
|
||||||
//Displays product in combobox (prodID + name)
|
|
||||||
@Override
|
@Override
|
||||||
public String toString(Product product) {
|
public String toString(Product product) {
|
||||||
return product == null ? "" : product.getProdId() + ": " + product.getProdName();
|
return product == null ? "" : product.getProdId() + ": " + product.getProdName();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Not needed
|
|
||||||
@Override
|
@Override
|
||||||
public Product fromString(String string) { return null; }
|
public Product fromString(String string) { return null; }
|
||||||
});
|
});
|
||||||
|
|
||||||
//Load product list from API into combobox
|
cbStore.setCellFactory(param -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(DropdownOption o, boolean empty) {
|
||||||
|
super.updateItem(o, empty);
|
||||||
|
setText(empty || o == null ? null : o.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cbStore.setButtonCell(new ListCell<>() {
|
||||||
|
@Override protected void updateItem(DropdownOption o, boolean empty) {
|
||||||
|
super.updateItem(o, empty);
|
||||||
|
setText(empty || o == null ? null : o.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
|
List<ProductResponse> productResponses = ProductApi.getInstance().listProducts(null);
|
||||||
if (productResponses != null) {
|
if (productResponses != null) {
|
||||||
@@ -89,10 +89,16 @@ public class InventoryDialogController {
|
|||||||
"InventoryDialogController.initialize",
|
"InventoryDialogController.initialize",
|
||||||
e,
|
e,
|
||||||
"Loading products for combo box");
|
"Loading products for combo box");
|
||||||
System.out.println("Error loading products: " + e.getMessage());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
//Save button handler
|
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||||
|
if (isAdmin) {
|
||||||
|
loadStores();
|
||||||
|
} else {
|
||||||
|
vbStoreField.setManaged(false);
|
||||||
|
vbStoreField.setVisible(false);
|
||||||
|
}
|
||||||
|
|
||||||
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
btnSave.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
||||||
@Override
|
@Override
|
||||||
public void handle(MouseEvent mouseEvent) {
|
public void handle(MouseEvent mouseEvent) {
|
||||||
@@ -100,7 +106,6 @@ public class InventoryDialogController {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
//Cancel button handler
|
|
||||||
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
btnCancel.setOnMouseClicked(new EventHandler<MouseEvent>() {
|
||||||
@Override
|
@Override
|
||||||
public void handle(MouseEvent mouseEvent) {
|
public void handle(MouseEvent mouseEvent) {
|
||||||
@@ -109,29 +114,66 @@ public class InventoryDialogController {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
//Handles save button click event
|
private void loadStores() {
|
||||||
|
new Thread(() -> {
|
||||||
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
cbStore.setItems(FXCollections.observableArrayList(stores));
|
||||||
|
applyPendingStore();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("InventoryDialogController.loadStores", e, "Loading stores");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
cbStore.setDisable(true);
|
||||||
|
cbStore.setPromptText("Unable to load stores");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPendingStore() {
|
||||||
|
if (pendingStoreId == null) return;
|
||||||
|
for (DropdownOption opt : cbStore.getItems()) {
|
||||||
|
if (opt.getId() != null && opt.getId().equals(pendingStoreId)) {
|
||||||
|
cbStore.setValue(opt);
|
||||||
|
pendingStoreId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void buttonSaveClicked(MouseEvent mouseEvent) {
|
private void buttonSaveClicked(MouseEvent mouseEvent) {
|
||||||
int numRow = 0;
|
|
||||||
String errorMsg = "";
|
String errorMsg = "";
|
||||||
|
|
||||||
if (cbProduct.getSelectionModel().getSelectedItem() == null) {
|
if (cbProduct.getSelectionModel().getSelectedItem() == null) {
|
||||||
errorMsg += "Product is required.\n";
|
errorMsg += "Product is required.\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
//Validate inputs
|
|
||||||
errorMsg += Validator.isPresent(txtQuantity.getText(), "Quantity");
|
errorMsg += Validator.isPresent(txtQuantity.getText(), "Quantity");
|
||||||
errorMsg += Validator.isLessThanVarChars(txtQuantity.getText(), "Quantity", 11);
|
errorMsg += Validator.isLessThanVarChars(txtQuantity.getText(), "Quantity", 11);
|
||||||
errorMsg += Validator.isPositiveInteger(txtQuantity.getText(), "Quantity");
|
errorMsg += Validator.isPositiveInteger(txtQuantity.getText(), "Quantity");
|
||||||
|
|
||||||
//Operation only occurs if there are no errors
|
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||||
|
Long storeId;
|
||||||
|
if (isAdmin) {
|
||||||
|
if (cbStore.getValue() == null) {
|
||||||
|
errorMsg += "Store is required.\n";
|
||||||
|
storeId = null;
|
||||||
|
} else {
|
||||||
|
storeId = cbStore.getValue().getId();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
storeId = UserSession.getInstance().getStoreId();
|
||||||
|
if (storeId == null || storeId <= 0) {
|
||||||
|
errorMsg += "Store is not set for this account.\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (errorMsg.isEmpty()) {
|
if (errorMsg.isEmpty()) {
|
||||||
try {
|
try {
|
||||||
InventoryRequest request = new InventoryRequest();
|
InventoryRequest request = new InventoryRequest();
|
||||||
Product selectedProduct = cbProduct.getSelectionModel().getSelectedItem();
|
Product selectedProduct = cbProduct.getSelectionModel().getSelectedItem();
|
||||||
Long storeId = UserSession.getInstance().getStoreId();
|
|
||||||
if (storeId == null || storeId <= 0) {
|
|
||||||
throw new IllegalStateException("Store is not set for this account");
|
|
||||||
}
|
|
||||||
request.setProdId((long) selectedProduct.getProdId());
|
request.setProdId((long) selectedProduct.getProdId());
|
||||||
int quantity;
|
int quantity;
|
||||||
try {
|
try {
|
||||||
@@ -168,10 +210,7 @@ public class InventoryDialogController {
|
|||||||
alert.setContentText(e.getMessage());
|
alert.setContentText(e.getMessage());
|
||||||
alert.showAndWait();
|
alert.showAndWait();
|
||||||
}
|
}
|
||||||
}
|
} else {
|
||||||
|
|
||||||
//Displays validation errors
|
|
||||||
else {
|
|
||||||
Alert alert = new Alert(Alert.AlertType.ERROR);
|
Alert alert = new Alert(Alert.AlertType.ERROR);
|
||||||
alert.setHeaderText("Input Error");
|
alert.setHeaderText("Input Error");
|
||||||
alert.setContentText(errorMsg);
|
alert.setContentText(errorMsg);
|
||||||
@@ -179,22 +218,16 @@ public class InventoryDialogController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Close dialog view
|
|
||||||
private void closeStage(MouseEvent mouseEvent) {
|
private void closeStage(MouseEvent mouseEvent) {
|
||||||
Node node = (Node) mouseEvent.getSource();
|
Node node = (Node) mouseEvent.getSource();
|
||||||
Stage stage = (Stage) node.getScene().getWindow();
|
Stage stage = (Stage) node.getScene().getWindow();
|
||||||
stage.close();
|
stage.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
//Editing
|
|
||||||
//Displays fields with existing inventory data
|
|
||||||
public void displayInventoryDetails(Inventory inventory) {
|
public void displayInventoryDetails(Inventory inventory) {
|
||||||
if (inventory != null) {
|
if (inventory != null) {
|
||||||
|
|
||||||
//Displays inventory ID
|
|
||||||
lblInventoryId.setText("ID: " + inventory.getInventoryId());
|
lblInventoryId.setText("ID: " + inventory.getInventoryId());
|
||||||
|
|
||||||
//Selecting matching product in combobox
|
|
||||||
for (Product product : cbProduct.getItems()) {
|
for (Product product : cbProduct.getItems()) {
|
||||||
if (product.getProdId() == inventory.getProdId()) {
|
if (product.getProdId() == inventory.getProdId()) {
|
||||||
cbProduct.getSelectionModel().select(product);
|
cbProduct.getSelectionModel().select(product);
|
||||||
@@ -203,10 +236,15 @@ public class InventoryDialogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
txtQuantity.setText(String.valueOf(inventory.getQuantity()));
|
txtQuantity.setText(String.valueOf(inventory.getQuantity()));
|
||||||
|
|
||||||
|
boolean isAdmin = UserSession.getInstance().isAdmin();
|
||||||
|
if (isAdmin && inventory.getStoreId() > 0) {
|
||||||
|
pendingStoreId = (long) inventory.getStoreId();
|
||||||
|
applyPendingStore();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//Sets dialog view to add/edit. Updates UI labels
|
|
||||||
public void setMode(String mode) {
|
public void setMode(String mode) {
|
||||||
this.mode = mode;
|
this.mode = mode;
|
||||||
lblMode.setText(mode + " Inventory");
|
lblMode.setText(mode + " Inventory");
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import javafx.beans.property.ReadOnlyObjectWrapper;
|
|||||||
import javafx.beans.property.ReadOnlyStringWrapper;
|
import javafx.beans.property.ReadOnlyStringWrapper;
|
||||||
import javafx.scene.control.*;
|
import javafx.scene.control.*;
|
||||||
import javafx.scene.control.cell.PropertyValueFactory;
|
import javafx.scene.control.cell.PropertyValueFactory;
|
||||||
|
import javafx.scene.layout.HBox;
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
|
import org.example.petshopdesktop.api.dto.sale.SaleItemRequest;
|
||||||
@@ -80,6 +81,21 @@ public class RefundDialogController {
|
|||||||
@FXML
|
@FXML
|
||||||
private ComboBox<String> cbPaymentMethod;
|
private ComboBox<String> cbPaymentMethod;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblRefundSubtotal;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox hbRefundCouponDiscount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblRefundCouponDiscount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private HBox hbRefundLoyaltyDiscount;
|
||||||
|
|
||||||
|
@FXML
|
||||||
|
private Label lblRefundLoyaltyDiscount;
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
private Label lblRefundTotal;
|
private Label lblRefundTotal;
|
||||||
|
|
||||||
@@ -448,8 +464,68 @@ public class RefundDialogController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void updateRefundTotal() {
|
private void updateRefundTotal() {
|
||||||
double total = refundItems.stream().mapToDouble(RefundItem::getTotal).sum();
|
if (currentSale == null || refundItems.isEmpty()) {
|
||||||
lblRefundTotal.setText(currency.format(total));
|
lblRefundSubtotal.setText(currency.format(0.0));
|
||||||
|
lblRefundTotal.setText(currency.format(0.0));
|
||||||
|
hbRefundCouponDiscount.setVisible(false);
|
||||||
|
hbRefundCouponDiscount.setManaged(false);
|
||||||
|
hbRefundLoyaltyDiscount.setVisible(false);
|
||||||
|
hbRefundLoyaltyDiscount.setManaged(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
double originalSubtotal = currentSale.getSubtotalAmount() != null
|
||||||
|
? currentSale.getSubtotalAmount().doubleValue() : 0;
|
||||||
|
if (originalSubtotal == 0 && currentSale.getItems() != null) {
|
||||||
|
for (var item : currentSale.getItems()) {
|
||||||
|
if (item.getUnitPrice() != null && item.getQuantity() != null) {
|
||||||
|
originalSubtotal += item.getUnitPrice().doubleValue() * Math.abs(item.getQuantity());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double cartSubtotal = refundItems.stream().mapToDouble(RefundItem::getTotal).sum();
|
||||||
|
double originalTotal = currentSale.getTotalAmount() != null
|
||||||
|
? Math.abs(currentSale.getTotalAmount().doubleValue()) : 0;
|
||||||
|
|
||||||
|
double refundTotal;
|
||||||
|
double couponDiscountRefunded = 0;
|
||||||
|
double loyaltyDiscountRefunded = 0;
|
||||||
|
|
||||||
|
if (originalSubtotal > 0) {
|
||||||
|
double ratio = cartSubtotal / originalSubtotal;
|
||||||
|
refundTotal = originalTotal * ratio;
|
||||||
|
if (currentSale.getCouponDiscountAmount() != null) {
|
||||||
|
couponDiscountRefunded = currentSale.getCouponDiscountAmount().doubleValue() * ratio;
|
||||||
|
}
|
||||||
|
if (currentSale.getLoyaltyDiscountAmount() != null) {
|
||||||
|
loyaltyDiscountRefunded = currentSale.getLoyaltyDiscountAmount().doubleValue() * ratio;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
refundTotal = cartSubtotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
lblRefundSubtotal.setText(currency.format(cartSubtotal));
|
||||||
|
|
||||||
|
if (couponDiscountRefunded > 0.001) {
|
||||||
|
lblRefundCouponDiscount.setText("-" + currency.format(couponDiscountRefunded));
|
||||||
|
hbRefundCouponDiscount.setVisible(true);
|
||||||
|
hbRefundCouponDiscount.setManaged(true);
|
||||||
|
} else {
|
||||||
|
hbRefundCouponDiscount.setVisible(false);
|
||||||
|
hbRefundCouponDiscount.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (loyaltyDiscountRefunded > 0.001) {
|
||||||
|
lblRefundLoyaltyDiscount.setText("-" + currency.format(loyaltyDiscountRefunded));
|
||||||
|
hbRefundLoyaltyDiscount.setVisible(true);
|
||||||
|
hbRefundLoyaltyDiscount.setManaged(true);
|
||||||
|
} else {
|
||||||
|
hbRefundLoyaltyDiscount.setVisible(false);
|
||||||
|
hbRefundLoyaltyDiscount.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
lblRefundTotal.setText(currency.format(refundTotal));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void closeDialog() {
|
private void closeDialog() {
|
||||||
|
|||||||
@@ -27,6 +27,13 @@ public class SaleDetailDialogController {
|
|||||||
@FXML private Label lblSaleDate;
|
@FXML private Label lblSaleDate;
|
||||||
@FXML private Label lblEmployee;
|
@FXML private Label lblEmployee;
|
||||||
@FXML private Label lblPayment;
|
@FXML private Label lblPayment;
|
||||||
|
@FXML private Label lblCustomer;
|
||||||
|
@FXML private javafx.scene.layout.HBox hbDetailSubtotal;
|
||||||
|
@FXML private Label lblDetailSubtotal;
|
||||||
|
@FXML private javafx.scene.layout.HBox hbDetailCouponDiscount;
|
||||||
|
@FXML private Label lblDetailCouponDiscount;
|
||||||
|
@FXML private javafx.scene.layout.HBox hbDetailLoyaltyDiscount;
|
||||||
|
@FXML private Label lblDetailLoyaltyDiscount;
|
||||||
@FXML private Label lblTotal;
|
@FXML private Label lblTotal;
|
||||||
@FXML private Button btnRefund;
|
@FXML private Button btnRefund;
|
||||||
@FXML private TableView<SaleDetail.SaleDetailItem> tvItems;
|
@FXML private TableView<SaleDetail.SaleDetailItem> tvItems;
|
||||||
@@ -61,6 +68,39 @@ public class SaleDetailDialogController {
|
|||||||
lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : "");
|
lblSaleDate.setText(sale.getSaleDate() != null ? sale.getSaleDate().format(DATE_FORMATTER) : "");
|
||||||
lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : "");
|
lblEmployee.setText(sale.getEmployeeName() != null ? sale.getEmployeeName() : "");
|
||||||
lblPayment.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : "");
|
lblPayment.setText(sale.getPaymentMethod() != null ? sale.getPaymentMethod() : "");
|
||||||
|
lblCustomer.setText(sale.getCustomerName() != null && !sale.getCustomerName().isEmpty() ? sale.getCustomerName() : "-");
|
||||||
|
|
||||||
|
if (!sale.isRefund() && sale.getSubtotalAmount() > 0) {
|
||||||
|
hbDetailSubtotal.setVisible(true);
|
||||||
|
hbDetailSubtotal.setManaged(true);
|
||||||
|
lblDetailSubtotal.setText(currency.format(sale.getSubtotalAmount()));
|
||||||
|
|
||||||
|
if (sale.getCouponDiscountAmount() > 0.001) {
|
||||||
|
lblDetailCouponDiscount.setText("-" + currency.format(sale.getCouponDiscountAmount()));
|
||||||
|
hbDetailCouponDiscount.setVisible(true);
|
||||||
|
hbDetailCouponDiscount.setManaged(true);
|
||||||
|
} else {
|
||||||
|
hbDetailCouponDiscount.setVisible(false);
|
||||||
|
hbDetailCouponDiscount.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sale.getLoyaltyDiscountAmount() > 0.001) {
|
||||||
|
lblDetailLoyaltyDiscount.setText("-" + currency.format(sale.getLoyaltyDiscountAmount()));
|
||||||
|
hbDetailLoyaltyDiscount.setVisible(true);
|
||||||
|
hbDetailLoyaltyDiscount.setManaged(true);
|
||||||
|
} else {
|
||||||
|
hbDetailLoyaltyDiscount.setVisible(false);
|
||||||
|
hbDetailLoyaltyDiscount.setManaged(false);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
hbDetailSubtotal.setVisible(false);
|
||||||
|
hbDetailSubtotal.setManaged(false);
|
||||||
|
hbDetailCouponDiscount.setVisible(false);
|
||||||
|
hbDetailCouponDiscount.setManaged(false);
|
||||||
|
hbDetailLoyaltyDiscount.setVisible(false);
|
||||||
|
hbDetailLoyaltyDiscount.setManaged(false);
|
||||||
|
}
|
||||||
|
|
||||||
lblTotal.setText(currency.format(sale.getTotalAmount()));
|
lblTotal.setText(currency.format(sale.getTotalAmount()));
|
||||||
tvItems.setItems(sale.getItems());
|
tvItems.setItems(sale.getItems());
|
||||||
if (btnRefund != null) {
|
if (btnRefund != null) {
|
||||||
|
|||||||
@@ -1,84 +1,128 @@
|
|||||||
package org.example.petshopdesktop.controllers.dialogcontrollers;
|
package org.example.petshopdesktop.controllers.dialogcontrollers;
|
||||||
|
|
||||||
import javafx.application.Platform;
|
import javafx.application.Platform;
|
||||||
|
import javafx.collections.FXCollections;
|
||||||
import javafx.event.ActionEvent;
|
import javafx.event.ActionEvent;
|
||||||
import javafx.fxml.FXML;
|
import javafx.fxml.FXML;
|
||||||
import javafx.scene.control.Button;
|
import javafx.scene.control.Button;
|
||||||
|
import javafx.scene.control.ComboBox;
|
||||||
import javafx.scene.control.Label;
|
import javafx.scene.control.Label;
|
||||||
|
import javafx.scene.control.ListCell;
|
||||||
import javafx.scene.control.PasswordField;
|
import javafx.scene.control.PasswordField;
|
||||||
import javafx.scene.control.TextField;
|
import javafx.scene.control.TextField;
|
||||||
import javafx.stage.Stage;
|
import javafx.stage.Stage;
|
||||||
import org.example.petshopdesktop.Validator;
|
import org.example.petshopdesktop.Validator;
|
||||||
import org.example.petshopdesktop.api.dto.user.UserRequest;
|
import org.example.petshopdesktop.api.dto.common.DropdownOption;
|
||||||
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
import org.example.petshopdesktop.api.dto.employee.EmployeeRequest;
|
||||||
import org.example.petshopdesktop.api.endpoints.UserApi;
|
import org.example.petshopdesktop.api.dto.employee.EmployeeResponse;
|
||||||
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
import org.example.petshopdesktop.api.endpoints.DropdownApi;
|
||||||
import org.example.petshopdesktop.auth.UserSession;
|
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
|
||||||
import org.example.petshopdesktop.util.ActivityLogger;
|
import org.example.petshopdesktop.util.ActivityLogger;
|
||||||
import org.example.petshopdesktop.util.TextFieldFormatSupport;
|
import org.example.petshopdesktop.util.TextFieldFormatSupport;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class StaffEditDialogController {
|
public class StaffEditDialogController {
|
||||||
|
|
||||||
@FXML
|
@FXML private TextField txtFirstName;
|
||||||
private TextField txtFirstName;
|
@FXML private TextField txtLastName;
|
||||||
|
@FXML private TextField txtEmail;
|
||||||
|
@FXML private TextField txtPhone;
|
||||||
|
@FXML private TextField txtUsername;
|
||||||
|
@FXML private PasswordField txtPassword;
|
||||||
|
@FXML private PasswordField txtPasswordConfirm;
|
||||||
|
@FXML private ComboBox<String> cbRole;
|
||||||
|
@FXML private ComboBox<String> cbActive;
|
||||||
|
@FXML private ComboBox<DropdownOption> cbStore;
|
||||||
|
@FXML private Label lblError;
|
||||||
|
@FXML private Button btnSave;
|
||||||
|
|
||||||
@FXML
|
private EmployeeResponse employee;
|
||||||
private TextField txtLastName;
|
private Long pendingStoreId = null;
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField txtEmail;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField txtPhone;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private TextField txtUsername;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private PasswordField txtPassword;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private PasswordField txtPasswordConfirm;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Label lblError;
|
|
||||||
|
|
||||||
@FXML
|
|
||||||
private Button btnSave;
|
|
||||||
|
|
||||||
private UserResponse user;
|
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void initialize() {
|
void initialize() {
|
||||||
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
|
TextFieldFormatSupport.applyPhoneNumberFormat(txtPhone);
|
||||||
|
|
||||||
|
cbRole.setItems(FXCollections.observableArrayList("STAFF", "ADMIN"));
|
||||||
|
cbActive.setItems(FXCollections.observableArrayList("Active", "Inactive"));
|
||||||
|
|
||||||
|
cbStore.setCellFactory(param -> new ListCell<>() {
|
||||||
|
@Override protected void updateItem(DropdownOption o, boolean empty) {
|
||||||
|
super.updateItem(o, empty);
|
||||||
|
setText(empty || o == null ? null : o.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
cbStore.setButtonCell(new ListCell<>() {
|
||||||
|
@Override protected void updateItem(DropdownOption o, boolean empty) {
|
||||||
|
super.updateItem(o, empty);
|
||||||
|
setText(empty || o == null ? null : o.getLabel());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
loadStores();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUser(UserResponse user) {
|
private void loadStores() {
|
||||||
this.user = user;
|
new Thread(() -> {
|
||||||
String fullName = user.getFullName() == null ? "" : user.getFullName();
|
try {
|
||||||
|
List<DropdownOption> stores = DropdownApi.getInstance().getStores();
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
cbStore.setItems(FXCollections.observableArrayList(stores));
|
||||||
|
applyPendingStore();
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
ActivityLogger.getInstance().logException("StaffEditDialogController.loadStores", e, "Loading stores");
|
||||||
|
Platform.runLater(() -> {
|
||||||
|
cbStore.setDisable(true);
|
||||||
|
cbStore.setPromptText("Unable to load stores");
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}).start();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applyPendingStore() {
|
||||||
|
if (pendingStoreId == null) return;
|
||||||
|
for (DropdownOption opt : cbStore.getItems()) {
|
||||||
|
if (opt.getId() != null && opt.getId().equals(pendingStoreId)) {
|
||||||
|
cbStore.setValue(opt);
|
||||||
|
pendingStoreId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmployee(EmployeeResponse emp) {
|
||||||
|
this.employee = emp;
|
||||||
|
String fullName = emp.getFullName() != null ? emp.getFullName() : "";
|
||||||
|
if (fullName.isEmpty() && emp.getFirstName() != null) {
|
||||||
|
fullName = emp.getFirstName() + (emp.getLastName() != null ? " " + emp.getLastName() : "");
|
||||||
|
}
|
||||||
String[] names = splitFullName(fullName);
|
String[] names = splitFullName(fullName);
|
||||||
txtFirstName.setText(names[0]);
|
txtFirstName.setText(names[0]);
|
||||||
txtLastName.setText(names[1]);
|
txtLastName.setText(names[1]);
|
||||||
txtEmail.setText(user.getEmail());
|
txtEmail.setText(emp.getEmail());
|
||||||
txtPhone.setText(user.getPhone());
|
txtPhone.setText(emp.getPhone());
|
||||||
txtUsername.setText(user.getUsername());
|
txtUsername.setText(emp.getUsername());
|
||||||
|
|
||||||
|
if (emp.getRole() != null) cbRole.setValue(emp.getRole());
|
||||||
|
cbActive.setValue(Boolean.TRUE.equals(emp.getActive()) ? "Active" : "Inactive");
|
||||||
|
|
||||||
|
pendingStoreId = emp.getPrimaryStoreId();
|
||||||
|
applyPendingStore();
|
||||||
}
|
}
|
||||||
|
|
||||||
private String[] splitFullName(String fullName) {
|
private String[] splitFullName(String fullName) {
|
||||||
if (fullName == null || fullName.trim().isEmpty()) {
|
if (fullName == null || fullName.trim().isEmpty()) return new String[]{"", ""};
|
||||||
return new String[]{"", ""};
|
|
||||||
}
|
|
||||||
String[] parts = fullName.trim().split("\\s+", 2);
|
String[] parts = fullName.trim().split("\\s+", 2);
|
||||||
String firstName = parts.length > 0 ? parts[0] : "";
|
return new String[]{parts.length > 0 ? parts[0] : "", parts.length > 1 ? parts[1] : ""};
|
||||||
String lastName = parts.length > 1 ? parts[1] : "";
|
|
||||||
return new String[]{firstName, lastName};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@FXML
|
@FXML
|
||||||
void btnSaveClicked(ActionEvent event) {
|
void btnSaveClicked(ActionEvent event) {
|
||||||
lblError.setText("");
|
lblError.setText("");
|
||||||
if (user == null) {
|
if (employee == null) {
|
||||||
lblError.setText("No user selected.");
|
lblError.setText("No employee selected.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,31 +163,41 @@ public class StaffEditDialogController {
|
|||||||
lblError.setText("Passwords do not match.");
|
lblError.setText("Passwords do not match.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
if (cbRole.getValue() == null) {
|
||||||
|
lblError.setText("Role is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (cbStore.getValue() == null) {
|
||||||
|
lblError.setText("Primary store is required.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
btnSave.setDisable(true);
|
btnSave.setDisable(true);
|
||||||
|
|
||||||
|
String role = cbRole.getValue();
|
||||||
|
boolean active = "Active".equals(cbActive.getValue());
|
||||||
|
Long storeId = cbStore.getValue().getId();
|
||||||
|
|
||||||
new Thread(() -> {
|
new Thread(() -> {
|
||||||
try {
|
try {
|
||||||
UserRequest request = new UserRequest();
|
EmployeeRequest request = new EmployeeRequest();
|
||||||
request.setUsername(username);
|
request.setUsername(username);
|
||||||
request.setPassword(password.isEmpty() ? null : password);
|
request.setPassword(password.isEmpty() ? null : password);
|
||||||
|
request.setFirstName(firstName);
|
||||||
|
request.setLastName(lastName);
|
||||||
request.setFullName(firstName + " " + lastName);
|
request.setFullName(firstName + " " + lastName);
|
||||||
request.setEmail(email);
|
request.setEmail(email);
|
||||||
request.setPhone(phone);
|
request.setPhone(phone);
|
||||||
request.setRole(user.getRole());
|
request.setRole(role);
|
||||||
request.setActive(user.getActive());
|
request.setActive(active);
|
||||||
|
request.setPrimaryStoreId(storeId);
|
||||||
|
|
||||||
UserSession session = UserSession.getInstance();
|
EmployeeApi.getInstance().updateEmployee(employee.getId(), request);
|
||||||
if (session.isAdmin()) {
|
|
||||||
UserApi.getInstance().updateUser(user.getId(), request);
|
|
||||||
} else {
|
|
||||||
CustomerApi.getInstance().updateCustomer(user.getId(), request);
|
|
||||||
}
|
|
||||||
|
|
||||||
Platform.runLater(this::close);
|
Platform.runLater(this::close);
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
ActivityLogger.getInstance().logException("StaffEditDialogController.btnSaveClicked", e, "Updating user");
|
ActivityLogger.getInstance().logException("StaffEditDialogController.btnSaveClicked", e, "Updating employee");
|
||||||
String msg = e.getMessage() == null ? "Could not update user." : e.getMessage();
|
String msg = e.getMessage() == null ? "Could not update employee." : e.getMessage();
|
||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
lblError.setText(msg);
|
lblError.setText(msg);
|
||||||
btnSave.setDisable(false);
|
btnSave.setDisable(false);
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user