Merge branch 'AttachmentsToChat'

This commit is contained in:
Alex
2026-04-14 23:20:16 -06:00
20 changed files with 528 additions and 27 deletions

View File

@@ -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);
} }

View File

@@ -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);
if (prevSize > 0 && list.size() == prevSize + 1) {
messageAdapter.notifyItemInserted(list.size() - 1);
} else {
messageAdapter.notifyDataSetChanged(); messageAdapter.notifyDataSetChanged();
}
scrollToBottom(); scrollToBottom();
}); });

View File

@@ -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,6 +276,10 @@ public class AnalyticsFragment extends Fragment {
} }
// Employee Performance // Employee Performance
boolean showEmployeeSection = viewModel.getViewMode().equals("store");
View empParent = (View) binding.llEmployeePerformance.getParent();
if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE);
if (showEmployeeSection) {
binding.llEmployeePerformance.removeAllViews(); binding.llEmployeePerformance.removeAllViews();
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) { if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
BigDecimal maxEmp = data.employeePerformance.get(0).getValue(); BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
@@ -236,6 +292,7 @@ public class AnalyticsFragment extends Fragment {
} else { } else {
addEmptyRow(binding.llEmployeePerformance, "No data"); addEmptyRow(binding.llEmployeePerformance, "No data");
} }
}
// Daily Revenue // Daily Revenue
binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle); binding.tvDailyRevenueTitle.setText(data.dailyRevenueTitle);

View File

@@ -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() {

View File

@@ -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;
} }

View File

@@ -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) {

View File

@@ -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"

View File

@@ -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>

View File

@@ -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;
}
} }

View File

@@ -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);
}
} }

View File

@@ -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";
} }
} }

View File

@@ -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();

View File

@@ -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(

View File

@@ -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) {

View File

@@ -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();

View File

@@ -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>

View File

@@ -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" />

View File

@@ -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" />

View File

@@ -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">

View File

@@ -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" />