updated the look and feel of the home page
This commit is contained in:
@@ -200,6 +200,12 @@ class _MyAppState extends State<MyApp> {
|
|||||||
path: '/receipt',
|
path: '/receipt',
|
||||||
builder: (context, state) => const ReceiptScreen(),
|
builder: (context, state) => const ReceiptScreen(),
|
||||||
),
|
),
|
||||||
|
GoRoute(
|
||||||
|
path: '/receipt/:transactionId',
|
||||||
|
builder: (context, state) => ReceiptScreen(
|
||||||
|
transactionId: state.pathParameters['transactionId'],
|
||||||
|
),
|
||||||
|
),
|
||||||
GoRoute(
|
GoRoute(
|
||||||
path: '/integration',
|
path: '/integration',
|
||||||
builder: (context, state) => const IntegrationScreen(),
|
builder: (context, state) => const IntegrationScreen(),
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:qpay/models/responsive_policy.dart';
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
|
import 'package:qpay/screens/receipt/receipt_controller.dart';
|
||||||
import 'package:qpay/screens/transaction_controller.dart';
|
import 'package:qpay/screens/transaction_controller.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
|
||||||
import 'package:shared_preferences/shared_preferences.dart';
|
import 'package:shared_preferences/shared_preferences.dart';
|
||||||
import 'package:qpay/screens/history/history_controller.dart';
|
import 'package:qpay/screens/history/history_controller.dart';
|
||||||
import 'package:timeago/timeago.dart' as timeago;
|
import 'package:qpay/widgets/transaction_item_widget.dart';
|
||||||
|
import 'package:qpay/screens/transactions/transaction_model.dart' as txn;
|
||||||
|
|
||||||
class HistoryScreen extends StatefulWidget {
|
class HistoryScreen extends StatefulWidget {
|
||||||
const HistoryScreen({super.key});
|
const HistoryScreen({super.key});
|
||||||
@@ -54,6 +55,13 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Handles the repeat action for a transaction.
|
||||||
|
Future<void> _repeatTransaction(Map<String, dynamic> item) async {
|
||||||
|
final receiptController = ReceiptController(context);
|
||||||
|
final transaction = txn.TransactionModel.fromJson(item);
|
||||||
|
await receiptController.repeatTransaction(transaction);
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
controller.dispose();
|
controller.dispose();
|
||||||
@@ -198,7 +206,7 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
borderSide: BorderSide(
|
borderSide: BorderSide(
|
||||||
color: Theme.of(
|
color: Theme.of(
|
||||||
context,
|
context,
|
||||||
).colorScheme.primary.withOpacity(0.2),
|
).colorScheme.primary.withValues(alpha: 0.2),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
suffixIcon: _searchController.text.isNotEmpty
|
suffixIcon: _searchController.text.isNotEmpty
|
||||||
@@ -247,72 +255,15 @@ class _HistoryScreenState extends State<HistoryScreen>
|
|||||||
|
|
||||||
return SlideTransition(
|
return SlideTransition(
|
||||||
position: animation,
|
position: animation,
|
||||||
child: Skeletonizer(
|
child: TransactionItemWidget(
|
||||||
|
transaction: e,
|
||||||
enabled: controller.model.isLoading,
|
enabled: controller.model.isLoading,
|
||||||
child: InkWell(
|
onRepeat: () async {
|
||||||
customBorder: RoundedRectangleBorder(
|
await _repeatTransaction(e);
|
||||||
borderRadius: BorderRadius.circular(12),
|
if (context.mounted) {
|
||||||
),
|
context.push('/make-payment');
|
||||||
onTap: () {
|
}
|
||||||
transactionController.updateReceiptData(e);
|
|
||||||
context.push("/receipt");
|
|
||||||
},
|
},
|
||||||
child: ListTile(
|
|
||||||
leading: Container(
|
|
||||||
padding: const EdgeInsets.all(8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Theme.of(
|
|
||||||
context,
|
|
||||||
).colorScheme.primary.withValues(alpha: 0.1),
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Image(
|
|
||||||
image: AssetImage("assets/${e['paymentProcessorImage']}"),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
e['billName'],
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold),
|
|
||||||
),
|
|
||||||
SizedBox(width: 10),
|
|
||||||
Icon(
|
|
||||||
e['integrationStatus'] == 'SUCCESS'
|
|
||||||
? Icons.check_circle
|
|
||||||
: e['integrationStatus'] == 'PENDING'
|
|
||||||
? Icons.pending
|
|
||||||
: Icons.cancel,
|
|
||||||
size: 16,
|
|
||||||
color: e['integrationStatus'] == 'SUCCESS'
|
|
||||||
? Colors.green
|
|
||||||
: e['integrationStatus'] == 'PENDING'
|
|
||||||
? Colors.orange
|
|
||||||
: Colors.red,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
subtitle: Text(
|
|
||||||
e['createdAt'] != null
|
|
||||||
? timeago.format(DateTime.parse(e['createdAt']))
|
|
||||||
: '',
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
trailing: Column(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
e['amount'].toStringAsFixed(2),
|
|
||||||
style: TextStyle(fontWeight: FontWeight.bold, fontSize: 18),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
e['totalAmount'].toStringAsFixed(2),
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -47,8 +47,10 @@ class IntegrationController extends ChangeNotifier {
|
|||||||
|
|
||||||
dynamic response = workflowResponse['body'];
|
dynamic response = workflowResponse['body'];
|
||||||
|
|
||||||
|
String? transactionId;
|
||||||
if (response['status'] == 'SUCCESS') {
|
if (response['status'] == 'SUCCESS') {
|
||||||
model.status = response['status'];
|
model.status = response['status'];
|
||||||
|
transactionId = response['id'];
|
||||||
transactionController.updateReceiptData(response);
|
transactionController.updateReceiptData(response);
|
||||||
|
|
||||||
await gatewayController.poll(
|
await gatewayController.poll(
|
||||||
@@ -61,7 +63,13 @@ class IntegrationController extends ChangeNotifier {
|
|||||||
if (!_disposed) notifyListeners();
|
if (!_disposed) notifyListeners();
|
||||||
|
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
if (!_disposed && context.mounted) context.go('/receipt');
|
if (!_disposed && context.mounted) {
|
||||||
|
if (transactionId != null) {
|
||||||
|
context.go('/receipt/$transactionId');
|
||||||
|
} else {
|
||||||
|
context.go('/receipt');
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return ApiResponse.success(Map<String, dynamic>.from(response));
|
return ApiResponse.success(Map<String, dynamic>.from(response));
|
||||||
|
|||||||
@@ -49,8 +49,13 @@ class _IntegrationScreenState extends State<IntegrationScreen> {
|
|||||||
listenable: controller,
|
listenable: controller,
|
||||||
builder: (context, child) {
|
builder: (context, child) {
|
||||||
if (controller.model.status == 'SUCCESS') {
|
if (controller.model.status == 'SUCCESS') {
|
||||||
|
final transactionId = controller.model.transaction?['id'];
|
||||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||||
|
if (transactionId != null) {
|
||||||
|
context.go('/receipt/$transactionId');
|
||||||
|
} else {
|
||||||
context.go('/receipt');
|
context.go('/receipt');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,10 @@ import 'package:qpay/models/api_response.dart';
|
|||||||
import 'package:qpay/screens/pay/pay_controller.dart' as pc;
|
import 'package:qpay/screens/pay/pay_controller.dart' as pc;
|
||||||
import 'package:qpay/screens/transaction_controller.dart' as tc;
|
import 'package:qpay/screens/transaction_controller.dart' as tc;
|
||||||
import 'package:qpay/screens/home/home_controller.dart' as hc;
|
import 'package:qpay/screens/home/home_controller.dart' as hc;
|
||||||
|
import 'package:qpay/screens/transactions/transaction_model.dart';
|
||||||
|
|
||||||
class ReceiptModel {
|
class ReceiptModel {
|
||||||
Map<String, dynamic>? receiptData;
|
TransactionModel? transaction;
|
||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
String? errorMessage;
|
String? errorMessage;
|
||||||
String status = '';
|
String status = '';
|
||||||
@@ -35,7 +36,7 @@ class ReceiptController extends ChangeNotifier {
|
|||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApiResponse<Map<String, dynamic>>> doIntegration(String id) async {
|
Future<ApiResponse<TransactionModel>> doIntegration(String id) async {
|
||||||
try {
|
try {
|
||||||
model.isLoading = true;
|
model.isLoading = true;
|
||||||
model.status = '';
|
model.status = '';
|
||||||
@@ -51,9 +52,13 @@ class ReceiptController extends ChangeNotifier {
|
|||||||
|
|
||||||
if (response['status'] == 'SUCCESS') {
|
if (response['status'] == 'SUCCESS') {
|
||||||
model.status = response['status'];
|
model.status = response['status'];
|
||||||
transactionController.updateReceiptData(response);
|
transactionController.updateReceiptData(
|
||||||
|
Map<String, dynamic>.from(response),
|
||||||
|
);
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
return ApiResponse.success(Map<String, dynamic>.from(response));
|
return ApiResponse.success(
|
||||||
|
TransactionModel.fromJson(Map<String, dynamic>.from(response)),
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
model.status = response['status'];
|
model.status = response['status'];
|
||||||
model.errorMessage = response['errorMessage'];
|
model.errorMessage = response['errorMessage'];
|
||||||
@@ -70,10 +75,10 @@ class ReceiptController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> repeatTransaction(BuildContext context) async {
|
Future<void> repeatTransaction(TransactionModel transaction) async {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
// fetch provider & update tran controller
|
// fetch provider & update tran controller
|
||||||
String providerId = model.receiptData?['billClientId'] ?? '';
|
String providerId = transaction.billClientId ?? '';
|
||||||
Map<String, dynamic> provider = await http.get(
|
Map<String, dynamic> provider = await http.get(
|
||||||
'/public/providers/client/$providerId',
|
'/public/providers/client/$providerId',
|
||||||
);
|
);
|
||||||
@@ -84,16 +89,16 @@ class ReceiptController extends ChangeNotifier {
|
|||||||
// update tran controller formdata with credit account, email, name, phone & providerLabel
|
// update tran controller formdata with credit account, email, name, phone & providerLabel
|
||||||
transactionController.model.formData = transactionController.model.formData
|
transactionController.model.formData = transactionController.model.formData
|
||||||
.copyWith(
|
.copyWith(
|
||||||
creditAccount: model.receiptData?['creditAccount'] ?? '',
|
creditAccount: transaction.creditAccount ?? '',
|
||||||
creditPhone: model.receiptData?['creditPhone'] ?? '',
|
creditPhone: transaction.creditPhone ?? '',
|
||||||
creditName: model.receiptData?['creditName'] ?? '',
|
creditName: transaction.creditName ?? '',
|
||||||
creditEmail: model.receiptData?['creditEmail'] ?? '',
|
creditEmail: transaction.creditEmail ?? '',
|
||||||
);
|
);
|
||||||
|
|
||||||
// fetch product and update tran controller formdata
|
// fetch product and update tran controller formdata
|
||||||
String providerUid =
|
String providerUid =
|
||||||
transactionController.model.selectedProvider?.uid ?? '';
|
transactionController.model.selectedProvider?.uid ?? '';
|
||||||
String productUid = model.receiptData?['productUid'] ?? '';
|
String productUid = transaction.productUid ?? '';
|
||||||
|
|
||||||
if (productUid.isNotEmpty) {
|
if (productUid.isNotEmpty) {
|
||||||
Map<String, dynamic> product = await http.get(
|
Map<String, dynamic> product = await http.get(
|
||||||
@@ -105,27 +110,26 @@ class ReceiptController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// update credit amount, notification phone number, payment processor
|
// update credit amount, notification phone number, payment processor
|
||||||
transactionController
|
transactionController.model.formData = transactionController.model.formData
|
||||||
.model
|
.copyWith(
|
||||||
.formData = transactionController.model.formData.copyWith(
|
amount: transaction.amount.toStringAsFixed(2),
|
||||||
amount: model.receiptData?['amount'].toStringAsFixed(2) ?? '',
|
debitPhone: transaction.debitPhone,
|
||||||
debitPhone: model.receiptData?['debitPhone'] ?? '',
|
paymentProcessorLabel: transaction.paymentProcessorLabel,
|
||||||
paymentProcessorLabel: model.receiptData?['paymentProcessorLabel'] ?? '',
|
paymentProcessorName: transaction.paymentProcessorName,
|
||||||
paymentProcessorName: model.receiptData?['paymentProcessorName'] ?? '',
|
|
||||||
id: null,
|
id: null,
|
||||||
);
|
);
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
context.push('/make-payment');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<ApiResponse<Map<String, dynamic>>> getReceiptData(String id) async {
|
Future<ApiResponse<TransactionModel>> getReceiptData(String id) async {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
Map<String, dynamic> response = await http.get('/public/transaction/$id');
|
Map<String, dynamic> response = await http.get('/public/transaction/$id');
|
||||||
model.receiptData = response;
|
final transaction = TransactionModel.fromJson(response);
|
||||||
|
model.transaction = transaction;
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return ApiResponse.success(response);
|
return ApiResponse.success(transaction);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
return ApiResponse.failure(
|
return ApiResponse.failure(
|
||||||
@@ -134,8 +138,8 @@ class ReceiptController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateReceiptData(Map<String, dynamic> data) {
|
void updateReceiptTransaction(TransactionModel data) {
|
||||||
model.receiptData = data;
|
model.transaction = data;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,20 @@ import 'package:intl/intl.dart';
|
|||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:qpay/models/responsive_policy.dart';
|
import 'package:qpay/models/responsive_policy.dart';
|
||||||
import 'package:qpay/screens/gateway/gateway_controller.dart';
|
import 'package:qpay/screens/gateway/gateway_controller.dart';
|
||||||
import 'package:qpay/screens/transaction_controller.dart';
|
import 'package:qpay/screens/transaction_controller.dart'
|
||||||
|
show TransactionController;
|
||||||
import 'package:go_router/go_router.dart';
|
import 'package:go_router/go_router.dart';
|
||||||
import 'package:qpay/screens/receipt/receipt_controller.dart';
|
import 'package:qpay/screens/receipt/receipt_controller.dart';
|
||||||
|
import 'package:qpay/screens/transactions/transaction_model.dart';
|
||||||
import 'package:share_plus/share_plus.dart';
|
import 'package:share_plus/share_plus.dart';
|
||||||
import 'package:skeletonizer/skeletonizer.dart';
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
import 'package:widgets_to_image/widgets_to_image.dart';
|
import 'package:widgets_to_image/widgets_to_image.dart';
|
||||||
|
import 'package:decimal/decimal.dart';
|
||||||
|
|
||||||
class ReceiptScreen extends StatefulWidget {
|
class ReceiptScreen extends StatefulWidget {
|
||||||
const ReceiptScreen({super.key});
|
final String? transactionId;
|
||||||
|
|
||||||
|
const ReceiptScreen({super.key, this.transactionId});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
State<ReceiptScreen> createState() => _ReceiptScreenState();
|
State<ReceiptScreen> createState() => _ReceiptScreenState();
|
||||||
@@ -36,6 +41,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
late Animation<double> _contentFadeAnimation;
|
late Animation<double> _contentFadeAnimation;
|
||||||
|
|
||||||
int _selectedTabIndex = 0;
|
int _selectedTabIndex = 0;
|
||||||
|
TransactionModel? _transaction;
|
||||||
|
|
||||||
late ReceiptController receiptController;
|
late ReceiptController receiptController;
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
@@ -52,9 +58,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
receiptController = ReceiptController(context);
|
receiptController = ReceiptController(context);
|
||||||
gatewayController = GatewayController(context);
|
gatewayController = GatewayController(context);
|
||||||
|
|
||||||
receiptController.getReceiptData(
|
_fetchTransaction();
|
||||||
transactionController.model.receiptData?["id"],
|
|
||||||
);
|
|
||||||
|
|
||||||
// Header animation
|
// Header animation
|
||||||
_headerAnimController = AnimationController(
|
_headerAnimController = AnimationController(
|
||||||
@@ -114,6 +118,22 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchTransaction() async {
|
||||||
|
final id =
|
||||||
|
widget.transactionId ??
|
||||||
|
transactionController.model.receiptData?["id"] ??
|
||||||
|
receiptController.model.transaction?.id;
|
||||||
|
|
||||||
|
if (id == null) return;
|
||||||
|
|
||||||
|
final response = await receiptController.getReceiptData(id);
|
||||||
|
if (response.isSuccess && response.data != null) {
|
||||||
|
setState(() {
|
||||||
|
_transaction = response.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _captureImage() async {
|
Future<void> _captureImage() async {
|
||||||
final image = await controller.capture();
|
final image = await controller.capture();
|
||||||
if (image != null) {
|
if (image != null) {
|
||||||
@@ -129,25 +149,21 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
|
|
||||||
_retry() async {
|
_retry() async {
|
||||||
receiptController.setLoading(true);
|
receiptController.setLoading(true);
|
||||||
await receiptController.doIntegration(
|
final id = _transaction?.id ?? '';
|
||||||
receiptController.model.receiptData?["id"],
|
await receiptController.doIntegration(id);
|
||||||
);
|
|
||||||
if (receiptController.model.status == "SUCCESS") {
|
if (receiptController.model.status == "SUCCESS") {
|
||||||
receiptController.getReceiptData(
|
await _fetchTransaction();
|
||||||
receiptController.model.receiptData?["id"],
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
receiptController.setLoading(false);
|
receiptController.setLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
_check() async {
|
_check() async {
|
||||||
receiptController.setLoading(true);
|
receiptController.setLoading(true);
|
||||||
await gatewayController.poll(receiptController.model.receiptData?["id"]);
|
final id = _transaction?.id ?? '';
|
||||||
|
await gatewayController.poll(id);
|
||||||
if (gatewayController.model.status == "SUCCESS") {
|
if (gatewayController.model.status == "SUCCESS") {
|
||||||
receiptController.getReceiptData(
|
await _fetchTransaction();
|
||||||
transactionController.model.receiptData?["id"],
|
if (_transaction?.pollingStatus == "SUCCESS") {
|
||||||
);
|
|
||||||
if (transactionController.model.receiptData?["pollingStatus"]) {
|
|
||||||
receiptController.setLoading(false);
|
receiptController.setLoading(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -193,22 +209,10 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
? const Color(0xFF0D0D0D)
|
? const Color(0xFF0D0D0D)
|
||||||
: const Color(0xFFF8F9FA),
|
: const Color(0xFFF8F9FA),
|
||||||
leading: context.canPop()
|
leading: context.canPop()
|
||||||
? Padding(
|
? IconButton(
|
||||||
padding: const EdgeInsets.only(left: 8),
|
|
||||||
child: Container(
|
|
||||||
margin: const EdgeInsets.symmetric(vertical: 8),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: theme.colorScheme.primary.withValues(
|
|
||||||
alpha: 0.08,
|
|
||||||
),
|
|
||||||
borderRadius: BorderRadius.circular(14),
|
|
||||||
),
|
|
||||||
child: IconButton(
|
|
||||||
onPressed: () => context.pop(),
|
onPressed: () => context.pop(),
|
||||||
icon: const Icon(Icons.arrow_back_rounded),
|
icon: const Icon(Icons.arrow_back_rounded),
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const SizedBox.shrink(),
|
: const SizedBox.shrink(),
|
||||||
title: Text(
|
title: Text(
|
||||||
@@ -359,13 +363,11 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
// Receipt Header Card
|
// Receipt Header Card
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
Widget _buildReceiptHeader(ThemeData theme, bool isDark) {
|
Widget _buildReceiptHeader(ThemeData theme, bool isDark) {
|
||||||
final status =
|
final status = _transaction?.integrationStatus;
|
||||||
transactionController.model.receiptData?["integrationStatus"];
|
final amount = _transaction?.amount;
|
||||||
final amount = receiptController.model.receiptData?["amount"];
|
final currency = _transaction?.debitCurrency ?? "";
|
||||||
final currency =
|
final billName = _transaction?.billName ?? "";
|
||||||
receiptController.model.receiptData?["debitCurrency"] ?? "";
|
final createdAt = _transaction?.createdAt;
|
||||||
final billName = receiptController.model.receiptData?["billName"] ?? "";
|
|
||||||
final createdAt = receiptController.model.receiptData?["createdAt"];
|
|
||||||
|
|
||||||
final primaryColor = theme.colorScheme.primary;
|
final primaryColor = theme.colorScheme.primary;
|
||||||
|
|
||||||
@@ -458,7 +460,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
// Date
|
// Date
|
||||||
if (createdAt != null)
|
if (createdAt != null)
|
||||||
Text(
|
Text(
|
||||||
DateFormat.yMMMd().add_jm().format(DateTime.parse(createdAt)),
|
DateFormat.yMMMd().add_jm().format(createdAt),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 13,
|
fontSize: 13,
|
||||||
fontWeight: FontWeight.w400,
|
fontWeight: FontWeight.w400,
|
||||||
@@ -502,9 +504,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
// Action Buttons Row
|
// Action Buttons Row
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
Widget _buildActionButtons(ThemeData theme) {
|
Widget _buildActionButtons(ThemeData theme) {
|
||||||
final integrationStatus =
|
final integrationStatus = _transaction?.integrationStatus;
|
||||||
receiptController.model.receiptData?["integrationStatus"];
|
final paymentStatus = _transaction?.paymentStatus;
|
||||||
final paymentStatus = receiptController.model.receiptData?["paymentStatus"];
|
|
||||||
|
|
||||||
return Wrap(
|
return Wrap(
|
||||||
spacing: 12,
|
spacing: 12,
|
||||||
@@ -516,7 +517,14 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
label: "Repeat",
|
label: "Repeat",
|
||||||
color: theme.colorScheme.primary,
|
color: theme.colorScheme.primary,
|
||||||
isLoading: receiptController.model.isLoading,
|
isLoading: receiptController.model.isLoading,
|
||||||
onPressed: () => receiptController.repeatTransaction(context),
|
onPressed: () async {
|
||||||
|
await receiptController.repeatTransaction(
|
||||||
|
receiptController.model.transaction!,
|
||||||
|
);
|
||||||
|
if (mounted) {
|
||||||
|
context.push('/make-payment');
|
||||||
|
}
|
||||||
|
},
|
||||||
),
|
),
|
||||||
_modernActionButton(
|
_modernActionButton(
|
||||||
icon: Icons.ios_share_rounded,
|
icon: Icons.ios_share_rounded,
|
||||||
@@ -534,8 +542,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
onPressed: _retry,
|
onPressed: _retry,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
if (receiptController.model.receiptData?["pollingStatus"] !=
|
if (_transaction?.pollingStatus != "SUCCESS") ...[
|
||||||
"SUCCESS") ...[
|
|
||||||
_modernActionButton(
|
_modernActionButton(
|
||||||
icon: Icons.search_rounded,
|
icon: Icons.search_rounded,
|
||||||
label: "Check",
|
label: "Check",
|
||||||
@@ -705,8 +712,8 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
// Provider Details Tab
|
// Provider Details Tab
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
Widget _buildProviderDetailsTab(ThemeData theme, bool isDark) {
|
Widget _buildProviderDetailsTab(ThemeData theme, bool isDark) {
|
||||||
final receiptData = receiptController.model.receiptData;
|
final receiptData = receiptController.model.transaction;
|
||||||
final additionalData = receiptData?["additionalData"];
|
final additionalData = receiptData?.additionalData;
|
||||||
|
|
||||||
final items = <_DetailItem>[];
|
final items = <_DetailItem>[];
|
||||||
|
|
||||||
@@ -715,14 +722,13 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.circle_outlined,
|
icon: Icons.circle_outlined,
|
||||||
label: "Status",
|
label: "Status",
|
||||||
value:
|
value: _transaction?.integrationStatus ?? "",
|
||||||
transactionController.model.receiptData?["integrationStatus"] ?? "",
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
// Error message
|
// Error message
|
||||||
final errorMsg = transactionController.model.receiptData?["errorMessage"];
|
final errorMsg = _transaction?.errorMessage;
|
||||||
if (errorMsg != null && errorMsg != "") {
|
if (errorMsg != null && errorMsg.isNotEmpty) {
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.warning_amber_rounded,
|
icon: Icons.warning_amber_rounded,
|
||||||
@@ -751,37 +757,37 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.tag_rounded,
|
icon: Icons.tag_rounded,
|
||||||
label: "Debit Reference",
|
label: "Debit Reference",
|
||||||
value: transactionController.model.receiptData?["debitRef"] ?? "",
|
value: _transaction?.debitRef ?? "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.phone_iphone_rounded,
|
icon: Icons.phone_iphone_rounded,
|
||||||
label: "From",
|
label: "From",
|
||||||
value: receiptData?["debitPhone"] ?? "",
|
value: receiptData?.debitPhone ?? "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.account_balance_wallet_rounded,
|
icon: Icons.account_balance_wallet_rounded,
|
||||||
label: "To",
|
label: "To",
|
||||||
value: receiptData?["creditAccount"] ?? "",
|
value: receiptData?.creditAccount ?? "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.memory_rounded,
|
icon: Icons.memory_rounded,
|
||||||
label: "Processor",
|
label: "Processor",
|
||||||
value: receiptData?["paymentProcessorName"] ?? "",
|
value: receiptData?.paymentProcessorName ?? "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
if (receiptData?["billProductName"] != null) {
|
if (receiptData?.billProductName != null) {
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.category_rounded,
|
icon: Icons.category_rounded,
|
||||||
label: "Bill Product",
|
label: "Bill Product",
|
||||||
value: receiptData?["billProductName"] ?? "",
|
value: receiptData?.billProductName ?? "",
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -822,12 +828,12 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
// Transaction Details Tab
|
// Transaction Details Tab
|
||||||
// ──────────────────────────────────────────────────────────────
|
// ──────────────────────────────────────────────────────────────
|
||||||
Widget _buildTransactionDetailsTab(ThemeData theme, bool isDark) {
|
Widget _buildTransactionDetailsTab(ThemeData theme, bool isDark) {
|
||||||
final receiptData = transactionController.model.receiptData;
|
final t = _transaction;
|
||||||
final amount = receiptData?["amount"] ?? 0;
|
final amount = t?.amount ?? Decimal.zero;
|
||||||
final charge = receiptData?["charge"] ?? 0;
|
final charge = t?.charge ?? Decimal.zero;
|
||||||
final gatewayCharge = receiptData?["gatewayCharge"] ?? 0;
|
final gatewayCharge = t?.gatewayCharge ?? Decimal.zero;
|
||||||
final tax = receiptData?["tax"] ?? 0;
|
final tax = t?.tax ?? Decimal.zero;
|
||||||
final totalAmount = receiptData?["totalAmount"] ?? 0;
|
final totalAmount = t?.totalAmount ?? Decimal.zero;
|
||||||
|
|
||||||
final items = <_DetailItem>[
|
final items = <_DetailItem>[
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
@@ -842,7 +848,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
),
|
),
|
||||||
];
|
];
|
||||||
|
|
||||||
if (charge != 0) {
|
if (charge != Decimal.zero) {
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.percent_rounded,
|
icon: Icons.percent_rounded,
|
||||||
@@ -851,7 +857,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (gatewayCharge != 0) {
|
if (gatewayCharge != Decimal.zero) {
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.account_balance_rounded,
|
icon: Icons.account_balance_rounded,
|
||||||
@@ -860,7 +866,7 @@ class _ReceiptScreenState extends State<ReceiptScreen>
|
|||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (tax != 0) {
|
if (tax != Decimal.zero) {
|
||||||
items.add(
|
items.add(
|
||||||
_DetailItem(
|
_DetailItem(
|
||||||
icon: Icons.receipt_rounded,
|
icon: Icons.receipt_rounded,
|
||||||
|
|||||||
153
lib/screens/transactions/transaction_model.dart
Normal file
153
lib/screens/transactions/transaction_model.dart
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
import 'package:json_annotation/json_annotation.dart';
|
||||||
|
import 'package:decimal/decimal.dart';
|
||||||
|
|
||||||
|
part 'transaction_model.g.dart';
|
||||||
|
|
||||||
|
/// JsonConverter for [Decimal] values — reads from [num] (int or double)
|
||||||
|
/// in JSON and writes back as [num].
|
||||||
|
class DecimalJsonConverter implements JsonConverter<Decimal, num> {
|
||||||
|
const DecimalJsonConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
Decimal fromJson(num json) => Decimal.parse(json.toString());
|
||||||
|
|
||||||
|
@override
|
||||||
|
num toJson(Decimal object) => object.toDouble();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JsonConverter for [DateTime] values — reads from ISO‑8601 [String]
|
||||||
|
/// and writes back as ISO‑8601 [String].
|
||||||
|
class DateTimeJsonConverter implements JsonConverter<DateTime, String> {
|
||||||
|
const DateTimeJsonConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
DateTime fromJson(String json) => DateTime.parse(json);
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toJson(DateTime object) => object.toIso8601String();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// JsonConverter for a list of key-value entries (additionalData).
|
||||||
|
class AdditionalDataJsonConverter
|
||||||
|
implements JsonConverter<List<Map<String, dynamic>>?, List<dynamic>?> {
|
||||||
|
const AdditionalDataJsonConverter();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Map<String, dynamic>>? fromJson(List<dynamic>? json) {
|
||||||
|
return json
|
||||||
|
?.map((e) => Map<String, dynamic>.from(e as Map))
|
||||||
|
.toList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<dynamic>? toJson(List<Map<String, dynamic>>? object) {
|
||||||
|
return object?.map((e) => e as dynamic).toList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@DecimalJsonConverter()
|
||||||
|
@DateTimeJsonConverter()
|
||||||
|
@AdditionalDataJsonConverter()
|
||||||
|
@JsonSerializable(explicitToJson: true)
|
||||||
|
class TransactionModel {
|
||||||
|
final String? id;
|
||||||
|
final DateTime? createdAt;
|
||||||
|
final String userId;
|
||||||
|
final String trace;
|
||||||
|
final Decimal amount;
|
||||||
|
final Decimal charge;
|
||||||
|
final Decimal gatewayCharge;
|
||||||
|
final Decimal tax;
|
||||||
|
final Decimal totalAmount;
|
||||||
|
final String reference;
|
||||||
|
final String paymentProcessorLabel;
|
||||||
|
final String integrationProcessorLabel;
|
||||||
|
final String confirmationProcessorLabel;
|
||||||
|
final String providerLabel;
|
||||||
|
final String debitPhone;
|
||||||
|
final String debitCurrency;
|
||||||
|
final String debitRef;
|
||||||
|
final String? creditPhone;
|
||||||
|
final String? creditAccount;
|
||||||
|
final String? creditRef;
|
||||||
|
final String? billClientId;
|
||||||
|
final String billName;
|
||||||
|
final String paymentProcessorName;
|
||||||
|
final String? providerImage;
|
||||||
|
final String? paymentProcessorImage;
|
||||||
|
final String confirmationStatus;
|
||||||
|
final String paymentStatus;
|
||||||
|
final String integrationStatus;
|
||||||
|
final String pollingStatus;
|
||||||
|
final String? orderId;
|
||||||
|
final String? orderTrace;
|
||||||
|
final String? orderTransactionTrace;
|
||||||
|
final String? erpSalesRef;
|
||||||
|
final String? erpSalesPaymentRef;
|
||||||
|
final int workflowId;
|
||||||
|
final String? type;
|
||||||
|
final String? authType;
|
||||||
|
final String? responseCode;
|
||||||
|
final String? status;
|
||||||
|
final String? errorMessage;
|
||||||
|
final String? billProductName;
|
||||||
|
final String? creditName;
|
||||||
|
final String? creditEmail;
|
||||||
|
final String? productUid;
|
||||||
|
final String? targetUrl;
|
||||||
|
final List<Map<String, dynamic>>? additionalData;
|
||||||
|
|
||||||
|
const TransactionModel({
|
||||||
|
this.id,
|
||||||
|
this.createdAt,
|
||||||
|
required this.userId,
|
||||||
|
required this.trace,
|
||||||
|
required this.amount,
|
||||||
|
required this.charge,
|
||||||
|
required this.gatewayCharge,
|
||||||
|
required this.tax,
|
||||||
|
required this.totalAmount,
|
||||||
|
required this.reference,
|
||||||
|
required this.paymentProcessorLabel,
|
||||||
|
required this.integrationProcessorLabel,
|
||||||
|
required this.confirmationProcessorLabel,
|
||||||
|
required this.providerLabel,
|
||||||
|
required this.debitPhone,
|
||||||
|
required this.debitCurrency,
|
||||||
|
required this.debitRef,
|
||||||
|
this.creditPhone,
|
||||||
|
this.creditAccount,
|
||||||
|
this.creditRef,
|
||||||
|
this.billClientId,
|
||||||
|
required this.billName,
|
||||||
|
required this.paymentProcessorName,
|
||||||
|
this.providerImage,
|
||||||
|
this.paymentProcessorImage,
|
||||||
|
required this.confirmationStatus,
|
||||||
|
required this.paymentStatus,
|
||||||
|
required this.integrationStatus,
|
||||||
|
required this.pollingStatus,
|
||||||
|
this.orderId,
|
||||||
|
this.orderTrace,
|
||||||
|
this.orderTransactionTrace,
|
||||||
|
this.erpSalesRef,
|
||||||
|
this.erpSalesPaymentRef,
|
||||||
|
required this.workflowId,
|
||||||
|
this.type,
|
||||||
|
this.authType,
|
||||||
|
this.responseCode,
|
||||||
|
this.status,
|
||||||
|
this.errorMessage,
|
||||||
|
this.billProductName,
|
||||||
|
this.creditName,
|
||||||
|
this.creditEmail,
|
||||||
|
this.productUid,
|
||||||
|
this.targetUrl,
|
||||||
|
this.additionalData,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory TransactionModel.fromJson(Map<String, dynamic> json) =>
|
||||||
|
_$TransactionModelFromJson(json);
|
||||||
|
|
||||||
|
Map<String, dynamic> toJson() => _$TransactionModelToJson(this);
|
||||||
|
}
|
||||||
132
lib/screens/transactions/transaction_model.g.dart
Normal file
132
lib/screens/transactions/transaction_model.g.dart
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||||
|
|
||||||
|
part of 'transaction_model.dart';
|
||||||
|
|
||||||
|
// **************************************************************************
|
||||||
|
// JsonSerializableGenerator
|
||||||
|
// **************************************************************************
|
||||||
|
|
||||||
|
TransactionModel _$TransactionModelFromJson(Map<String, dynamic> json) =>
|
||||||
|
TransactionModel(
|
||||||
|
id: json['id'] as String?,
|
||||||
|
createdAt: _$JsonConverterFromJson<String, DateTime>(
|
||||||
|
json['createdAt'],
|
||||||
|
const DateTimeJsonConverter().fromJson,
|
||||||
|
),
|
||||||
|
userId: json['userId'] as String,
|
||||||
|
trace: json['trace'] as String,
|
||||||
|
amount: const DecimalJsonConverter().fromJson(json['amount'] as num),
|
||||||
|
charge: const DecimalJsonConverter().fromJson(json['charge'] as num),
|
||||||
|
gatewayCharge: const DecimalJsonConverter().fromJson(
|
||||||
|
json['gatewayCharge'] as num,
|
||||||
|
),
|
||||||
|
tax: const DecimalJsonConverter().fromJson(json['tax'] as num),
|
||||||
|
totalAmount: const DecimalJsonConverter().fromJson(
|
||||||
|
json['totalAmount'] as num,
|
||||||
|
),
|
||||||
|
reference: json['reference'] as String,
|
||||||
|
paymentProcessorLabel: json['paymentProcessorLabel'] as String,
|
||||||
|
integrationProcessorLabel: json['integrationProcessorLabel'] as String,
|
||||||
|
confirmationProcessorLabel: json['confirmationProcessorLabel'] as String,
|
||||||
|
providerLabel: json['providerLabel'] as String,
|
||||||
|
debitPhone: json['debitPhone'] as String,
|
||||||
|
debitCurrency: json['debitCurrency'] as String,
|
||||||
|
debitRef: json['debitRef'] as String,
|
||||||
|
creditPhone: json['creditPhone'] as String?,
|
||||||
|
creditAccount: json['creditAccount'] as String?,
|
||||||
|
creditRef: json['creditRef'] as String?,
|
||||||
|
billClientId: json['billClientId'] as String?,
|
||||||
|
billName: json['billName'] as String,
|
||||||
|
paymentProcessorName: json['paymentProcessorName'] as String,
|
||||||
|
providerImage: json['providerImage'] as String?,
|
||||||
|
paymentProcessorImage: json['paymentProcessorImage'] as String?,
|
||||||
|
confirmationStatus: json['confirmationStatus'] as String,
|
||||||
|
paymentStatus: json['paymentStatus'] as String,
|
||||||
|
integrationStatus: json['integrationStatus'] as String,
|
||||||
|
pollingStatus: json['pollingStatus'] as String,
|
||||||
|
orderId: json['orderId'] as String?,
|
||||||
|
orderTrace: json['orderTrace'] as String?,
|
||||||
|
orderTransactionTrace: json['orderTransactionTrace'] as String?,
|
||||||
|
erpSalesRef: json['erpSalesRef'] as String?,
|
||||||
|
erpSalesPaymentRef: json['erpSalesPaymentRef'] as String?,
|
||||||
|
workflowId: (json['workflowId'] as num).toInt(),
|
||||||
|
type: json['type'] as String?,
|
||||||
|
authType: json['authType'] as String?,
|
||||||
|
responseCode: json['responseCode'] as String?,
|
||||||
|
status: json['status'] as String?,
|
||||||
|
errorMessage: json['errorMessage'] as String?,
|
||||||
|
billProductName: json['billProductName'] as String?,
|
||||||
|
creditName: json['creditName'] as String?,
|
||||||
|
creditEmail: json['creditEmail'] as String?,
|
||||||
|
productUid: json['productUid'] as String?,
|
||||||
|
targetUrl: json['targetUrl'] as String?,
|
||||||
|
additionalData: const AdditionalDataJsonConverter().fromJson(
|
||||||
|
json['additionalData'] as List?,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, dynamic> _$TransactionModelToJson(
|
||||||
|
TransactionModel instance,
|
||||||
|
) => <String, dynamic>{
|
||||||
|
'id': instance.id,
|
||||||
|
'createdAt': _$JsonConverterToJson<String, DateTime>(
|
||||||
|
instance.createdAt,
|
||||||
|
const DateTimeJsonConverter().toJson,
|
||||||
|
),
|
||||||
|
'userId': instance.userId,
|
||||||
|
'trace': instance.trace,
|
||||||
|
'amount': const DecimalJsonConverter().toJson(instance.amount),
|
||||||
|
'charge': const DecimalJsonConverter().toJson(instance.charge),
|
||||||
|
'gatewayCharge': const DecimalJsonConverter().toJson(instance.gatewayCharge),
|
||||||
|
'tax': const DecimalJsonConverter().toJson(instance.tax),
|
||||||
|
'totalAmount': const DecimalJsonConverter().toJson(instance.totalAmount),
|
||||||
|
'reference': instance.reference,
|
||||||
|
'paymentProcessorLabel': instance.paymentProcessorLabel,
|
||||||
|
'integrationProcessorLabel': instance.integrationProcessorLabel,
|
||||||
|
'confirmationProcessorLabel': instance.confirmationProcessorLabel,
|
||||||
|
'providerLabel': instance.providerLabel,
|
||||||
|
'debitPhone': instance.debitPhone,
|
||||||
|
'debitCurrency': instance.debitCurrency,
|
||||||
|
'debitRef': instance.debitRef,
|
||||||
|
'creditPhone': instance.creditPhone,
|
||||||
|
'creditAccount': instance.creditAccount,
|
||||||
|
'creditRef': instance.creditRef,
|
||||||
|
'billClientId': instance.billClientId,
|
||||||
|
'billName': instance.billName,
|
||||||
|
'paymentProcessorName': instance.paymentProcessorName,
|
||||||
|
'providerImage': instance.providerImage,
|
||||||
|
'paymentProcessorImage': instance.paymentProcessorImage,
|
||||||
|
'confirmationStatus': instance.confirmationStatus,
|
||||||
|
'paymentStatus': instance.paymentStatus,
|
||||||
|
'integrationStatus': instance.integrationStatus,
|
||||||
|
'pollingStatus': instance.pollingStatus,
|
||||||
|
'orderId': instance.orderId,
|
||||||
|
'orderTrace': instance.orderTrace,
|
||||||
|
'orderTransactionTrace': instance.orderTransactionTrace,
|
||||||
|
'erpSalesRef': instance.erpSalesRef,
|
||||||
|
'erpSalesPaymentRef': instance.erpSalesPaymentRef,
|
||||||
|
'workflowId': instance.workflowId,
|
||||||
|
'type': instance.type,
|
||||||
|
'authType': instance.authType,
|
||||||
|
'responseCode': instance.responseCode,
|
||||||
|
'status': instance.status,
|
||||||
|
'errorMessage': instance.errorMessage,
|
||||||
|
'billProductName': instance.billProductName,
|
||||||
|
'creditName': instance.creditName,
|
||||||
|
'creditEmail': instance.creditEmail,
|
||||||
|
'productUid': instance.productUid,
|
||||||
|
'targetUrl': instance.targetUrl,
|
||||||
|
'additionalData': const AdditionalDataJsonConverter().toJson(
|
||||||
|
instance.additionalData,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
|
||||||
|
Value? _$JsonConverterFromJson<Json, Value>(
|
||||||
|
Object? json,
|
||||||
|
Value? Function(Json json) fromJson,
|
||||||
|
) => json == null ? null : fromJson(json as Json);
|
||||||
|
|
||||||
|
Json? _$JsonConverterToJson<Json, Value>(
|
||||||
|
Value? value,
|
||||||
|
Json? Function(Value value) toJson,
|
||||||
|
) => value == null ? null : toJson(value);
|
||||||
249
lib/widgets/transaction_item_widget.dart
Normal file
249
lib/widgets/transaction_item_widget.dart
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:go_router/go_router.dart';
|
||||||
|
import 'package:skeletonizer/skeletonizer.dart';
|
||||||
|
import 'package:timeago/timeago.dart' as timeago;
|
||||||
|
|
||||||
|
/// A reusable transaction list item widget.
|
||||||
|
/// Used by both [HomeScreen] and [HistoryScreen].
|
||||||
|
class TransactionItemWidget extends StatefulWidget {
|
||||||
|
final Map<String, dynamic> transaction;
|
||||||
|
final bool enabled;
|
||||||
|
final Future<void> Function()? onRepeat;
|
||||||
|
|
||||||
|
const TransactionItemWidget({
|
||||||
|
super.key,
|
||||||
|
required this.transaction,
|
||||||
|
this.enabled = false,
|
||||||
|
this.onRepeat,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<TransactionItemWidget> createState() => _TransactionItemWidgetState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _TransactionItemWidgetState extends State<TransactionItemWidget> {
|
||||||
|
bool _isRepeating = false;
|
||||||
|
|
||||||
|
Future<void> _handleRepeat() async {
|
||||||
|
if (_isRepeating || widget.onRepeat == null) return;
|
||||||
|
setState(() => _isRepeating = true);
|
||||||
|
try {
|
||||||
|
await widget.onRepeat!();
|
||||||
|
} finally {
|
||||||
|
if (mounted) setState(() => _isRepeating = false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final theme = Theme.of(context);
|
||||||
|
final isDark = theme.brightness == Brightness.dark;
|
||||||
|
|
||||||
|
final status = widget.transaction['integrationStatus'] as String?;
|
||||||
|
final isSuccess = status == 'SUCCESS';
|
||||||
|
final isPending = status == 'PENDING';
|
||||||
|
|
||||||
|
final statusIcon = isSuccess
|
||||||
|
? Icons.check_circle_rounded
|
||||||
|
: isPending
|
||||||
|
? Icons.pending_rounded
|
||||||
|
: Icons.cancel_rounded;
|
||||||
|
final statusColor = isSuccess
|
||||||
|
? const Color(0xFF10B981)
|
||||||
|
: isPending
|
||||||
|
? const Color(0xFFF59E0B)
|
||||||
|
: const Color(0xFFEF4444);
|
||||||
|
|
||||||
|
return Skeletonizer(
|
||||||
|
enabled: widget.enabled,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 8),
|
||||||
|
child: Material(
|
||||||
|
color: Colors.transparent,
|
||||||
|
child: InkWell(
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
onTap: () {
|
||||||
|
context.push("/receipt/${widget.transaction['id']}");
|
||||||
|
},
|
||||||
|
child: Container(
|
||||||
|
padding:
|
||||||
|
const EdgeInsets.symmetric(vertical: 12, horizontal: 14),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isDark ? const Color(0xFF1A1A2E) : Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(16),
|
||||||
|
border: Border.all(
|
||||||
|
color: isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.06)
|
||||||
|
: Colors.grey.shade200,
|
||||||
|
width: 1,
|
||||||
|
),
|
||||||
|
boxShadow: [
|
||||||
|
BoxShadow(
|
||||||
|
color: isDark
|
||||||
|
? Colors.black.withValues(alpha: 0.2)
|
||||||
|
: Colors.black.withValues(alpha: 0.03),
|
||||||
|
blurRadius: 8,
|
||||||
|
offset: const Offset(0, 2),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
// Main row: provider image, bill name & date, amount
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
// Provider image
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
padding: const EdgeInsets.all(8),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: theme.colorScheme.primary.withValues(
|
||||||
|
alpha: 0.1,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
child: Image(
|
||||||
|
image: AssetImage(
|
||||||
|
"assets/${widget.transaction['paymentProcessorImage'] ?? ''}",
|
||||||
|
),
|
||||||
|
errorBuilder: (_, __, ___) => Icon(
|
||||||
|
Icons.receipt_long_rounded,
|
||||||
|
size: 20,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 14),
|
||||||
|
|
||||||
|
// Bill name & date
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Flexible(
|
||||||
|
child: Text(
|
||||||
|
widget.transaction['billName'] ?? '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isDark
|
||||||
|
? Colors.white
|
||||||
|
: Colors.black87,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Icon(
|
||||||
|
statusIcon,
|
||||||
|
size: 16,
|
||||||
|
color: statusColor,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
widget.transaction['createdAt'] != null
|
||||||
|
? timeago.format(
|
||||||
|
DateTime.parse(
|
||||||
|
widget.transaction['createdAt'],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: '',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w400,
|
||||||
|
color: isDark
|
||||||
|
? Colors.white54
|
||||||
|
: Colors.grey.shade500,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
// Amount
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
(widget.transaction['amount'] as num?)
|
||||||
|
?.toStringAsFixed(2) ??
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w700,
|
||||||
|
color: isDark ? Colors.white : Colors.black87,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
(widget.transaction['totalAmount'] as num?)
|
||||||
|
?.toStringAsFixed(2) ??
|
||||||
|
'',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: theme.colorScheme.primary.withValues(
|
||||||
|
alpha: 0.7,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
|
||||||
|
// Quick actions row (only shown when onRepeat is provided)
|
||||||
|
if (widget.onRepeat != null) ...[
|
||||||
|
const SizedBox(height: 10),
|
||||||
|
Divider(
|
||||||
|
height: 1,
|
||||||
|
color: isDark
|
||||||
|
? Colors.white.withValues(alpha: 0.06)
|
||||||
|
: Colors.grey.shade200,
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Align(
|
||||||
|
alignment: Alignment.centerRight,
|
||||||
|
child: TextButton.icon(
|
||||||
|
onPressed: _handleRepeat,
|
||||||
|
icon: _isRepeating
|
||||||
|
? SizedBox(
|
||||||
|
width: 16,
|
||||||
|
height: 16,
|
||||||
|
child: CircularProgressIndicator(
|
||||||
|
strokeWidth: 2,
|
||||||
|
color: theme.colorScheme.primary,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Icon(Icons.repeat_rounded, size: 16),
|
||||||
|
label: Text(
|
||||||
|
_isRepeating ? 'Repeating...' : 'Repeat',
|
||||||
|
style: const TextStyle(fontSize: 12),
|
||||||
|
),
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
horizontal: 12,
|
||||||
|
vertical: 4,
|
||||||
|
),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
16
pubspec.lock
16
pubspec.lock
@@ -177,6 +177,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.2"
|
version: "3.1.2"
|
||||||
|
decimal:
|
||||||
|
dependency: "direct main"
|
||||||
|
description:
|
||||||
|
name: decimal
|
||||||
|
sha256: fc706a5618b81e5b367b01dd62621def37abc096f2b46a9bd9068b64c1fa36d0
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.2.4"
|
||||||
dio:
|
dio:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
@@ -680,6 +688,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "1.5.0"
|
version: "1.5.0"
|
||||||
|
rational:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: rational
|
||||||
|
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.3"
|
||||||
share_plus:
|
share_plus:
|
||||||
dependency: "direct main"
|
dependency: "direct main"
|
||||||
description:
|
description:
|
||||||
|
|||||||
@@ -61,6 +61,7 @@ dependencies:
|
|||||||
google_sign_in_web: ^1.1.0
|
google_sign_in_web: ^1.1.0
|
||||||
flutter_web_plugins:
|
flutter_web_plugins:
|
||||||
sdk: flutter
|
sdk: flutter
|
||||||
|
decimal: ^3.2.4
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user