added personal and store analytics
This commit is contained in:
@@ -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,30 @@ 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");
|
||||
});
|
||||
|
||||
binding.btnStoreAnalytics.setOnClickListener(v -> {
|
||||
viewModel.setViewMode("store");
|
||||
updateViewModeButtonStyles("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));
|
||||
}
|
||||
|
||||
// Filter Panel
|
||||
|
||||
private void setupFilterPanel() {
|
||||
@@ -224,17 +254,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
|
||||
|
||||
@@ -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,6 +31,7 @@ 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);
|
||||
@@ -37,10 +40,12 @@ public class AnalyticsViewModel extends ViewModel {
|
||||
|
||||
private List<SaleDTO> cachedSales = new ArrayList<>();
|
||||
private FilterState currentFilter = new FilterState();
|
||||
private String viewMode = "store";
|
||||
|
||||
@Inject
|
||||
public AnalyticsViewModel(SaleRepository saleRepository) {
|
||||
public AnalyticsViewModel(SaleRepository saleRepository, TokenManager tokenManager) {
|
||||
this.saleRepository = saleRepository;
|
||||
this.tokenManager = tokenManager;
|
||||
}
|
||||
|
||||
public LiveData<AnalyticsData> getAnalyticsData() { return analyticsData; }
|
||||
@@ -76,8 +81,26 @@ public class AnalyticsViewModel extends ViewModel {
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public void setViewMode(String mode) {
|
||||
viewMode = mode;
|
||||
applyCurrentFilter();
|
||||
}
|
||||
|
||||
public String getViewMode() {
|
||||
return viewMode;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
List<SaleDTO> filtered = filterSales(salesForMode, currentFilter);
|
||||
computeAnalytics(filtered, currentFilter);
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,17 @@ public class AnalyticsController {
|
||||
@FXML
|
||||
private ComboBox<String> cbTopN;
|
||||
|
||||
@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();
|
||||
|
||||
@@ -126,6 +140,23 @@ public class AnalyticsController {
|
||||
|
||||
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();
|
||||
applyCurrentFilter();
|
||||
});
|
||||
|
||||
hbViewToggle.setVisible(true);
|
||||
hbViewToggle.setManaged(true);
|
||||
|
||||
loadAnalyticsData();
|
||||
}
|
||||
|
||||
@@ -196,9 +227,30 @@ 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;
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -392,11 +444,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user