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.model.GlideUrl;
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
import com.bumptech.glide.signature.ObjectKey;
|
||||
import com.example.petstoremobile.R;
|
||||
import com.example.petstoremobile.databinding.ItemMessageReceivedBinding;
|
||||
import com.example.petstoremobile.databinding.ItemMessageSentBinding;
|
||||
@@ -35,6 +36,13 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
public MessageAdapter(List<Message> messages, Long currentUserId) {
|
||||
this.messages = messages;
|
||||
this.currentUserId = currentUserId;
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
Message m = messages.get(position);
|
||||
return m.getId() != null ? m.getId() : position;
|
||||
}
|
||||
|
||||
public void setCurrentUserId(Long id) {
|
||||
@@ -150,6 +158,7 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
}
|
||||
|
||||
if (url == null) {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.GONE);
|
||||
return;
|
||||
@@ -166,18 +175,25 @@ public class MessageAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder
|
||||
.build());
|
||||
}
|
||||
|
||||
// Use a signature to prevent Glide from showing stale cached images for the same URL/ID
|
||||
String signatureKey = (m.getTimestamp() != null ? m.getTimestamp() : "") + m.getId();
|
||||
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
Glide.with(iv.getContext())
|
||||
.load(loadTarget)
|
||||
.signature(new ObjectKey(signatureKey))
|
||||
.diskCacheStrategy(DiskCacheStrategy.ALL)
|
||||
.placeholder(R.drawable.placeholder)
|
||||
.error(R.drawable.placeholder)
|
||||
.into(iv);
|
||||
} else {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.VISIBLE);
|
||||
tvName.setText(m.getAttachmentName() != null ? m.getAttachmentName() : "Attachment");
|
||||
}
|
||||
} else {
|
||||
Glide.with(iv.getContext()).clear(iv);
|
||||
iv.setVisibility(View.GONE);
|
||||
tvName.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@@ -182,6 +182,7 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
LinearLayoutManager lm = new LinearLayoutManager(getContext());
|
||||
lm.setStackFromEnd(true);
|
||||
binding.rvMessages.setLayoutManager(lm);
|
||||
binding.rvMessages.setItemAnimator(null);
|
||||
binding.rvMessages.setAdapter(messageAdapter);
|
||||
setConversationActive(false, null);
|
||||
}
|
||||
@@ -285,9 +286,14 @@ public class ChatFragment extends Fragment implements ChatAdapter.OnChatClickLis
|
||||
});
|
||||
|
||||
viewModel.getMessageList().observe(getViewLifecycleOwner(), list -> {
|
||||
int prevSize = messageList.size();
|
||||
messageList.clear();
|
||||
messageList.addAll(list);
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
if (prevSize > 0 && list.size() == prevSize + 1) {
|
||||
messageAdapter.notifyItemInserted(list.size() - 1);
|
||||
} else {
|
||||
messageAdapter.notifyDataSetChanged();
|
||||
}
|
||||
scrollToBottom();
|
||||
});
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@ import android.widget.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.databinding.FragmentAnalyticsBinding;
|
||||
import com.example.petstoremobile.utils.SpinnerUtils;
|
||||
import com.example.petstoremobile.utils.UIUtils;
|
||||
import com.example.petstoremobile.viewmodels.AnalyticsViewModel;
|
||||
import dagger.hilt.android.AndroidEntryPoint;
|
||||
import javax.inject.Inject;
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.util.*;
|
||||
@@ -19,6 +21,9 @@ import java.util.*;
|
||||
@AndroidEntryPoint
|
||||
public class AnalyticsFragment extends Fragment {
|
||||
|
||||
@Inject
|
||||
TokenManager tokenManager;
|
||||
|
||||
private FragmentAnalyticsBinding binding;
|
||||
private AnalyticsViewModel viewModel;
|
||||
private boolean filtersExpanded = false;
|
||||
@@ -33,6 +38,7 @@ public class AnalyticsFragment extends Fragment {
|
||||
viewModel = new ViewModelProvider(this).get(AnalyticsViewModel.class);
|
||||
|
||||
setupFilterPanel();
|
||||
setupViewModeToggle();
|
||||
observeViewModel();
|
||||
viewModel.loadAnalytics();
|
||||
|
||||
@@ -42,6 +48,39 @@ public class AnalyticsFragment extends Fragment {
|
||||
return binding.getRoot();
|
||||
}
|
||||
|
||||
private static final int COLOR_SELECTED = 0xFF4ECDC4;
|
||||
private static final int COLOR_UNSELECTED = 0xFFCBD5E1;
|
||||
|
||||
private void setupViewModeToggle() {
|
||||
updateViewModeButtonStyles(viewModel.getViewMode());
|
||||
|
||||
binding.btnMyAnalytics.setOnClickListener(v -> {
|
||||
viewModel.setViewMode("mine");
|
||||
updateViewModeButtonStyles("mine");
|
||||
updateStoreFilterVisibility("mine");
|
||||
});
|
||||
|
||||
binding.btnStoreAnalytics.setOnClickListener(v -> {
|
||||
viewModel.setViewMode("store");
|
||||
updateViewModeButtonStyles("store");
|
||||
updateStoreFilterVisibility("store");
|
||||
});
|
||||
}
|
||||
|
||||
private void updateViewModeButtonStyles(String mode) {
|
||||
binding.btnMyAnalytics.setBackgroundTintList(
|
||||
android.content.res.ColorStateList.valueOf(mode.equals("mine") ? COLOR_SELECTED : COLOR_UNSELECTED));
|
||||
binding.btnStoreAnalytics.setBackgroundTintList(
|
||||
android.content.res.ColorStateList.valueOf(mode.equals("store") ? COLOR_SELECTED : COLOR_UNSELECTED));
|
||||
}
|
||||
|
||||
private void updateStoreFilterVisibility(String mode) {
|
||||
boolean isAdmin = "ADMIN".equalsIgnoreCase(tokenManager.getRole());
|
||||
int vis = (isAdmin && mode.equals("store")) ? View.VISIBLE : View.GONE;
|
||||
binding.tvStoreFilterLabel.setVisibility(vis);
|
||||
binding.spinnerFilterStore.setVisibility(vis);
|
||||
}
|
||||
|
||||
// Filter Panel
|
||||
|
||||
private void setupFilterPanel() {
|
||||
@@ -96,6 +135,9 @@ public class AnalyticsFragment extends Fragment {
|
||||
int topNPos = binding.spinnerTopN.getSelectedItemPosition();
|
||||
filter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
||||
|
||||
Object store = binding.spinnerFilterStore.getSelectedItem();
|
||||
viewModel.setStoreFilter(store != null ? store.toString() : "All Stores");
|
||||
|
||||
updateFilterSummary();
|
||||
viewModel.applyFilter(filter);
|
||||
}
|
||||
@@ -104,8 +146,8 @@ public class AnalyticsFragment extends Fragment {
|
||||
binding.etFilterStartDate.setText("");
|
||||
binding.etFilterEndDate.setText("");
|
||||
binding.spinnerTopN.setSelection(0);
|
||||
// Reset payment method to "All"
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, "All");
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, "All Stores");
|
||||
updateFilterSummary();
|
||||
viewModel.resetFilter();
|
||||
}
|
||||
@@ -162,6 +204,16 @@ public class AnalyticsFragment extends Fragment {
|
||||
methods.toArray(new String[0]));
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterPayment, currentSelection);
|
||||
});
|
||||
|
||||
viewModel.getAvailableStores().observe(getViewLifecycleOwner(), stores -> {
|
||||
if (stores == null || stores.isEmpty()) return;
|
||||
String currentSelection = binding.spinnerFilterStore.getSelectedItem() != null
|
||||
? binding.spinnerFilterStore.getSelectedItem().toString() : "All Stores";
|
||||
SpinnerUtils.setupStringSpinner(requireContext(), binding.spinnerFilterStore,
|
||||
stores.toArray(new String[0]));
|
||||
SpinnerUtils.setSelectionByValue(binding.spinnerFilterStore, currentSelection);
|
||||
updateStoreFilterVisibility(viewModel.getViewMode());
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -224,17 +276,22 @@ public class AnalyticsFragment extends Fragment {
|
||||
}
|
||||
|
||||
// Employee Performance
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
||||
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
|
||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
|
||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
|
||||
boolean showEmployeeSection = viewModel.getViewMode().equals("store");
|
||||
View empParent = (View) binding.llEmployeePerformance.getParent();
|
||||
if (empParent != null) empParent.setVisibility(showEmployeeSection ? View.VISIBLE : View.GONE);
|
||||
if (showEmployeeSection) {
|
||||
binding.llEmployeePerformance.removeAllViews();
|
||||
if (data.employeePerformance != null && !data.employeePerformance.isEmpty()) {
|
||||
BigDecimal maxEmp = data.employeePerformance.get(0).getValue();
|
||||
if (maxEmp.compareTo(BigDecimal.ZERO) == 0) maxEmp = BigDecimal.ONE;
|
||||
for (Map.Entry<String, BigDecimal> e : data.employeePerformance) {
|
||||
addBarRow(binding.llEmployeePerformance, e.getKey(),
|
||||
"$" + e.getValue().setScale(2, RoundingMode.HALF_UP),
|
||||
e.getValue().floatValue() / maxEmp.floatValue(), "#1a759f");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llEmployeePerformance, "No data");
|
||||
}
|
||||
} else {
|
||||
addEmptyRow(binding.llEmployeePerformance, "No data");
|
||||
}
|
||||
|
||||
// Daily Revenue
|
||||
|
||||
@@ -111,6 +111,7 @@ public class SaleDetailFragment extends Fragment {
|
||||
binding.llLoyaltyPoints.setVisibility(View.GONE);
|
||||
binding.cbUseLoyaltyPoints.setChecked(false);
|
||||
}
|
||||
updateTotal();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -420,6 +421,15 @@ public class SaleDetailFragment extends Fragment {
|
||||
}
|
||||
|
||||
binding.tvSaleDetailTotal.setText("Total: $" + String.format(Locale.getDefault(), "%.2f", total));
|
||||
|
||||
CustomerDTO customer = viewModel.getSelectedCustomerData().getValue();
|
||||
if (customer != null && !viewModel.isViewOnly()) {
|
||||
int pointsToEarn = total.max(BigDecimal.ZERO).intValue();
|
||||
binding.tvPointsToEarn.setText("+" + pointsToEarn + " pts");
|
||||
binding.llPointsToEarn.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
binding.llPointsToEarn.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveSale() {
|
||||
|
||||
@@ -100,13 +100,12 @@ public class InputValidator {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Checks if the phone number is valid in (XXX) XXX-XXXX format
|
||||
// Checks if the phone number is valid: XXX-XXX-XXXX, (XXX) XXX-XXXX, or XXXXXXXXXX
|
||||
public static boolean isValidPhone(EditText field) {
|
||||
String phone = field.getText().toString().trim();
|
||||
// Matches (XXX) XXX-XXXX format
|
||||
String pattern = "^\\(\\d{3}\\) \\d{3}-\\d{4}$";
|
||||
String pattern = "^(\\(\\d{3}\\) \\d{3}-\\d{4}|\\d{3}-\\d{3}-\\d{4}|\\d{10})$";
|
||||
if (phone.isEmpty() || !phone.matches(pattern)) {
|
||||
field.setError("Enter a valid phone number: (XXX) XXX-XXXX");
|
||||
field.setError("Enter a valid phone number");
|
||||
field.requestFocus();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
|
||||
import com.example.petstoremobile.api.auth.TokenManager;
|
||||
import com.example.petstoremobile.dtos.SaleDTO;
|
||||
import com.example.petstoremobile.repositories.SaleRepository;
|
||||
import com.example.petstoremobile.utils.Resource;
|
||||
@@ -21,6 +22,7 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@@ -29,24 +31,30 @@ import dagger.hilt.android.lifecycle.HiltViewModel;
|
||||
@HiltViewModel
|
||||
public class AnalyticsViewModel extends ViewModel {
|
||||
private final SaleRepository saleRepository;
|
||||
private final TokenManager tokenManager;
|
||||
|
||||
private final MutableLiveData<AnalyticsData> analyticsData = new MutableLiveData<>();
|
||||
private final MutableLiveData<Boolean> isLoading = new MutableLiveData<>(false);
|
||||
private final MutableLiveData<String> errorMessage = new MutableLiveData<>();
|
||||
private final MutableLiveData<List<String>> availablePaymentMethods = new MutableLiveData<>(new ArrayList<>());
|
||||
private final MutableLiveData<List<String>> availableStores = new MutableLiveData<>(new ArrayList<>());
|
||||
|
||||
private List<SaleDTO> cachedSales = new ArrayList<>();
|
||||
private FilterState currentFilter = new FilterState();
|
||||
private String viewMode = "store";
|
||||
private String storeFilter = "All Stores";
|
||||
|
||||
@Inject
|
||||
public AnalyticsViewModel(SaleRepository saleRepository) {
|
||||
public AnalyticsViewModel(SaleRepository saleRepository, TokenManager tokenManager) {
|
||||
this.saleRepository = saleRepository;
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
|
||||
public LiveData<Boolean> getIsLoading() { return isLoading; }
|
||||
public LiveData<String> getErrorMessage() { return errorMessage; }
|
||||
public LiveData<List<String>> getAvailablePaymentMethods() { return availablePaymentMethods; }
|
||||
public LiveData<List<String>> getAvailableStores() { return availableStores; }
|
||||
|
||||
public void loadAnalytics() {
|
||||
isLoading.setValue(true);
|
||||
@@ -56,6 +64,7 @@ public class AnalyticsViewModel extends ViewModel {
|
||||
if (resource.status == Resource.Status.SUCCESS && resource.data != null) {
|
||||
cachedSales = resource.data.getContent();
|
||||
derivePaymentMethods();
|
||||
deriveStores();
|
||||
applyCurrentFilter();
|
||||
isLoading.setValue(false);
|
||||
} else if (resource.status == Resource.Status.ERROR) {
|
||||
@@ -73,14 +82,61 @@ public class AnalyticsViewModel extends ViewModel {
|
||||
|
||||
public void resetFilter() {
|
||||
currentFilter = new FilterState();
|
||||
storeFilter = "All Stores";
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public void setViewMode(String mode) {
|
||||
viewMode = mode;
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public String getViewMode() {
|
||||
return viewMode;
|
||||
}
|
||||
|
||||
public void setStoreFilter(String store) {
|
||||
storeFilter = (store != null && !store.isEmpty()) ? store : "All Stores";
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public String getStoreFilter() {
|
||||
return storeFilter;
|
||||
}
|
||||
|
||||
private void applyCurrentFilter() {
|
||||
List<SaleDTO> filtered = filterSales(cachedSales, currentFilter);
|
||||
List<SaleDTO> salesForMode;
|
||||
if (viewMode.equals("mine")) {
|
||||
String currentUser = tokenManager.getUsername();
|
||||
salesForMode = cachedSales.stream()
|
||||
.filter(s -> currentUser != null && currentUser.equalsIgnoreCase(s.getEmployeeName() != null ? s.getEmployeeName() : ""))
|
||||
.collect(Collectors.toList());
|
||||
} else {
|
||||
salesForMode = cachedSales;
|
||||
}
|
||||
if (!storeFilter.equals("All Stores") && !storeFilter.isEmpty()) {
|
||||
final String sf = storeFilter;
|
||||
salesForMode = salesForMode.stream()
|
||||
.filter(s -> sf.equalsIgnoreCase(s.getStoreName() != null ? s.getStoreName() : ""))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
List<SaleDTO> filtered = filterSales(salesForMode, currentFilter);
|
||||
computeAnalytics(filtered, currentFilter);
|
||||
}
|
||||
|
||||
private void deriveStores() {
|
||||
java.util.Set<String> stores = new java.util.TreeSet<>();
|
||||
for (SaleDTO s : cachedSales) {
|
||||
if (s.getStoreName() != null && !s.getStoreName().isEmpty()) {
|
||||
stores.add(s.getStoreName());
|
||||
}
|
||||
}
|
||||
List<String> result = new ArrayList<>();
|
||||
result.add("All Stores");
|
||||
result.addAll(stores);
|
||||
availableStores.setValue(result);
|
||||
}
|
||||
|
||||
private void derivePaymentMethods() {
|
||||
java.util.Set<String> methods = new java.util.TreeSet<>();
|
||||
for (SaleDTO s : cachedSales) {
|
||||
|
||||
@@ -45,6 +45,40 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llViewModeToggle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="16dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="4dp">
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnMyAnalytics"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_weight="1"
|
||||
android:text="My Analytics"
|
||||
android:textSize="12sp"
|
||||
android:backgroundTint="#CBD5E1"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginEnd="4dp"/>
|
||||
|
||||
<Button
|
||||
android:id="@+id/btnStoreAnalytics"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="36dp"
|
||||
android:layout_weight="1"
|
||||
android:text="Store Analytics"
|
||||
android:textSize="12sp"
|
||||
android:backgroundTint="@color/primary_medium"
|
||||
android:textColor="@color/white"
|
||||
android:layout_marginStart="4dp"/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
@@ -102,6 +136,23 @@
|
||||
android:layout_marginTop="12dp"
|
||||
android:visibility="gone">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/tvStoreFilterLabel"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Store"
|
||||
android:textColor="@color/text_light"
|
||||
android:textSize="11sp"
|
||||
android:layout_marginBottom="4dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<Spinner
|
||||
android:id="@+id/spinnerFilterStore"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginBottom="12dp"
|
||||
android:visibility="gone"/>
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -444,6 +444,30 @@
|
||||
android:textColor="@color/accent_coral"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="12dp"/>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/llPointsToEarn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="horizontal"
|
||||
android:layout_gravity="end"
|
||||
android:layout_marginTop="4dp"
|
||||
android:visibility="gone">
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="Points to earn: "
|
||||
android:textColor="#4ECDC4"
|
||||
android:textSize="13sp"/>
|
||||
<TextView
|
||||
android:id="@+id/tvPointsToEarn"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="+0 pts"
|
||||
android:textColor="#4ECDC4"
|
||||
android:textSize="13sp"
|
||||
android:textStyle="bold"/>
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@@ -12,6 +12,8 @@ public class MessageResponse {
|
||||
private String content;
|
||||
private LocalDateTime timestamp;
|
||||
private Boolean isRead;
|
||||
private String attachmentName;
|
||||
private String attachmentUrl;
|
||||
|
||||
public MessageResponse() {
|
||||
}
|
||||
@@ -87,4 +89,20 @@ public class MessageResponse {
|
||||
public void setIsRead(Boolean 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 java.nio.file.Path;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class AuthApi {
|
||||
private static final AuthApi INSTANCE = new AuthApi();
|
||||
@@ -33,4 +35,10 @@ public class AuthApi {
|
||||
public void deleteAvatar() throws Exception {
|
||||
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.Tab;
|
||||
import javafx.scene.control.TabPane;
|
||||
import javafx.scene.control.ToggleButton;
|
||||
import javafx.scene.control.ToggleGroup;
|
||||
import javafx.scene.layout.HBox;
|
||||
import javafx.scene.layout.VBox;
|
||||
import org.example.petshopdesktop.api.dto.analytics.DailySales;
|
||||
import org.example.petshopdesktop.api.dto.analytics.DashboardResponse;
|
||||
@@ -98,6 +101,20 @@ public class AnalyticsController {
|
||||
@FXML
|
||||
private ComboBox<String> cbTopN;
|
||||
|
||||
@FXML
|
||||
private ComboBox<String> cbStoreFilter;
|
||||
|
||||
@FXML
|
||||
private HBox hbViewToggle;
|
||||
|
||||
@FXML
|
||||
private ToggleButton tbnMyAnalytics;
|
||||
|
||||
@FXML
|
||||
private ToggleButton tbnStoreAnalytics;
|
||||
|
||||
private String viewMode = "store";
|
||||
|
||||
private List<SaleResponse> cachedSales = new ArrayList<>();
|
||||
private FilterState currentFilter = new FilterState();
|
||||
|
||||
@@ -124,8 +141,29 @@ public class AnalyticsController {
|
||||
cbPaymentFilter.setItems(FXCollections.observableArrayList("All"));
|
||||
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||
|
||||
cbStoreFilter.setItems(FXCollections.observableArrayList("All Stores"));
|
||||
cbStoreFilter.getSelectionModel().selectFirst();
|
||||
|
||||
lblFilterSummary.setText("All time");
|
||||
|
||||
ToggleGroup tgViewMode = new ToggleGroup();
|
||||
tbnMyAnalytics.setToggleGroup(tgViewMode);
|
||||
tbnStoreAnalytics.setToggleGroup(tgViewMode);
|
||||
tbnStoreAnalytics.setSelected(true);
|
||||
tgViewMode.selectedToggleProperty().addListener((obs, oldVal, newVal) -> {
|
||||
if (newVal == null) {
|
||||
(viewMode.equals("mine") ? tbnMyAnalytics : tbnStoreAnalytics).setSelected(true);
|
||||
return;
|
||||
}
|
||||
viewMode = (newVal == tbnMyAnalytics) ? "mine" : "store";
|
||||
updateViewModeStyles();
|
||||
updateStoreFilterVisibility();
|
||||
applyCurrentFilter();
|
||||
});
|
||||
|
||||
hbViewToggle.setVisible(true);
|
||||
hbViewToggle.setManaged(true);
|
||||
|
||||
loadAnalyticsData();
|
||||
}
|
||||
|
||||
@@ -182,6 +220,8 @@ public class AnalyticsController {
|
||||
Platform.runLater(() -> {
|
||||
cachedSales = sales;
|
||||
derivePaymentMethods();
|
||||
deriveStores();
|
||||
updateStoreFilterVisibility();
|
||||
applyCurrentFilter();
|
||||
btnRefresh.setDisable(false);
|
||||
});
|
||||
@@ -196,9 +236,36 @@ public class AnalyticsController {
|
||||
}).start();
|
||||
}
|
||||
|
||||
private void updateViewModeStyles() {
|
||||
String selectedStyle = "-fx-background-color: #4ECDC4; -fx-text-fill: white; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;";
|
||||
String unselectedStyle = "-fx-background-color: #e2e8f0; -fx-text-fill: #475569; -fx-cursor: hand; -fx-focus-color: transparent; -fx-faint-focus-color: transparent;";
|
||||
if (viewMode.equals("mine")) {
|
||||
tbnMyAnalytics.setStyle(selectedStyle + " -fx-background-radius: 6 0 0 6;");
|
||||
tbnStoreAnalytics.setStyle(unselectedStyle + " -fx-background-radius: 0 6 6 0;");
|
||||
} else {
|
||||
tbnMyAnalytics.setStyle(unselectedStyle + " -fx-background-radius: 6 0 0 6;");
|
||||
tbnStoreAnalytics.setStyle(selectedStyle + " -fx-background-radius: 0 6 6 0;");
|
||||
}
|
||||
}
|
||||
|
||||
private void applyCurrentFilter() {
|
||||
try {
|
||||
List<SaleResponse> 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 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() {
|
||||
String start = currentFilter.startDate;
|
||||
String end = currentFilter.endDate;
|
||||
@@ -392,11 +484,12 @@ public class AnalyticsController {
|
||||
}
|
||||
|
||||
private void applyRoleVisibility(boolean isAdmin) {
|
||||
chartEmployeePerformance.setVisible(isAdmin);
|
||||
chartEmployeePerformance.setManaged(isAdmin);
|
||||
boolean showEmpChart = isAdmin && viewMode.equals("store");
|
||||
chartEmployeePerformance.setVisible(showEmpChart);
|
||||
chartEmployeePerformance.setManaged(showEmpChart);
|
||||
if (chartEmployeePerformance.getParent() != null) {
|
||||
chartEmployeePerformance.getParent().setVisible(isAdmin);
|
||||
chartEmployeePerformance.getParent().setManaged(isAdmin);
|
||||
chartEmployeePerformance.getParent().setVisible(showEmpChart);
|
||||
chartEmployeePerformance.getParent().setManaged(showEmpChart);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -612,6 +705,7 @@ public class AnalyticsController {
|
||||
dpEndDate.setValue(null);
|
||||
cbPaymentFilter.getSelectionModel().selectFirst();
|
||||
cbTopN.getSelectionModel().selectFirst();
|
||||
cbStoreFilter.getSelectionModel().selectFirst();
|
||||
currentFilter = new FilterState();
|
||||
applyCurrentFilter();
|
||||
}
|
||||
@@ -630,6 +724,8 @@ public class AnalyticsController {
|
||||
currentFilter.paymentMethod = pm != null ? pm : "All";
|
||||
int topNPos = cbTopN.getSelectionModel().getSelectedIndex();
|
||||
currentFilter.topN = (topNPos >= 0 && topNPos < TOP_N_VALUES.length) ? TOP_N_VALUES[topNPos] : 5;
|
||||
String sf = cbStoreFilter.getValue();
|
||||
currentFilter.storeFilter = (sf != null && !sf.isBlank()) ? sf : "All Stores";
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
@@ -638,5 +734,6 @@ public class AnalyticsController {
|
||||
String endDate = "";
|
||||
String paymentMethod = "All";
|
||||
int topN = 5;
|
||||
String storeFilter = "All Stores";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,13 +7,17 @@ import javafx.collections.transformation.FilteredList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.stage.Modality;
|
||||
import javafx.stage.Stage;
|
||||
import org.example.petshopdesktop.api.dto.user.UserResponse;
|
||||
import org.example.petshopdesktop.api.endpoints.CustomerApi;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||
import org.example.petshopdesktop.util.TableViewSupport;
|
||||
|
||||
import java.util.Comparator;
|
||||
@@ -25,6 +29,9 @@ public class CustomerAccountsController {
|
||||
@FXML
|
||||
private TableView<UserResponse> tvCustomers;
|
||||
|
||||
@FXML
|
||||
private TableColumn<UserResponse, String> colCustomerAvatar;
|
||||
|
||||
@FXML
|
||||
private TableColumn<UserResponse, String> colCustomerUsername;
|
||||
|
||||
@@ -69,6 +76,13 @@ public class CustomerAccountsController {
|
||||
|
||||
@FXML
|
||||
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()));
|
||||
colCustomerName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName()));
|
||||
colCustomerEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail()));
|
||||
@@ -94,6 +108,27 @@ public class CustomerAccountsController {
|
||||
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
|
||||
void btnRefreshClicked(ActionEvent event) {
|
||||
txtSearchCustomer.clear();
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
package org.example.petshopdesktop.controllers;
|
||||
|
||||
import javafx.application.Platform;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.Alert;
|
||||
import javafx.scene.control.Button;
|
||||
import javafx.scene.control.Label;
|
||||
import javafx.scene.control.PasswordField;
|
||||
import javafx.scene.control.TextInputDialog;
|
||||
import javafx.scene.control.TextField;
|
||||
import javafx.scene.layout.StackPane;
|
||||
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.LoginResponse;
|
||||
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.UserSession;
|
||||
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() {
|
||||
try {
|
||||
FXMLLoader loader = new FXMLLoader(
|
||||
|
||||
@@ -199,6 +199,12 @@ public class SaleController {
|
||||
@FXML
|
||||
private Label lblLoyaltyDiscount;
|
||||
|
||||
@FXML
|
||||
private HBox hbPointsToEarn;
|
||||
|
||||
@FXML
|
||||
private Label lblPointsToEarn;
|
||||
|
||||
private final ObservableList<SaleCartItem> cartItems = FXCollections.observableArrayList();
|
||||
private final ObservableList<SaleLineItem> saleItems = FXCollections.observableArrayList();
|
||||
private FilteredList<SaleLineItem> filteredSales;
|
||||
@@ -389,12 +395,15 @@ public class SaleController {
|
||||
|
||||
task.setOnSucceeded(event -> {
|
||||
selectedCustomerData = task.getValue();
|
||||
if (selectedCustomerData != null && selectedCustomerData.getLoyaltyPoints() != null && selectedCustomerData.getLoyaltyPoints() >= 20) {
|
||||
lblLoyaltyPoints.setText(selectedCustomerData.getLoyaltyPoints() + " pts available");
|
||||
if (selectedCustomerData != null) {
|
||||
int pts = selectedCustomerData.getLoyaltyPoints() != null ? selectedCustomerData.getLoyaltyPoints() : 0;
|
||||
lblLoyaltyPoints.setText(pts + " pts available");
|
||||
lblLoyaltyPoints.setVisible(true);
|
||||
lblLoyaltyPoints.setManaged(true);
|
||||
chkUseLoyaltyPoints.setVisible(true);
|
||||
chkUseLoyaltyPoints.setManaged(true);
|
||||
boolean canRedeem = pts >= 20;
|
||||
chkUseLoyaltyPoints.setVisible(canRedeem);
|
||||
chkUseLoyaltyPoints.setManaged(canRedeem);
|
||||
if (!canRedeem) chkUseLoyaltyPoints.setSelected(false);
|
||||
} else {
|
||||
lblLoyaltyPoints.setVisible(false);
|
||||
lblLoyaltyPoints.setManaged(false);
|
||||
@@ -701,6 +710,8 @@ public class SaleController {
|
||||
hbCouponDiscount.setManaged(false);
|
||||
hbLoyaltyDiscount.setVisible(false);
|
||||
hbLoyaltyDiscount.setManaged(false);
|
||||
hbPointsToEarn.setVisible(false);
|
||||
hbPointsToEarn.setManaged(false);
|
||||
cbCustomer.setValue(null);
|
||||
selectedCustomerData = null;
|
||||
lblLoyaltyPoints.setVisible(false);
|
||||
@@ -875,6 +886,16 @@ public class SaleController {
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
@@ -7,8 +7,11 @@ import javafx.collections.transformation.FilteredList;
|
||||
import javafx.event.ActionEvent;
|
||||
import javafx.fxml.FXML;
|
||||
import javafx.fxml.FXMLLoader;
|
||||
import javafx.geometry.Pos;
|
||||
import javafx.scene.Scene;
|
||||
import javafx.scene.control.*;
|
||||
import javafx.scene.image.ImageView;
|
||||
import javafx.scene.layout.StackPane;
|
||||
import javafx.scene.layout.VBox;
|
||||
import javafx.stage.Modality;
|
||||
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.auth.UserSession;
|
||||
import org.example.petshopdesktop.util.ActivityLogger;
|
||||
import org.example.petshopdesktop.util.DesktopImageSupport;
|
||||
import org.example.petshopdesktop.util.TableViewSupport;
|
||||
|
||||
import java.util.Comparator;
|
||||
@@ -26,6 +30,7 @@ public class StaffAccountsController {
|
||||
|
||||
@FXML private VBox staffSection;
|
||||
@FXML private TableView<EmployeeResponse> tvStaff;
|
||||
@FXML private TableColumn<EmployeeResponse, String> colStaffAvatar;
|
||||
@FXML private TableColumn<EmployeeResponse, String> colUsername;
|
||||
@FXML private TableColumn<EmployeeResponse, String> colName;
|
||||
@FXML private TableColumn<EmployeeResponse, String> colEmail;
|
||||
@@ -47,6 +52,13 @@ public class StaffAccountsController {
|
||||
|
||||
@FXML
|
||||
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()));
|
||||
colName.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getFullName()));
|
||||
colEmail.setCellValueFactory(data -> new javafx.beans.property.SimpleStringProperty(data.getValue().getEmail()));
|
||||
@@ -81,6 +93,27 @@ public class StaffAccountsController {
|
||||
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
|
||||
void btnRefreshClicked(ActionEvent event) {
|
||||
txtSearch.clear();
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
<?import javafx.geometry.Insets?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.Hyperlink?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.PasswordField?>
|
||||
<?import javafx.scene.control.TextField?>
|
||||
@@ -83,6 +84,15 @@
|
||||
<Insets top="8.0" />
|
||||
</VBox.margin>
|
||||
</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>
|
||||
</VBox>
|
||||
</children>
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
<?import javafx.scene.chart.PieChart?>
|
||||
<?import javafx.scene.control.Button?>
|
||||
<?import javafx.scene.control.ComboBox?>
|
||||
<?import javafx.scene.control.ToggleButton?>
|
||||
<?import javafx.scene.control.DatePicker?>
|
||||
<?import javafx.scene.control.Label?>
|
||||
<?import javafx.scene.control.Tab?>
|
||||
@@ -30,6 +31,16 @@
|
||||
</font>
|
||||
</Label>
|
||||
<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">
|
||||
<font>
|
||||
<Font size="13.0" />
|
||||
@@ -91,6 +102,7 @@
|
||||
</FlowPane>
|
||||
<HBox alignment="CENTER_LEFT" spacing="8.0">
|
||||
<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="cbTopN" prefWidth="110.0" />
|
||||
<Region HBox.hgrow="ALWAYS" />
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
|
||||
<TableView fx:id="tvCustomers" style="-fx-background-color: white; -fx-background-radius: 12;" VBox.vgrow="ALWAYS">
|
||||
<columns>
|
||||
<TableColumn fx:id="colCustomerAvatar" prefWidth="70.0" text="Avatar" sortable="false" />
|
||||
<TableColumn fx:id="colCustomerUsername" prefWidth="130.0" text="Username" />
|
||||
<TableColumn fx:id="colCustomerName" prefWidth="160.0" text="Name" />
|
||||
<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>
|
||||
</children>
|
||||
</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>
|
||||
</VBox>
|
||||
<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">
|
||||
<columns>
|
||||
<TableColumn fx:id="colStaffAvatar" prefWidth="70.0" text="Avatar" sortable="false" />
|
||||
<TableColumn fx:id="colUsername" prefWidth="140.0" text="Username" />
|
||||
<TableColumn fx:id="colName" prefWidth="170.0" text="Name" />
|
||||
<TableColumn fx:id="colEmail" prefWidth="210.0" text="Email" />
|
||||
|
||||
Reference in New Issue
Block a user