# Cart Quantity Update Debounce Implementation

## Overview
Implemented a 3-second debounce for cart quantity updates to prevent excessive API calls. UI updates happen instantly, but API sync is delayed until the user stops changing quantities.

## Problem Solved
**Before**: Every increment/decrement button press triggered an immediate API call
- Multiple rapid clicks = multiple API calls
- Poor performance and UX
- Unnecessary server load
- Potential rate limiting issues

**After**: UI updates instantly, API syncs after 3 seconds of inactivity
- User can rapidly change quantities
- Only one API call after user stops
- Smooth, responsive UI
- Reduced server load

## Implementation Details

### 1. Debounce Timer in Cart Provider
**File**: `lib/features/cart/presentation/providers/cart_provider.dart`

```dart
@Riverpod(keepAlive: true)
class Cart extends _$Cart {
  /// Debounce timer for quantity updates (3 seconds)
  Timer? _debounceTimer;

  /// Map to track pending quantity updates (productId -> quantity)
  final Map<String, double> _pendingQuantityUpdates = {};

  @override
  CartState build() {
    // Cancel debounce timer when provider is disposed
    ref.onDispose(() {
      _debounceTimer?.cancel();
    });

    return CartState.initial().copyWith(
      memberTier: 'Diamond',
      memberDiscountPercent: 15.0,
    );
  }
}
```

### 2. Local Update Method (Instant UI Update)

```dart
/// Update item quantity immediately (local only, no API call)
///
/// Used for instant UI updates. Actual API sync happens after debounce.
void updateQuantityLocal(String productId, double newQuantity) {
  if (newQuantity <= 0) {
    removeFromCart(productId);
    return;
  }

  final currentState = state;
  final itemIndex = currentState.items.indexWhere(
    (item) => item.product.productId == productId,
  );

  if (itemIndex == -1) return;
  final item = currentState.items[itemIndex];

  // Update local state immediately (instant UI update)
  final converted = _calculateConversion(
    newQuantity,
    item.product.conversionOfSm,
  );

  final updatedItems = List<CartItemData>.from(currentState.items);
  updatedItems[itemIndex] = item.copyWith(
    quantity: newQuantity,
    quantityConverted: converted.convertedQuantity,
    boxes: converted.boxes,
  );

  final newState = currentState.copyWith(items: updatedItems);
  state = _recalculateTotal(newState);

  // Track pending update for API sync
  _pendingQuantityUpdates[productId] = newQuantity;

  // Schedule debounced API sync
  _scheduleDebouncedSync();
}
```

### 3. Debounce Scheduling

```dart
/// Schedule debounced sync to API (3 seconds after last change)
void _scheduleDebouncedSync() {
  // Cancel existing timer (restarts the 3s countdown)
  _debounceTimer?.cancel();

  // Start new timer (3 seconds debounce)
  _debounceTimer = Timer(const Duration(seconds: 3), () {
    _syncPendingQuantityUpdates();
  });
}
```

### 4. Background API Sync

```dart
/// Sync all pending quantity updates to API
Future<void> _syncPendingQuantityUpdates() async {
  if (_pendingQuantityUpdates.isEmpty) return;

  final repository = await ref.read(cartRepositoryProvider.future);
  final currentState = state;

  // Create a copy of pending updates
  final updates = Map<String, double>.from(_pendingQuantityUpdates);
  _pendingQuantityUpdates.clear();

  // Sync each update to API (background, no loading state)
  for (final entry in updates.entries) {
    final productId = entry.key;
    final quantity = entry.value;

    final item = currentState.items.firstWhere(
      (item) => item.product.productId == productId,
      orElse: () => throw Exception('Item not found'),
    );

    try {
      await repository.updateQuantity(
        itemId: item.product.erpnextItemCode ?? productId,
        quantity: quantity,
        price: item.product.basePrice,
      );
    } catch (e) {
      // Silent fail - keep local state, user can retry later
      print('[Cart] Failed to sync quantity for $productId: $e');
    }
  }
}
```

### 5. Updated Increment/Decrement Methods

