From b0ee2e8ee6464f00360adf2e69f72948bac580bf Mon Sep 17 00:00:00 2001 From: riz081 Date: Tue, 10 Jun 2025 12:10:35 +0700 Subject: [PATCH] memperbaiki list cetak ulang --- .../java/com/example/bdkipoc/StyleHelper.java | 106 ++++ .../example/bdkipoc/TransactionActivity.java | 462 ++++++++++++++---- .../example/bdkipoc/TransactionAdapter.java | 173 +++++-- .../main/res/layout/activity_transaction.xml | 192 +++++++- app/src/main/res/layout/item_transaction.xml | 75 ++- 5 files changed, 814 insertions(+), 194 deletions(-) create mode 100644 app/src/main/java/com/example/bdkipoc/StyleHelper.java diff --git a/app/src/main/java/com/example/bdkipoc/StyleHelper.java b/app/src/main/java/com/example/bdkipoc/StyleHelper.java new file mode 100644 index 0000000..83b51f7 --- /dev/null +++ b/app/src/main/java/com/example/bdkipoc/StyleHelper.java @@ -0,0 +1,106 @@ +package com.example.bdkipoc; + +import android.content.Context; +import android.graphics.drawable.GradientDrawable; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import androidx.core.content.ContextCompat; + +public class StyleHelper { + + /** + * Create rounded rectangle drawable programmatically + */ + public static GradientDrawable createRoundedDrawable(int color, int strokeColor, int strokeWidth, int radius) { + GradientDrawable drawable = new GradientDrawable(); + drawable.setShape(GradientDrawable.RECTANGLE); + drawable.setColor(color); + drawable.setStroke(strokeWidth, strokeColor); + drawable.setCornerRadius(radius); + return drawable; + } + + /** + * Apply search input styling + */ + public static void applySearchInputStyle(View view, Context context) { + int white = ContextCompat.getColor(context, android.R.color.white); + int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray); + // ✅ IMPROVED: Larger corner radius and lighter border like in the image + GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border + view.setBackground(drawable); + } + + /** + * Apply filter button styling + */ + public static void applyFilterButtonStyle(View view, Context context) { + int white = ContextCompat.getColor(context, android.R.color.white); + int lightGrey = ContextCompat.getColor(context, android.R.color.darker_gray); + // ✅ IMPROVED: Larger corner radius like in the image + GradientDrawable drawable = createRoundedDrawable(white, lightGrey, 1, 75); // 25dp radius, thinner border + view.setBackground(drawable); + } + + /** + * Apply pagination button styling (simple version) + */ + public static void applyPaginationButtonStyle(View view, Context context, boolean isActive) { + int backgroundColor, strokeColor; + + if (isActive) { + backgroundColor = ContextCompat.getColor(context, android.R.color.holo_red_dark); + strokeColor = ContextCompat.getColor(context, android.R.color.holo_red_dark); + } else { + backgroundColor = ContextCompat.getColor(context, android.R.color.white); + strokeColor = ContextCompat.getColor(context, android.R.color.transparent); + } + + // ✅ IMPROVED: Larger corner radius for modern look (like in the image) + GradientDrawable drawable = createRoundedDrawable(backgroundColor, strokeColor, 0, 48); // 16dp radius + view.setBackground(drawable); + + // Set text color if it's a TextView + if (view instanceof TextView) { + int textColor = isActive ? + ContextCompat.getColor(context, android.R.color.white) : + ContextCompat.getColor(context, android.R.color.black); + ((TextView) view).setTextColor(textColor); + } + } + + /** + * Apply status text color only (no background badge) + */ + public static void applyStatusTextColor(TextView textView, Context context, String status) { + String statusLower = status != null ? status.toLowerCase() : ""; + int textColor; + + if (statusLower.equals("failed") || statusLower.equals("failure") || + statusLower.equals("error") || statusLower.equals("declined") || + statusLower.equals("expire") || statusLower.equals("cancel")) { + // Red text for failed/error statuses + textColor = ContextCompat.getColor(context, android.R.color.holo_red_dark); + } else if (statusLower.equals("success") || statusLower.equals("paid") || + statusLower.equals("settlement") || statusLower.equals("completed") || + statusLower.equals("capture")) { + // Green text for successful statuses + textColor = ContextCompat.getColor(context, android.R.color.holo_green_dark); + } else if (statusLower.equals("pending") || statusLower.equals("processing") || + statusLower.equals("waiting") || statusLower.equals("checking...") || + statusLower.equals("checking")) { + // Orange text for pending/processing statuses + textColor = ContextCompat.getColor(context, android.R.color.holo_orange_dark); + } else if (statusLower.equals("init")) { + // Blue text for init status + textColor = ContextCompat.getColor(context, android.R.color.holo_blue_dark); + } else { + // Default gray text for unknown statuses + textColor = ContextCompat.getColor(context, android.R.color.darker_gray); + } + + textView.setTextColor(textColor); + textView.setBackground(null); // Remove any background + } +} \ No newline at end of file diff --git a/app/src/main/java/com/example/bdkipoc/TransactionActivity.java b/app/src/main/java/com/example/bdkipoc/TransactionActivity.java index a5bc7c8..33e1f37 100644 --- a/app/src/main/java/com/example/bdkipoc/TransactionActivity.java +++ b/app/src/main/java/com/example/bdkipoc/TransactionActivity.java @@ -11,6 +11,8 @@ import android.widget.EditText; import android.widget.ImageButton; import android.widget.ProgressBar; import android.widget.Toast; +import android.widget.TextView; +import android.widget.LinearLayout; import android.content.Intent; import android.util.Log; @@ -50,16 +52,25 @@ public class TransactionActivity extends AppCompatActivity implements Transactio private EditText searchEditText; private ImageButton searchButton; + // ✅ PAGINATION UI ELEMENTS + private LinearLayout infoBar; + private TextView textTotalRecords; + private TextView textPageInfo; + private LinearLayout paginationControls; + private LinearLayout pageNumbersContainer; + private ImageButton btnFirstPage, btnPrevPage, btnNextPage, btnLastPage; + // ✅ FRONTEND DEDUPLICATION: Local caching and tracking private Map transactionCache = new HashMap<>(); private Set processedReferences = new HashSet<>(); private SharedPreferences prefs; - // Pagination variables - private int page = 0; - private final int limit = 50; // ✅ INCREASED: Fetch more data for better deduplication + // ✅ UPDATED PAGINATION VARIABLES + private int currentPage = 1; // Start from page 1 instead of 0 + private final int itemsPerPage = 15; // ✅ 15 items per page as requested + private int totalRecords = 0; + private int totalPages = 0; private boolean isLoading = false; - private boolean isLastPage = false; private String currentSearchQuery = ""; private boolean isRefreshing = false; @@ -83,15 +94,38 @@ public class TransactionActivity extends AppCompatActivity implements Transactio // Setup search functionality setupSearch(); + // ✅ Setup pagination controls + setupPaginationControls(); + // Load initial data - loadTransactions(0); + loadTransactions(1); // Start from page 1 } private void initViews() { recyclerView = findViewById(R.id.recyclerView); progressBar = findViewById(R.id.progressBar); searchEditText = findViewById(R.id.searchEditText); - searchButton = findViewById(R.id.searchButton); + + // ✅ APPLY PROGRAMMATIC STYLING + LinearLayout searchContainer = findViewById(R.id.searchContainer); + LinearLayout filterButton = findViewById(R.id.filterButton); + + StyleHelper.applySearchInputStyle(searchContainer, this); + StyleHelper.applyFilterButtonStyle(filterButton, this); + + // ✅ PAGINATION UI ELEMENTS + paginationControls = findViewById(R.id.paginationControls); + pageNumbersContainer = findViewById(R.id.pageNumbersContainer); + btnFirstPage = findViewById(R.id.btnFirstPage); + btnPrevPage = findViewById(R.id.btnPrevPage); + btnNextPage = findViewById(R.id.btnNextPage); + btnLastPage = findViewById(R.id.btnLastPage); + + // Apply pagination button styling (updated sizes) + StyleHelper.applyPaginationButtonStyle(btnFirstPage, this, false); + StyleHelper.applyPaginationButtonStyle(btnPrevPage, this, false); + StyleHelper.applyPaginationButtonStyle(btnNextPage, this, false); + StyleHelper.applyPaginationButtonStyle(btnLastPage, this, false); } private void setupToolbar() { @@ -114,32 +148,12 @@ public class TransactionActivity extends AppCompatActivity implements Transactio recyclerView.setLayoutManager(layoutManager); recyclerView.setAdapter(adapter); - // Add scroll listener for pagination - recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { - @Override - public void onScrolled(RecyclerView recyclerView, int dx, int dy) { - super.onScrolled(recyclerView, dx, dy); - - // Pagination: Load more when reaching the bottom - if (!recyclerView.canScrollVertically(1) && !isLoading && !isLastPage && currentSearchQuery.isEmpty()) { - loadTransactions(page + 1); - } - } - }); + // ✅ REMOVED: Auto-pagination scroll listener since we use manual pagination now + // Manual pagination is better for user control and performance } private void setupSearch() { - // Search button click listener - searchButton.setOnClickListener(v -> performSearch()); - - // Search button long press for refresh - searchButton.setOnLongClickListener(v -> { - refreshTransactions(); - Toast.makeText(this, "Refreshing data...", Toast.LENGTH_SHORT).show(); - return true; - }); - - // Search EditText listener + // Search on text change searchEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) {} @@ -147,12 +161,89 @@ public class TransactionActivity extends AppCompatActivity implements Transactio @Override public void onTextChanged(CharSequence s, int start, int before, int count) { currentSearchQuery = s.toString().trim(); + // ✅ RESET TO PAGE 1 when searching + currentPage = 1; filterTransactions(currentSearchQuery); } @Override public void afterTextChanged(Editable s) {} }); + + // Filter button click + findViewById(R.id.filterButton).setOnClickListener(v -> { + Toast.makeText(this, "Filter clicked", Toast.LENGTH_SHORT).show(); + }); + } + + // ✅ NEW METHOD: Setup pagination controls + private void setupPaginationControls() { + btnFirstPage.setOnClickListener(v -> { + if (currentPage > 1) { + goToPage(1); + } + }); + + btnPrevPage.setOnClickListener(v -> { + if (currentPage > 1) { + goToPage(currentPage - 1); + } + }); + + btnNextPage.setOnClickListener(v -> { + if (currentPage < totalPages) { + goToPage(currentPage + 1); + } + }); + + btnLastPage.setOnClickListener(v -> { + if (currentPage < totalPages) { + goToPage(totalPages); + } + }); + } + + // ✅ NEW METHOD: Navigate to specific page + private void goToPage(int page) { + if (page < 1 || page > totalPages || page == currentPage || isLoading) { + return; + } + + Log.d("TransactionActivity", "🔄 Navigating to page " + page); + + if (currentSearchQuery.isEmpty()) { + // Load from API + loadTransactions(page); + } else { + // Search mode - just update current page and refresh display + currentPage = page; + updatePaginationDisplay(); + displayCurrentPageData(); + } + } + + // ✅ NEW METHOD: Display current page data for search results + private void displayCurrentPageData() { + if (currentSearchQuery.isEmpty()) { + return; // This method is only for search results + } + + int startIndex = (currentPage - 1) * itemsPerPage; + int endIndex = Math.min(startIndex + itemsPerPage, filteredList.size()); + + List pageData = new ArrayList<>(); + for (int i = startIndex; i < endIndex; i++) { + pageData.add(filteredList.get(i)); + } + + // Update adapter with current page data + adapter.updateData(pageData, startIndex); // Pass startIndex for numbering + + // Scroll to top + recyclerView.scrollToPosition(0); + + Log.d("TransactionActivity", "📄 Displaying search results page " + currentPage + + " (items " + (startIndex + 1) + "-" + endIndex + " of " + filteredList.size() + ")"); } private void refreshTransactions() { @@ -166,17 +257,17 @@ public class TransactionActivity extends AppCompatActivity implements Transactio // Clear search when refreshing searchEditText.setText(""); currentSearchQuery = ""; - page = 0; - isLastPage = false; + currentPage = 1; // ✅ Reset to page 1 transactionList.clear(); filteredList.clear(); adapter.notifyDataSetChanged(); - loadTransactions(0); + loadTransactions(1); } private void performSearch() { String query = searchEditText.getText().toString().trim(); currentSearchQuery = query; + currentPage = 1; // ✅ Reset to page 1 when searching filterTransactions(query); // Hide keyboard @@ -187,17 +278,53 @@ public class TransactionActivity extends AppCompatActivity implements Transactio filteredList.clear(); if (query.isEmpty()) { + // ✅ NO SEARCH: Show current API page data (already sorted) filteredList.addAll(transactionList); + totalRecords = totalRecords; // Use API total + totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage); + + // ✅ VERIFY FILTERED LIST ORDER + Log.d("TransactionActivity", "📋 FILTERED LIST ORDER (no search):"); + for (int i = 0; i < Math.min(5, filteredList.size()); i++) { + Transaction tx = filteredList.get(i); + Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId); + } } else { + // ✅ SEARCH MODE: Filter all available data for (Transaction transaction : transactionList) { if (transaction.referenceId.toLowerCase().contains(query.toLowerCase()) || transaction.amount.contains(query)) { filteredList.add(transaction); } } + + // ✅ SORT SEARCH RESULTS by date + filteredList.sort((t1, t2) -> { + try { + Date date1 = parseCreatedAtDate(t1.createdAt); + Date date2 = parseCreatedAtDate(t2.createdAt); + if (date1 != null && date2 != null) { + return date2.compareTo(date1); // Newest first + } + } catch (Exception e) { + // Fallback + } + return Integer.compare(t2.id, t1.id); + }); + + // ✅ SEARCH PAGINATION: Calculate pages for filtered results + totalRecords = filteredList.size(); + totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage); + + // ✅ Display current page of search results + displayCurrentPageData(); + updatePaginationDisplay(); + return; // Early return for search } - adapter.notifyDataSetChanged(); + // ✅ For non-search, just update adapter normally + adapter.updateData(filteredList, (currentPage - 1) * itemsPerPage); + updatePaginationDisplay(); // Scroll to top after filtering if (!filteredList.isEmpty()) { @@ -205,18 +332,104 @@ public class TransactionActivity extends AppCompatActivity implements Transactio } } + // ✅ NEW METHOD: Update pagination display + private void updatePaginationDisplay() { + // Update page info - remove total records display for cleaner look + String pageText = "Halaman " + currentPage + " dari " + Math.max(1, totalPages); + + // Show/hide pagination controls based on data availability + if (totalRecords > 0) { + if (totalPages > 1) { + paginationControls.setVisibility(View.VISIBLE); + updatePageButtons(); + createPageNumbers(); + } else { + paginationControls.setVisibility(View.GONE); + } + } else { + paginationControls.setVisibility(View.GONE); + } + + Log.d("TransactionActivity", "📊 Pagination updated: " + + "Page " + currentPage + "/" + totalPages + ", Total: " + totalRecords); + } + + // ✅ NEW METHOD: Update pagination button states + private void updatePageButtons() { + // Enable/disable buttons based on current page + btnFirstPage.setEnabled(currentPage > 1); + btnPrevPage.setEnabled(currentPage > 1); + btnNextPage.setEnabled(currentPage < totalPages); + btnLastPage.setEnabled(currentPage < totalPages); + + // Update button opacity for visual feedback + float enabledAlpha = 1.0f; + float disabledAlpha = 0.3f; + + btnFirstPage.setAlpha(btnFirstPage.isEnabled() ? enabledAlpha : disabledAlpha); + btnPrevPage.setAlpha(btnPrevPage.isEnabled() ? enabledAlpha : disabledAlpha); + btnNextPage.setAlpha(btnNextPage.isEnabled() ? enabledAlpha : disabledAlpha); + btnLastPage.setAlpha(btnLastPage.isEnabled() ? enabledAlpha : disabledAlpha); + } + + // ✅ NEW METHOD: Create page number buttons + private void createPageNumbers() { + pageNumbersContainer.removeAllViews(); + + // Calculate which page numbers to show (max 5 numbers) + int maxPageButtons = 5; + int startPage = Math.max(1, currentPage - 2); + int endPage = Math.min(totalPages, startPage + maxPageButtons - 1); + + // Adjust start if we're near the end + if (endPage - startPage < maxPageButtons - 1) { + startPage = Math.max(1, endPage - maxPageButtons + 1); + } + + // ✅ CONSISTENT BUTTON SIZE for all devices (more modern size) + int buttonSize = (int) (44 * getResources().getDisplayMetrics().density); // 44dp (iOS standard) + + for (int i = startPage; i <= endPage; i++) { + final int pageNumber = i; + + TextView pageButton = new TextView(this); + pageButton.setText(String.valueOf(pageNumber)); + pageButton.setTextSize(16); // Slightly larger text + pageButton.setClickable(true); + pageButton.setFocusable(true); + pageButton.setGravity(android.view.Gravity.CENTER); + + // ✅ USE STYLE HELPER + boolean isActive = (pageNumber == currentPage); + StyleHelper.applyPaginationButtonStyle(pageButton, this, isActive); + + pageButton.setOnClickListener(v -> goToPage(pageNumber)); + + // ✅ IMPROVED: Better spacing like in the image + LinearLayout.LayoutParams params = new LinearLayout.LayoutParams( + buttonSize, // Consistent width + buttonSize // Consistent height + ); + params.setMargins(6, 0, 6, 0); // 6dp margin for better spacing + pageButton.setLayoutParams(params); + + pageNumbersContainer.addView(pageButton); + } + + Log.d("TransactionActivity", "🔢 Page buttons created: " + startPage + " to " + endPage + + " with size: " + buttonSize + "px"); + } + private void loadTransactions(int pageToLoad) { isLoading = true; - if (pageToLoad == 0) { - progressBar.setVisibility(View.VISIBLE); - } + progressBar.setVisibility(View.VISIBLE); new FetchTransactionsTask(pageToLoad).execute(); } private class FetchTransactionsTask extends AsyncTask> { private int pageToLoad; private boolean error = false; - private int total = 0; + private int apiTotal = 0; FetchTransactionsTask(int page) { this.pageToLoad = page; @@ -226,13 +439,14 @@ public class TransactionActivity extends AppCompatActivity implements Transactio protected List doInBackground(Void... voids) { List result = new ArrayList<>(); try { - // ✅ FETCH MORE DATA: Increased limit for better deduplication - int fetchLimit = limit * 3; // Get more records to handle all duplicates + // ✅ PAGINATION API CALL: Use page-based API call + int apiPage = pageToLoad - 1; // API uses 0-based indexing - String urlString = "https://be-edc.msvc.app/transactions?page=" + pageToLoad + - "&limit=" + fetchLimit + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=created_at"; + String urlString = "https://be-edc.msvc.app/transactions?page=" + apiPage + + "&limit=" + itemsPerPage + "&sortOrder=DESC&from_date=&to_date=&location_id=0&merchant_id=0&tid=73001500&mid=71000026521&sortColumn=created_at"; - Log.d("TransactionActivity", "🔍 Fetching transactions page " + pageToLoad + " with limit " + fetchLimit); + Log.d("TransactionActivity", "🔍 Fetching transactions page " + pageToLoad + + " (API page " + apiPage + ") with limit " + itemsPerPage + " - SORT: DESC by created_at"); URI uri = new URI(urlString); URL url = uri.toURL(); @@ -256,10 +470,11 @@ public class TransactionActivity extends AppCompatActivity implements Transactio JSONObject jsonObject = new JSONObject(response.toString()); JSONObject results = jsonObject.getJSONObject("results"); - total = results.getInt("total"); + apiTotal = results.getInt("total"); JSONArray data = results.getJSONArray("data"); - Log.d("TransactionActivity", "📊 Raw API response: " + data.length() + " records"); + Log.d("TransactionActivity", "📊 API response: " + data.length() + + " records, total: " + apiTotal); // ✅ STEP 1: Parse all transactions from API List rawTransactions = new ArrayList<>(); @@ -304,64 +519,109 @@ public class TransactionActivity extends AppCompatActivity implements Transactio if (error) { Toast.makeText(TransactionActivity.this, "Failed to fetch transactions", Toast.LENGTH_SHORT).show(); + updatePaginationDisplay(); // Show current state even on error return; } - if (pageToLoad == 0) { - transactionList.clear(); - transactionCache.clear(); // Clear cache on refresh - } + // ✅ UPDATE PAGINATION DATA + currentPage = pageToLoad; + totalRecords = apiTotal; + totalPages = (int) Math.ceil((double) totalRecords / itemsPerPage); - // ✅ SMART MERGE: Only add truly new transactions - int addedCount = 0; - for (Transaction newTx : transactions) { - String refId = newTx.referenceId; - - // Check if we already have a better version of this transaction - Transaction cachedTx = transactionCache.get(refId); - if (cachedTx == null || isBetterTransaction(newTx, cachedTx)) { - // Update cache with better transaction - transactionCache.put(refId, newTx); + // ✅ UPDATE TRANSACTION LIST + transactionList.clear(); + transactionList.addAll(transactions); + + // ✅ CRITICAL: FORCE SORT AGAIN after adding to main list + transactionList.sort((t1, t2) -> { + try { + Date date1 = parseCreatedAtDate(t1.createdAt); + Date date2 = parseCreatedAtDate(t2.createdAt); - // Update or add to main list - boolean updated = false; - for (int i = 0; i < transactionList.size(); i++) { - if (transactionList.get(i).referenceId.equals(refId)) { - transactionList.set(i, newTx); - updated = true; - break; - } - } - - if (!updated) { - transactionList.add(newTx); - addedCount++; + if (date1 != null && date2 != null) { + int comparison = date2.compareTo(date1); // Newest first + Log.d("TransactionActivity", "🔄 Final sort: " + t1.createdAt + " vs " + t2.createdAt + " = " + comparison); + return comparison; } + } catch (Exception e) { + Log.w("TransactionActivity", "Date comparison error: " + e.getMessage()); } - } + return Integer.compare(t2.id, t1.id); // Fallback by ID + }); - Log.d("TransactionActivity", "📋 Added " + addedCount + " new unique transactions. Total: " + transactionList.size()); + Log.d("TransactionActivity", "📋 Page " + currentPage + " loaded and sorted: " + + transactions.size() + " transactions. Total: " + totalRecords + "/" + totalPages + " pages"); + + // ✅ LOG FINAL ORDER VERIFICATION + Log.d("TransactionActivity", "📋 FINAL DISPLAY ORDER:"); + for (int i = 0; i < Math.min(10, transactionList.size()); i++) { + Transaction tx = transactionList.get(i); + Log.d("TransactionActivity", " " + (i+1) + ". " + tx.createdAt + " - " + tx.referenceId); + } // Update filtered list based on current search filterTransactions(currentSearchQuery); - page = pageToLoad; - if (transactions.size() < limit) { // No more pages if returned less than requested - isLastPage = true; - } - - // Scroll to top if it's a refresh - if (pageToLoad == 0 && !filteredList.isEmpty()) { + // Scroll to top + if (!filteredList.isEmpty()) { recyclerView.scrollToPosition(0); } } } + /** + * ✅ ENHANCED DATE PARSING: Handle multiple date formats from API + */ + private Date parseCreatedAtDate(String rawDate) { + if (rawDate == null || rawDate.isEmpty()) { + return null; + } + + // List of possible date formats from API + String[] possibleFormats = { + "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", // ISO format with milliseconds + "yyyy-MM-dd'T'HH:mm:ss'Z'", // ISO format without milliseconds + "yyyy-MM-dd HH:mm:ss.SSS", // Standard format with milliseconds + "yyyy-MM-dd HH:mm:ss", // Standard format + "yyyy-MM-dd'T'HH:mm:ss.SSSSSS'Z'" // ISO format with microseconds + }; + + for (String format : possibleFormats) { + try { + SimpleDateFormat sdf = new SimpleDateFormat(format, Locale.getDefault()); + return sdf.parse(rawDate); + } catch (Exception e) { + // Continue to next format + } + } + + // Manual parsing fallback for complex formats + try { + String cleanedDate = rawDate.replace("T", " ").replace("Z", ""); + + // Remove microseconds/milliseconds if present + if (cleanedDate.contains(".")) { + cleanedDate = cleanedDate.substring(0, cleanedDate.indexOf(".")); + } + + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + return sdf.parse(cleanedDate); + } catch (Exception e) { + Log.w("TransactionActivity", "❌ Could not parse date: " + rawDate); + return null; + } + } + /** * ✅ ADVANCED DEDUPLICATION: Enhanced algorithm with multiple strategies */ private List applyAdvancedDeduplication(List rawTransactions) { Log.d("TransactionActivity", "🧠 Starting advanced deduplication..."); + Log.d("TransactionActivity", "📥 Input transactions order (first 5):"); + for (int i = 0; i < Math.min(5, rawTransactions.size()); i++) { + Transaction tx = rawTransactions.get(i); + Log.d("TransactionActivity", " " + (i+1) + ". ID:" + tx.id + " Date:" + tx.createdAt + " Ref:" + tx.referenceId); + } // Strategy 1: Group by reference_id Map> groupedByRef = new HashMap<>(); @@ -384,13 +644,26 @@ public class TransactionActivity extends AppCompatActivity implements Transactio deduplicatedList.add(group.get(0)); Log.d("TransactionActivity", "✅ Unique transaction: " + referenceId); } else { - // Multiple transactions with same reference_id + // Multiple transactions with same reference_id - sort group by date first + group.sort((t1, t2) -> { + try { + Date date1 = parseCreatedAtDate(t1.createdAt); + Date date2 = parseCreatedAtDate(t2.createdAt); + if (date1 != null && date2 != null) { + return date2.compareTo(date1); // Newest first in group + } + } catch (Exception e) { + // Fallback to ID + } + return Integer.compare(t2.id, t1.id); + }); + Transaction bestTransaction = selectBestTransactionAdvanced(group, referenceId); deduplicatedList.add(bestTransaction); duplicatesRemoved += (group.size() - 1); Log.d("TransactionActivity", "🔄 Deduplicated " + group.size() + " → 1 for ref: " + referenceId + - " (kept ID: " + bestTransaction.id + ", status: " + bestTransaction.status + ")"); + " (kept ID: " + bestTransaction.id + ", status: " + bestTransaction.status + ", date: " + bestTransaction.createdAt + ")"); } } @@ -410,10 +683,12 @@ public class TransactionActivity extends AppCompatActivity implements Transactio Transaction bestTransaction = duplicates.get(0); int bestPriority = getStatusPriority(bestTransaction.status); + Date bestDate = parseCreatedAtDate(bestTransaction.createdAt); // Detailed analysis of each candidate for (Transaction tx : duplicates) { int currentPriority = getStatusPriority(tx.status); + Date currentDate = parseCreatedAtDate(tx.createdAt); Log.d("TransactionActivity", " 📊 Candidate: ID=" + tx.id + ", Status=" + tx.status + " (priority=" + currentPriority + ")" + @@ -429,20 +704,24 @@ public class TransactionActivity extends AppCompatActivity implements Transactio } // Rule 2: Same priority, choose newer timestamp else if (currentPriority == bestPriority) { - if (isNewerTransaction(tx, bestTransaction)) { + if (currentDate != null && bestDate != null && currentDate.after(bestDate)) { shouldSelect = true; reason = "newer timestamp"; } // Rule 3: Same priority and time, choose higher ID - else if (tx.createdAt.equals(bestTransaction.createdAt) && tx.id > bestTransaction.id) { - shouldSelect = true; - reason = "higher ID"; + else if ((currentDate == null && bestDate == null) || + (currentDate != null && bestDate != null && currentDate.equals(bestDate))) { + if (tx.id > bestTransaction.id) { + shouldSelect = true; + reason = "higher ID"; + } } } if (shouldSelect) { bestTransaction = tx; bestPriority = currentPriority; + bestDate = currentDate; Log.d("TransactionActivity", " ⭐ NEW BEST selected: " + reason); } } @@ -454,19 +733,18 @@ public class TransactionActivity extends AppCompatActivity implements Transactio } /** - * ✅ TIMESTAMP COMPARISON: Smart date comparison + * ✅ TIMESTAMP COMPARISON: Smart date comparison using enhanced parsing */ private boolean isNewerTransaction(Transaction tx1, Transaction tx2) { try { - SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); - Date date1 = sdf.parse(tx1.createdAt); - Date date2 = sdf.parse(tx2.createdAt); + Date date1 = parseCreatedAtDate(tx1.createdAt); + Date date2 = parseCreatedAtDate(tx2.createdAt); if (date1 != null && date2 != null) { return date1.after(date2); } } catch (Exception e) { - Log.w("TransactionActivity", "Date parsing error, falling back to ID comparison"); + Log.w("TransactionActivity", "Date comparison error, falling back to ID comparison"); } // Fallback: higher ID usually means newer @@ -474,7 +752,7 @@ public class TransactionActivity extends AppCompatActivity implements Transactio } /** - * ✅ COMPARISON HELPER: Check if one transaction is better than another + * ✅ COMPARISON HELPER: Check if one transaction is better than another using enhanced parsing */ private boolean isBetterTransaction(Transaction newTx, Transaction existingTx) { int newPriority = getStatusPriority(newTx.status); diff --git a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java b/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java index 0b13e85..9089b78 100644 --- a/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java +++ b/app/src/main/java/com/example/bdkipoc/TransactionAdapter.java @@ -11,18 +11,16 @@ import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; import androidx.core.content.ContextCompat; -// ✅ TAMBAHKAN MISSING IMPORTS INI: import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.HttpURLConnection; import java.net.URL; -import java.net.URLEncoder; import java.util.List; -import java.text.NumberFormat; import java.util.Locale; +import java.text.SimpleDateFormat; +import java.util.Date; -// ✅ TAMBAHKAN JSON IMPORTS: import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -43,6 +41,16 @@ public class TransactionAdapter extends RecyclerView.Adapter newData, int startIndex) { + this.transactionList = newData; + notifyDataSetChanged(); + + Log.d("TransactionAdapter", "📋 Data updated: " + newData.size() + " items"); + } + @NonNull @Override public TransactionViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { @@ -54,6 +62,16 @@ public class TransactionAdapter extends RecyclerView.Adapter " + formattedDate); + // Set click listeners holder.itemView.setOnClickListener(v -> { if (printClickListener != null) { @@ -188,35 +211,6 @@ public class TransactionAdapter extends RecyclerView.Adapter { try { @@ -344,7 +338,7 @@ public class TransactionAdapter extends RecyclerView.Adapter { statusTextView.setText(finalStatus); - setStatusColor(statusTextView, finalStatus); + StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), finalStatus); Log.d("TransactionAdapter", "🎨 UI UPDATED:"); Log.d("TransactionAdapter", " Reference: " + referenceId); @@ -356,7 +350,7 @@ public class TransactionAdapter extends RecyclerView.Adapter { statusTextView.setText("ERROR"); - setStatusColor(statusTextView, "ERROR"); + StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "ERROR"); }); } @@ -364,12 +358,108 @@ public class TransactionAdapter extends RecyclerView.Adapter { statusTextView.setText("INIT"); - setStatusColor(statusTextView, "INIT"); + StyleHelper.applyStatusTextColor(statusTextView, statusTextView.getContext(), "INIT"); }); } }).start(); } + /** + * Format created_at date to readable format + */ + private String formatCreatedAtDate(String rawDate) { + if (rawDate == null || rawDate.isEmpty()) { + return "N/A"; + } + + Log.d("TransactionAdapter", "📅 Input date: '" + rawDate + "'"); + + try { + // Handle different possible input formats from API + SimpleDateFormat inputFormat; + String cleanedDate = rawDate; + + if (rawDate.contains("T")) { + // ISO format: "2025-06-10T04:31:19.565Z" + cleanedDate = rawDate.replace("T", " ").replace("Z", ""); + // Remove microseconds if present + if (cleanedDate.contains(".")) { + cleanedDate = cleanedDate.substring(0, cleanedDate.indexOf(".")); + } + inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + } else if (rawDate.length() > 19 && rawDate.contains(".")) { + // Format with microseconds: "2025-06-10 04:31:19.565" + cleanedDate = rawDate.substring(0, 19); // Cut off microseconds + inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + } else { + // Standard format: "2025-06-10 04:31:19" + inputFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()); + } + + Log.d("TransactionAdapter", "📅 Cleaned date: '" + cleanedDate + "'"); + + // Output format: d/M/yyyy H:mm:ss + SimpleDateFormat outputFormat = new SimpleDateFormat("d/M/yyyy H:mm:ss", Locale.getDefault()); + + Date date = inputFormat.parse(cleanedDate); + if (date != null) { + String formatted = outputFormat.format(date); + Log.d("TransactionAdapter", "📅 Date formatted: " + rawDate + " -> " + formatted); + return formatted; + } + } catch (Exception e) { + Log.e("TransactionAdapter", "❌ Date formatting error for: " + rawDate, e); + } + + // Fallback: Manual parsing + try { + // Handle format like "2025-06-10T04:31:19.565Z" manually + String workingDate = rawDate.replace("T", " ").replace("Z", ""); + + // Remove microseconds if present + if (workingDate.contains(".")) { + workingDate = workingDate.substring(0, workingDate.indexOf(".")); + } + + Log.d("TransactionAdapter", "📅 Manual parsing attempt: '" + workingDate + "'"); + + // Split into date and time parts + String[] parts = workingDate.split(" "); + if (parts.length >= 2) { + String datePart = parts[0]; // "2025-06-10" + String timePart = parts[1]; // "04:31:19" + + String[] dateComponents = datePart.split("-"); + if (dateComponents.length == 3) { + String year = dateComponents[0]; + String month = dateComponents[1]; + String day = dateComponents[2]; + + // Remove leading zeros and format as d/M/yyyy H:mm:ss + int dayInt = Integer.parseInt(day); + int monthInt = Integer.parseInt(month); + + // Parse time to remove leading zeros from hour + String[] timeComponents = timePart.split(":"); + if (timeComponents.length >= 3) { + int hour = Integer.parseInt(timeComponents[0]); + String minute = timeComponents[1]; + String second = timeComponents[2]; + + String result = dayInt + "/" + monthInt + "/" + year + " " + hour + ":" + minute + ":" + second; + Log.d("TransactionAdapter", "📅 Manual format result: " + result); + return result; + } + } + } + } catch (Exception e) { + Log.w("TransactionAdapter", "❌ Manual date formatting failed: " + e.getMessage()); + } + + Log.w("TransactionAdapter", "📅 Using fallback - returning original date: " + rawDate); + return rawDate; + } + private String getPaymentMethodName(String channelCode, String channelCategory) { // Convert channel code to readable payment method name if (channelCode == null) return "Unknown"; @@ -408,7 +498,7 @@ public class TransactionAdapter extends RecyclerView.Adapter - + - + + android:orientation="horizontal" + android:gravity="center_vertical" + android:paddingLeft="20dp" + android:paddingRight="20dp" + android:layout_marginEnd="16dp"> - + + + + + + + + + + + + + + + + + + + + + + + + + @@ -70,8 +151,73 @@ + android:layout_height="0dp" + android:layout_weight="1" + android:background="#ffffff" /> + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/item_transaction.xml b/app/src/main/res/layout/item_transaction.xml index 8eeaf70..f14d6cd 100644 --- a/app/src/main/res/layout/item_transaction.xml +++ b/app/src/main/res/layout/item_transaction.xml @@ -1,23 +1,23 @@ + android:id="@+id/itemContainer"> + android:gravity="center_vertical" + android:minHeight="64dp"> - + @@ -28,16 +28,17 @@ android:text="ref-eowu3pin" android:textColor="#333333" android:textSize="16sp" - android:textStyle="bold" /> + android:textStyle="bold" + android:layout_marginBottom="4dp" /> - + + - - + android:textColor="@android:color/white" + android:paddingLeft="8dp" + android:paddingRight="8dp" + android:paddingTop="2dp" + android:paddingBottom="2dp" /> + + android:textColor="#666666" + android:layout_marginStart="12dp" /> + + + - + + android:textStyle="bold" + android:gravity="center" /> - + - - + android:textSize="12sp" + android:drawableLeft="@android:drawable/ic_menu_edit" + android:drawablePadding="4dp" + android:gravity="center_vertical" /> - - - \ No newline at end of file