Skip to main content

Transaction History

Riwayat transaksi lengkap dengan search dan filter

๐ŸŽฏ Overview

Transaction History adalah salah satu fitur utama dalam MStore Mobile yang menyediakan fungsionalitas untuk riwayat transaksi lengkap dengan search dan filter.

๐Ÿ“‹ Features

  • โœ… Complete transaction records
  • โœ… Search and filter
  • โœ… Transaction details
  • โœ… Receipt reprint
  • โœ… Export to PDF/Excel
  • โœ… Date range filter
  • โœ… Payment method filter

๐Ÿ—๏ธ Architecture

BLoC Implementation

BLoC: TransactionHistoryBloc
// Events
abstract class TransactionHistoryEvent extends Equatable {}

class LoadTransactionHistory extends TransactionHistoryEvent {}
class CreateTransactionHistory extends TransactionHistoryEvent {}
class UpdateTransactionHistory extends TransactionHistoryEvent {}
class DeleteTransactionHistory extends TransactionHistoryEvent {}

// States
abstract class TransactionHistoryState extends Equatable {}

class TransactionHistoryInitial extends TransactionHistoryState {}
class TransactionHistoryLoading extends TransactionHistoryState {}
class TransactionHistoryLoaded extends TransactionHistoryState {}
class TransactionHistoryError extends TransactionHistoryState {}

// BLoC
class TransactionHistoryBloc extends Bloc<TransactionHistoryEvent, TransactionHistoryState> {
  final Transaction HistoryRepository _repository;
  
  TransactionHistoryBloc({required Transaction HistoryRepository repository})
      : _repository = repository,
        super(TransactionHistoryInitial()) {
    on<LoadTransactionHistory>(_onLoad);
    on<CreateTransactionHistory>(_onCreate);
    on<UpdateTransactionHistory>(_onUpdate);
    on<DeleteTransactionHistory>(_onDelete);
  }
  
  Future<void> _onLoad(
    LoadTransactionHistory event,
    Emitter<TransactionHistoryState> emit,
  ) async {
    emit(TransactionHistoryLoading());
    
    final result = await _repository.getTransaction Historys();
    
    result.fold(
      (failure) => emit(TransactionHistoryError(failure.message)),
      (data) => emit(TransactionHistoryLoaded(data)),
    );
  }
}

Repository Pattern

abstract class Transaction HistoryRepository {
  Future<Either<Failure, List<Transaction History>>> getTransaction Historys();
  Future<Either<Failure, Transaction History>> getTransaction HistoryById(String id);
  Future<Either<Failure, Transaction History>> createTransaction History(Transaction History data);
  Future<Either<Failure, Transaction History>> updateTransaction History(String id, Transaction History data);
  Future<Either<Failure, void>> deleteTransaction History(String id);
}

class Transaction HistoryRepositoryImpl implements Transaction HistoryRepository {
  final Transaction HistoryApi _api;
  final Transaction HistoryLocalRepository _localRepo;
  
  @override
  Future<Either<Failure, List<Transaction History>>> getTransaction Historys() async {
    try {
      // Try local first (offline-first)
      final local = await _localRepo.getTransaction Historys();
      
      // Sync with API in background
      final result = await _api.getTransaction Historys();
      result.fold(
        (failure) => null,
        (data) => _localRepo.saveTransaction Historys(data),
      );
      
      return Right(local.isNotEmpty ? local : result.getOrElse(() => []));
    } catch (e) {
      return Left(UnexpectedFailure(e.toString()));
    }
  }
}

๐Ÿ“ก API Integration

