completed group batch feature

This commit is contained in:
2026-06-08 09:17:36 +02:00
parent 2dd790fe6e
commit fc616d6316
46 changed files with 4610 additions and 2669 deletions

View File

@@ -1,11 +1,13 @@
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import 'package:provider/provider.dart';
import 'package:qpay/auth_state.dart';
import 'package:qpay/models/responsive_policy.dart';
import 'package:qpay/screens/home/home_controller.dart';
import 'package:qpay/screens/receipt/receipt_controller.dart';
import 'package:qpay/screens/transaction_controller.dart';
import 'package:qpay/screens/transactions/transaction_model.dart' as txn;
import 'package:qpay/widgets/app_snack_bar.dart';
import 'package:qpay/widgets/transaction_item_widget.dart';
import 'package:skeletonizer/skeletonizer.dart';
import 'package:shared_preferences/shared_preferences.dart';
@@ -194,6 +196,8 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
return LayoutBuilder(
builder: (context, constraints) {
final isMobile = constraints.maxWidth < ResponsivePolicy.md;
return Scaffold(
backgroundColor: isDark
? const Color(0xFF0D0D0D)
@@ -206,69 +210,21 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
child: CustomScrollView(
physics: const BouncingScrollPhysics(),
slivers: [
SliverAppBar(
pinned: false,
floating: true,
stretch: true,
surfaceTintColor: Colors.transparent,
backgroundColor: isDark
? const Color(0xFF0D0D0D)
: const Color(0xFFF8F9FA),
title: constraints.maxWidth < ResponsivePolicy.md
? Image(
image: const AssetImage('assets/velocity.png'),
width: 130,
)
: null,
actions: [
if (_isLoggedIn) ...[
Container(
width: 30,
height: 30,
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.12)
: Colors.black87.withAlpha(20),
),
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
theme.colorScheme.primary.withValues(
alpha: 0.15,
),
theme.colorScheme.tertiary.withValues(
alpha: 0.05,
),
],
stops: const [0.3, 0.7],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
),
),
child: Center(
child: Text(
initials,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
),
),
IconButton(
icon: Icon(
Icons.logout_rounded,
color: isDark ? Colors.white54 : Colors.black54,
size: 20,
),
onPressed: _showLogoutDialog,
),
],
],
),
if (isMobile)
SliverAppBar(
pinned: false,
floating: true,
stretch: true,
surfaceTintColor: Colors.transparent,
backgroundColor: isDark
? const Color(0xFF0D0D0D)
: const Color(0xFFF8F9FA),
title: Image(
image: const AssetImage('assets/velocity.png'),
width: 130,
),
actions: _buildAppBarActions(theme, isDark),
),
SliverToBoxAdapter(
child: Center(
child: SizedBox(
@@ -282,6 +238,10 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isMobile) ...[
_buildWebHeader(theme, isDark),
const SizedBox(height: 16),
],
FadeTransition(
opacity: _providersFadeAnimation,
child: SlideTransition(
@@ -323,6 +283,71 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
);
}
// ──────────────────────────────────────────────────────────────
// App Bar Actions (shared between mobile SliverAppBar and web header)
// ──────────────────────────────────────────────────────────────
List<Widget> _buildAppBarActions(ThemeData theme, bool isDark) {
return [
if (_isLoggedIn) ...[
Container(
width: 30,
height: 30,
margin: const EdgeInsets.only(right: 4),
decoration: BoxDecoration(
border: Border.all(
color: isDark
? Colors.white.withValues(alpha: 0.12)
: Colors.black87.withAlpha(20),
),
shape: BoxShape.circle,
gradient: LinearGradient(
colors: [
theme.colorScheme.primary.withValues(alpha: 0.15),
theme.colorScheme.tertiary.withValues(alpha: 0.05),
],
stops: const [0.3, 0.7],
begin: Alignment.topRight,
end: Alignment.bottomLeft,
),
),
child: Center(
child: Text(
initials,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: isDark ? Colors.white : Colors.black87,
),
),
),
),
IconButton(
icon: Icon(
Icons.logout_rounded,
color: isDark ? Colors.white54 : Colors.black54,
size: 20,
),
onPressed: _showLogoutDialog,
),
],
];
}
// ──────────────────────────────────────────────────────────────
// Web Header (replaces SliverAppBar on web/tablet)
// ──────────────────────────────────────────────────────────────
Widget _buildWebHeader(ThemeData theme, bool isDark) {
return Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
Row(
mainAxisSize: MainAxisSize.min,
children: _buildAppBarActions(theme, isDark),
),
],
);
}
// ──────────────────────────────────────────────────────────────
// Providers Section
// ──────────────────────────────────────────────────────────────
@@ -384,21 +409,17 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
_isZWGSelected = !_isZWGSelected;
});
if (_isZWGSelected) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Center(
child: const Text(
'ZWG currency not supported yet',
),
),
behavior: SnackBarBehavior.floating,
backgroundColor: theme.colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: const Duration(seconds: 3),
width: 300,
AppSnackBar.show(
context,
'ZWG currency not supported yet',
customContent: const Center(
child: Text('ZWG currency not supported yet'),
),
backgroundColor: theme.colorScheme.primary,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
duration: const Duration(seconds: 3),
);
}
@@ -749,28 +770,12 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Register or Login for more',
'Register or Login for more features',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: isDark ? Colors.white : Colors.black87,
),
),
const SizedBox(height: 2),
InkWell(
onTap: () => _showFeaturesPopup(context),
borderRadius: BorderRadius.circular(4),
child: Text(
'features',
style: TextStyle(
fontSize: 13,
fontWeight: FontWeight.w500,
decoration: TextDecoration.underline,
decorationColor: theme.colorScheme.primary,
color: theme.colorScheme.primary,
),
),
),
],
),
),
@@ -825,6 +830,7 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
setState(() {
_isLoggedIn = false;
});
authState.refresh();
setupDataAndTransitions();
},
),
@@ -833,201 +839,4 @@ class _HomeScreenState extends State<HomeScreen> with TickerProviderStateMixin {
},
);
}
// ──────────────────────────────────────────────────────────────
// Features Popup
// ──────────────────────────────────────────────────────────────
void _showFeaturesPopup(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
elevation: 0,
backgroundColor: Colors.transparent,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 600),
child: Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
shape: BoxShape.rectangle,
color: Theme.of(context).colorScheme.surface,
borderRadius: BorderRadius.circular(20),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.1),
blurRadius: 20,
offset: const Offset(0, 10),
),
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
color: Theme.of(
context,
).colorScheme.primary.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Image(
image: AssetImage("assets/favicon.png"),
width: 45,
),
),
const SizedBox(height: 16),
Text(
'Unlock Additional Features',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Theme.of(context).colorScheme.onSurface,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
Text(
'Register or login to access these great features',
style: TextStyle(
fontSize: 14,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.7),
),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
_buildFeatureItem(
Icons.account_balance_wallet,
'Dual Currency Wallets',
'USD and ZWG wallets for prepayments that can be utilized later',
),
const SizedBox(height: 16),
_buildFeatureItem(
Icons.history,
'Transaction History',
'All transactions are saved for reference from any device',
),
const SizedBox(height: 16),
_buildFeatureItem(
Icons.people,
'Recipient Management',
'Save and manage recipient details for future use',
),
const SizedBox(height: 16),
_buildFeatureItem(
Icons.schedule,
'Smart Payments',
'Schedule payments and split bills with friends',
),
const SizedBox(height: 24),
Row(
children: [
Expanded(
child: TextButton(
onPressed: () => Navigator.of(context).pop(),
style: TextButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: Text(
'Maybe Later',
style: TextStyle(
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.6),
fontSize: 16,
),
),
),
),
const SizedBox(width: 12),
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
context.push('/onboarding/landing');
},
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(vertical: 12),
backgroundColor: Theme.of(
context,
).colorScheme.primary,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
child: const Text(
'Get Started',
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
),
),
),
),
],
),
],
),
),
),
);
},
);
}
Widget _buildFeatureItem(IconData icon, String title, String description) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.primary.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
),
child: Icon(
icon,
size: 20,
color: Theme.of(context).colorScheme.primary,
),
),
const SizedBox(width: 10),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: TextStyle(
fontSize: 16,
fontWeight: FontWeight.w600,
color: Theme.of(context).colorScheme.onSurface,
),
),
const SizedBox(height: 8),
Text(
description,
style: TextStyle(
fontSize: 14,
color: Theme.of(
context,
).colorScheme.onSurface.withValues(alpha: 0.7),
height: 1.4,
),
),
],
),
),
],
);
}
}