Merge branch 'AttachmentsToChat'
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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,24 +31,30 @@ 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);
|
||||||
@@ -56,6 +64,7 @@ public class AnalyticsViewModel extends ViewModel {
|
|||||||
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) {
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,9 @@ 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 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;
|
||||||
@@ -98,6 +101,20 @@ public class AnalyticsController {
|
|||||||
@FXML
|
@FXML
|
||||||
private ComboBox<String> cbTopN;
|
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 List<SaleResponse> cachedSales = new ArrayList<>();
|
||||||
private FilterState currentFilter = new FilterState();
|
private FilterState currentFilter = new FilterState();
|
||||||
|
|
||||||
@@ -124,8 +141,29 @@ public class AnalyticsController {
|
|||||||
cbPaymentFilter.setItems(FXCollections.observableArrayList("All"));
|
cbPaymentFilter.setItems(FXCollections.observableArrayList("All"));
|
||||||
cbPaymentFilter.getSelectionModel().selectFirst();
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
|
cbStoreFilter.setItems(FXCollections.observableArrayList("All Stores"));
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
|
|
||||||
lblFilterSummary.setText("All time");
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -182,6 +220,8 @@ public class AnalyticsController {
|
|||||||
Platform.runLater(() -> {
|
Platform.runLater(() -> {
|
||||||
cachedSales = sales;
|
cachedSales = sales;
|
||||||
derivePaymentMethods();
|
derivePaymentMethods();
|
||||||
|
deriveStores();
|
||||||
|
updateStoreFilterVisibility();
|
||||||
applyCurrentFilter();
|
applyCurrentFilter();
|
||||||
btnRefresh.setDisable(false);
|
btnRefresh.setDisable(false);
|
||||||
});
|
});
|
||||||
@@ -196,9 +236,36 @@ public class AnalyticsController {
|
|||||||
}).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() {
|
private void applyCurrentFilter() {
|
||||||
try {
|
try {
|
||||||
List<SaleResponse> filtered = filterSales(cachedSales, currentFilter);
|
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 start = currentFilter.startDate.isEmpty() ? LocalDate.now().minusDays(6).toString() : currentFilter.startDate;
|
||||||
String end = currentFilter.endDate.isEmpty() ? LocalDate.now().toString() : currentFilter.endDate;
|
String end = currentFilter.endDate.isEmpty() ? LocalDate.now().toString() : currentFilter.endDate;
|
||||||
|
|
||||||
@@ -256,6 +323,31 @@ public class AnalyticsController {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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() {
|
private void updateFilterSummary() {
|
||||||
String start = currentFilter.startDate;
|
String start = currentFilter.startDate;
|
||||||
String end = currentFilter.endDate;
|
String end = currentFilter.endDate;
|
||||||
@@ -392,11 +484,12 @@ 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -612,6 +705,7 @@ public class AnalyticsController {
|
|||||||
dpEndDate.setValue(null);
|
dpEndDate.setValue(null);
|
||||||
cbPaymentFilter.getSelectionModel().selectFirst();
|
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||||
cbTopN.getSelectionModel().selectFirst();
|
cbTopN.getSelectionModel().selectFirst();
|
||||||
|
cbStoreFilter.getSelectionModel().selectFirst();
|
||||||
currentFilter = new FilterState();
|
currentFilter = new FilterState();
|
||||||
applyCurrentFilter();
|
applyCurrentFilter();
|
||||||
}
|
}
|
||||||
@@ -630,6 +724,8 @@ public class AnalyticsController {
|
|||||||
currentFilter.paymentMethod = pm != null ? pm : "All";
|
currentFilter.paymentMethod = pm != null ? pm : "All";
|
||||||
int topNPos = cbTopN.getSelectionModel().getSelectedIndex();
|
int topNPos = cbTopN.getSelectionModel().getSelectedIndex();
|
||||||
currentFilter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
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();
|
applyCurrentFilter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -638,5 +734,6 @@ public class AnalyticsController {
|
|||||||
String endDate = "";
|
String endDate = "";
|
||||||
String paymentMethod = "All";
|
String paymentMethod = "All";
|
||||||
int topN = 5;
|
int topN = 5;
|
||||||
|
String storeFilter = "All Stores";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|
||||||
@@ -69,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()));
|
||||||
@@ -94,6 +108,27 @@ public class CustomerAccountsController {
|
|||||||
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();
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -199,6 +199,12 @@ public class SaleController {
|
|||||||
@FXML
|
@FXML
|
||||||
private Label lblLoyaltyDiscount;
|
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;
|
||||||
@@ -389,12 +395,15 @@ public class SaleController {
|
|||||||
|
|
||||||
task.setOnSucceeded(event -> {
|
task.setOnSucceeded(event -> {
|
||||||
selectedCustomerData = task.getValue();
|
selectedCustomerData = task.getValue();
|
||||||
if (selectedCustomerData != null && selectedCustomerData.getLoyaltyPoints() != null && selectedCustomerData.getLoyaltyPoints() >= 20) {
|
if (selectedCustomerData != null) {
|
||||||
lblLoyaltyPoints.setText(selectedCustomerData.getLoyaltyPoints() + " pts available");
|
int pts = selectedCustomerData.getLoyaltyPoints() != null ? selectedCustomerData.getLoyaltyPoints() : 0;
|
||||||
|
lblLoyaltyPoints.setText(pts + " pts available");
|
||||||
lblLoyaltyPoints.setVisible(true);
|
lblLoyaltyPoints.setVisible(true);
|
||||||
lblLoyaltyPoints.setManaged(true);
|
lblLoyaltyPoints.setManaged(true);
|
||||||
chkUseLoyaltyPoints.setVisible(true);
|
boolean canRedeem = pts >= 20;
|
||||||
chkUseLoyaltyPoints.setManaged(true);
|
chkUseLoyaltyPoints.setVisible(canRedeem);
|
||||||
|
chkUseLoyaltyPoints.setManaged(canRedeem);
|
||||||
|
if (!canRedeem) chkUseLoyaltyPoints.setSelected(false);
|
||||||
} else {
|
} else {
|
||||||
lblLoyaltyPoints.setVisible(false);
|
lblLoyaltyPoints.setVisible(false);
|
||||||
lblLoyaltyPoints.setManaged(false);
|
lblLoyaltyPoints.setManaged(false);
|
||||||
@@ -701,6 +710,8 @@ public class SaleController {
|
|||||||
hbCouponDiscount.setManaged(false);
|
hbCouponDiscount.setManaged(false);
|
||||||
hbLoyaltyDiscount.setVisible(false);
|
hbLoyaltyDiscount.setVisible(false);
|
||||||
hbLoyaltyDiscount.setManaged(false);
|
hbLoyaltyDiscount.setManaged(false);
|
||||||
|
hbPointsToEarn.setVisible(false);
|
||||||
|
hbPointsToEarn.setManaged(false);
|
||||||
cbCustomer.setValue(null);
|
cbCustomer.setValue(null);
|
||||||
selectedCustomerData = null;
|
selectedCustomerData = null;
|
||||||
lblLoyaltyPoints.setVisible(false);
|
lblLoyaltyPoints.setVisible(false);
|
||||||
@@ -875,6 +886,16 @@ public class SaleController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lblCartTotal.setText(currency.format(Math.max(0, total.doubleValue())));
|
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) {
|
private BigDecimal calculateCouponDiscount(BigDecimal subtotal) {
|
||||||
|
|||||||
@@ -7,8 +7,11 @@ 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;
|
||||||
@@ -16,6 +19,7 @@ import org.example.petshopdesktop.api.dto.employee.EmployeeResponse;
|
|||||||
import org.example.petshopdesktop.api.endpoints.EmployeeApi;
|
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;
|
||||||
@@ -26,6 +30,7 @@ public class StaffAccountsController {
|
|||||||
|
|
||||||
@FXML private VBox staffSection;
|
@FXML private VBox staffSection;
|
||||||
@FXML private TableView<EmployeeResponse> tvStaff;
|
@FXML private TableView<EmployeeResponse> tvStaff;
|
||||||
|
@FXML private TableColumn<EmployeeResponse, String> colStaffAvatar;
|
||||||
@FXML private TableColumn<EmployeeResponse, String> colUsername;
|
@FXML private TableColumn<EmployeeResponse, String> colUsername;
|
||||||
@FXML private TableColumn<EmployeeResponse, String> colName;
|
@FXML private TableColumn<EmployeeResponse, String> colName;
|
||||||
@FXML private TableColumn<EmployeeResponse, String> colEmail;
|
@FXML private TableColumn<EmployeeResponse, String> colEmail;
|
||||||
@@ -47,6 +52,13 @@ public class StaffAccountsController {
|
|||||||
|
|
||||||
@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()));
|
||||||
@@ -81,6 +93,27 @@ public class StaffAccountsController {
|
|||||||
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();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
<?import javafx.geometry.Insets?>
|
<?import javafx.geometry.Insets?>
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
|
<?import javafx.scene.control.Hyperlink?>
|
||||||
<?import javafx.scene.control.Label?>
|
<?import javafx.scene.control.Label?>
|
||||||
<?import javafx.scene.control.PasswordField?>
|
<?import javafx.scene.control.PasswordField?>
|
||||||
<?import javafx.scene.control.TextField?>
|
<?import javafx.scene.control.TextField?>
|
||||||
@@ -83,6 +84,15 @@
|
|||||||
<Insets top="8.0" />
|
<Insets top="8.0" />
|
||||||
</VBox.margin>
|
</VBox.margin>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Hyperlink maxWidth="Infinity" mnemonicParsing="false" onAction="#lnkForgotPasswordClicked"
|
||||||
|
text="Forgot Password?"
|
||||||
|
style="-fx-text-fill: #91a4b7; -fx-border-color: transparent; -fx-cursor: hand;"
|
||||||
|
alignment="CENTER_RIGHT">
|
||||||
|
<font>
|
||||||
|
<Font size="12.0" />
|
||||||
|
</font>
|
||||||
|
</Hyperlink>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
</children>
|
</children>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
<?import javafx.scene.chart.PieChart?>
|
<?import javafx.scene.chart.PieChart?>
|
||||||
<?import javafx.scene.control.Button?>
|
<?import javafx.scene.control.Button?>
|
||||||
<?import javafx.scene.control.ComboBox?>
|
<?import javafx.scene.control.ComboBox?>
|
||||||
|
<?import javafx.scene.control.ToggleButton?>
|
||||||
<?import javafx.scene.control.DatePicker?>
|
<?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?>
|
||||||
@@ -30,6 +31,16 @@
|
|||||||
</font>
|
</font>
|
||||||
</Label>
|
</Label>
|
||||||
<Region HBox.hgrow="ALWAYS" />
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
<HBox fx:id="hbViewToggle" spacing="0.0" alignment="CENTER" visible="false" managed="false">
|
||||||
|
<ToggleButton fx:id="tbnMyAnalytics" text="My Analytics" style="-fx-background-color: #e2e8f0; -fx-text-fill: #475569; -fx-background-radius: 6 0 0 6; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;">
|
||||||
|
<font><Font size="12.0" /></font>
|
||||||
|
<padding><Insets bottom="6.0" left="14.0" right="14.0" top="6.0" /></padding>
|
||||||
|
</ToggleButton>
|
||||||
|
<ToggleButton fx:id="tbnStoreAnalytics" text="Store Analytics" style="-fx-background-color: #4ECDC4; -fx-text-fill: white; -fx-background-radius: 0 6 6 0; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;">
|
||||||
|
<font><Font size="12.0" /></font>
|
||||||
|
<padding><Insets bottom="6.0" left="14.0" right="14.0" top="6.0" /></padding>
|
||||||
|
</ToggleButton>
|
||||||
|
</HBox>
|
||||||
<Button fx:id="btnRefresh" onAction="#handleRefresh" style="-fx-background-color: #4ECDC4; -fx-text-fill: white; -fx-background-radius: 5; -fx-cursor: hand;" text="Refresh">
|
<Button fx:id="btnRefresh" onAction="#handleRefresh" style="-fx-background-color: #4ECDC4; -fx-text-fill: white; -fx-background-radius: 5; -fx-cursor: hand;" text="Refresh">
|
||||||
<font>
|
<font>
|
||||||
<Font size="13.0" />
|
<Font size="13.0" />
|
||||||
@@ -91,6 +102,7 @@
|
|||||||
</FlowPane>
|
</FlowPane>
|
||||||
<HBox alignment="CENTER_LEFT" spacing="8.0">
|
<HBox alignment="CENTER_LEFT" spacing="8.0">
|
||||||
<children>
|
<children>
|
||||||
|
<ComboBox fx:id="cbStoreFilter" prefWidth="145.0" promptText="All Stores" visible="false" managed="false" />
|
||||||
<ComboBox fx:id="cbPaymentFilter" prefWidth="145.0" promptText="All Payments" />
|
<ComboBox fx:id="cbPaymentFilter" prefWidth="145.0" promptText="All Payments" />
|
||||||
<ComboBox fx:id="cbTopN" prefWidth="110.0" />
|
<ComboBox fx:id="cbTopN" prefWidth="110.0" />
|
||||||
<Region HBox.hgrow="ALWAYS" />
|
<Region HBox.hgrow="ALWAYS" />
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
|
|
||||||
<TableView fx:id="tvCustomers" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
<TableView fx:id="tvCustomers" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||||
<columns>
|
<columns>
|
||||||
|
<TableColumn fx:id="colCustomerAvatar" prefWidth="70.0" text="Avatar" sortable="false" />
|
||||||
<TableColumn fx:id="colCustomerUsername" prefWidth="130.0" text="Username" />
|
<TableColumn fx:id="colCustomerUsername" prefWidth="130.0" text="Username" />
|
||||||
<TableColumn fx:id="colCustomerName" prefWidth="160.0" text="Name" />
|
<TableColumn fx:id="colCustomerName" prefWidth="160.0" text="Name" />
|
||||||
<TableColumn fx:id="colCustomerEmail" prefWidth="200.0" text="Email" />
|
<TableColumn fx:id="colCustomerEmail" prefWidth="200.0" text="Email" />
|
||||||
|
|||||||
@@ -173,6 +173,12 @@
|
|||||||
<Label fx:id="lblCartTotal" text="" textFill="#2c3e50"><font><Font name="System Bold" size="16.0" /></font></Label>
|
<Label fx:id="lblCartTotal" text="" textFill="#2c3e50"><font><Font name="System Bold" size="16.0" /></font></Label>
|
||||||
</children>
|
</children>
|
||||||
</HBox>
|
</HBox>
|
||||||
|
<HBox fx:id="hbPointsToEarn" spacing="8.0" alignment="CENTER_LEFT" visible="false" managed="false">
|
||||||
|
<children>
|
||||||
|
<Label text="Points to earn:" textFill="#4ECDC4"><font><Font size="12.0" /></font></Label>
|
||||||
|
<Label fx:id="lblPointsToEarn" text="+0 pts" textFill="#4ECDC4"><font><Font size="12.0" /></font></Label>
|
||||||
|
</children>
|
||||||
|
</HBox>
|
||||||
</children>
|
</children>
|
||||||
</VBox>
|
</VBox>
|
||||||
<FlowPane hgap="8.0" prefWrapLength="220.0" vgap="8.0">
|
<FlowPane hgap="8.0" prefWrapLength="220.0" vgap="8.0">
|
||||||
|
|||||||
@@ -78,6 +78,7 @@
|
|||||||
|
|
||||||
<TableView fx:id="tvStaff" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
<TableView fx:id="tvStaff" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||||
<columns>
|
<columns>
|
||||||
|
<TableColumn fx:id="colStaffAvatar" prefWidth="70.0" text="Avatar" sortable="false" />
|
||||||
<TableColumn fx:id="colUsername" prefWidth="140.0" text="Username" />
|
<TableColumn fx:id="colUsername" prefWidth="140.0" text="Username" />
|
||||||
<TableColumn fx:id="colName" prefWidth="170.0" text="Name" />
|
<TableColumn fx:id="colName" prefWidth="170.0" text="Name" />
|
||||||
<TableColumn fx:id="colEmail" prefWidth="210.0" text="Email" />
|
<TableColumn fx:id="colEmail" prefWidth="210.0" text="Email" />
|
||||||
|
|||||||
Reference in New Issue
Block a user