636 lines
24 KiB
Dart
636 lines
24 KiB
Dart
import 'dart:async';
|
|
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:qpay/models/responsive_policy.dart';
|
|
import 'package:qpay/screens/transaction_controller.dart';
|
|
import 'package:skeletonizer/skeletonizer.dart';
|
|
import 'package:qpay/screens/recipient/recipients_controller.dart';
|
|
import 'package:shared_preferences/shared_preferences.dart';
|
|
|
|
class RecipientsScreen extends StatefulWidget {
|
|
const RecipientsScreen({super.key});
|
|
|
|
@override
|
|
State<RecipientsScreen> createState() => _RecipientsScreenState();
|
|
}
|
|
|
|
class _RecipientsScreenState extends State<RecipientsScreen>
|
|
with TickerProviderStateMixin {
|
|
late SharedPreferences prefs;
|
|
late AnimationController _animationController;
|
|
late RecipientsController controller;
|
|
late TransactionController transactionController;
|
|
|
|
final List<Animation<Offset>> _alignListAnimations = [];
|
|
final _accountController = TextEditingController();
|
|
final _queryParams = <String, String>{};
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
|
|
controller = RecipientsController(context);
|
|
setupData();
|
|
|
|
controller.model.creditAccount = _accountController.text;
|
|
transactionController = Provider.of<TransactionController>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
|
|
_animationController = AnimationController(
|
|
duration: const Duration(milliseconds: 600),
|
|
vsync: this,
|
|
)..forward();
|
|
|
|
List tranListIndices = [0, 1];
|
|
for (int i = 0; i < tranListIndices.length; i++) {
|
|
_alignListAnimations.add(
|
|
Tween<Offset>(begin: Offset(0, -0.25), end: Offset.zero).animate(
|
|
CurvedAnimation(
|
|
parent: _animationController,
|
|
curve: Interval(0.075 * i, 1.0, curve: Curves.easeOut),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
Future<void> setupData() async {
|
|
prefs = await SharedPreferences.getInstance();
|
|
_queryParams['userId'] = prefs.getString("userId")!;
|
|
controller.getRecipients(_queryParams);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animationController.dispose();
|
|
controller.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: AppBar(
|
|
title: Text("Recipients", style: TextStyle(color: Colors.black87)),
|
|
),
|
|
body: ListenableBuilder(
|
|
listenable: controller,
|
|
builder: (context, child) {
|
|
return SafeArea(
|
|
child: SingleChildScrollView(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Center(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
return SizedBox(
|
|
width: double.parse(
|
|
constraints.maxWidth > ResponsivePolicy.md
|
|
? ResponsivePolicy.md.toString()
|
|
: constraints.maxWidth.toString(),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
_buildTransactionDetailsCard(
|
|
label: "Provider",
|
|
value:
|
|
controller.model.selectedProvider?.name ?? "",
|
|
),
|
|
const SizedBox(height: 20),
|
|
_buildAccountField(controller),
|
|
const SizedBox(height: 20),
|
|
controller.model.creditAccount != null &&
|
|
controller.model.creditAccount!.isNotEmpty
|
|
? SlideTransition(
|
|
position: _alignListAnimations[0],
|
|
child: ElevatedButton(
|
|
onPressed: () {
|
|
String phone = '';
|
|
if (!transactionController
|
|
.model
|
|
.selectedProvider!
|
|
.requiresAccount) {
|
|
phone = _accountController.text;
|
|
}
|
|
transactionController.model.formData =
|
|
transactionController.model.formData
|
|
.copyWith(
|
|
creditAccount:
|
|
_accountController.text,
|
|
creditPhone: phone,
|
|
);
|
|
context.push("/make-payment");
|
|
},
|
|
style: ElevatedButton.styleFrom(
|
|
backgroundColor: Theme.of(
|
|
context,
|
|
).colorScheme.primary,
|
|
foregroundColor: Theme.of(
|
|
context,
|
|
).colorScheme.onPrimary,
|
|
),
|
|
child: Row(
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.center,
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
"Use as new recipient",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
SizedBox(width: 5),
|
|
Icon(Icons.arrow_forward_ios, size: 12),
|
|
],
|
|
),
|
|
),
|
|
)
|
|
: SizedBox(),
|
|
const SizedBox(height: 20),
|
|
Text(
|
|
"Recent Recipients",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.bold,
|
|
),
|
|
),
|
|
const SizedBox(height: 10),
|
|
_buildRecipientItems(controller),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildAccountField(RecipientsController controller) {
|
|
String fieldName =
|
|
controller.model.selectedProvider?.accountFieldName ?? "Account Number";
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
children: [
|
|
Text(fieldName, style: TextStyle(fontSize: 16)),
|
|
if (!kIsWeb)
|
|
TextButton(
|
|
onPressed: () async {
|
|
if (await FlutterContacts.requestPermission()) {
|
|
final contact = await FlutterContacts.openExternalPick();
|
|
if (contact != null) {
|
|
_accountController.text = contact.phones.first.number;
|
|
controller.updateCreditAccount(
|
|
contact.phones.first.number,
|
|
);
|
|
}
|
|
}
|
|
},
|
|
child: Text(
|
|
"Fetch from contacts",
|
|
style: TextStyle(fontSize: 12, color: Colors.red),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: TextFormField(
|
|
controller: _accountController,
|
|
keyboardType: TextInputType.number,
|
|
decoration: InputDecoration(
|
|
hintText: 'Enter / Search $fieldName',
|
|
focusedErrorBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(context).colorScheme.secondary,
|
|
),
|
|
),
|
|
enabledBorder: OutlineInputBorder(
|
|
borderRadius: BorderRadius.circular(12),
|
|
borderSide: BorderSide(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.2),
|
|
),
|
|
),
|
|
),
|
|
onChanged: (value) {
|
|
controller.updateCreditAccount(value);
|
|
},
|
|
validator: (value) {
|
|
if (value == null || value.isEmpty) {
|
|
return 'Please enter an amount';
|
|
}
|
|
if (double.tryParse(value) == null) {
|
|
return 'Please enter a valid amount';
|
|
}
|
|
return null;
|
|
},
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () {
|
|
_queryParams.clear();
|
|
setState(() {
|
|
_queryParams['account'] = _accountController.text;
|
|
_queryParams['email'] = _accountController.text;
|
|
_queryParams['phoneNumber'] = _accountController.text;
|
|
_queryParams['name'] = _accountController.text;
|
|
_queryParams['userId'] = prefs.getString("userId")!;
|
|
|
|
controller.getRecipients(_queryParams);
|
|
});
|
|
},
|
|
icon: Icon(
|
|
Icons.search,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
size: 24,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRecipientItems(RecipientsController controller) {
|
|
if (controller.model.recipients.isEmpty && !controller.model.isLoading) {
|
|
return Center(
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(vertical: 40),
|
|
child: Column(
|
|
children: [
|
|
Icon(Icons.people_outline, size: 64, color: Colors.grey.shade400),
|
|
const SizedBox(height: 16),
|
|
Text(
|
|
"No recipients yet",
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
color: Colors.grey.shade600,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
return ListView.separated(
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
itemCount: controller.model.recipients.length,
|
|
separatorBuilder: (_, __) => const SizedBox(height: 10),
|
|
itemBuilder: (context, index) {
|
|
final recipient = controller.model.recipients[index];
|
|
final animIdx = index < _alignListAnimations.length
|
|
? index
|
|
: _alignListAnimations.length - 1;
|
|
|
|
return Skeletonizer(
|
|
enabled: controller.model.isLoading,
|
|
child: SlideTransition(
|
|
position:
|
|
_alignListAnimations[animIdx.clamp(
|
|
0,
|
|
_alignListAnimations.length - 1,
|
|
)],
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: InkWell(
|
|
onTap: () {
|
|
String account = recipient.account ?? '';
|
|
if (!transactionController
|
|
.model
|
|
.selectedProvider!
|
|
.requiresAccount) {
|
|
account = recipient.phoneNumber ?? '';
|
|
}
|
|
transactionController
|
|
.model
|
|
.formData = transactionController.model.formData.copyWith(
|
|
creditAccount: account,
|
|
creditName: recipient.name ?? '',
|
|
creditPhone: recipient.phoneNumber ?? '',
|
|
creditEmail: recipient.email ?? '',
|
|
providerLabel:
|
|
transactionController
|
|
.model
|
|
.formData
|
|
.providerLabel
|
|
.isEmpty
|
|
? recipient.latestProviderLabel ?? ''
|
|
: transactionController.model.formData.providerLabel,
|
|
);
|
|
context.push("/make-payment");
|
|
},
|
|
borderRadius: BorderRadius.circular(16),
|
|
child: Container(
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.grey.shade200, width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(
|
|
vertical: 14,
|
|
horizontal: 16,
|
|
),
|
|
child: Row(
|
|
children: [
|
|
// Avatar
|
|
Container(
|
|
width: 50,
|
|
height: 50,
|
|
decoration: BoxDecoration(
|
|
gradient: LinearGradient(
|
|
colors: [
|
|
Theme.of(context).colorScheme.primary,
|
|
Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.7),
|
|
],
|
|
begin: Alignment.topLeft,
|
|
end: Alignment.bottomRight,
|
|
),
|
|
shape: BoxShape.circle,
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary.withValues(alpha: 0.25),
|
|
blurRadius: 6,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
recipient.initials ?? 'PK',
|
|
style: TextStyle(
|
|
fontSize: 18,
|
|
fontWeight: FontWeight.bold,
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
),
|
|
const SizedBox(width: 16),
|
|
// Details
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Name
|
|
if (recipient.name != null &&
|
|
recipient.name!.isNotEmpty)
|
|
Text(
|
|
recipient.name ?? '',
|
|
style: TextStyle(
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
if (recipient.name != null &&
|
|
recipient.name!.isNotEmpty)
|
|
const SizedBox(height: 4),
|
|
// Account number and provider label row
|
|
Row(
|
|
children: [
|
|
if (recipient.latestProviderLabel != null &&
|
|
recipient.latestProviderLabel!.isNotEmpty)
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 8,
|
|
vertical: 2,
|
|
),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context)
|
|
.colorScheme
|
|
.primary
|
|
.withValues(alpha: 0.08),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
child: Text(
|
|
recipient.latestProviderLabel ?? '',
|
|
style: TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: FontWeight.w600,
|
|
color: Theme.of(
|
|
context,
|
|
).colorScheme.primary,
|
|
),
|
|
),
|
|
),
|
|
if (recipient.latestProviderLabel != null &&
|
|
recipient
|
|
.latestProviderLabel!
|
|
.isNotEmpty &&
|
|
recipient.account != null &&
|
|
recipient.account!.isNotEmpty)
|
|
const SizedBox(width: 6),
|
|
if (recipient.account != null &&
|
|
recipient.account!.isNotEmpty)
|
|
Expanded(
|
|
child: Text(
|
|
recipient.account ?? '',
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
color: Colors.grey.shade700,
|
|
fontWeight: FontWeight.w500,
|
|
),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 4),
|
|
// Phone / Email row
|
|
if ((recipient.phoneNumber != null &&
|
|
recipient.phoneNumber!.isNotEmpty) ||
|
|
(recipient.email != null &&
|
|
recipient.email!.isNotEmpty))
|
|
Row(
|
|
spacing: 8,
|
|
children: [
|
|
if (recipient.phoneNumber != null &&
|
|
recipient.phoneNumber!.isNotEmpty)
|
|
_buildContactChip(
|
|
Icons.phone_outlined,
|
|
recipient.phoneNumber!,
|
|
Colors.grey.shade600,
|
|
),
|
|
if (recipient.email != null &&
|
|
recipient.email!.isNotEmpty)
|
|
_buildContactChip(
|
|
Icons.email_outlined,
|
|
recipient.email!,
|
|
Colors.grey.shade500,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Chevron
|
|
Container(
|
|
width: 28,
|
|
height: 28,
|
|
decoration: BoxDecoration(
|
|
color: Colors.grey.shade100,
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: Icon(
|
|
Icons.arrow_forward_ios,
|
|
size: 12,
|
|
color: Colors.grey.shade400,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildContactChip(IconData icon, String text, Color color) {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon, size: 13, color: color),
|
|
const SizedBox(width: 3),
|
|
Flexible(
|
|
child: Text(
|
|
text,
|
|
style: TextStyle(fontSize: 12, color: color),
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildTransactionDetailsCard({
|
|
required String label,
|
|
String? value,
|
|
List<Widget>? children,
|
|
}) {
|
|
return Container(
|
|
width: double.infinity,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(16),
|
|
border: Border.all(color: Colors.grey.shade200, width: 1),
|
|
boxShadow: [
|
|
BoxShadow(
|
|
color: Colors.black.withValues(alpha: 0.04),
|
|
blurRadius: 8,
|
|
offset: const Offset(0, 2),
|
|
),
|
|
],
|
|
),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(20),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
Icon(
|
|
Icons.receipt_long_outlined,
|
|
size: 18,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
),
|
|
const SizedBox(width: 8),
|
|
Text(
|
|
"TRANSACTION DETAILS",
|
|
style: TextStyle(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: Theme.of(context).colorScheme.primary,
|
|
letterSpacing: 1.0,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 16),
|
|
Divider(height: 1, color: Colors.grey.shade200),
|
|
const SizedBox(height: 16),
|
|
if (children != null)
|
|
...children
|
|
else
|
|
_buildDetailRow(
|
|
icon: Icons.business_outlined,
|
|
label: label,
|
|
value: value ?? '',
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildDetailRow({
|
|
required IconData icon,
|
|
required String label,
|
|
required String value,
|
|
}) {
|
|
return Padding(
|
|
padding: const EdgeInsets.only(bottom: 12),
|
|
child: Row(
|
|
children: [
|
|
Icon(icon, size: 16, color: Colors.grey.shade500),
|
|
const SizedBox(width: 10),
|
|
Text(
|
|
label,
|
|
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
value,
|
|
style: TextStyle(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w600,
|
|
color: Colors.black87,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|