final final part 1
This commit is contained in:
parent
6646955726
commit
8cc8d14ad6
@ -2,5 +2,6 @@ class AppConfig {
|
||||
const AppConfig._();
|
||||
|
||||
static const String apiBaseUrl = 'https://api-tms-uat.kbzbank.com:8443';
|
||||
// static const String apiBaseUrl = 'https://receipt-nest.utsmyanmar.com';
|
||||
static const String apiSecret = 'y812J21lhha11OS';
|
||||
}
|
||||
|
||||
@ -19,10 +19,14 @@ class ApiTransactionRepository implements TransactionRepository {
|
||||
Future<List<TransactionRecord>> getTransactions({
|
||||
required String token,
|
||||
required String serial,
|
||||
required String range,
|
||||
}) async {
|
||||
final uri = Uri.parse(
|
||||
'$baseUrl/receipt/transaction',
|
||||
).replace(queryParameters: {'serial': serial});
|
||||
final uri = Uri.parse('$baseUrl/receipt/transaction').replace(
|
||||
queryParameters: {
|
||||
'serial': serial,
|
||||
'range': range,
|
||||
},
|
||||
);
|
||||
|
||||
final response = await _client.get(
|
||||
uri,
|
||||
@ -88,11 +92,11 @@ class ApiTransactionRepository implements TransactionRepository {
|
||||
|
||||
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']),
|
||||
rrn: _pickString(item, const ['rrn', 'referenceNo', 'reference_no', 'DE37']),
|
||||
amount: _pickNum(item, const ['amount', 'totalAmount', 'total_amount', 'DE4']),
|
||||
status: _pickString(item, const ['status', 'description', 'DE39']),
|
||||
type: _pickString(item, const ['type', 'transactionType', 'DE3']),
|
||||
createdAt: _pickString(item, const ['createdAt', 'created_at', 'CREATED_AT']),
|
||||
raw: item,
|
||||
);
|
||||
}
|
||||
|
||||
@ -4,5 +4,6 @@ abstract class TransactionRepository {
|
||||
Future<List<TransactionRecord>> getTransactions({
|
||||
required String token,
|
||||
required String serial,
|
||||
required String range,
|
||||
});
|
||||
}
|
||||
|
||||
@ -184,33 +184,43 @@ class HomeScreen extends ConsumerWidget {
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: merchants.isEmpty
|
||||
? const Center(child: Text('No merchants found'))
|
||||
: ListView.separated(
|
||||
itemCount: merchants.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final merchant = merchants[index];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: merchant.id == null
|
||||
? null
|
||||
: () => _openTerminalSelection(
|
||||
context,
|
||||
merchant.id!,
|
||||
merchant.name ?? '-',
|
||||
),
|
||||
leading: const Icon(Icons.storefront_outlined),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text(
|
||||
'${index + 1}. ${merchant.name ?? '-'}',
|
||||
child: RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(merchantListProvider.future),
|
||||
child: merchants.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
Center(child: Text('No merchants found')),
|
||||
],
|
||||
)
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: merchants.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(height: 10),
|
||||
itemBuilder: (context, index) {
|
||||
final merchant = merchants[index];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: merchant.id == null
|
||||
? null
|
||||
: () => _openTerminalSelection(
|
||||
context,
|
||||
merchant.id!,
|
||||
merchant.name ?? '-',
|
||||
),
|
||||
leading: const Icon(Icons.storefront_outlined),
|
||||
trailing: const Icon(Icons.chevron_right),
|
||||
title: Text(
|
||||
'${index + 1}. ${merchant.name ?? '-'}',
|
||||
),
|
||||
subtitle: Text(merchant.address ?? '-'),
|
||||
),
|
||||
subtitle: Text(merchant.address ?? '-'),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||
import 'package:e_receipt_mobile/presentation/terminal/transaction_receipt_screen.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 {
|
||||
class TerminalNextScreen extends ConsumerStatefulWidget {
|
||||
const TerminalNextScreen({
|
||||
required this.merchantName,
|
||||
required this.terminal,
|
||||
@ -14,11 +15,23 @@ class TerminalNextScreen extends ConsumerWidget {
|
||||
final Terminal terminal;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final serial = terminal.serial?.trim() ?? '';
|
||||
ConsumerState<TerminalNextScreen> createState() => _TerminalNextScreenState();
|
||||
}
|
||||
|
||||
class _TerminalNextScreenState extends ConsumerState<TerminalNextScreen> {
|
||||
static const List<String> _ranges = <String>['1d', '1w', '1m'];
|
||||
String _selectedRange = '1d';
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final serial = widget.terminal.serial?.trim() ?? '';
|
||||
final hasSerial = serial.isNotEmpty;
|
||||
final transactionsAsync = hasSerial
|
||||
? ref.watch(transactionListProvider(serial))
|
||||
? ref.watch(
|
||||
transactionListProvider(
|
||||
TransactionQuery(serial: serial, range: _selectedRange),
|
||||
),
|
||||
)
|
||||
: null;
|
||||
|
||||
return Scaffold(
|
||||
@ -29,25 +42,53 @@ class TerminalNextScreen extends ConsumerWidget {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Merchant: $merchantName',
|
||||
'Merchant: ${widget.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(', ')}',
|
||||
'Terminal: ${widget.terminal.name ?? widget.terminal.tid ?? '-'}',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('Serial: ${terminal.serial ?? '-'}'),
|
||||
Text('TID: ${widget.terminal.tid ?? '-'}'),
|
||||
const SizedBox(height: 4),
|
||||
Text('Address: ${terminal.address ?? '-'}'),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Transactions',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
'MIDs: ${(widget.terminal.mids == null || widget.terminal.mids!.isEmpty) ? '-' : widget.terminal.mids!.join(', ')}',
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text('Serial: ${widget.terminal.serial ?? '-'}'),
|
||||
const SizedBox(height: 4),
|
||||
Text('Address: ${widget.terminal.address ?? '-'}'),
|
||||
const SizedBox(height: 16),
|
||||
Row(
|
||||
children: [
|
||||
Text(
|
||||
'Transactions',
|
||||
style: Theme.of(context).textTheme.titleMedium,
|
||||
),
|
||||
const Spacer(),
|
||||
const Text('Range: '),
|
||||
const SizedBox(width: 8),
|
||||
DropdownButton<String>(
|
||||
value: _selectedRange,
|
||||
items: _ranges
|
||||
.map(
|
||||
(range) => DropdownMenuItem<String>(
|
||||
value: range,
|
||||
child: Text(range),
|
||||
),
|
||||
)
|
||||
.toList(),
|
||||
onChanged: (value) {
|
||||
if (value == null || value == _selectedRange) {
|
||||
return;
|
||||
}
|
||||
setState(() {
|
||||
_selectedRange = value;
|
||||
});
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
@ -62,29 +103,59 @@ class TerminalNextScreen extends ConsumerWidget {
|
||||
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 ?? '-'}',
|
||||
),
|
||||
return RefreshIndicator(
|
||||
onRefresh: () => ref.refresh(
|
||||
transactionListProvider(
|
||||
TransactionQuery(
|
||||
serial: serial,
|
||||
range: _selectedRange,
|
||||
),
|
||||
);
|
||||
},
|
||||
).future,
|
||||
),
|
||||
child: transactions.isEmpty
|
||||
? ListView(
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(),
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
Center(child: Text('No transactions found')),
|
||||
],
|
||||
)
|
||||
: ListView.separated(
|
||||
physics:
|
||||
const AlwaysScrollableScrollPhysics(),
|
||||
itemCount: transactions.length,
|
||||
separatorBuilder: (_, __) =>
|
||||
const SizedBox(height: 8),
|
||||
itemBuilder: (context, index) {
|
||||
final item = transactions[index];
|
||||
return Card(
|
||||
child: ListTile(
|
||||
onTap: () {
|
||||
Navigator.of(context).push(
|
||||
MaterialPageRoute<void>(
|
||||
builder: (_) =>
|
||||
TransactionReceiptScreen(
|
||||
merchantName:
|
||||
widget.merchantName,
|
||||
terminal: widget.terminal,
|
||||
transaction: item,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
title: Text(
|
||||
'Amount: ${item.amount?.toString() ?? '-'}',
|
||||
),
|
||||
subtitle: Text(
|
||||
'Status: ${item.status ?? '-'} | Type: ${item.type ?? '-'} | RRN: ${item.rrn ?? '-'}\nTap to view receipt detail',
|
||||
),
|
||||
trailing:
|
||||
const Icon(Icons.receipt_long_outlined),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
@ -33,28 +33,38 @@ class _TerminalSelectionScreenState
|
||||
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(', ')}',
|
||||
return RefreshIndicator(
|
||||
onRefresh: () =>
|
||||
ref.refresh(terminalListProvider(widget.merchantId).future),
|
||||
child: terminals.isEmpty
|
||||
? ListView(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
children: const [
|
||||
SizedBox(height: 120),
|
||||
Center(child: Text('No terminals found')),
|
||||
],
|
||||
)
|
||||
: ListView.separated(
|
||||
physics: const AlwaysScrollableScrollPhysics(),
|
||||
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(', ')}',
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
|
||||
144
lib/presentation/terminal/transaction_receipt_screen.dart
Normal file
144
lib/presentation/terminal/transaction_receipt_screen.dart
Normal file
@ -0,0 +1,144 @@
|
||||
import 'package:e_receipt_mobile/domain/entities/terminal.dart';
|
||||
import 'package:e_receipt_mobile/domain/entities/transaction_record.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class TransactionReceiptScreen extends StatelessWidget {
|
||||
const TransactionReceiptScreen({
|
||||
required this.merchantName,
|
||||
required this.terminal,
|
||||
required this.transaction,
|
||||
super.key,
|
||||
});
|
||||
|
||||
final String merchantName;
|
||||
final Terminal terminal;
|
||||
final TransactionRecord transaction;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final raw = transaction.raw ?? const <String, dynamic>{};
|
||||
final amount = _first(raw, const ['DE4']) ?? transaction.amount?.toString();
|
||||
final currency = _first(raw, const ['DE49']) ?? 'MMK';
|
||||
final status = _statusLabel(
|
||||
_first(raw, const ['description', 'DE39', 'status']) ?? transaction.status,
|
||||
);
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('Receipt Detail')),
|
||||
body: SafeArea(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Card(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: DefaultTextStyle(
|
||||
style: Theme.of(context).textTheme.bodyMedium!,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
Center(
|
||||
child: Text(
|
||||
'E-Receipt',
|
||||
style: Theme.of(context).textTheme.titleLarge,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Center(child: Text(merchantName)),
|
||||
const SizedBox(height: 12),
|
||||
const Text('--------------------------------'),
|
||||
_line('Terminal', _first(raw, const ['DE41']) ?? terminal.tid),
|
||||
_line('Merchant ID', _first(raw, const ['DE42'])),
|
||||
_line('Serial', terminal.serial),
|
||||
_line(
|
||||
'Invoice',
|
||||
_first(raw, const ['invoice_number', 'invoiceNo']),
|
||||
),
|
||||
_line('Date/Time', _first(raw, const ['CREATED_AT', 'de7_date'])),
|
||||
_line('Trace No', _first(raw, const ['DE11'])),
|
||||
_line('RRN', _first(raw, const ['DE37']) ?? transaction.rrn),
|
||||
_line('Auth Code', _first(raw, const ['DE38'])),
|
||||
_line('Type', _first(raw, const ['DE3']) ?? transaction.type),
|
||||
const Text('--------------------------------'),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'AMOUNT',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
'${amount ?? '-'} $currency',
|
||||
textAlign: TextAlign.center,
|
||||
style: Theme.of(context).textTheme.headlineSmall,
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
status,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontWeight: FontWeight.w700,
|
||||
color: status == 'SUCCESS'
|
||||
? Colors.green.shade700
|
||||
: Colors.red.shade700,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
const Text('--------------------------------'),
|
||||
Center(
|
||||
child: Text(
|
||||
'Thank you',
|
||||
style: Theme.of(context).textTheme.bodySmall,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _line(String label, String? value) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 2),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(child: Text(label)),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
value == null || value.trim().isEmpty ? '-' : value,
|
||||
textAlign: TextAlign.right,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String? _first(Map<String, dynamic> raw, List<String> keys) {
|
||||
for (final key in keys) {
|
||||
final value = raw[key];
|
||||
if (value != null) {
|
||||
final text = value.toString().trim();
|
||||
if (text.isNotEmpty) {
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
String _statusLabel(String? status) {
|
||||
final normalized = (status ?? '').trim().toUpperCase();
|
||||
if (normalized == 'A' || normalized == 'SUCCESS' || normalized == 'PAY_SUCCESS') {
|
||||
return 'SUCCESS';
|
||||
}
|
||||
if (normalized == 'E' || normalized == 'FAILED') {
|
||||
return 'FAILED';
|
||||
}
|
||||
return normalized.isEmpty ? '-' : normalized;
|
||||
}
|
||||
}
|
||||
@ -14,14 +14,38 @@ final transactionRepositoryProvider = Provider<TransactionRepository>((ref) {
|
||||
);
|
||||
});
|
||||
|
||||
final transactionListProvider =
|
||||
FutureProvider.family<List<TransactionRecord>, String>((ref, serial) async {
|
||||
final sessionUser = ref.watch(sessionControllerProvider);
|
||||
if (sessionUser == null) {
|
||||
throw Exception('No active session');
|
||||
}
|
||||
class TransactionQuery {
|
||||
const TransactionQuery({required this.serial, required this.range});
|
||||
|
||||
return ref
|
||||
.watch(transactionRepositoryProvider)
|
||||
.getTransactions(token: sessionUser.token, serial: serial);
|
||||
});
|
||||
final String serial;
|
||||
final String range;
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
if (identical(this, other)) {
|
||||
return true;
|
||||
}
|
||||
return other is TransactionQuery &&
|
||||
other.serial == serial &&
|
||||
other.range == range;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode => Object.hash(serial, range);
|
||||
}
|
||||
|
||||
final transactionListProvider =
|
||||
FutureProvider.family<List<TransactionRecord>, TransactionQuery>(
|
||||
(ref, query) async {
|
||||
final sessionUser = ref.watch(sessionControllerProvider);
|
||||
if (sessionUser == null) {
|
||||
throw Exception('No active session');
|
||||
}
|
||||
|
||||
return ref.watch(transactionRepositoryProvider).getTransactions(
|
||||
token: sessionUser.token,
|
||||
serial: query.serial,
|
||||
range: query.range,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
Loading…
Reference in New Issue
Block a user