added wallet functionality

This commit is contained in:
Prince
2026-06-11 23:48:26 +02:00
parent ad79bf2af8
commit 2bcee59fd7
17 changed files with 1309 additions and 292 deletions

View File

@@ -1,6 +1,7 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:qpay/screens/groups/models/group_batch_model.dart';
import 'package:intl/intl.dart';
import 'package:qpay/screens/groups/models/recipient_group_model.dart';
import 'package:qpay/models/responsive_policy.dart';
import 'package:qpay/screens/groups/controllers/batch_controller.dart';
@@ -108,10 +109,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
return Column(
children: [
_buildMembersSheetHandle(),
_buildMembersSheetHeader(
setSheetState,
scrollController,
),
_buildMembersSheetHeader(setSheetState, scrollController),
Expanded(
child: ListenableBuilder(
listenable: groupController,
@@ -119,9 +117,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
final allMembers = groupController.model.members;
final filtered = _filteredMembers(allMembers);
if (groupController.model.isLoading) {
return _buildMembersSkeletonList(
scrollController,
);
return _buildMembersSkeletonList(scrollController);
}
if (allMembers.isEmpty) {
return _buildMembersEmptyState();
@@ -185,10 +181,9 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
width: 40,
height: 40,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.1),
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(12),
),
child: Icon(
@@ -237,10 +232,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
},
decoration: InputDecoration(
hintText: 'Search members...',
hintStyle: TextStyle(
color: Colors.grey.shade500,
fontSize: 14,
),
hintStyle: TextStyle(color: Colors.grey.shade500, fontSize: 14),
prefixIcon: Icon(
Icons.search,
color: Colors.grey.shade600,
@@ -301,19 +293,17 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
Widget _buildAnimatedMemberCard(int index, RecipientGroupMember member) {
final clampedIndex = index.clamp(0, 5);
final animation = Tween<Offset>(
begin: const Offset(0, 0.06),
end: Offset.zero,
).animate(
CurvedAnimation(
parent: _membersAnimController,
curve: Interval(
0.05 * clampedIndex,
1.0,
curve: Curves.easeOutCubic,
),
),
);
final animation =
Tween<Offset>(begin: const Offset(0, 0.06), end: Offset.zero).animate(
CurvedAnimation(
parent: _membersAnimController,
curve: Interval(
0.05 * clampedIndex,
1.0,
curve: Curves.easeOutCubic,
),
),
);
return FadeTransition(
opacity: _membersAnimController,
child: SlideTransition(
@@ -350,10 +340,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
],
),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 14,
horizontal: 14,
),
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 14),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
@@ -411,10 +398,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
gradient: LinearGradient(
colors: [
Theme.of(context).colorScheme.primary,
Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.7),
Theme.of(context).colorScheme.primary.withValues(alpha: 0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
@@ -422,10 +406,9 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.25),
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.25),
blurRadius: 6,
offset: const Offset(0, 2),
),
@@ -444,15 +427,13 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
);
}
Widget _buildMemberSubtitleRow(
Recipient recipient,
String? memberAccount,
) {
Widget _buildMemberSubtitleRow(Recipient recipient, String? memberAccount) {
final account = memberAccount?.isNotEmpty == true
? memberAccount
: recipient.account;
final hasAccount = account != null && account.isNotEmpty;
final hasProvider = recipient.latestProviderLabel != null &&
final hasProvider =
recipient.latestProviderLabel != null &&
recipient.latestProviderLabel!.isNotEmpty;
if (!hasAccount && !hasProvider) {
return const SizedBox.shrink();
@@ -464,15 +445,11 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
children: [
if (hasProvider)
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 2,
),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.08),
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.08),
borderRadius: BorderRadius.circular(6),
),
child: Text(
@@ -507,10 +484,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
Flexible(
child: Text(
text,
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade600,
),
style: TextStyle(fontSize: 12, color: Colors.grey.shade600),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
@@ -521,17 +495,11 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
Widget _buildMemberPopupMenu(RecipientGroupMember member) {
return PopupMenuButton<String>(
icon: Icon(
Icons.more_vert,
color: Colors.grey.shade500,
size: 20,
),
icon: Icon(Icons.more_vert, color: Colors.grey.shade500, size: 20),
padding: EdgeInsets.zero,
splashRadius: 20,
tooltip: 'More options',
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
onSelected: (value) async {
if (value == 'remove') {
await _confirmRemoveMember(member);
@@ -542,8 +510,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
value: 'remove',
child: Row(
children: [
Icon(Icons.person_remove_outlined,
size: 18, color: Colors.red),
Icon(Icons.person_remove_outlined, size: 18, color: Colors.red),
SizedBox(width: 10),
Text(
'Remove from group',
@@ -564,9 +531,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: const Text('Remove member'),
content: Text('Are you sure you want to remove $name from this group?'),
actions: [
@@ -628,10 +593,9 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
width: 88,
height: 88,
decoration: BoxDecoration(
color: Theme.of(context)
.colorScheme
.primary
.withValues(alpha: 0.08),
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.08),
shape: BoxShape.circle,
),
child: Icon(
@@ -690,10 +654,7 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
Text(
'No results for "$_memberQuery"',
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 13,
color: Colors.grey.shade600,
),
style: TextStyle(fontSize: 13, color: Colors.grey.shade600),
),
],
),
@@ -1163,6 +1124,8 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
final inSelectMode = batchController.model.selectMode;
final status = batch.status ?? 'UNKNOWN';
final createdAt = batch.createdAt;
final parsedDate = createdAt != null ? DateTime.tryParse(createdAt) : null;
return Card(
elevation: 0,
shape: RoundedRectangleBorder(
@@ -1227,25 +1190,44 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
color: Colors.grey.shade600,
),
),
if (batch.count != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
_countPill('${batch.count}', Colors.blue),
const SizedBox(width: 3),
_countPill(
'${batch.successfulCount ?? 0} ok',
Colors.green,
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (parsedDate != null)
Padding(
padding: const EdgeInsets.only(top: 6),
child: Text(
DateFormat.yMMMd().add_jm().format(parsedDate),
style: TextStyle(
fontSize: 12,
color: Colors.grey.shade500,
),
),
const SizedBox(width: 3),
_countPill(
'${batch.failedCount ?? 0} failed',
Colors.red,
),
if (batch.count != null)
Padding(
padding: const EdgeInsets.only(top: 4),
child: Row(
children: [
_statIcon(
Icons.people_outline,
'${batch.count}',
),
const SizedBox(width: 4),
_statIcon(
Icons.check_circle_outline,
'${batch.successfulCount ?? 0}',
),
const SizedBox(width: 4),
_statIcon(
Icons.cancel_outlined,
'${batch.failedCount ?? 0}',
),
],
),
],
),
),
),
],
),
],
),
),
@@ -1258,22 +1240,21 @@ class _GroupDetailScreenState extends State<GroupDetailScreen>
);
}
Widget _countPill(String label, Color color) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
decoration: BoxDecoration(
color: color.withAlpha(20),
borderRadius: BorderRadius.circular(4),
border: Border.all(color: color.withAlpha(60)),
),
child: Text(
label,
style: TextStyle(
fontSize: 11,
color: color,
fontWeight: FontWeight.w500,
Widget _statIcon(IconData icon, String label) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 12, color: Colors.grey.shade500),
const SizedBox(width: 3),
Text(
label,
style: TextStyle(
fontSize: 11,
color: Colors.grey.shade600,
fontWeight: FontWeight.w500,
),
),
),
],
);
}