```dart
/// Increment quantity (with debounce)
///
/// Updates UI immediately, syncs to API after 3s of no changes.
void incrementQuantity(String productId) {
  final currentState = state;
  final item = currentState.items.firstWhere(
    (item) => item.product.productId == productId,
  );
  updateQuantityLocal(productId, item.quantity + 1);
}

/// Decrement quantity (minimum 1, with debounce)
///
/// Updates UI immediately, syncs to API after 3s of no changes.
void decrementQuantity(String productId) {
  final currentState = state;
  final item = currentState.items.firstWhere(
    (item) => item.product.productId == productId,
  );
  // Keep minimum quantity at 1, don't go to 0
  if (item.quantity > 1) {
    updateQuantityLocal(productId, item.quantity - 1);
  }
}
```

### 6. Force Sync on Navigation & Checkout

**File**: `lib/features/cart/presentation/pages/cart_page.dart`

#### A. Force Sync on Page Disposal
```dart
class _CartPageState extends ConsumerState<CartPage> {
  @override
  void dispose() {
    // Force sync any pending quantity updates before leaving cart page
    ref.read(cartProvider.notifier).forceSyncPendingUpdates();
    super.dispose();
  }
}
```

#### B. Force Sync on Checkout Button (Skip Debounce) ⚡
```dart
class _CartPageState extends ConsumerState<CartPage> {
  bool _isSyncing = false;

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: hasSelection && !_isSyncing
          ? () async {
              // Set syncing state (show loading)
              setState(() {
                _isSyncing = true;
              });

              // Force sync immediately - NO WAITING for debounce!
              await ref
                  .read(cartProvider.notifier)
                  .forceSyncPendingUpdates();

              // Reset syncing state
              if (mounted) {
                setState(() {
                  _isSyncing = false;
                });

                // Navigate to checkout with synced data
                context.push(RouteNames.checkout);
              }
            }
          : null,
      child: _isSyncing
          ? const CustomLoadingIndicator() // Show loading while syncing
          : Text('Tiến hành đặt hàng'),
    );
  }
}
```

**Provider Method**:
```dart
/// Force sync all pending quantity updates immediately
///
/// Useful when:
/// - User taps checkout button (skip 3s debounce)
/// - User navigates away or closes cart
/// - Need to ensure data is synced before critical operations
Future<void> forceSyncPendingUpdates() async {
  _debounceTimer?.cancel();
  await _syncPendingQuantityUpdates();
}
```

## User Flow

### Scenario 1: Rapid Clicks (Debounced)
```
User clicks +5 times rapidly (within 3 seconds)
  ↓
Each click: UI updates instantly (1→2→3→4→5)
  ↓
Timer restarts on each click
  ↓
User stops clicking
  ↓
3 seconds pass
  ↓
Single API call: updateQuantity(productId, 5)
```

### Scenario 2: Manual Text Input (Immediate)
```
User types "10" in quantity field
  ↓
User presses Enter
  ↓
Immediate API call: updateQuantity(productId, 10)
  ↓
No debounce (direct input needs immediate sync)
```

### Scenario 3: Navigate Away (Force Sync)
```
User clicks + button 3 times
  ↓
UI updates: 1→2→3
  ↓
Timer is running (1 second passed)
  ↓
User navigates back
  ↓
dispose() called
  ↓
forceSyncPendingUpdates() executes
  ↓
Immediate API call: updateQuantity(productId, 3)
```

### Scenario 4: Checkout Button (Force Sync - Skip Debounce) ⚡ NEW
```
User clicks + button 5 times
  ↓
UI updates: 1→2→3→4→5
  ↓
Timer is running (1 second passed, would wait 2 more seconds)
  ↓
User clicks "Tiến hành đặt hàng" (Checkout)
  ↓
Button shows loading spinner
  ↓
forceSyncPendingUpdates() called IMMEDIATELY
  ↓
Debounce timer cancelled
  ↓
API call: updateQuantity(productId, 5) - NO WAITING!
  ↓
Navigate to checkout page with synced data ✅
```

## Benefits

