completed velocity integration

This commit is contained in:
2026-05-27 19:01:24 +02:00
parent fec46fb6e9
commit 6fdd3183fb
29 changed files with 2760 additions and 2293 deletions

View File

@@ -89,43 +89,18 @@ class _RecipientsScreenState extends State<RecipientsScreen>
child: LayoutBuilder(
builder: (context, constraints) {
return SizedBox(
width: double.parse(constraints.maxWidth > ResponsivePolicy.md ?
ResponsivePolicy.md.toString() :
constraints.maxWidth.toString()),
width: double.parse(
constraints.maxWidth > ResponsivePolicy.md
? ResponsivePolicy.md.toString()
: constraints.maxWidth.toString(),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black87.withAlpha(20)),
borderRadius: BorderRadius.circular(12),
),
padding: EdgeInsets.symmetric(vertical: 10, horizontal: 20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
"TRANSACTIONS DETAILS",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text("Provider",
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
)),
Text(
controller.model.selectedProvider?.name ?? "",
style: TextStyle(fontSize: 16),
),
],
),
],
),
_buildTransactionDetailsCard(
label: "Provider",
value:
controller.model.selectedProvider?.name ?? "",
),
const SizedBox(height: 20),
_buildAccountField(controller),
@@ -133,58 +108,67 @@ class _RecipientsScreenState extends State<RecipientsScreen>
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,
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(width: 5),
Icon(Icons.arrow_forward_ios, size: 12),
],
),
),
),
)
)
: SizedBox(),
const SizedBox(height: 20),
Text(
"Recent Recipients",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 10),
_buildRecipientItems(controller),
],
),
);
}
},
),
),
),
@@ -204,22 +188,24 @@ class _RecipientsScreenState extends State<RecipientsScreen>
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);
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),
},
child: Text(
"Fetch from contacts",
style: TextStyle(fontSize: 12, color: Colors.red),
),
),
),
],
),
const SizedBox(height: 12),
@@ -242,7 +228,7 @@ class _RecipientsScreenState extends State<RecipientsScreen>
borderSide: BorderSide(
color: Theme.of(
context,
).colorScheme.primary.withOpacity(0.2),
).colorScheme.primary.withValues(alpha: 0.2),
),
),
),
@@ -287,121 +273,363 @@ class _RecipientsScreenState extends State<RecipientsScreen>
Widget _buildRecipientItems(RecipientsController controller) {
if (controller.model.recipients.isEmpty && !controller.model.isLoading) {
return Center(child: Text("No recipients yet"));
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 Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
...controller.model.recipients.map(
(recipient) => Skeletonizer(
enabled: controller.model.isLoading,
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(12),
child: Container(
padding: EdgeInsets.all(5),
child: SlideTransition(
position: _alignListAnimations[0],
child: Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: Text(
recipient.initials ?? 'PK',
style: TextStyle(
fontSize: 25,
fontWeight: FontWeight.bold,
),
),
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),
),
SizedBox(width: 20),
Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text(
recipient.latestProviderLabel ?? '',
style: TextStyle(fontWeight: FontWeight.bold),
),
SizedBox(width: 5),
Text(
recipient.account ?? '',
style: TextStyle(fontWeight: FontWeight.normal),
],
),
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),
),
],
),
if ((recipient.phoneNumber != null &&
recipient.phoneNumber!.isNotEmpty) ||
(recipient.email != null &&
recipient.email!.isNotEmpty))
Row(
spacing: 5,
children: [
if (recipient.phoneNumber != null &&
recipient.phoneNumber!.isNotEmpty)
Text(
recipient.phoneNumber ?? '',
style: TextStyle(
fontWeight: FontWeight.bold,
),
),
if (recipient.email != null &&
recipient.email!.isNotEmpty)
Text(
recipient.email ?? '',
style: TextStyle(
fontWeight: FontWeight.normal,
),
),
],
alignment: Alignment.center,
child: Text(
recipient.initials ?? 'PK',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.white,
),
if (recipient.name != null &&
recipient.name!.isNotEmpty)
Text(
recipient.name ?? '',
style: TextStyle(fontWeight: FontWeight.normal),
),
],
),
],
),
),
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,
),
),
],
),
);
}
}