489 lines
20 KiB
Dart
489 lines
20 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
|
import 'package:flutter_svg/svg.dart';
|
|
import 'package:kmobile/di/injection.dart';
|
|
import 'package:kmobile/features/accounts/screens/account_info_screen.dart';
|
|
import 'package:kmobile/features/accounts/screens/account_statement_screen.dart';
|
|
import 'package:kmobile/features/auth/controllers/auth_cubit.dart';
|
|
import 'package:kmobile/features/auth/controllers/auth_state.dart';
|
|
import 'package:kmobile/features/customer_info/screens/customer_info_screen.dart';
|
|
import 'package:kmobile/features/beneficiaries/screens/manage_beneficiaries_screen.dart';
|
|
import 'package:kmobile/features/dashboard/widgets/transaction_list_placeholder.dart';
|
|
import 'package:kmobile/features/enquiry/screens/enquiry_screen.dart';
|
|
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
|
|
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
|
|
import 'package:kmobile/security/secure_storage.dart';
|
|
import 'package:kmobile/src/preferences/preference.dart';
|
|
import 'package:local_auth/local_auth.dart';
|
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:shimmer/shimmer.dart';
|
|
|
|
class DashboardScreen extends StatefulWidget {
|
|
const DashboardScreen({super.key});
|
|
|
|
@override
|
|
State<DashboardScreen> createState() => _DashboardScreenState();
|
|
}
|
|
|
|
class _DashboardScreenState extends State<DashboardScreen> {
|
|
int selectedAccountIndex = 0;
|
|
bool isVisible = false;
|
|
bool isRefreshing = false;
|
|
bool isBalanceLoading = false;
|
|
bool _biometricPromptShown = false;
|
|
|
|
Future<void> _refreshAccountData(BuildContext context) async {
|
|
setState(() {
|
|
isRefreshing = true;
|
|
});
|
|
try {
|
|
// Call your AuthCubit or repository to refresh user/accounts data
|
|
await context.read<AuthCubit>().refreshUserData();
|
|
} catch (e) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('Failed to refresh data')),
|
|
);
|
|
}
|
|
setState(() {
|
|
isRefreshing = false;
|
|
});
|
|
}
|
|
|
|
Widget _buildBalanceShimmer() {
|
|
return Shimmer.fromColors(
|
|
baseColor: Colors.white.withOpacity(0.7),
|
|
highlightColor: Colors.white.withOpacity(0.3),
|
|
child: Container(
|
|
width: 100,
|
|
height: 32,
|
|
color: Colors.white,
|
|
),
|
|
);
|
|
}
|
|
|
|
String getProcessedFirstName(String? name) {
|
|
if (name == null || name.trim().isEmpty) return '';
|
|
// Remove common titles
|
|
final titles = [
|
|
'mr.',
|
|
'mrs.',
|
|
'ms.',
|
|
'miss',
|
|
'dr.',
|
|
'shri',
|
|
'smt.',
|
|
'kumari'
|
|
];
|
|
String processed = name.trim().toLowerCase();
|
|
for (final title in titles) {
|
|
if (processed.startsWith(title)) {
|
|
processed = processed.replaceFirst(title, '').trim();
|
|
break;
|
|
}
|
|
}
|
|
// Take the first word (first name)
|
|
final firstName = processed.split(' ').first;
|
|
// Convert to title case
|
|
if (firstName.isEmpty) return '';
|
|
return firstName[0].toUpperCase() + firstName.substring(1);
|
|
}
|
|
|
|
String getFullAccountType(String? accountType) {
|
|
if (accountType == null || accountType.isEmpty) return 'N/A';
|
|
// Convert to title case
|
|
switch (accountType.toLowerCase()) {
|
|
case 'sa':
|
|
return 'Savings Account';
|
|
case 'ln':
|
|
return 'Loan Account';
|
|
case 'td':
|
|
return 'Term Deposit Account';
|
|
case 'rd':
|
|
return 'Recurring Deposit Account';
|
|
default:
|
|
return 'Unknown Account Type';
|
|
}
|
|
}
|
|
|
|
Future<void> _showBiometricOptInDialog() async {
|
|
final storage = SecureStorage();
|
|
showDialog(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (_) => AlertDialog(
|
|
title: const Text('Enable Biometric Authentication'),
|
|
content: const Text('Use fingerprint/face ID for faster login?'),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () async {
|
|
await storage.write('biometric_prompt_shown', 'true');
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: const Text('Later'),
|
|
),
|
|
TextButton(
|
|
onPressed: () async {
|
|
final auth = LocalAuthentication();
|
|
final canCheck = await auth.canCheckBiometrics;
|
|
bool ok = false;
|
|
if (canCheck) {
|
|
ok = await auth.authenticate(
|
|
localizedReason: 'Scan to enable biometric login',
|
|
);
|
|
}
|
|
if (ok) {
|
|
await storage.write('biometric_enabled', 'true');
|
|
}
|
|
await storage.write('biometric_prompt_shown', 'true');
|
|
Navigator.of(context).pop();
|
|
},
|
|
child: const Text('Enable'),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return BlocListener<AuthCubit, AuthState>(
|
|
listener: (context, state) async {
|
|
if (state is Authenticated && !_biometricPromptShown) {
|
|
_biometricPromptShown = true;
|
|
final storage = getIt<SecureStorage>();
|
|
final already = await storage.read('biometric_prompt_shown');
|
|
if (already == null) {
|
|
_showBiometricOptInDialog();
|
|
}
|
|
}
|
|
},
|
|
child: Scaffold(
|
|
backgroundColor: const Color(0xfff5f9fc),
|
|
appBar: AppBar(
|
|
backgroundColor: const Color(0xfff5f9fc),
|
|
automaticallyImplyLeading: false,
|
|
title: Text(
|
|
'kMobile',
|
|
style: TextStyle(
|
|
color: Theme.of(context).primaryColor,
|
|
fontWeight: FontWeight.w500),
|
|
),
|
|
actions: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(right: 10.0),
|
|
child: InkWell(
|
|
borderRadius: BorderRadius.circular(20),
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const Preference()));
|
|
},
|
|
child: CircleAvatar(
|
|
backgroundColor: Colors.grey[200],
|
|
radius: 20,
|
|
child: SvgPicture.asset(
|
|
'assets/images/avatar_male.svg',
|
|
width: 40,
|
|
height: 40,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
body: BlocBuilder<AuthCubit, AuthState>(builder: (context, state) {
|
|
if (state is AuthLoading || state is AuthInitial) {
|
|
return const Center(child: CircularProgressIndicator());
|
|
}
|
|
if (state is Authenticated) {
|
|
final users = state.users;
|
|
final currAccount = users[selectedAccountIndex];
|
|
final firstName = getProcessedFirstName(currAccount.name);
|
|
|
|
return SingleChildScrollView(
|
|
physics: const AlwaysScrollableScrollPhysics(),
|
|
child: Padding(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.only(left: 8.0),
|
|
child: Text(
|
|
"Hi! $firstName",
|
|
style: GoogleFonts.montserrat().copyWith(
|
|
fontSize: 25,
|
|
color: Theme.of(context).primaryColorDark,
|
|
fontWeight: FontWeight.w700),
|
|
),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Account Info Card
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(
|
|
horizontal: 18, vertical: 10),
|
|
decoration: BoxDecoration(
|
|
color: Theme.of(context).primaryColor,
|
|
borderRadius: BorderRadius.circular(16),
|
|
),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Row(
|
|
children: [
|
|
const Text("Account Number: ",
|
|
style: TextStyle(
|
|
color: Colors.white, fontSize: 12)),
|
|
DropdownButton<int>(
|
|
value: selectedAccountIndex,
|
|
dropdownColor: Theme.of(context).primaryColor,
|
|
underline: const SizedBox(),
|
|
icon: const Icon(Icons.keyboard_arrow_down),
|
|
iconEnabledColor: Colors.white,
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 14),
|
|
items: List.generate(users.length, (index) {
|
|
return DropdownMenuItem<int>(
|
|
value: index,
|
|
child: Text(
|
|
users[index].accountNo ?? 'N/A',
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 14),
|
|
),
|
|
);
|
|
}),
|
|
onChanged: (int? newIndex) async {
|
|
if (newIndex == null ||
|
|
newIndex == selectedAccountIndex) {
|
|
return;
|
|
}
|
|
if (isBalanceLoading) return;
|
|
if (isVisible) {
|
|
setState(() {
|
|
isBalanceLoading = true;
|
|
selectedAccountIndex = newIndex;
|
|
});
|
|
await Future.delayed(
|
|
const Duration(seconds: 1));
|
|
setState(() {
|
|
isBalanceLoading = false;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
selectedAccountIndex = newIndex;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
const Spacer(),
|
|
IconButton(
|
|
icon: isRefreshing
|
|
? const SizedBox(
|
|
width: 20,
|
|
height: 20,
|
|
child: CircularProgressIndicator(
|
|
color: Colors.white,
|
|
strokeWidth: 2,
|
|
),
|
|
)
|
|
: const Icon(Icons.refresh,
|
|
color: Colors.white),
|
|
onPressed: isRefreshing
|
|
? null
|
|
: () => _refreshAccountData(context),
|
|
tooltip: 'Refresh',
|
|
),
|
|
],
|
|
),
|
|
Text(
|
|
getFullAccountType(currAccount.accountType),
|
|
style: const TextStyle(
|
|
color: Colors.white, fontSize: 16),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
const Text("₹ ",
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 40,
|
|
fontWeight: FontWeight.w700)),
|
|
isRefreshing || isBalanceLoading
|
|
? _buildBalanceShimmer()
|
|
: Text(
|
|
isVisible
|
|
? currAccount.currentBalance ?? '0.00'
|
|
: '********',
|
|
style: const TextStyle(
|
|
color: Colors.white,
|
|
fontSize: 40,
|
|
fontWeight: FontWeight.w700)),
|
|
const Spacer(),
|
|
InkWell(
|
|
onTap: () async {
|
|
if (isBalanceLoading) return;
|
|
if (!isVisible) {
|
|
setState(() {
|
|
isBalanceLoading = true;
|
|
});
|
|
await Future.delayed(
|
|
const Duration(seconds: 1));
|
|
setState(() {
|
|
isVisible = true;
|
|
isBalanceLoading = false;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
isVisible = false;
|
|
});
|
|
}
|
|
},
|
|
child: Icon(
|
|
isVisible
|
|
? Symbols.visibility_lock
|
|
: Symbols.visibility,
|
|
color: Colors.white),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 15),
|
|
],
|
|
),
|
|
),
|
|
const SizedBox(height: 18),
|
|
const Text(
|
|
'Quick Links',
|
|
style: TextStyle(fontSize: 17),
|
|
),
|
|
const SizedBox(height: 16),
|
|
|
|
// Quick Links
|
|
GridView.count(
|
|
crossAxisCount: 4,
|
|
shrinkWrap: true,
|
|
physics: const NeverScrollableScrollPhysics(),
|
|
children: [
|
|
_buildQuickLink(Symbols.id_card, "Customer \n Info",
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => CustomerInfoScreen(
|
|
user: users[selectedAccountIndex],
|
|
)));
|
|
}),
|
|
_buildQuickLink(Symbols.currency_rupee, "Quick \n Pay",
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
const QuickPayScreen()));
|
|
}),
|
|
_buildQuickLink(Symbols.send_money, "Fund \n Transfer",
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
const FundTransferBeneficiaryScreen()));
|
|
}),
|
|
_buildQuickLink(
|
|
Symbols.server_person, "Account \n Info", () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => AccountInfoScreen(
|
|
user: users[selectedAccountIndex])));
|
|
}),
|
|
_buildQuickLink(
|
|
Symbols.receipt_long, "Account \n Statement", () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
const AccountStatementScreen()));
|
|
}),
|
|
_buildQuickLink(
|
|
Symbols.checkbook, "Handle \n Cheque", () {},
|
|
disable: true),
|
|
_buildQuickLink(Icons.group, "Manage \n Beneficiary",
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) =>
|
|
const ManageBeneficiariesScreen()));
|
|
}),
|
|
_buildQuickLink(Symbols.support_agent, "Contact \n Us",
|
|
() {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (context) => const EnquiryScreen()));
|
|
}),
|
|
],
|
|
),
|
|
const SizedBox(height: 5),
|
|
|
|
// Recent Transactions
|
|
const Text(
|
|
'Recent Transactions',
|
|
style: TextStyle(fontSize: 17),
|
|
),
|
|
const SizedBox(height: 16),
|
|
if (currAccount.transactions != null &&
|
|
currAccount.transactions!.isNotEmpty)
|
|
...currAccount.transactions!.map((tx) => ListTile(
|
|
leading: Icon(
|
|
tx.type == 'CR'
|
|
? Symbols.call_received
|
|
: Symbols.call_made,
|
|
color: tx.type == 'CR'
|
|
? Colors.green
|
|
: Colors.red),
|
|
title: Text(tx.name ?? ''),
|
|
subtitle: Text(tx.date ?? ''),
|
|
trailing: Text("₹${tx.amount}",
|
|
style: const TextStyle(
|
|
fontSize: 16,
|
|
)),
|
|
))
|
|
else
|
|
const EmptyTransactionsPlaceholder(),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
return const Center(child: Text("Something went wrong"));
|
|
}),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildQuickLink(IconData icon, String label, VoidCallback onTap,
|
|
{bool disable = false}) {
|
|
return InkWell(
|
|
onTap: disable ? null : onTap,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Icon(icon,
|
|
size: 30,
|
|
color: disable ? Colors.grey : Theme.of(context).primaryColor),
|
|
const SizedBox(height: 4),
|
|
Text(label,
|
|
textAlign: TextAlign.center,
|
|
style: const TextStyle(fontSize: 13)),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|