terminal and trans records api impl
This commit is contained in:
parent
a6b1993c27
commit
6a04ad02a1
@ -2,6 +2,7 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
|
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
|
||||||
import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart';
|
import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart';
|
||||||
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
import 'package:http/http.dart' as http;
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
class ApiMerchantRepository implements MerchantRepository {
|
class ApiMerchantRepository implements MerchantRepository {
|
||||||
@ -34,12 +35,40 @@ class ApiMerchantRepository implements MerchantRepository {
|
|||||||
throw Exception(_extractErrorMessage(response.body));
|
throw Exception(_extractErrorMessage(response.body));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Terminal>> getTerminalsByMerchantId({
|
||||||
|
required String token,
|
||||||
|
required String merchantId,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse('$baseUrl/receipt/terminal/list/$merchantId');
|
||||||
|
final response = await _client.get(
|
||||||
|
uri,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': apiSecret,
|
||||||
|
'Authorization': 'Bearer ${token.trim()}',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
return _parseTerminals(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(_extractErrorMessage(response.body));
|
||||||
|
}
|
||||||
|
|
||||||
List<Merchant> _parseMerchants(String body) {
|
List<Merchant> _parseMerchants(String body) {
|
||||||
final decoded = _tryDecode(body);
|
final decoded = _tryDecode(body);
|
||||||
final list = _extractList(decoded);
|
final list = _extractList(decoded);
|
||||||
return list.map(_merchantFromItem).whereType<Merchant>().toList();
|
return list.map(_merchantFromItem).whereType<Merchant>().toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<Terminal> _parseTerminals(String body) {
|
||||||
|
final decoded = _tryDecode(body);
|
||||||
|
final list = _extractTerminalList(decoded);
|
||||||
|
return list.map(_terminalFromItem).whereType<Terminal>().toList();
|
||||||
|
}
|
||||||
|
|
||||||
dynamic _tryDecode(String body) {
|
dynamic _tryDecode(String body) {
|
||||||
try {
|
try {
|
||||||
return jsonDecode(body);
|
return jsonDecode(body);
|
||||||
@ -76,6 +105,34 @@ class ApiMerchantRepository implements MerchantRepository {
|
|||||||
return const <dynamic>[];
|
return const <dynamic>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<dynamic> _extractTerminalList(dynamic decoded) {
|
||||||
|
if (decoded is List<dynamic>) {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
final directTerminals = decoded['terminals'];
|
||||||
|
if (directTerminals is List<dynamic>) {
|
||||||
|
return directTerminals;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data =
|
||||||
|
decoded['data'] ??
|
||||||
|
decoded['result'] ??
|
||||||
|
decoded['payload'] ??
|
||||||
|
decoded['terminalData'];
|
||||||
|
if (data is List<dynamic>) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
|
final nestedTerminals = data['terminals'];
|
||||||
|
if (nestedTerminals is List<dynamic>) {
|
||||||
|
return nestedTerminals;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const <dynamic>[];
|
||||||
|
}
|
||||||
|
|
||||||
Merchant? _merchantFromItem(dynamic item) {
|
Merchant? _merchantFromItem(dynamic item) {
|
||||||
if (item is! Map<String, dynamic>) {
|
if (item is! Map<String, dynamic>) {
|
||||||
return null;
|
return null;
|
||||||
@ -98,6 +155,33 @@ class ApiMerchantRepository implements MerchantRepository {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Terminal? _terminalFromItem(dynamic item) {
|
||||||
|
if (item is! Map<String, dynamic>) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return Terminal(
|
||||||
|
id: _pickString(item, const ['id', 'terminalId', 'terminal_id']),
|
||||||
|
serial: _pickString(item, const ['serial']),
|
||||||
|
address: _pickString(item, const ['address']),
|
||||||
|
address2: _pickString(item, const ['address2']),
|
||||||
|
address3: _pickString(item, const ['address3']),
|
||||||
|
name: _pickString(item, const ['name', 'terminalName', 'terminal_name']),
|
||||||
|
appId: _pickString(item, const ['appId', 'app_id']),
|
||||||
|
tid: _pickString(item, const ['tid', 'terminalNo', 'terminal_no']),
|
||||||
|
mids: _pickStringList(item, const ['mid', 'mids']),
|
||||||
|
merchantId: _pickString(item, const ['merchantId', 'merchant_id']),
|
||||||
|
status: _pickString(item, const ['status']),
|
||||||
|
totalTransactions: _pickInt(
|
||||||
|
item,
|
||||||
|
const ['totalTransactions', 'total_transactions'],
|
||||||
|
),
|
||||||
|
totalAmount: _pickNum(item, const ['totalAmount', 'total_amount']),
|
||||||
|
createdAt: _pickString(item, const ['createdAt', 'created_at']),
|
||||||
|
updatedAt: _pickString(item, const ['updatedAt', 'updated_at']),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
String? _pickString(Map<String, dynamic> data, List<String> keys) {
|
String? _pickString(Map<String, dynamic> data, List<String> keys) {
|
||||||
for (final key in keys) {
|
for (final key in keys) {
|
||||||
final value = data[key];
|
final value = data[key];
|
||||||
@ -108,6 +192,58 @@ class ApiMerchantRepository implements MerchantRepository {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<String>? _pickStringList(Map<String, dynamic> data, List<String> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
final value = data[key];
|
||||||
|
if (value is List) {
|
||||||
|
final results = value
|
||||||
|
.where((e) => e != null && e.toString().trim().isNotEmpty)
|
||||||
|
.map((e) => e.toString().trim())
|
||||||
|
.toList();
|
||||||
|
return results.isEmpty ? null : results;
|
||||||
|
}
|
||||||
|
if (value != null && value.toString().trim().isNotEmpty) {
|
||||||
|
return <String>[value.toString().trim()];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
int? _pickInt(Map<String, dynamic> data, List<String> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
final value = data[key];
|
||||||
|
if (value is int) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value is num) {
|
||||||
|
return value.toInt();
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
final parsed = int.tryParse(value.toString().trim());
|
||||||
|
if (parsed != null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
num? _pickNum(Map<String, dynamic> data, List<String> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
final value = data[key];
|
||||||
|
if (value is num) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
final parsed = num.tryParse(value.toString().trim());
|
||||||
|
if (parsed != null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
String _extractErrorMessage(String body) {
|
String _extractErrorMessage(String body) {
|
||||||
final decoded = _tryDecode(body);
|
final decoded = _tryDecode(body);
|
||||||
if (decoded is Map<String, dynamic>) {
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
|||||||
136
lib/data/repositories/api_transaction_repository.dart
Normal file
136
lib/data/repositories/api_transaction_repository.dart
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import 'dart:convert';
|
||||||
|
|
||||||
|
import 'package:e_receipt_mobile/domain/entities/transaction_record.dart';
|
||||||
|
import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart';
|
||||||
|
import 'package:http/http.dart' as http;
|
||||||
|
|
||||||
|
class ApiTransactionRepository implements TransactionRepository {
|
||||||
|
ApiTransactionRepository({
|
||||||
|
required this.baseUrl,
|
||||||
|
required this.apiSecret,
|
||||||
|
required http.Client client,
|
||||||
|
}) : _client = client;
|
||||||
|
|
||||||
|
final String baseUrl;
|
||||||
|
final String apiSecret;
|
||||||
|
final http.Client _client;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<TransactionRecord>> getTransactions({
|
||||||
|
required String token,
|
||||||
|
required String serial,
|
||||||
|
}) async {
|
||||||
|
final uri = Uri.parse(
|
||||||
|
'$baseUrl/receipt/transaction',
|
||||||
|
).replace(queryParameters: {'serial': serial});
|
||||||
|
|
||||||
|
final response = await _client.get(
|
||||||
|
uri,
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'x-api-key': apiSecret,
|
||||||
|
'Authorization': 'Bearer ${token.trim()}',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.statusCode >= 200 && response.statusCode < 300) {
|
||||||
|
return _parseTransactions(response.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
throw Exception(_extractErrorMessage(response.body));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<TransactionRecord> _parseTransactions(String body) {
|
||||||
|
final decoded = _tryDecode(body);
|
||||||
|
final list = _extractTransactionList(decoded);
|
||||||
|
return list
|
||||||
|
.map((item) => _transactionFromItem(item))
|
||||||
|
.whereType<TransactionRecord>()
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
dynamic _tryDecode(String body) {
|
||||||
|
try {
|
||||||
|
return jsonDecode(body);
|
||||||
|
} catch (_) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<dynamic> _extractTransactionList(dynamic decoded) {
|
||||||
|
if (decoded is List<dynamic>) {
|
||||||
|
return decoded;
|
||||||
|
}
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
final direct = decoded['transactions'];
|
||||||
|
if (direct is List<dynamic>) {
|
||||||
|
return direct;
|
||||||
|
}
|
||||||
|
|
||||||
|
final data = decoded['data'] ?? decoded['result'] ?? decoded['payload'];
|
||||||
|
if (data is List<dynamic>) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (data is Map<String, dynamic>) {
|
||||||
|
final nested = data['transactions'];
|
||||||
|
if (nested is List<dynamic>) {
|
||||||
|
return nested;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return const <dynamic>[];
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionRecord? _transactionFromItem(dynamic item) {
|
||||||
|
if (item is! Map<String, dynamic>) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TransactionRecord(
|
||||||
|
id: _pickString(item, const ['id', 'transactionId', 'transaction_id']),
|
||||||
|
rrn: _pickString(item, const ['rrn', 'referenceNo', 'reference_no']),
|
||||||
|
amount: _pickNum(item, const ['amount', 'totalAmount', 'total_amount']),
|
||||||
|
status: _pickString(item, const ['status']),
|
||||||
|
type: _pickString(item, const ['type', 'transactionType']),
|
||||||
|
createdAt: _pickString(item, const ['createdAt', 'created_at']),
|
||||||
|
raw: item,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
String? _pickString(Map<String, dynamic> data, List<String> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
final value = data[key];
|
||||||
|
if (value != null && value.toString().trim().isNotEmpty) {
|
||||||
|
return value.toString().trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
num? _pickNum(Map<String, dynamic> data, List<String> keys) {
|
||||||
|
for (final key in keys) {
|
||||||
|
final value = data[key];
|
||||||
|
if (value is num) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
if (value != null) {
|
||||||
|
final parsed = num.tryParse(value.toString().trim());
|
||||||
|
if (parsed != null) {
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String _extractErrorMessage(String body) {
|
||||||
|
final decoded = _tryDecode(body);
|
||||||
|
if (decoded is Map<String, dynamic>) {
|
||||||
|
final message = decoded['message'] ?? decoded['error'] ?? decoded['detail'];
|
||||||
|
if (message != null) {
|
||||||
|
return message.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 'Failed to load transactions';
|
||||||
|
}
|
||||||
|
}
|
||||||
35
lib/domain/entities/terminal.dart
Normal file
35
lib/domain/entities/terminal.dart
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
class Terminal {
|
||||||
|
const Terminal({
|
||||||
|
this.id,
|
||||||
|
this.serial,
|
||||||
|
this.address,
|
||||||
|
this.address2,
|
||||||
|
this.address3,
|
||||||
|
this.name,
|
||||||
|
this.appId,
|
||||||
|
this.tid,
|
||||||
|
this.mids,
|
||||||
|
this.merchantId,
|
||||||
|
this.status,
|
||||||
|
this.totalTransactions,
|
||||||
|
this.totalAmount,
|
||||||
|
this.createdAt,
|
||||||
|
this.updatedAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? id;
|
||||||
|
final String? serial;
|
||||||
|
final String? address;
|
||||||
|
final String? address2;
|
||||||
|
final String? address3;
|
||||||
|
final String? name;
|
||||||
|
final String? appId;
|
||||||
|
final String? tid;
|
||||||
|
final List<String>? mids;
|
||||||
|
final String? merchantId;
|
||||||
|
final String? status;
|
||||||
|
final int? totalTransactions;
|
||||||
|
final num? totalAmount;
|
||||||
|
final String? createdAt;
|
||||||
|
final String? updatedAt;
|
||||||
|
}
|
||||||
19
lib/domain/entities/transaction_record.dart
Normal file
19
lib/domain/entities/transaction_record.dart
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
class TransactionRecord {
|
||||||
|
const TransactionRecord({
|
||||||
|
this.id,
|
||||||
|
this.rrn,
|
||||||
|
this.amount,
|
||||||
|
this.status,
|
||||||
|
this.type,
|
||||||
|
this.createdAt,
|
||||||
|
this.raw,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String? id;
|
||||||
|
final String? rrn;
|
||||||
|
final num? amount;
|
||||||
|
final String? status;
|
||||||
|
final String? type;
|
||||||
|
final String? createdAt;
|
||||||
|
final Map<String, dynamic>? raw;
|
||||||
|
}
|
||||||
@ -1,5 +1,11 @@
|
|||||||
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
|
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
|
||||||
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
|
|
||||||
abstract class MerchantRepository {
|
abstract class MerchantRepository {
|
||||||
Future<List<Merchant>> getMerchants({required String token});
|
Future<List<Merchant>> getMerchants({required String token});
|
||||||
|
|
||||||
|
Future<List<Terminal>> getTerminalsByMerchantId({
|
||||||
|
required String token,
|
||||||
|
required String merchantId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
8
lib/domain/repositories/transaction_repository.dart
Normal file
8
lib/domain/repositories/transaction_repository.dart
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
import 'package:e_receipt_mobile/domain/entities/transaction_record.dart';
|
||||||
|
|
||||||
|
abstract class TransactionRepository {
|
||||||
|
Future<List<TransactionRecord>> getTransactions({
|
||||||
|
required String token,
|
||||||
|
required String serial,
|
||||||
|
});
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ import 'package:e_receipt_mobile/domain/entities/login_user.dart';
|
|||||||
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/home/home_view_model.dart';
|
import 'package:e_receipt_mobile/presentation/home/home_view_model.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/login/login_page.dart';
|
import 'package:e_receipt_mobile/presentation/login/login_page.dart';
|
||||||
|
import 'package:e_receipt_mobile/presentation/terminal/terminal_selection_screen.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
@ -41,11 +42,11 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
_showProfile(context);
|
_showProfile(context);
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
ListTile(
|
// ListTile(
|
||||||
leading: const Icon(Icons.storefront_outlined),
|
// leading: const Icon(Icons.storefront_outlined),
|
||||||
title: const Text('Merchants'),
|
// title: const Text('Merchants'),
|
||||||
onTap: () => Navigator.of(context).pop(),
|
// onTap: () => Navigator.of(context).pop(),
|
||||||
),
|
// ),
|
||||||
ListTile(
|
ListTile(
|
||||||
leading: const Icon(Icons.analytics_outlined),
|
leading: const Icon(Icons.analytics_outlined),
|
||||||
title: const Text('Reports'),
|
title: const Text('Reports'),
|
||||||
@ -115,30 +116,20 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
itemBuilder: (context, index) {
|
itemBuilder: (context, index) {
|
||||||
final merchant = merchants[index];
|
final merchant = merchants[index];
|
||||||
return Card(
|
return Card(
|
||||||
elevation: 2,
|
child: ListTile(
|
||||||
shape: RoundedRectangleBorder(
|
onTap: merchant.id == null
|
||||||
borderRadius: BorderRadius.circular(12),
|
? null
|
||||||
),
|
: () => _openTerminalSelection(
|
||||||
child: Padding(
|
context,
|
||||||
padding: const EdgeInsets.all(12),
|
merchant.id!,
|
||||||
child: Column(
|
merchant.name ?? '-',
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
),
|
||||||
children: [
|
leading: const Icon(Icons.storefront_outlined),
|
||||||
Text(
|
trailing: const Icon(Icons.chevron_right),
|
||||||
'${index + 1}. ${merchant.name ?? '-'}',
|
title: Text(
|
||||||
style: Theme.of(context)
|
'${index + 1}. ${merchant.name ?? '-'}',
|
||||||
.textTheme
|
|
||||||
.titleMedium,
|
|
||||||
),
|
|
||||||
const SizedBox(height: 6),
|
|
||||||
Text(
|
|
||||||
merchant.address ?? '-',
|
|
||||||
style: Theme.of(context)
|
|
||||||
.textTheme
|
|
||||||
.bodyMedium,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
subtitle: Text(merchant.address ?? '-'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -183,4 +174,19 @@ class HomeScreen extends ConsumerWidget {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _openTerminalSelection(
|
||||||
|
BuildContext context,
|
||||||
|
String merchantId,
|
||||||
|
String merchantName,
|
||||||
|
) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => TerminalSelectionScreen(
|
||||||
|
merchantId: merchantId,
|
||||||
|
merchantName: merchantName,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import 'package:e_receipt_mobile/core/config/app_config.dart';
|
import 'package:e_receipt_mobile/core/config/app_config.dart';
|
||||||
import 'package:e_receipt_mobile/data/repositories/api_merchant_repository.dart';
|
import 'package:e_receipt_mobile/data/repositories/api_merchant_repository.dart';
|
||||||
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
|
import 'package:e_receipt_mobile/domain/entities/merchant.dart';
|
||||||
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart';
|
import 'package:e_receipt_mobile/domain/repositories/merchant_repository.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
||||||
import 'package:e_receipt_mobile/presentation/login/login_view_model.dart';
|
import 'package:e_receipt_mobile/presentation/login/login_view_model.dart';
|
||||||
@ -24,3 +25,18 @@ final merchantListProvider = FutureProvider<List<Merchant>>((ref) async {
|
|||||||
.watch(merchantRepositoryProvider)
|
.watch(merchantRepositoryProvider)
|
||||||
.getMerchants(token: sessionUser.token);
|
.getMerchants(token: sessionUser.token);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
final terminalListProvider =
|
||||||
|
FutureProvider.family<List<Terminal>, String>((ref, merchantId) async {
|
||||||
|
final sessionUser = ref.watch(sessionControllerProvider);
|
||||||
|
if (sessionUser == null) {
|
||||||
|
throw Exception('No active session');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref
|
||||||
|
.watch(merchantRepositoryProvider)
|
||||||
|
.getTerminalsByMerchantId(
|
||||||
|
token: sessionUser.token,
|
||||||
|
merchantId: merchantId,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
97
lib/presentation/terminal/terminal_next_screen.dart
Normal file
97
lib/presentation/terminal/terminal_next_screen.dart
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
|
import 'package:e_receipt_mobile/presentation/terminal/transaction_view_model.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class TerminalNextScreen extends ConsumerWidget {
|
||||||
|
const TerminalNextScreen({
|
||||||
|
required this.merchantName,
|
||||||
|
required this.terminal,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String merchantName;
|
||||||
|
final Terminal terminal;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
|
final serial = terminal.serial?.trim() ?? '';
|
||||||
|
final hasSerial = serial.isNotEmpty;
|
||||||
|
final transactionsAsync = hasSerial
|
||||||
|
? ref.watch(transactionListProvider(serial))
|
||||||
|
: null;
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(title: const Text('Terminal Transactions')),
|
||||||
|
body: Padding(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Merchant: $merchantName',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Text('Terminal: ${terminal.name ?? terminal.tid ?? '-'}'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('TID: ${terminal.tid ?? '-'}'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
'MIDs: ${(terminal.mids == null || terminal.mids!.isEmpty) ? '-' : terminal.mids!.join(', ')}',
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('Serial: ${terminal.serial ?? '-'}'),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text('Address: ${terminal.address ?? '-'}'),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'Transactions',
|
||||||
|
style: Theme.of(context).textTheme.titleMedium,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Expanded(
|
||||||
|
child: !hasSerial
|
||||||
|
? const Center(
|
||||||
|
child: Text('Serial is required to load transactions'),
|
||||||
|
)
|
||||||
|
: transactionsAsync!.when(
|
||||||
|
loading: () =>
|
||||||
|
const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, _) => Center(
|
||||||
|
child: Text('Failed to load transactions: $error'),
|
||||||
|
),
|
||||||
|
data: (transactions) {
|
||||||
|
if (transactions.isEmpty) {
|
||||||
|
return const Center(
|
||||||
|
child: Text('No transactions found'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
itemCount: transactions.length,
|
||||||
|
separatorBuilder: (_, __) =>
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final item = transactions[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
title: Text(
|
||||||
|
'Amount: ${item.amount?.toString() ?? '-'}',
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
'Status: ${item.status ?? '-'} | RRN: ${item.rrn ?? '-'}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
74
lib/presentation/terminal/terminal_selection_screen.dart
Normal file
74
lib/presentation/terminal/terminal_selection_screen.dart
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||||
|
import 'package:e_receipt_mobile/presentation/home/home_view_model.dart';
|
||||||
|
import 'package:e_receipt_mobile/presentation/terminal/terminal_next_screen.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
class TerminalSelectionScreen extends ConsumerStatefulWidget {
|
||||||
|
const TerminalSelectionScreen({
|
||||||
|
required this.merchantId,
|
||||||
|
required this.merchantName,
|
||||||
|
super.key,
|
||||||
|
});
|
||||||
|
|
||||||
|
final String merchantId;
|
||||||
|
final String merchantName;
|
||||||
|
|
||||||
|
@override
|
||||||
|
ConsumerState<TerminalSelectionScreen> createState() =>
|
||||||
|
_TerminalSelectionScreenState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TerminalSelectionScreenState
|
||||||
|
extends ConsumerState<TerminalSelectionScreen> {
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final terminalsAsync = ref.watch(terminalListProvider(widget.merchantId));
|
||||||
|
|
||||||
|
return Scaffold(
|
||||||
|
appBar: AppBar(
|
||||||
|
title: Text('Select Terminals - ${widget.merchantName}'),
|
||||||
|
),
|
||||||
|
body: terminalsAsync.when(
|
||||||
|
loading: () => const Center(child: CircularProgressIndicator()),
|
||||||
|
error: (error, _) => Center(child: Text('Failed to load terminals: $error')),
|
||||||
|
data: (terminals) {
|
||||||
|
if (terminals.isEmpty) {
|
||||||
|
return const Center(child: Text('No terminals found'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return ListView.separated(
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
itemCount: terminals.length,
|
||||||
|
separatorBuilder: (_, __) => const SizedBox(height: 8),
|
||||||
|
itemBuilder: (context, index) {
|
||||||
|
final terminal = terminals[index];
|
||||||
|
return Card(
|
||||||
|
child: ListTile(
|
||||||
|
onTap: () => _openNextScreen(context, terminal),
|
||||||
|
leading: const Icon(Icons.point_of_sale),
|
||||||
|
trailing: const Icon(Icons.chevron_right),
|
||||||
|
title: Text(terminal.name ?? terminal.tid ?? '-'),
|
||||||
|
subtitle: Text(
|
||||||
|
'TID: ${terminal.tid ?? '-'} | MIDs: ${(terminal.mids == null || terminal.mids!.isEmpty) ? '-' : terminal.mids!.join(', ')}',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _openNextScreen(BuildContext context, Terminal terminal) {
|
||||||
|
Navigator.of(context).push(
|
||||||
|
MaterialPageRoute<void>(
|
||||||
|
builder: (_) => TerminalNextScreen(
|
||||||
|
merchantName: widget.merchantName,
|
||||||
|
terminal: terminal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
27
lib/presentation/terminal/transaction_view_model.dart
Normal file
27
lib/presentation/terminal/transaction_view_model.dart
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
import 'package:e_receipt_mobile/core/config/app_config.dart';
|
||||||
|
import 'package:e_receipt_mobile/data/repositories/api_transaction_repository.dart';
|
||||||
|
import 'package:e_receipt_mobile/domain/entities/transaction_record.dart';
|
||||||
|
import 'package:e_receipt_mobile/domain/repositories/transaction_repository.dart';
|
||||||
|
import 'package:e_receipt_mobile/presentation/auth/session_controller.dart';
|
||||||
|
import 'package:e_receipt_mobile/presentation/login/login_view_model.dart';
|
||||||
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
|
|
||||||
|
final transactionRepositoryProvider = Provider<TransactionRepository>((ref) {
|
||||||
|
return ApiTransactionRepository(
|
||||||
|
baseUrl: AppConfig.apiBaseUrl,
|
||||||
|
apiSecret: AppConfig.apiSecret,
|
||||||
|
client: ref.watch(authenticatedHttpClientProvider),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
final transactionListProvider =
|
||||||
|
FutureProvider.family<List<TransactionRecord>, String>((ref, serial) async {
|
||||||
|
final sessionUser = ref.watch(sessionControllerProvider);
|
||||||
|
if (sessionUser == null) {
|
||||||
|
throw Exception('No active session');
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref
|
||||||
|
.watch(transactionRepositoryProvider)
|
||||||
|
.getTransactions(token: sessionUser.token, serial: serial);
|
||||||
|
});
|
||||||
Loading…
Reference in New Issue
Block a user