✅ **Instant UI feedback** - No waiting for API responses
✅ **Reduced API calls** - Only 1 call per product after changes stop
✅ **Better UX** - Smooth, responsive interface
✅ **Server-friendly** - Minimizes unnecessary requests
✅ **Offline-ready** - Local state updates work offline
✅ **Force sync on exit** - Ensures changes are saved
✅ **Skip debounce on checkout** - Immediate sync when user clicks checkout ⚡ NEW

## Configuration

### Debounce Duration
Default: **3 seconds** ✅

To change:
```dart
_debounceTimer = Timer(const Duration(seconds: 3), () {
  _syncPendingQuantityUpdates();
});
```

Recommended values:
- **2-3 seconds**: Responsive, good balance (current setting) ✅
- **5 seconds**: More conservative (fewer API calls)
- **1 second**: Very aggressive (more API calls, but faster sync)

## Testing

### Manual Testing
1. **Test rapid clicks**:
   - Open cart
   - Click + button 10 times rapidly
   - Watch console: Should see only 1 API call after 3s

2. **Test text input**:
   - Type quantity directly
   - Press Enter
   - Should see immediate API call

3. **Test navigation sync**:
   - Click + button 3 times
   - Immediately navigate back
   - Should see API call before page closes

4. **Test multiple products**:
   - Change quantity on product A
   - Change quantity on product B
   - Wait 3 seconds
   - Should batch update both products

5. **Test checkout force sync** ⚡ NEW:
   - Click + button 5 times rapidly
   - Immediately click "Tiến hành đặt hàng" (within 3s)
   - Button should show loading spinner
   - API call should happen immediately (skip debounce)
   - Should navigate to checkout with synced data

### Expected Behavior
```
// Rapid increments (debounced)
Click +1 → UI: 2, API: none
Click +1 → UI: 3, API: none
Click +1 → UI: 4, API: none
Wait 3s → UI: 4, API: updateQuantity(4) ✅

// Direct input (immediate)
Type "10" → UI: 10, API: none
Press Enter → UI: 10, API: updateQuantity(10) ✅

// Navigate away (force sync)
Click +1 → UI: 2, API: none
Navigate back → UI: 2, API: updateQuantity(2) ✅

// Checkout button (force sync - skip debounce) ⚡ NEW
Click +5 times → UI: 1→2→3→4→5, API: none
Click checkout (after 1s) → Loading spinner shown
→ API: updateQuantity(5) IMMEDIATELY (skip remaining 2s debounce)
→ Navigate to checkout ✅
```

## Error Handling

### API Sync Failure
- Local state is preserved
- User sees correct quantity in UI
- Error is logged silently
- User can retry by refreshing cart

### Offline Behavior
- All updates work in local state
- API calls fail silently
- TODO: Add to offline queue for retry when online

## Performance Impact

### Before Debounce
- 10 rapid clicks = 10 API calls
- Each call takes ~200-500ms
- Total time: 2-5 seconds of loading
- Poor UX, server strain

### After Debounce
- 10 rapid clicks = 1 API call (after 3s)
- UI updates are instant (<16ms per frame)
- Total time: 3 seconds wait + 1 API call
- Great UX, minimal server load

## Future Enhancements

1. **Batch Updates**: Combine multiple product updates into single API call
2. **Offline Queue**: Persist pending updates to Hive for offline resilience
3. **Visual Indicator**: Show "syncing..." badge when pending updates exist
4. **Configurable Timeout**: Allow users to adjust debounce duration
5. **Smart Sync**: Sync immediately before checkout/payment

## Related Files

- **Cart Provider**: `lib/features/cart/presentation/providers/cart_provider.dart`
- **Cart Page**: `lib/features/cart/presentation/pages/cart_page.dart`
- **Cart Item Widget**: `lib/features/cart/presentation/widgets/cart_item_widget.dart`
- **Cart Repository**: `lib/features/cart/data/repositories/cart_repository_impl.dart`

## Summary

The debounce implementation provides a smooth, responsive cart experience while minimizing server load. Users get instant feedback, and the app intelligently batches API calls. This is a best practice for any real-time data synchronization scenario! 🎉