Endpoints

  • /api/v1/transactions/*

Request/Response Examples

Get List

GET /api/v1/transactions/*
Authorization: Bearer {access_token}
Response:
{
  "success": true,
  "data": [
    {
      "id": "123",
      "name": "Example",
      "created_at": "2024-10-14T10:00:00Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 100
  }
}

๐Ÿ’พ Local Database (Isar)

Schema

  • TransactionLocal
@collection
class Transaction HistoryLocal {
  Id id = Isar.autoIncrement;
  
  @Index()
  String? transaction_historyId;
  
  String? name;
  DateTime? createdAt;
  DateTime? updatedAt;
  DateTime? syncedAt;
  
  bool? isSynced;
  bool? isDeleted;
}

Queries

// Get all
final items = await isar.transaction_historyLocals.where().findAll();

// Get by ID
final item = await isar.transaction_historyLocals
    .filter()
    .transaction_historyIdEqualTo(id)
    .findFirst();

// Search
final results = await isar.transaction_historyLocals
    .filter()
    .nameContains(query, caseSensitive: false)
    .findAll();

// Get unsynced
final unsynced = await isar.transaction_historyLocals
    .filter()
    .isSyncedEqualTo(false)
    .findAll();

๐Ÿ”„ Offline-First Strategy

Write Operations

  1. Save to local Isar immediately
  2. Show success to user
  3. Add to sync queue
  4. Background sync when online
  5. Update with server response

Read Operations

  1. Read from local Isar (fast)
  2. Show to user immediately
  3. Background fetch from API
  4. Update local cache if changed
  5. Notify UI if data updated

Conflict Resolution

  • Strategy: Last-write-wins
  • Timestamp: Server timestamp as source of truth
  • Logging: All conflicts logged for audit

๐ŸŽจ UI Components

Main Screen

class Transaction HistoryPage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      create: (context) => getIt<TransactionHistoryBloc>()..add(LoadTransactionHistory()),
      child: Scaffold(
        appBar: AppBar(title: Text('Transaction History')),
        body: BlocBuilder<TransactionHistoryBloc, TransactionHistoryState>(
          builder: (context, state) {
            if (state is TransactionHistoryLoading) {
              return Center(child: CircularProgressIndicator());
            }
            
            if (state is TransactionHistoryError) {
              return ErrorWidget(message: state.message);
            }
            
            if (state is TransactionHistoryLoaded) {
              return Transaction HistoryListView(items: state.items);
            }
            
            return SizedBox.shrink();
          },
        ),
        floatingActionButton: FloatingActionButton(
          onPressed: () => _navigateToCreate(context),
          child: Icon(Icons.add),
        ),
      ),
    );
  }
}

๐Ÿงช Testing

Unit Tests

void main() {
  group('TransactionHistoryBloc', () {
    late TransactionHistoryBloc bloc;
    late MockTransaction HistoryRepository mockRepository;

    setUp(() {
      mockRepository = MockTransaction HistoryRepository();
      bloc = TransactionHistoryBloc(repository: mockRepository);
    });

    tearDown(() {
      bloc.close();
    });

    test('initial state is TransactionHistoryInitial', () {
      expect(bloc.state, equals(TransactionHistoryInitial()));
    });

    blocTest<TransactionHistoryBloc, TransactionHistoryState>(
      'emits [Loading, Loaded] when Load succeeds',
      build: () {
        when(() => mockRepository.getTransaction Historys()).thenAnswer(
          (_) async => Right([Transaction History(id: '1', name: 'Test')]),
        );
        return bloc;
      },
      act: (bloc) => bloc.add(LoadTransactionHistory()),
      expect: () => [
        TransactionHistoryLoading(),
        isA<TransactionHistoryLoaded>(),
      ],
    );
  });
}

๐Ÿ“Š Performance Considerations

  • Lazy Loading: Load data on demand
  • Pagination: Implement pagination for large datasets
  • Caching: Cache frequently accessed data
  • Indexing: Use Isar indexes for fast queries
  • Background Sync: Sync in background to avoid blocking UI

๐Ÿ” Security

  • Authorization: Check user permissions before operations
  • Data Encryption: Sensitive data encrypted in Isar
  • Input Validation: Validate all user inputs
  • Audit Trail: Log all operations for audit

๐Ÿ“ฑ Platform-Specific

iOS

  • Use Cupertino widgets
  • Follow iOS HIG
  • Handle safe area insets

Android

  • Use Material widgets
  • Follow Material Design
  • Handle back button

Last Updated: October 14, 2024
Status: โœ… Production Ready