From 74472976d55e529127980498bf37d7428879e36c Mon Sep 17 00:00:00 2001 From: Nikitha Date: Mon, 6 Apr 2026 22:55:45 -0600 Subject: [PATCH 01/15] Sale, refund and analytics documents view sale history and refund for sales analytics of sale , employee and store performances --- .../activities/HomeActivity.java | 16 +- .../petstoremobile/adapters/SaleAdapter.java | 56 +- .../example/petstoremobile/api/RefundApi.java | 25 + .../petstoremobile/api/RetrofitClient.java | 14 +- .../example/petstoremobile/api/SaleApi.java | 15 +- .../petstoremobile/dtos/RefundDTO.java | 58 ++ .../example/petstoremobile/dtos/SaleDTO.java | 161 +++--- .../fragments/ListFragment.java | 33 ++ .../listfragments/AnalyticsFragment.java | 328 +++++++++++ .../fragments/listfragments/SaleFragment.java | 168 +++--- .../detailfragments/RefundDetailFragment.java | 135 ----- .../detailfragments/RefundFragment.java | 512 ++++++++++++++++++ .../detailfragments/SaleDetailFragment.java | 373 +++++++++++++ .../main/res/layout/fragment_analytics.xml | 318 +++++++++++ .../app/src/main/res/layout/fragment_list.xml | 43 +- .../src/main/res/layout/fragment_refund.xml | 222 ++++++++ .../app/src/main/res/layout/fragment_sale.xml | 53 +- .../main/res/layout/fragment_sale_detail.xml | 221 ++++++++ .../src/main/res/layout/fragment_staff.xml | 82 +++ android/app/src/main/res/layout/item_sale.xml | 176 +++--- desktop/.idea/misc.xml | 1 - 21 files changed, 2589 insertions(+), 421 deletions(-) create mode 100644 android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java delete mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java create mode 100644 android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java create mode 100644 android/app/src/main/res/layout/fragment_analytics.xml create mode 100644 android/app/src/main/res/layout/fragment_refund.xml create mode 100644 android/app/src/main/res/layout/fragment_sale_detail.xml create mode 100644 android/app/src/main/res/layout/fragment_staff.xml diff --git a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java index 2f6c9722..ae7b8132 100644 --- a/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java +++ b/android/app/src/main/java/com/example/petstoremobile/activities/HomeActivity.java @@ -90,16 +90,24 @@ public class HomeActivity extends AppCompatActivity { loadFragment(chatFragment); bottomNav.setSelectedItemId(R.id.nav_chat); } else { - loadFragment(new ListFragment()); - bottomNav.setSelectedItemId(R.id.nav_list); + new android.os.Handler().postDelayed(() -> { + loadFragment(new ListFragment()); + bottomNav.setSelectedItemId(R.id.nav_list); + }, 100); } } // Helper function to start the notification service in the background // to receive notifications when a new conversation is created private void startNotificationService() { - Intent serviceIntent = new Intent(this, ChatNotificationService.class); - startService(serviceIntent); + new Thread(() -> { + try { + Intent serviceIntent = new Intent(this, ChatNotificationService.class); + startService(serviceIntent); + } catch (Exception e) { + Log.e("HomeActivity", "Failed to start notification service: " + e.getMessage()); + } + }).start(); } //Helper function to request for notification permission diff --git a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java index d5a73e42..098f58d9 100644 --- a/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java +++ b/android/app/src/main/java/com/example/petstoremobile/adapters/SaleAdapter.java @@ -1,78 +1,70 @@ package com.example.petstoremobile.adapters; -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; +import android.view.*; import android.widget.TextView; import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import com.example.petstoremobile.R; -import com.example.petstoremobile.models.Sale; +import com.example.petstoremobile.dtos.SaleDTO; import java.util.List; public class SaleAdapter extends RecyclerView.Adapter { - private List saleList; - private OnSaleClickListener saleClickListener; + private List saleList; + private OnSaleClickListener listener; public interface OnSaleClickListener { void onSaleClick(int position); } - public SaleAdapter(List saleList, OnSaleClickListener saleClickListener) { + public SaleAdapter(List saleList, OnSaleClickListener listener) { this.saleList = saleList; - this.saleClickListener = saleClickListener; + this.listener = listener; } public static class SaleViewHolder extends RecyclerView.ViewHolder { - TextView tvSaleId, tvItemName, tvEmployeeName, tvSaleDate, tvTotal, tvPaymentMethod, tvRefundBadge; + TextView tvId, tvEmployee, tvDate, tvPayment, tvTotal, tvRefundBadge; public SaleViewHolder(@NonNull View v) { super(v); - tvSaleId = v.findViewById(R.id.tvSaleId); - tvItemName = v.findViewById(R.id.tvSaleItemName); - tvEmployeeName = v.findViewById(R.id.tvSaleEmployee); - tvSaleDate = v.findViewById(R.id.tvSaleDate); + tvId = v.findViewById(R.id.tvSaleId); + tvEmployee = v.findViewById(R.id.tvSaleEmployee); + tvDate = v.findViewById(R.id.tvSaleDate); + tvPayment = v.findViewById(R.id.tvSalePayment); tvTotal = v.findViewById(R.id.tvSaleTotal); - tvPaymentMethod = v.findViewById(R.id.tvSalePayment); - tvRefundBadge = v.findViewById(R.id.tvRefundBadge); + tvRefundBadge = v.findViewById(R.id.tvSaleRefundBadge); } } @NonNull @Override public SaleViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - View v = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_sale, parent, false); + View v = LayoutInflater.from(parent.getContext()) + .inflate(R.layout.item_sale, parent, false); return new SaleViewHolder(v); } @Override public void onBindViewHolder(@NonNull SaleViewHolder holder, int position) { - Sale sale = saleList.get(position); + SaleDTO s = saleList.get(position); + holder.tvId.setText("Sale #" + (s.getSaleId() != null ? s.getSaleId() : "")); + holder.tvEmployee.setText("By: " + (s.getEmployeeName() != null ? s.getEmployeeName() : "")); + holder.tvDate.setText(s.getSaleDate() != null ? s.getSaleDate().substring(0, 10) : ""); + holder.tvPayment.setText(s.getPaymentMethod() != null ? s.getPaymentMethod() : ""); + holder.tvTotal.setText(s.getTotalAmount() != null ? "$" + s.getTotalAmount() : ""); - holder.tvSaleId.setText("ID: " + sale.getSaleId()); - holder.tvItemName.setText(sale.getItemName()); - holder.tvEmployeeName.setText("By: " + sale.getEmployeeName()); - holder.tvSaleDate.setText(sale.getSaleDate()); - holder.tvTotal.setText("$" + String.format("%.2f", sale.getTotal())); - holder.tvPaymentMethod.setText(sale.getPaymentMethod()); - - // Show refund badge if it's a refund - if (sale.isRefund()) { + // Show refund badge + if (Boolean.TRUE.equals(s.getIsRefund())) { holder.tvRefundBadge.setVisibility(View.VISIBLE); - holder.tvRefundBadge.setBackgroundColor(Color.parseColor("#F44336")); - holder.tvTotal.setTextColor(Color.parseColor("#F44336")); } else { holder.tvRefundBadge.setVisibility(View.GONE); - holder.tvTotal.setTextColor(Color.parseColor("#4CAF50")); } - holder.itemView.setOnClickListener(v -> saleClickListener.onSaleClick(position)); + holder.itemView.setOnClickListener(v -> listener.onSaleClick(position)); } @Override public int getItemCount() { return saleList.size(); } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java b/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java new file mode 100644 index 00000000..d7ba9575 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/api/RefundApi.java @@ -0,0 +1,25 @@ +package com.example.petstoremobile.api; + +import com.example.petstoremobile.dtos.RefundDTO; +import retrofit2.Call; +import retrofit2.http.*; + +import java.util.List; + +public interface RefundApi { + + @GET("api/v1/refunds") + Call> getAllRefunds(); + + @GET("api/v1/refunds/{id}") + Call getRefundById(@Path("id") Long id); + + @POST("api/v1/refunds") + Call createRefund(@Body RefundDTO refund); + + @PUT("api/v1/refunds/{id}") + Call updateRefund(@Path("id") Long id, @Body RefundDTO refund); + + @DELETE("api/v1/refunds/{id}") + Call deleteRefund(@Path("id") Long id); +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java index 3a93deb2..2ae969a4 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/RetrofitClient.java @@ -28,9 +28,14 @@ public class RetrofitClient { || (Build.BRAND.startsWith("generic") && Build.DEVICE.startsWith("generic")) || "google_sdk".equals(Build.PRODUCT)) { return "http://10.0.2.2:8080/"; //emulator testing - } else { + } + else { return "http://10.0.0.200:8080/"; //Hardware testing } + + /*else { + return "http://192.168.1.148:8080/"; //Hardware testing + } */ } private static Retrofit retrofit = null; @@ -123,4 +128,11 @@ public class RetrofitClient { return getClient(context).create(CategoryApi.class); } + public static RefundApi getRefundApi(Context context) { + return getClient(context).create(RefundApi.class); + } + public static EmployeeApi getEmployeeApi(Context context) { + return getClient(context).create(EmployeeApi.class); + } + } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java index 55d1eef8..c89ead10 100644 --- a/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java +++ b/android/app/src/main/java/com/example/petstoremobile/api/SaleApi.java @@ -2,15 +2,8 @@ package com.example.petstoremobile.api; import com.example.petstoremobile.dtos.PageResponse; import com.example.petstoremobile.dtos.SaleDTO; - import retrofit2.Call; -import retrofit2.http.Body; -import retrofit2.http.DELETE; -import retrofit2.http.GET; -import retrofit2.http.POST; -import retrofit2.http.PUT; -import retrofit2.http.Path; -import retrofit2.http.Query; +import retrofit2.http.*; public interface SaleApi { @@ -24,10 +17,4 @@ public interface SaleApi { @POST("api/v1/sales") Call createSale(@Body SaleDTO sale); - - @PUT("api/v1/sales/{id}") - Call updateSale(@Path("id") Long id, @Body SaleDTO sale); - - @DELETE("api/v1/sales/{id}") - Call deleteSale(@Path("id") Long id); } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java new file mode 100644 index 00000000..5f47e1ce --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/RefundDTO.java @@ -0,0 +1,58 @@ +package com.example.petstoremobile.dtos; + +import java.math.BigDecimal; + +public class RefundDTO { + // Response fields + private Long id; + private Long saleId; + private Long customerId; + private BigDecimal amount; + private String reason; + private String status; + private String createdAt; + private String updatedAt; + + // Constructor for create request + public RefundDTO(Long saleId, String reason) { + this.saleId = saleId; + this.reason = reason; + } + + // Constructor for update request + public RefundDTO(String status) { + this.status = status; + } + + public Long getId() { + return id; + } + + public Long getSaleId() { + return saleId; + } + + public Long getCustomerId() { + return customerId; + } + + public BigDecimal getAmount() { + return amount; + } + + public String getReason() { + return reason; + } + + public String getStatus() { + return status; + } + + public String getCreatedAt() { + return createdAt; + } + + public String getUpdatedAt() { + return updatedAt; + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java index 2cdb625f..2de34a38 100644 --- a/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java +++ b/android/app/src/main/java/com/example/petstoremobile/dtos/SaleDTO.java @@ -1,82 +1,121 @@ package com.example.petstoremobile.dtos; +import java.math.BigDecimal; +import java.util.List; + public class SaleDTO { - + // Response fields private Long saleId; - - private Long productId; - private String productName; - - private Integer quantity; - private Double price; - private Double totalAmount; - private String saleDate; - private String customerName; + private Long employeeId; + private String employeeName; + private Long storeId; + private String storeName; + private BigDecimal totalAmount; + private String paymentMethod; + private Boolean isRefund; + private Long originalSaleId; + private List items; + private String createdAt; - public SaleDTO() {} + // Request fields + private Long customerId; + + // Constructor for create request + public SaleDTO(Long storeId, String paymentMethod, List items, + Boolean isRefund, Long originalSaleId, Long customerId) { + this.storeId = storeId; + this.paymentMethod = paymentMethod; + this.items = items; + this.isRefund = isRefund; + this.originalSaleId = originalSaleId; + this.customerId = customerId; + } public Long getSaleId() { return saleId; } - public void setSaleId(Long saleId) { - this.saleId = saleId; - } - - public Long getProductId() { - return productId; - } - - public void setProductId(Long productId) { - this.productId = productId; - } - - public String getProductName() { - return productName; - } - - public void setProductName(String productName) { - this.productName = productName; - } - - public Integer getQuantity() { - return quantity; - } - - public void setQuantity(Integer quantity) { - this.quantity = quantity; - } - - public Double getPrice() { - return price; - } - - public void setPrice(Double price) { - this.price = price; - } - - public Double getTotalAmount() { - return totalAmount; - } - - public void setTotalAmount(Double totalAmount) { - this.totalAmount = totalAmount; - } - public String getSaleDate() { return saleDate; } - public void setSaleDate(String saleDate) { - this.saleDate = saleDate; + public Long getEmployeeId() { + return employeeId; } - public String getCustomerName() { - return customerName; + public String getEmployeeName() { + return employeeName; } - public void setCustomerName(String customerName) { - this.customerName = customerName; + public Long getStoreId() { + return storeId; + } + + public String getStoreName() { + return storeName; + } + + public BigDecimal getTotalAmount() { + return totalAmount; + } + + public String getPaymentMethod() { + return paymentMethod; + } + + public Boolean getIsRefund() { + return isRefund; + } + + public Long getOriginalSaleId() { + return originalSaleId; + } + + public List getItems() { + return items; + } + + public String getCreatedAt() { + return createdAt; + } + + public Long getCustomerId() { + return customerId; + } + + // Nested SaleItemDTO + public static class SaleItemDTO { + private Long saleItemId; + private Long prodId; + private String productName; + private Integer quantity; + private BigDecimal unitPrice; + + // Constructor for request + public SaleItemDTO(Long prodId, Integer quantity) { + this.prodId = prodId; + this.quantity = quantity; + } + + public Long getSaleItemId() { + return saleItemId; + } + + public Long getProdId() { + return prodId; + } + + public String getProductName() { + return productName; + } + + public Integer getQuantity() { + return quantity; + } + + public BigDecimal getUnitPrice() { + return unitPrice; + } } } \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java index b63b42b1..93d6c18b 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/ListFragment.java @@ -17,6 +17,7 @@ import com.example.petstoremobile.R; import com.example.petstoremobile.api.auth.TokenManager; import com.example.petstoremobile.fragments.listfragments.PetFragment; import com.example.petstoremobile.fragments.listfragments.ServiceFragment; +import com.example.petstoremobile.fragments.listfragments.StaffFragment; import com.example.petstoremobile.fragments.listfragments.SupplierFragment; import com.example.petstoremobile.fragments.listfragments.AdoptionFragment; import com.example.petstoremobile.fragments.listfragments.AppointmentFragment; @@ -25,6 +26,8 @@ import com.example.petstoremobile.fragments.listfragments.ProductFragment; import com.example.petstoremobile.fragments.listfragments.ProductSupplierFragment; import com.example.petstoremobile.fragments.listfragments.PurchaseOrderFragment; import com.example.petstoremobile.fragments.listfragments.SaleFragment; +import com.example.petstoremobile.fragments.listfragments.AnalyticsFragment; + //The Fragment for the displaying the list of entities to be viewed public class ListFragment extends Fragment { @@ -37,6 +40,12 @@ public class ListFragment extends Fragment { private LinearLayout drawerAdoptions, drawerAppointments, drawerInventory, drawerProducts, drawerProductSupplier, drawerPurchaseOrderView, drawerSale; + private LinearLayout drawerAnalytics; + + //Staff + + private LinearLayout drawerStaff; + @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, @@ -54,7 +63,9 @@ public class ListFragment extends Fragment { drawerProducts = view.findViewById(R.id.drawerProducts); drawerProductSupplier=view.findViewById(R.id.drawerProductSupplier); drawerSale=view.findViewById(R.id.drawerSale); + drawerAnalytics = view.findViewById(R.id.drawerAnalytics); drawerPurchaseOrderView=view.findViewById(R.id.drawerPurchaseOrderView); + drawerStaff = view.findViewById(R.id.drawerStaff); // Check user role and restrict access for STAFF @@ -72,6 +83,13 @@ public class ListFragment extends Fragment { loadFragment(new PetFragment()); } + // Only show for ADMIN + if ("ADMIN".equalsIgnoreCase(role)) { + drawerStaff.setVisibility(View.VISIBLE); + } else { + drawerStaff.setVisibility(View.GONE); + } + //add Listeners to the drawer so user won't be able to interact with the innerContainer (the list fragments) //while the drawer is open drawerLayout.addDrawerListener(new DrawerLayout.DrawerListener() { @@ -162,6 +180,21 @@ public class ListFragment extends Fragment { drawerLayout.closeDrawers(); }); + //Analytics + + drawerAnalytics.setOnClickListener(v -> { + loadFragment(new AnalyticsFragment()); + drawerLayout.closeDrawers(); + }); + + // Click listener + drawerStaff.setOnClickListener(v -> { + loadFragment(new StaffFragment()); + drawerLayout.closeDrawers(); + }); + + + return view; } diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java new file mode 100644 index 00000000..9677b44a --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/AnalyticsFragment.java @@ -0,0 +1,328 @@ +package com.example.petstoremobile.fragments.listfragments; + +import android.graphics.Color; +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.fragment.app.Fragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.fragments.ListFragment; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import retrofit2.*; + +public class AnalyticsFragment extends Fragment { + + private TextView tvTotalRevenue, tvTotalTransactions, tvAvgTransaction, tvTotalItems; + private LinearLayout llTopRevenue, llTopQuantity, llPaymentMethods, llEmployeePerformance, llDailyRevenue; + private Button btnRefresh; + private ImageButton hamburger; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_analytics, container, false); + + initViews(view); + loadAnalytics(); + + btnRefresh.setOnClickListener(v -> loadAnalytics()); + hamburger.setOnClickListener(v -> { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.openDrawer(); + }); + + return view; + } + + private void initViews(View v) { + tvTotalRevenue = v.findViewById(R.id.tvTotalRevenue); + tvTotalTransactions = v.findViewById(R.id.tvTotalTransactions); + tvAvgTransaction = v.findViewById(R.id.tvAvgTransaction); + tvTotalItems = v.findViewById(R.id.tvTotalItems); + llTopRevenue = v.findViewById(R.id.llTopRevenue); + llTopQuantity = v.findViewById(R.id.llTopQuantity); + llPaymentMethods = v.findViewById(R.id.llPaymentMethods); + llEmployeePerformance = v.findViewById(R.id.llEmployeePerformance); + llDailyRevenue = v.findViewById(R.id.llDailyRevenue); + btnRefresh = v.findViewById(R.id.btnRefreshAnalytics); + hamburger = v.findViewById(R.id.btnHamburgerAnalytics); + } + + private void loadAnalytics() { + // Clear all sections + llTopRevenue.removeAllViews(); + llTopQuantity.removeAllViews(); + llPaymentMethods.removeAllViews(); + llEmployeePerformance.removeAllViews(); + llDailyRevenue.removeAllViews(); + + // Show loading + tvTotalRevenue.setText("Loading..."); + tvTotalTransactions.setText("..."); + tvAvgTransaction.setText("..."); + tvTotalItems.setText("..."); + + RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + computeAndDisplay(r.body().getContent()); + } else { + showError("Failed to load sales data"); + } + } + + public void onFailure(Call> c, Throwable t) { + Log.e("Analytics", t.getMessage()); + showError("Network error"); + } + }); + } + + private void computeAndDisplay(List sales) { + // Filter out refunds for most metrics + List regularSales = new ArrayList<>(); + for (SaleDTO s : sales) { + if (!Boolean.TRUE.equals(s.getIsRefund())) + regularSales.add(s); + } + + // ── Summary ────────────────────────────────────────── + BigDecimal totalRevenue = BigDecimal.ZERO; + int totalItems = 0; + + for (SaleDTO s : regularSales) { + if (s.getTotalAmount() != null) + totalRevenue = totalRevenue.add(s.getTotalAmount()); + if (s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getQuantity() != null) + totalItems += Math.abs(item.getQuantity()); + } + } + } + + int totalTx = regularSales.size(); + BigDecimal avgTx = totalTx > 0 + ? totalRevenue.divide(BigDecimal.valueOf(totalTx), 2, RoundingMode.HALF_UP) + : BigDecimal.ZERO; + + tvTotalRevenue.setText("$" + totalRevenue.setScale(2, RoundingMode.HALF_UP)); + tvTotalTransactions.setText(String.valueOf(totalTx)); + tvAvgTransaction.setText("$" + avgTx); + tvTotalItems.setText(String.valueOf(totalItems)); + + // ── Top Products by Revenue ─────────────────────────── + Map revenueByProduct = new LinkedHashMap<>(); + Map quantityByProduct = new LinkedHashMap<>(); + + for (SaleDTO s : regularSales) { + if (s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + String name = item.getProductName() != null ? item.getProductName() : "Unknown"; + int qty = item.getQuantity() != null ? Math.abs(item.getQuantity()) : 0; + BigDecimal lineTotal = item.getUnitPrice() != null + ? item.getUnitPrice().multiply(BigDecimal.valueOf(qty)) + : BigDecimal.ZERO; + + revenueByProduct.merge(name, lineTotal, BigDecimal::add); + quantityByProduct.merge(name, qty, Integer::sum); + } + } + } + + // Sort by revenue desc, take top 5 + List> topRevenue = new ArrayList<>(revenueByProduct.entrySet()); + topRevenue.sort((a, b) -> b.getValue().compareTo(a.getValue())); + BigDecimal maxRevenue = topRevenue.isEmpty() ? BigDecimal.ONE : topRevenue.get(0).getValue(); + + llTopRevenue.removeAllViews(); + for (int i = 0; i < Math.min(5, topRevenue.size()); i++) { + Map.Entry e = topRevenue.get(i); + addBarRow(llTopRevenue, e.getKey(), "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxRevenue.floatValue(), "#ff6b35"); + } + if (topRevenue.isEmpty()) + addEmptyRow(llTopRevenue, "No data"); + + // Sort by quantity desc, take top 5 + List> topQuantity = new ArrayList<>(quantityByProduct.entrySet()); + topQuantity.sort((a, b) -> b.getValue() - a.getValue()); + int maxQty = topQuantity.isEmpty() ? 1 : topQuantity.get(0).getValue(); + + llTopQuantity.removeAllViews(); + for (int i = 0; i < Math.min(5, topQuantity.size()); i++) { + Map.Entry e = topQuantity.get(i); + addBarRow(llTopQuantity, e.getKey(), e.getValue() + " units", + (float) e.getValue() / maxQty, "#4ecdc4"); + } + if (topQuantity.isEmpty()) + addEmptyRow(llTopQuantity, "No data"); + + // ── Payment Methods ─────────────────────────────────── + Map paymentCount = new LinkedHashMap<>(); + for (SaleDTO s : regularSales) { + String method = s.getPaymentMethod() != null ? s.getPaymentMethod() : "Unknown"; + paymentCount.merge(method, 1, Integer::sum); + } + + int maxPayment = paymentCount.values().stream().max(Integer::compare).orElse(1); + String[] paymentColors = { "#1a759f", "#ff9f1c", "#577590", "#90be6d" }; + int ci = 0; + llPaymentMethods.removeAllViews(); + for (Map.Entry e : paymentCount.entrySet()) { + addBarRow(llPaymentMethods, e.getKey(), + e.getValue() + " transactions", + (float) e.getValue() / maxPayment, + paymentColors[ci % paymentColors.length]); + ci++; + } + if (paymentCount.isEmpty()) + addEmptyRow(llPaymentMethods, "No data"); + + // ── Employee Performance ────────────────────────────── + Map employeeRevenue = new LinkedHashMap<>(); + for (SaleDTO s : regularSales) { + String emp = s.getEmployeeName() != null ? s.getEmployeeName() : "Unknown"; + if (s.getTotalAmount() != null) + employeeRevenue.merge(emp, s.getTotalAmount(), BigDecimal::add); + } + + List> empList = new ArrayList<>(employeeRevenue.entrySet()); + empList.sort((a, b) -> b.getValue().compareTo(a.getValue())); + BigDecimal maxEmp = empList.isEmpty() ? BigDecimal.ONE : empList.get(0).getValue(); + + llEmployeePerformance.removeAllViews(); + for (Map.Entry e : empList) { + addBarRow(llEmployeePerformance, e.getKey(), + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxEmp.floatValue(), + "#1a759f"); + } + if (empList.isEmpty()) + addEmptyRow(llEmployeePerformance, "No data"); + + // ── Daily Revenue (last 7 days) ─────────────────────── + Map dailyRevenue = new TreeMap<>(); + + // Initialize last 7 days + Calendar cal = Calendar.getInstance(); + for (int i = 6; i >= 0; i--) { + Calendar day = Calendar.getInstance(); + day.add(Calendar.DAY_OF_YEAR, -i); + String key = String.format("%04d-%02d-%02d", + day.get(Calendar.YEAR), + day.get(Calendar.MONTH) + 1, + day.get(Calendar.DAY_OF_MONTH)); + dailyRevenue.put(key, BigDecimal.ZERO); + } + + for (SaleDTO s : regularSales) { + if (s.getSaleDate() != null && s.getTotalAmount() != null) { + String date = s.getSaleDate().length() >= 10 + ? s.getSaleDate().substring(0, 10) + : s.getSaleDate(); + if (dailyRevenue.containsKey(date)) { + dailyRevenue.merge(date, s.getTotalAmount(), BigDecimal::add); + } + } + } + + BigDecimal maxDaily = dailyRevenue.values().stream() + .max(BigDecimal::compareTo).orElse(BigDecimal.ONE); + if (maxDaily.compareTo(BigDecimal.ZERO) == 0) + maxDaily = BigDecimal.ONE; + + llDailyRevenue.removeAllViews(); + for (Map.Entry e : dailyRevenue.entrySet()) { + // Show just MM-DD + String label = e.getKey().length() >= 10 + ? e.getKey().substring(5) + : e.getKey(); + addBarRow(llDailyRevenue, label, + "$" + e.getValue().setScale(2, RoundingMode.HALF_UP), + e.getValue().floatValue() / maxDaily.floatValue(), + "#ff6b35"); + } + } + + // Adds a horizontal bar row with label, value and a proportional bar + private void addBarRow(LinearLayout parent, String label, String value, float ratio, String color) { + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(LinearLayout.VERTICAL); + row.setPadding(0, 6, 0, 6); + + // Label + value row + LinearLayout labelRow = new LinearLayout(getContext()); + labelRow.setOrientation(LinearLayout.HORIZONTAL); + + TextView tvLabel = new TextView(getContext()); + tvLabel.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvLabel.setText(label); + tvLabel.setTextColor(Color.parseColor("#444441")); + tvLabel.setTextSize(13f); + + TextView tvValue = new TextView(getContext()); + tvValue.setText(value); + tvValue.setTextColor(Color.parseColor("#444441")); + tvValue.setTextSize(13f); + tvValue.setTextAlignment(View.TEXT_ALIGNMENT_VIEW_END); + + labelRow.addView(tvLabel); + labelRow.addView(tvValue); + + // Bar background + LinearLayout barBg = new LinearLayout(getContext()); + LinearLayout.LayoutParams bgParams = new LinearLayout.LayoutParams( + LinearLayout.LayoutParams.MATCH_PARENT, 12); + bgParams.setMargins(0, 4, 0, 0); + barBg.setLayoutParams(bgParams); + barBg.setBackgroundColor(Color.parseColor("#EEEEEE")); + + // Bar fill + View barFill = new View(getContext()); + int fillWidth = (int) (ratio * 100); + LinearLayout.LayoutParams fillParams = new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.MATCH_PARENT, ratio); + barFill.setLayoutParams(fillParams); + barFill.setBackgroundColor(Color.parseColor(color)); + barBg.addView(barFill); + + // Empty space + View spacer = new View(getContext()); + spacer.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.MATCH_PARENT, 1f - ratio)); + barBg.addView(spacer); + + row.addView(labelRow); + row.addView(barBg); + parent.addView(row); + } + + private void addEmptyRow(LinearLayout parent, String message) { + TextView tv = new TextView(getContext()); + tv.setText(message); + tv.setTextColor(Color.parseColor("#888780")); + tv.setTextSize(13f); + parent.addView(tv); + } + + private void showError(String msg) { + if (getContext() == null) + return; + tvTotalRevenue.setText("Error"); + tvTotalTransactions.setText("—"); + tvAvgTransaction.setText("—"); + tvTotalItems.setText("—"); + Toast.makeText(getContext(), msg, Toast.LENGTH_SHORT).show(); + } +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java index 544cd378..0fdd2103 100644 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/SaleFragment.java @@ -1,30 +1,32 @@ package com.example.petstoremobile.fragments.listfragments; import android.os.Bundle; +import android.text.*; +import android.util.Log; +import android.view.*; +import android.widget.*; import androidx.fragment.app.Fragment; import androidx.recyclerview.widget.LinearLayoutManager; import androidx.recyclerview.widget.RecyclerView; import androidx.swiperefreshlayout.widget.SwipeRefreshLayout; -import android.text.Editable; -import android.text.TextWatcher; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.EditText; import com.example.petstoremobile.R; import com.example.petstoremobile.adapters.SaleAdapter; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.dtos.SaleDTO; import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.detailfragments.RefundDetailFragment; -import com.example.petstoremobile.models.Sale; -import java.util.ArrayList; -import java.util.List; +import com.example.petstoremobile.fragments.listfragments.detailfragments.SaleDetailFragment; +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.example.petstoremobile.fragments.listfragments.detailfragments.RefundFragment; +import java.util.*; +import retrofit2.*; public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickListener { - private List saleList = new ArrayList<>(); - private List filteredList = new ArrayList<>(); + private List saleList = new ArrayList<>(); + private List filteredList = new ArrayList<>(); private SaleAdapter adapter; - private SwipeRefreshLayout swipeRefreshLayout; + private SwipeRefreshLayout swipeRefresh; private EditText etSearch; @Override @@ -33,43 +35,63 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis View view = inflater.inflate(R.layout.fragment_sale, container, false); setupRecyclerView(view); - loadSaleData(); setupSearch(view); setupSwipeRefresh(view); + loadSales(); + + FloatingActionButton fab = view.findViewById(R.id.fabAddSale); + fab.setOnClickListener(v -> openDetail(-1, null)); + + ImageButton hamburger = view.findViewById(R.id.btnHamburgerSale); + hamburger.setOnClickListener(v -> { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.openDrawer(); + }); + + // ← moved inside onCreateView + Button btnRefund = view.findViewById(R.id.btnOpenRefund); + btnRefund.setOnClickListener(v -> { + RefundFragment refundFragment = new RefundFragment(); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.loadFragment(refundFragment); + }); return view; } + private void setupRecyclerView(View view) { + RecyclerView rv = view.findViewById(R.id.recyclerViewSales); + adapter = new SaleAdapter(filteredList, this); + rv.setLayoutManager(new LinearLayoutManager(getContext())); + rv.setAdapter(adapter); + } + private void setupSearch(View view) { etSearch = view.findViewById(R.id.etSearchSale); etSearch.addTextChangedListener(new TextWatcher() { - @Override - public void beforeTextChanged(CharSequence s, int start, int count, int after) { - } - - @Override - public void onTextChanged(CharSequence s, int start, int before, int count) { - filterSales(s.toString()); - } - - @Override - public void afterTextChanged(Editable s) { + public void beforeTextChanged(CharSequence s, int a, int b, int c) {} + public void afterTextChanged(Editable s) {} + public void onTextChanged(CharSequence s, int a, int b, int c) { + filter(s.toString()); } }); } - private void filterSales(String query) { + private void setupSwipeRefresh(View view) { + swipeRefresh = view.findViewById(R.id.swipeRefreshSale); + swipeRefresh.setOnRefreshListener(this::loadSales); + } + + private void filter(String query) { filteredList.clear(); if (query.isEmpty()) { filteredList.addAll(saleList); } else { String lower = query.toLowerCase(); - for (Sale s : saleList) { - if (s.getItemName().toLowerCase().contains(lower) - || s.getEmployeeName().toLowerCase().contains(lower) - || s.getSaleDate().toLowerCase().contains(lower) - || s.getPaymentMethod().toLowerCase().contains(lower) - || String.valueOf(s.getSaleId()).contains(lower)) { + for (SaleDTO s : saleList) { + if ((s.getEmployeeName() != null && s.getEmployeeName().toLowerCase().contains(lower)) + || (s.getStoreName() != null && s.getStoreName().toLowerCase().contains(lower)) + || (s.getPaymentMethod() != null && s.getPaymentMethod().toLowerCase().contains(lower))) { filteredList.add(s); } } @@ -77,54 +99,44 @@ public class SaleFragment extends Fragment implements SaleAdapter.OnSaleClickLis adapter.notifyDataSetChanged(); } - private void setupSwipeRefresh(View view) { - swipeRefreshLayout = view.findViewById(R.id.swipeRefreshSale); - swipeRefreshLayout.setOnRefreshListener(() -> { - loadSaleData(); - swipeRefreshLayout.setRefreshing(false); - }); + public void loadSales() { + if (swipeRefresh != null) swipeRefresh.setRefreshing(true); + RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 100) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + if (r.isSuccessful() && r.body() != null) { + saleList.clear(); + saleList.addAll(r.body().getContent()); + filter(etSearch != null ? etSearch.getText().toString() : ""); + } else { + Toast.makeText(getContext(), "Failed to load sales", + Toast.LENGTH_SHORT).show(); + } + } + public void onFailure(Call> c, Throwable t) { + if (swipeRefresh != null) swipeRefresh.setRefreshing(false); + Log.e("SaleFragment", t.getMessage()); + } + }); + } + + public void openDetail(int position, SaleDTO sale) { + SaleDetailFragment detail = new SaleDetailFragment(); + Bundle args = new Bundle(); + if (position != -1 && sale != null) { + args.putLong("saleId", sale.getSaleId()); + args.putBoolean("isRefund", Boolean.TRUE.equals(sale.getIsRefund())); + args.putBoolean("viewOnly", true); + } + detail.setArguments(args); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.loadFragment(detail); } - // When a sale row is clicked, open the refund screen for that sale @Override public void onSaleClick(int position) { - Sale sale = filteredList.get(position); - RefundDetailFragment refundFragment = new RefundDetailFragment(); - Bundle args = new Bundle(); - args.putInt("saleId", sale.getSaleId()); - args.putString("saleDate", sale.getSaleDate()); - args.putString("employeeName", sale.getEmployeeName()); - args.putDouble("total", sale.getTotal()); - args.putString("paymentMethod", sale.getPaymentMethod()); - refundFragment.setArguments(args); - refundFragment.setSaleFragment(this); - - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) - listFragment.loadFragment(refundFragment); + openDetail(position, filteredList.get(position)); } - - public void reloadSales() { - loadSaleData(); - } - - // TODO: Replace with actual API call - GET v1/sales - private void loadSaleData() { - saleList.clear(); - saleList.add(new Sale(1, "2026-03-01", "John Smith", "Premium Dog Food", 2, 45.99, 91.98, "Card", false)); - saleList.add(new Sale(2, "2026-03-02", "Jane Doe", "Cat Toy Bundle", 1, 19.99, 19.99, "Cash", false)); - saleList.add(new Sale(3, "2026-03-03", "John Smith", "Pet Shampoo", 3, 12.99, 38.97, "Card", false)); - saleList.add(new Sale(4, "2026-03-04", "Jane Doe", "Dog Bed - Large", 1, 89.99, 89.99, "Cash", true)); - filteredList.clear(); - filteredList.addAll(saleList); - if (adapter != null) - adapter.notifyDataSetChanged(); - } - - private void setupRecyclerView(View view) { - RecyclerView recyclerView = view.findViewById(R.id.recyclerViewSales); - adapter = new SaleAdapter(filteredList, this); - recyclerView.setLayoutManager(new LinearLayoutManager(getContext())); - recyclerView.setAdapter(adapter); - } -} +} \ No newline at end of file diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java deleted file mode 100644 index 55c0561e..00000000 --- a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundDetailFragment.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.example.petstoremobile.fragments.listfragments.detailfragments; - -import android.os.Bundle; -import androidx.fragment.app.Fragment; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.EditText; -import android.widget.Spinner; -import android.widget.TextView; -import android.widget.Toast; -import com.example.petstoremobile.R; -import com.example.petstoremobile.fragments.ListFragment; -import com.example.petstoremobile.fragments.listfragments.SaleFragment; -import com.example.petstoremobile.utils.ActivityLogger; -import com.example.petstoremobile.utils.InputValidator; - -public class RefundDetailFragment extends Fragment { - - private EditText etRefundSaleId, etRefundReason; - private TextView tvSaleInfo; - private Spinner spinnerRefundPayment; - private Button btnLoadSale, btnProcessRefund, btnBack; - private int saleId; - private SaleFragment saleFragment; - - public void setSaleFragment(SaleFragment fragment) { - this.saleFragment = fragment; - } - - @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { - View view = inflater.inflate(R.layout.fragment_refund_detail, container, false); - - initViews(view); - setupSpinner(); - handleArguments(); - - btnBack.setOnClickListener(v -> goBack()); - btnLoadSale.setOnClickListener(v -> loadSaleDetails()); - btnProcessRefund.setOnClickListener(v -> processRefund()); - - return view; - } - - private void loadSaleDetails() { - String idText = etRefundSaleId.getText().toString().trim(); - if (idText.isEmpty()) { - Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); - return; - } - - try { - int id = Integer.parseInt(idText); - // TODO: Replace with actual API call - GET v1/sales/{id} - // For now show placeholder info - tvSaleInfo.setText("Sale ID: " + id + " loaded. Enter reason and payment method to process refund."); - tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); - } catch (NumberFormatException e) { - Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); - } - } - - private void processRefund() { - if (!InputValidator.isNotEmpty(etRefundSaleId, "Sale ID")) - return; - if (!InputValidator.isNotEmpty(etRefundReason, "Refund Reason")) - return; - - String idText = etRefundSaleId.getText().toString().trim(); - String reason = etRefundReason.getText().toString().trim(); - String payment = spinnerRefundPayment.getSelectedItem().toString(); - - try { - int id = Integer.parseInt(idText); - // TODO: Replace with actual API call - POST v1/refunds - ActivityLogger.log(requireContext(), "Processed refund for Sale ID: " + id + " - Reason: " + reason); - Toast.makeText(getContext(), "Refund processed for Sale ID: " + id, Toast.LENGTH_SHORT).show(); - if (saleFragment != null) - saleFragment.reloadSales(); - goBack(); - } catch (NumberFormatException e) { - Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); - } - } - - private void handleArguments() { - if (getArguments() != null && getArguments().containsKey("saleId")) { - saleId = getArguments().getInt("saleId"); - etRefundSaleId.setText(String.valueOf(saleId)); - String info = "Sale Date: " + getArguments().getString("saleDate") - + " | Employee: " + getArguments().getString("employeeName") - + " | Total: $" + String.format("%.2f", getArguments().getDouble("total")) - + " | Payment: " + getArguments().getString("paymentMethod"); - tvSaleInfo.setText(info); - tvSaleInfo.setTextColor(getResources().getColor(android.R.color.holo_green_dark)); - - // Pre-select payment method - String payment = getArguments().getString("paymentMethod"); - ArrayAdapter adapter = (ArrayAdapter) spinnerRefundPayment.getAdapter(); - if (adapter != null && payment != null) { - int pos = adapter.getPosition(payment); - if (pos >= 0) - spinnerRefundPayment.setSelection(pos); - } - } - } - - private void goBack() { - ListFragment listFragment = (ListFragment) getParentFragment(); - if (listFragment != null) - listFragment.getChildFragmentManager().popBackStack(); - } - - private void initViews(View view) { - etRefundSaleId = view.findViewById(R.id.etRefundSaleId); - etRefundReason = view.findViewById(R.id.etRefundReason); - tvSaleInfo = view.findViewById(R.id.tvSaleInfo); - spinnerRefundPayment = view.findViewById(R.id.spinnerRefundPayment); - btnLoadSale = view.findViewById(R.id.btnLoadSale); - btnProcessRefund = view.findViewById(R.id.btnProcessRefund); - btnBack = view.findViewById(R.id.btnRefundBack); - } - - private void setupSpinner() { - ArrayAdapter adapter = new ArrayAdapter<>(requireContext(), - android.R.layout.simple_spinner_item, - new String[] { "Cash", "Card", "Debit" }); - adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - spinnerRefundPayment.setAdapter(adapter); - } -} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java new file mode 100644 index 00000000..782947e2 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/RefundFragment.java @@ -0,0 +1,512 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.app.AlertDialog; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.annotation.NonNull; +import androidx.fragment.app.Fragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.RetrofitClient; +import com.example.petstoremobile.dtos.SaleDTO; +import com.example.petstoremobile.dtos.PageResponse; +import com.example.petstoremobile.fragments.ListFragment; +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.*; +import retrofit2.*; + +public class RefundFragment extends Fragment { + + private EditText etSaleId; + private Button btnLoadSale, btnProcessRefund, btnBack; + private TextView tvSaleInfo, tvRefundTotal; + private LinearLayout llOriginalItems, llRefundItems; + private LinearLayout cardOriginalItems, cardRefundItems, cardPayment; + private Spinner spinnerPayment; + + private SaleDTO currentSale; + private List allSales = new ArrayList<>(); + + // Items available to refund (after accounting for previous refunds) + private List availableItems = new ArrayList<>(); + // Items user has added to refund cart + private List refundCart = new ArrayList<>(); + + private final String[] PAYMENT_METHODS = {"Cash", "Card", "Debit"}; + + // Inner class to track refund items + static class RefundItem { + long prodId; + String productName; + int quantity; + BigDecimal unitPrice; + + RefundItem(long prodId, String productName, int quantity, BigDecimal unitPrice) { + this.prodId = prodId; + this.productName = productName; + this.quantity = quantity; + this.unitPrice = unitPrice; + } + + BigDecimal getTotal() { + return unitPrice != null + ? unitPrice.multiply(BigDecimal.valueOf(quantity)) + : BigDecimal.ZERO; + } + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_refund, container, false); + initViews(view); + setupSpinner(); + loadAllSales(); + + // Pre-fill sale ID if passed from SaleFragment + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + long saleId = args.getLong("saleId"); + etSaleId.setText(String.valueOf(saleId)); + // Auto-load after sales are fetched + } + + btnLoadSale.setOnClickListener(v -> loadSale()); + btnProcessRefund.setOnClickListener(v -> processRefund()); + btnBack.setOnClickListener(v -> navigateBack()); + + return view; + } + + private void initViews(View v) { + etSaleId = v.findViewById(R.id.etRefundSaleId); + btnLoadSale = v.findViewById(R.id.btnLoadSale); + btnProcessRefund= v.findViewById(R.id.btnProcessRefund); + btnBack = v.findViewById(R.id.btnRefundBack); + tvSaleInfo = v.findViewById(R.id.tvSaleInfo); + tvRefundTotal = v.findViewById(R.id.tvRefundTotal); + llOriginalItems = v.findViewById(R.id.llOriginalItems); + llRefundItems = v.findViewById(R.id.llRefundItems); + cardOriginalItems = v.findViewById(R.id.cardOriginalItems); + cardRefundItems = v.findViewById(R.id.cardRefundItems); + cardPayment = v.findViewById(R.id.cardPayment); + spinnerPayment = v.findViewById(R.id.spinnerRefundPayment); + } + + private void setupSpinner() { + spinnerPayment.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, PAYMENT_METHODS)); + } + + private void loadAllSales() { + RetrofitClient.getSaleApi(requireContext()).getAllSales(0, 1000) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + allSales = r.body().getContent(); + // Auto-load if saleId was pre-filled + Bundle args = getArguments(); + if (args != null && args.containsKey("saleId")) { + loadSale(); + } + } + } + public void onFailure(Call> c, Throwable t) { + Log.e("Refund", "Failed to load sales: " + t.getMessage()); + } + }); + } + + private void loadSale() { + String idStr = etSaleId.getText().toString().trim(); + if (idStr.isEmpty()) { + Toast.makeText(getContext(), "Enter a Sale ID", Toast.LENGTH_SHORT).show(); + return; + } + + long saleId; + try { saleId = Long.parseLong(idStr); } + catch (Exception e) { + Toast.makeText(getContext(), "Invalid Sale ID", Toast.LENGTH_SHORT).show(); + return; + } + + // Find sale in loaded list + SaleDTO found = null; + for (SaleDTO s : allSales) { + if (s.getSaleId() != null && s.getSaleId() == saleId) { + found = s; break; + } + } + + if (found == null) { + Toast.makeText(getContext(), "Sale #" + saleId + " not found", Toast.LENGTH_SHORT).show(); + return; + } + + if (Boolean.TRUE.equals(found.getIsRefund())) { + Toast.makeText(getContext(), "Select an original sale, not a refund record", + Toast.LENGTH_LONG).show(); + return; + } + + currentSale = found; + + // Show sale info + tvSaleInfo.setVisibility(View.VISIBLE); + tvSaleInfo.setText("Sale #" + currentSale.getSaleId() + + " | " + (currentSale.getSaleDate() != null + ? currentSale.getSaleDate().substring(0, 10) : "") + + " | Employee: " + (currentSale.getEmployeeName() != null + ? currentSale.getEmployeeName() : "") + + " | Total: $" + currentSale.getTotalAmount() + + " | Payment: " + currentSale.getPaymentMethod()); + + // Pre-select payment method + if (currentSale.getPaymentMethod() != null) { + for (int i = 0; i < PAYMENT_METHODS.length; i++) { + if (PAYMENT_METHODS[i].equalsIgnoreCase(currentSale.getPaymentMethod())) { + spinnerPayment.setSelection(i); break; + } + } + } + + // Build refundable items accounting for previous refunds + buildRefundableItems(); + + if (availableItems.isEmpty()) { + Toast.makeText(getContext(), + "This sale has no remaining refundable items", Toast.LENGTH_LONG).show(); + return; + } + + // Reset refund cart + refundCart.clear(); + + // Show cards + cardOriginalItems.setVisibility(View.VISIBLE); + cardRefundItems.setVisibility(View.VISIBLE); + cardPayment.setVisibility(View.VISIBLE); + btnProcessRefund.setVisibility(View.VISIBLE); + + renderOriginalItems(); + renderRefundCart(); + updateRefundTotal(); + } + + private void buildRefundableItems() { + availableItems.clear(); + if (currentSale.getItems() == null) return; + + // Find all previous refunds for this sale + Map alreadyRefunded = new HashMap<>(); + for (SaleDTO s : allSales) { + if (Boolean.TRUE.equals(s.getIsRefund()) + && currentSale.getSaleId().equals(s.getOriginalSaleId()) + && s.getItems() != null) { + for (SaleDTO.SaleItemDTO item : s.getItems()) { + if (item.getProdId() != null && item.getQuantity() != null) { + alreadyRefunded.merge(item.getProdId(), + Math.abs(item.getQuantity()), Integer::sum); + } + } + } + } + + // Build available items + for (SaleDTO.SaleItemDTO item : currentSale.getItems()) { + if (item.getProdId() == null || item.getQuantity() == null) continue; + int refunded = alreadyRefunded.getOrDefault(item.getProdId(), 0); + int remaining = item.getQuantity() - refunded; + if (remaining > 0) { + availableItems.add(new RefundItem( + item.getProdId(), + item.getProductName() != null ? item.getProductName() : "Unknown", + remaining, + item.getUnitPrice() + )); + } + } + } + + private void renderOriginalItems() { + llOriginalItems.removeAllViews(); + + // Header + addTableHeader(llOriginalItems); + + for (RefundItem item : availableItems) { + // Calculate pending in cart + int pendingQty = 0; + for (RefundItem r : refundCart) { + if (r.prodId == item.prodId) { pendingQty = r.quantity; break; } + } + int displayQty = item.quantity - pendingQty; + if (displayQty <= 0) continue; + + LinearLayout row = buildItemRow( + item.productName, + displayQty, + item.unitPrice, + true, // show add button + () -> showQuantityDialog(item) + ); + llOriginalItems.addView(row); + } + } + + private void renderRefundCart() { + llRefundItems.removeAllViews(); + + if (refundCart.isEmpty()) { + TextView empty = new TextView(getContext()); + empty.setText("No items added to refund yet"); + empty.setTextColor(0xFF888780); + empty.setTextSize(13f); + llRefundItems.addView(empty); + return; + } + + addTableHeader(llRefundItems); + + for (RefundItem item : refundCart) { + LinearLayout row = buildItemRow( + item.productName, + item.quantity, + item.unitPrice, + false, // show remove button + () -> { + refundCart.remove(item); + renderOriginalItems(); + renderRefundCart(); + updateRefundTotal(); + } + ); + llRefundItems.addView(row); + } + } + + private void addTableHeader(LinearLayout parent) { + LinearLayout header = new LinearLayout(getContext()); + header.setOrientation(LinearLayout.HORIZONTAL); + header.setPadding(0, 0, 0, 8); + + String[] cols = {"Product", "Qty", "Unit Price", ""}; + float[] weights = {2f, 1f, 1f, 0.8f}; + for (int i = 0; i < cols.length; i++) { + TextView tv = new TextView(getContext()); + tv.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, weights[i])); + tv.setText(cols[i]); + tv.setTextColor(0xFF888780); + tv.setTextSize(11f); + header.addView(tv); + } + parent.addView(header); + } + + private LinearLayout buildItemRow(String name, int qty, BigDecimal unitPrice, + boolean isAdd, Runnable action) { + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setGravity(android.view.Gravity.CENTER_VERTICAL); + row.setPadding(0, 8, 0, 8); + + TextView tvName = new TextView(getContext()); + tvName.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); + tvName.setText(name); + tvName.setTextSize(13f); + tvName.setTextColor(0xFF3d3d3a); + + TextView tvQty = new TextView(getContext()); + tvQty.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvQty.setText(String.valueOf(qty)); + tvQty.setTextSize(13f); + tvQty.setTextColor(0xFF3d3d3a); + + TextView tvPrice = new TextView(getContext()); + tvPrice.setLayoutParams(new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvPrice.setText(unitPrice != null ? "$" + unitPrice : ""); + tvPrice.setTextSize(13f); + tvPrice.setTextColor(0xFF3d3d3a); + + Button btn = new Button(getContext()); + LinearLayout.LayoutParams btnParams = new LinearLayout.LayoutParams(0, + LinearLayout.LayoutParams.WRAP_CONTENT, 0.8f); + btn.setLayoutParams(btnParams); + btn.setText(isAdd ? "Add" : "Remove"); + btn.setTextSize(11f); + btn.setBackgroundColor(isAdd ? 0xFF1a759f : 0xFFE24B4A); + btn.setTextColor(0xFFFFFFFF); + btn.setPadding(4, 4, 4, 4); + btn.setOnClickListener(v -> action.run()); + + row.addView(tvName); + row.addView(tvQty); + row.addView(tvPrice); + row.addView(btn); + return row; + } + + private void showQuantityDialog(RefundItem item) { + // Calculate how many are already in cart + int inCart = 0; + for (RefundItem r : refundCart) { + if (r.prodId == item.prodId) { inCart = r.quantity; break; } + } + int available = item.quantity - inCart; + if (available <= 0) { + Toast.makeText(getContext(), "All units already added to refund", + Toast.LENGTH_SHORT).show(); + return; + } + + // Build dialog + AlertDialog.Builder builder = new AlertDialog.Builder(requireContext()); + builder.setTitle("Refund Quantity"); + builder.setMessage("Product: " + item.productName + + "\nAvailable: " + available); + + EditText input = new EditText(getContext()); + input.setInputType(android.text.InputType.TYPE_CLASS_NUMBER); + input.setText(String.valueOf(available)); + input.setSelectAllOnFocus(true); + builder.setView(input); + + builder.setPositiveButton("Add to Refund", (d, w) -> { + String val = input.getText().toString().trim(); + if (val.isEmpty()) return; + int qty; + try { qty = Integer.parseInt(val); } + catch (Exception e) { + Toast.makeText(getContext(), "Invalid quantity", Toast.LENGTH_SHORT).show(); + return; + } + if (qty <= 0) { + Toast.makeText(getContext(), "Quantity must be at least 1", + Toast.LENGTH_SHORT).show(); + return; + } + if (qty > available) { + Toast.makeText(getContext(), "Cannot exceed " + available, + Toast.LENGTH_SHORT).show(); + return; + } + + // Add or merge into cart + boolean merged = false; + for (int i = 0; i < refundCart.size(); i++) { + if (refundCart.get(i).prodId == item.prodId) { + RefundItem existing = refundCart.get(i); + refundCart.set(i, new RefundItem(existing.prodId, + existing.productName, + existing.quantity + qty, + existing.unitPrice)); + merged = true; break; + } + } + if (!merged) { + refundCart.add(new RefundItem(item.prodId, item.productName, + qty, item.unitPrice)); + } + + renderOriginalItems(); + renderRefundCart(); + updateRefundTotal(); + }); + + builder.setNegativeButton("Cancel", null); + builder.show(); + } + + private void updateRefundTotal() { + BigDecimal total = BigDecimal.ZERO; + for (RefundItem item : refundCart) total = total.add(item.getTotal()); + tvRefundTotal.setText("Refund Total: $" + total.setScale(2, RoundingMode.HALF_UP)); + } + + private void processRefund() { + if (currentSale == null) { + Toast.makeText(getContext(), "Load a sale first", Toast.LENGTH_SHORT).show(); + return; + } + if (refundCart.isEmpty()) { + Toast.makeText(getContext(), "Add at least one item to refund", + Toast.LENGTH_SHORT).show(); + return; + } + + String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; + + // Confirm dialog + BigDecimal total = BigDecimal.ZERO; + for (RefundItem item : refundCart) total = total.add(item.getTotal()); + final BigDecimal finalTotal = total; + + new AlertDialog.Builder(requireContext()) + .setTitle("Confirm Refund") + .setMessage("Process refund for Sale #" + currentSale.getSaleId() + + "?\nRefund amount: $" + finalTotal.setScale(2, RoundingMode.HALF_UP)) + .setPositiveButton("Yes", (d, w) -> submitRefund(payment)) + .setNegativeButton("No", null) + .show(); + } + + private void submitRefund(String payment) { + // Build sale items list + List items = new ArrayList<>(); + for (RefundItem item : refundCart) { + // Backend expects negative quantity for refunds + items.add(new SaleDTO.SaleItemDTO(item.prodId, -item.quantity)); + } + + SaleDTO dto = new SaleDTO( + currentSale.getStoreId(), + payment, + items, + true, // isRefund = true + currentSale.getSaleId(), // originalSaleId + null // no customer needed + ); + + Log.d("REFUND", "Submitting refund for saleId=" + currentSale.getSaleId() + + " items=" + items.size()); + + RetrofitClient.getSaleApi(requireContext()).createSale(dto) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful() && r.body() != null) { + Toast.makeText(getContext(), + "Refund #" + r.body().getSaleId() + " processed successfully!", + Toast.LENGTH_LONG).show(); + navigateBack(); + } else { + try { + String err = r.errorBody().string(); + Log.e("REFUND", "Error: " + err); + Toast.makeText(getContext(), "Error: " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("REFUND", "Failed to read error"); + } + } + } + public void onFailure(Call c, Throwable t) { + Log.e("REFUND", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void navigateBack() { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.getChildFragmentManager().popBackStack(); + } +} diff --git a/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java new file mode 100644 index 00000000..9116aa20 --- /dev/null +++ b/android/app/src/main/java/com/example/petstoremobile/fragments/listfragments/detailfragments/SaleDetailFragment.java @@ -0,0 +1,373 @@ +package com.example.petstoremobile.fragments.listfragments.detailfragments; + +import android.os.Bundle; +import android.util.Log; +import android.view.*; +import android.widget.*; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.Fragment; +import com.example.petstoremobile.R; +import com.example.petstoremobile.api.*; +import com.example.petstoremobile.dtos.*; +import com.example.petstoremobile.fragments.ListFragment; +import java.math.BigDecimal; +import java.util.*; +import retrofit2.*; + +public class SaleDetailFragment extends Fragment { + + private TextView tvMode, tvSaleDetailId, tvTotal; + private Spinner spinnerStore, spinnerCustomer, spinnerPayment, spinnerProduct; + private EditText etQuantity; + private Button btnAddItem, btnSave, btnBack, btnRefund; + private LinearLayout llItems; + + private boolean viewOnly = false; + private long saleId = -1; + + private List storeList = new ArrayList<>(); + private List customerList = new ArrayList<>(); + private List productList = new ArrayList<>(); + private List cartItems = new ArrayList<>(); + + private final String[] PAYMENT_METHODS = { "Cash", "Credit Card", "Debit Card", "Online" }; + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + View view = inflater.inflate(R.layout.fragment_sale_detail, container, false); + initViews(view); + handleArguments(); + + if (!viewOnly) { + loadData(); + setupAddItem(); + } + + btnBack.setOnClickListener(v -> navigateBack()); + btnSave.setOnClickListener(v -> saveSale()); + btnRefund.setOnClickListener(v -> showRefundDialog()); + + return view; + } + + private void initViews(View v) { + tvMode = v.findViewById(R.id.tvSaleMode); + tvSaleDetailId = v.findViewById(R.id.tvSaleDetailId); + tvTotal = v.findViewById(R.id.tvSaleDetailTotal); + spinnerStore = v.findViewById(R.id.spinnerSaleStore); + spinnerCustomer = v.findViewById(R.id.spinnerSaleCustomer); + spinnerPayment = v.findViewById(R.id.spinnerPaymentMethod); + spinnerProduct = v.findViewById(R.id.spinnerSaleProduct); + etQuantity = v.findViewById(R.id.etSaleQuantity); + btnAddItem = v.findViewById(R.id.btnAddItem); + btnSave = v.findViewById(R.id.btnSaveSale); + btnBack = v.findViewById(R.id.btnSaleBack); + btnRefund = v.findViewById(R.id.btnRefundSale); + llItems = v.findViewById(R.id.llSaleItems); + + spinnerPayment.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, PAYMENT_METHODS)); + } + + private void handleArguments() { + Bundle a = getArguments(); + if (a != null && a.containsKey("saleId")) { + saleId = a.getLong("saleId"); + viewOnly = a.getBoolean("viewOnly", false); + tvMode.setText("Sale #" + saleId); + tvSaleDetailId.setText("ID: " + saleId); + + // Show refund button for existing non-refund sales + if (!a.getBoolean("isRefund", false)) { + btnRefund.setVisibility(View.VISIBLE); + } + + // Hide save and input controls for view only + if (viewOnly) { + btnSave.setVisibility(View.GONE); + spinnerStore.setEnabled(false); + spinnerCustomer.setEnabled(false); + spinnerPayment.setEnabled(false); + spinnerProduct.setEnabled(false); + etQuantity.setEnabled(false); + btnAddItem.setEnabled(false); + } + + // Load sale details + loadSaleDetails(); + } else { + tvMode.setText("New Sale"); + tvSaleDetailId.setVisibility(View.GONE); + btnRefund.setVisibility(View.GONE); + } + } + + private void loadData() { + loadStores(); + loadCustomers(); + loadProducts(); + } + + private void loadStores() { + // Hardcoded since store endpoint is admin only + storeList = new ArrayList<>(); + storeList.add(new StoreDTO(1L, "Downtown Branch")); + List names = new ArrayList<>(); + names.add("-- Select Store --"); + names.add("Downtown Branch"); + spinnerStore.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + } + + private void loadCustomers() { + RetrofitClient.getCustomerApi(requireContext()).getAllCustomers(0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + customerList = r.body().getContent(); + List names = new ArrayList<>(); + names.add("-- No Customer --"); + for (CustomerDTO cu : customerList) + names.add(cu.getFirstName() + " " + cu.getLastName()); + spinnerCustomer.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + } + } + + public void onFailure(Call> c, Throwable t) { + Log.e("SaleDetail", "Customer load failed: " + t.getMessage()); + } + }); + } + + private void loadProducts() { + RetrofitClient.getProductApi(requireContext()).getAllProducts(null, 0, 200) + .enqueue(new Callback>() { + public void onResponse(Call> c, + Response> r) { + if (r.isSuccessful() && r.body() != null) { + productList = r.body().getContent(); + List names = new ArrayList<>(); + names.add("-- Select Product --"); + for (ProductDTO p : productList) + names.add(p.getProdName()); + spinnerProduct.setAdapter(new ArrayAdapter<>(requireContext(), + android.R.layout.simple_spinner_item, names)); + } + } + + public void onFailure(Call> c, Throwable t) { + Log.e("SaleDetail", "Product load failed: " + t.getMessage()); + } + }); + } + + private void loadSaleDetails() { + RetrofitClient.getSaleApi(requireContext()).getSaleById(saleId) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful() && r.body() != null) { + SaleDTO sale = r.body(); + tvTotal.setText("Total: $" + sale.getTotalAmount()); + // Display items + if (sale.getItems() != null) { + llItems.removeAllViews(); + for (SaleDTO.SaleItemDTO item : sale.getItems()) { + addItemRow(item.getProductName(), + Math.abs(item.getQuantity()), + item.getUnitPrice()); + } + } + } + } + + public void onFailure(Call c, Throwable t) { + Log.e("SaleDetail", "Load failed: " + t.getMessage()); + } + }); + } + + private void setupAddItem() { + btnAddItem.setOnClickListener(v -> { + if (spinnerProduct.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a product", Toast.LENGTH_SHORT).show(); + return; + } + String qtyStr = etQuantity.getText().toString().trim(); + if (qtyStr.isEmpty()) { + etQuantity.setError("Enter quantity"); + return; + } + int qty; + try { + qty = Integer.parseInt(qtyStr); + } catch (Exception e) { + etQuantity.setError("Invalid quantity"); + return; + } + + ProductDTO product = productList.get(spinnerProduct.getSelectedItemPosition() - 1); + + // Check if product already in cart + for (SaleDTO.SaleItemDTO existing : cartItems) { + if (existing.getProdId().equals(product.getProdId())) { + Toast.makeText(getContext(), "Product already added", Toast.LENGTH_SHORT).show(); + return; + } + } + + SaleDTO.SaleItemDTO item = new SaleDTO.SaleItemDTO(product.getProdId(), qty); + cartItems.add(item); + addItemRow(product.getProdName(), qty, product.getProdPrice()); + updateTotal(); + etQuantity.setText(""); + }); + } + + private void addItemRow(String name, int qty, BigDecimal price) { + LinearLayout row = new LinearLayout(getContext()); + row.setOrientation(LinearLayout.HORIZONTAL); + row.setPadding(0, 8, 0, 8); + + TextView tvName = new TextView(getContext()); + tvName.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)); + tvName.setText(name); + + TextView tvQty = new TextView(getContext()); + tvQty.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvQty.setText("x" + qty); + + TextView tvPrice = new TextView(getContext()); + tvPrice.setLayoutParams(new LinearLayout.LayoutParams( + 0, LinearLayout.LayoutParams.WRAP_CONTENT, 1f)); + tvPrice.setText(price != null ? "$" + price : ""); + + row.addView(tvName); + row.addView(tvQty); + row.addView(tvPrice); + llItems.addView(row); + } + + private void updateTotal() { + BigDecimal total = BigDecimal.ZERO; + int productIdx = 0; + for (SaleDTO.SaleItemDTO item : cartItems) { + if (productIdx < productList.size()) { + for (ProductDTO p : productList) { + if (p.getProdId().equals(item.getProdId()) && p.getProdPrice() != null) { + total = total.add(p.getProdPrice() + .multiply(BigDecimal.valueOf(item.getQuantity()))); + break; + } + } + } + } + tvTotal.setText("Total: $" + total); + } + + private void saveSale() { + if (spinnerStore.getSelectedItemPosition() == 0) { + Toast.makeText(getContext(), "Select a store", Toast.LENGTH_SHORT).show(); + return; + } + if (cartItems.isEmpty()) { + Toast.makeText(getContext(), "Add at least one item", Toast.LENGTH_SHORT).show(); + return; + } + + StoreDTO store = storeList.get(0); // only one store + String payment = PAYMENT_METHODS[spinnerPayment.getSelectedItemPosition()]; + + // Optional customer + Long customerId = null; + if (spinnerCustomer.getSelectedItemPosition() > 0) { + customerId = customerList.get(spinnerCustomer.getSelectedItemPosition() - 1) + .getCustomerId(); + } + + SaleDTO dto = new SaleDTO( + store.getStoreId(), + payment, + cartItems, + false, + null, + customerId); + + Log.d("SALE_SAVE", "storeId=" + store.getStoreId() + + " payment=" + payment + + " items=" + cartItems.size() + + " customerId=" + customerId); + + RetrofitClient.getSaleApi(requireContext()).createSale(dto) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful()) { + Toast.makeText(getContext(), "Sale saved!", Toast.LENGTH_SHORT).show(); + navigateBack(); + } else { + try { + String err = r.errorBody().string(); + Log.e("SALE_SAVE", "Error: " + err); + Toast.makeText(getContext(), "Error " + r.code() + ": " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("SALE_SAVE", "Failed to read error"); + } + } + } + + public void onFailure(Call c, Throwable t) { + Log.e("SALE_SAVE", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void showRefundDialog() { + RefundFragment refundFragment = new RefundFragment(); + Bundle args = new Bundle(); + args.putLong("saleId", saleId); + refundFragment.setArguments(args); + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) lf.loadFragment(refundFragment); + } + + private void submitRefund() { + RefundDTO refundDTO = new RefundDTO(saleId, "Refund requested from mobile app"); + RetrofitClient.getRefundApi(requireContext()).createRefund(refundDTO) + .enqueue(new Callback() { + public void onResponse(Call c, Response r) { + if (r.isSuccessful()) { + Toast.makeText(getContext(), "Refund request submitted!", + Toast.LENGTH_SHORT).show(); + btnRefund.setVisibility(View.GONE); + } else { + try { + String err = r.errorBody().string(); + Log.e("REFUND", "Error: " + err); + Toast.makeText(getContext(), "Error: " + err, + Toast.LENGTH_LONG).show(); + } catch (Exception e) { + Log.e("REFUND", "Failed to read error"); + } + } + } + + public void onFailure(Call c, Throwable t) { + Log.e("REFUND", "Failure: " + t.getMessage()); + Toast.makeText(getContext(), "Network error", Toast.LENGTH_SHORT).show(); + } + }); + } + + private void navigateBack() { + ListFragment lf = (ListFragment) getParentFragment(); + if (lf != null) + lf.getChildFragmentManager().popBackStack(); + } +} diff --git a/android/app/src/main/res/layout/fragment_analytics.xml b/android/app/src/main/res/layout/fragment_analytics.xml new file mode 100644 index 00000000..15ea7bd8 --- /dev/null +++ b/android/app/src/main/res/layout/fragment_analytics.xml @@ -0,0 +1,318 @@ + + + + + + + + + +