Language Changes
This commit is contained in:
574
lib/features/dashboard/screens/dashboard_screen.dart
Normal file
574
lib/features/dashboard/screens/dashboard_screen.dart
Normal file
@@ -0,0 +1,574 @@
|
||||
import 'dart:developer';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_svg/svg.dart';
|
||||
import 'package:kmobile/data/repositories/transaction_repository.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/enquiry/screens/enquiry_screen.dart';
|
||||
import 'package:kmobile/features/fund_transfer/screens/fund_transfer_beneficiary_screen.dart';
|
||||
import 'package:kmobile/features/profile/profile_screen.dart';
|
||||
import 'package:kmobile/features/quick_pay/screens/quick_pay_screen.dart';
|
||||
import 'package:kmobile/security/secure_storage.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';
|
||||
import 'package:kmobile/data/models/transaction.dart';
|
||||
import 'package:flutter_gen/gen_l10n/app_localizations.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;
|
||||
bool _txLoading = false;
|
||||
List<Transaction> _transactions = [];
|
||||
bool _txInitialized = false;
|
||||
|
||||
Future<void> _loadTransactions(String accountNo) async {
|
||||
setState(() {
|
||||
_txLoading = true;
|
||||
_transactions = [];
|
||||
});
|
||||
try {
|
||||
final repo = getIt<TransactionRepository>();
|
||||
final txs = await repo.fetchTransactions(accountNo);
|
||||
var fiveTxns = <Transaction>[];
|
||||
//only take the first 5 transactions
|
||||
if (txs.length > 5) {
|
||||
fiveTxns = txs.sublist(0, 5);
|
||||
} else {
|
||||
fiveTxns = txs;
|
||||
}
|
||||
setState(() => _transactions = fiveTxns);
|
||||
} catch (e) {
|
||||
log(accountNo, error: e);
|
||||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
|
||||
content:
|
||||
Text(AppLocalizations.of(context).failedToLoad(e.toString()))));
|
||||
} finally {
|
||||
setState(() => _txLoading = 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(AppLocalizations.of(context).failedToRefresh)),
|
||||
);
|
||||
}
|
||||
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 AppLocalizations.of(context).savingsAccount;
|
||||
case 'ln':
|
||||
return AppLocalizations.of(context).loanAccount;
|
||||
case 'td':
|
||||
return AppLocalizations.of(context).termDeposit;
|
||||
case 'rd':
|
||||
return AppLocalizations.of(context).recurringDeposit;
|
||||
default:
|
||||
return AppLocalizations.of(context).unknownAccount;
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _showBiometricOptInDialog() async {
|
||||
final storage = SecureStorage();
|
||||
showDialog(
|
||||
context: context,
|
||||
barrierDismissible: false,
|
||||
builder: (_) => AlertDialog(
|
||||
title: Text(AppLocalizations.of(context).enableBiometric),
|
||||
content: Text(AppLocalizations.of(context).useBiometricPrompt),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
await storage.write('biometric_prompt_shown', 'true');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).later),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () async {
|
||||
final auth = LocalAuthentication();
|
||||
final canCheck = await auth.canCheckBiometrics;
|
||||
bool ok = false;
|
||||
if (canCheck) {
|
||||
ok = await auth.authenticate(
|
||||
localizedReason: AppLocalizations.of(context).scanBiometric,
|
||||
);
|
||||
}
|
||||
if (ok) {
|
||||
await storage.write('biometric_enabled', 'true');
|
||||
}
|
||||
await storage.write('biometric_prompt_shown', 'true');
|
||||
Navigator.of(context).pop();
|
||||
},
|
||||
child: Text(AppLocalizations.of(context).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(
|
||||
AppLocalizations.of(context).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 ProfileScreen()),
|
||||
);
|
||||
},
|
||||
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];
|
||||
// first‐time load
|
||||
if (!_txInitialized) {
|
||||
_txInitialized = true;
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
_loadTransactions(currAccount.accountNo!);
|
||||
});
|
||||
}
|
||||
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(
|
||||
"${AppLocalizations.of(context).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: [
|
||||
Text(AppLocalizations.of(context).accountNumber,
|
||||
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(milliseconds: 200));
|
||||
setState(() {
|
||||
isBalanceLoading = false;
|
||||
});
|
||||
} else {
|
||||
setState(() {
|
||||
selectedAccountIndex = newIndex;
|
||||
});
|
||||
}
|
||||
await _loadTransactions(
|
||||
users[newIndex].accountNo!);
|
||||
},
|
||||
),
|
||||
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),
|
||||
Text(
|
||||
AppLocalizations.of(context).quickLinks,
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// Quick Links
|
||||
GridView.count(
|
||||
crossAxisCount: 4,
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
children: [
|
||||
_buildQuickLink(Symbols.id_card,
|
||||
AppLocalizations.of(context).customerInfo, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CustomerInfoScreen(
|
||||
user: users[selectedAccountIndex],
|
||||
)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.currency_rupee,
|
||||
AppLocalizations.of(context).quickPay, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => QuickPayScreen(
|
||||
debitAccount: currAccount.accountNo!)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.send_money,
|
||||
AppLocalizations.of(context).fundTransfer, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const FundTransferBeneficiaryScreen()));
|
||||
}, disable: false),
|
||||
_buildQuickLink(Symbols.server_person,
|
||||
AppLocalizations.of(context).accountInfo, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AccountInfoScreen(
|
||||
user: users[selectedAccountIndex])));
|
||||
}),
|
||||
_buildQuickLink(Symbols.receipt_long,
|
||||
AppLocalizations.of(context).accountStatement, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => AccountStatementScreen(
|
||||
accountNo: users[selectedAccountIndex]
|
||||
.accountNo!,
|
||||
)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.checkbook,
|
||||
AppLocalizations.of(context).handleCheque, () {},
|
||||
disable: false),
|
||||
_buildQuickLink(Icons.group,
|
||||
AppLocalizations.of(context).manageBeneficiary, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) =>
|
||||
const ManageBeneficiariesScreen()));
|
||||
}, disable: false),
|
||||
_buildQuickLink(Symbols.support_agent,
|
||||
AppLocalizations.of(context).contactUs, () {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const EnquiryScreen()));
|
||||
}),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 5),
|
||||
|
||||
// Recent Transactions
|
||||
Text(
|
||||
AppLocalizations.of(context).recentTransactions,
|
||||
style: TextStyle(fontSize: 17),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
if (_txLoading)
|
||||
..._buildTransactionShimmer()
|
||||
else if (_transactions.isNotEmpty)
|
||||
..._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 != null
|
||||
? (tx.name!.length > 18
|
||||
? tx.name!.substring(0, 20)
|
||||
: tx.name!)
|
||||
: '',
|
||||
style: const TextStyle(fontSize: 14),
|
||||
),
|
||||
subtitle: Text(tx.date ?? '',
|
||||
style: const TextStyle(fontSize: 12)),
|
||||
trailing: Text("₹${tx.amount}",
|
||||
style: const TextStyle(fontSize: 16)),
|
||||
))
|
||||
else
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 24.0),
|
||||
child: Center(
|
||||
child: Text(
|
||||
AppLocalizations.of(context).noTransactions,
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
color: Colors.grey[600],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return Center(
|
||||
child: Text(AppLocalizations.of(context).somethingWentWrong));
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
List<Widget> _buildTransactionShimmer() {
|
||||
return List.generate(3, (i) {
|
||||
return ListTile(
|
||||
leading: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: const CircleAvatar(radius: 12, backgroundColor: Colors.white),
|
||||
),
|
||||
title: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(height: 10, width: 100, color: Colors.white),
|
||||
),
|
||||
subtitle: Shimmer.fromColors(
|
||||
baseColor: Colors.grey[300]!,
|
||||
highlightColor: Colors.grey[100]!,
|
||||
child: Container(height: 8, width: 60, color: Colors.white),
|
||||
),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
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)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user