From b513664a47c30c25c69212d11b5639df0ed7f96e Mon Sep 17 00:00:00 2001 From: Nilanjan Chakrabarti Date: Mon, 8 Sep 2025 16:10:56 +0530 Subject: [PATCH] Download PDF #1 --- .../screens/account_statement_screen.dart | 338 +++++++++++++----- .../dashboard/screens/dashboard_screen.dart | 2 + lib/features/profile/profile_screen.dart | 9 +- lib/l10n/app_en.arb | 3 +- lib/l10n/app_hi.arb | 3 +- 5 files changed, 259 insertions(+), 96 deletions(-) diff --git a/lib/features/accounts/screens/account_statement_screen.dart b/lib/features/accounts/screens/account_statement_screen.dart index 6e87cc2..aa37ad7 100644 --- a/lib/features/accounts/screens/account_statement_screen.dart +++ b/lib/features/accounts/screens/account_statement_screen.dart @@ -2,7 +2,6 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:shimmer/shimmer.dart'; import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/repositories/transaction_repository.dart'; @@ -12,14 +11,19 @@ import 'transaction_details_screen.dart'; import 'package:pdf/widgets.dart' as pw; import 'package:permission_handler/permission_handler.dart'; import 'package:device_info_plus/device_info_plus.dart'; +import 'package:flutter/foundation.dart' show kIsWeb; +import 'dart:html' as html; // Import for web-specific code +import 'dart:typed_data'; class AccountStatementScreen extends StatefulWidget { final String accountNo; final String balance; + final String accountType; const AccountStatementScreen({ super.key, required this.accountNo, required this.balance, + required this.accountType, }); @override @@ -33,7 +37,7 @@ class _AccountStatementScreen extends State { List _transactions = []; final _minAmountController = TextEditingController(); final _maxAmountController = TextEditingController(); - Future?>? accountStatementsFuture; + //Future?>? accountStatementsFuture; @override void initState() { @@ -325,35 +329,155 @@ class _AccountStatementScreen extends State { } Future _exportToPdf() async { - if (accountStatementsFuture == null) return; - if (Platform.isAndroid) { - final androidInfo = await DeviceInfoPlugin().androidInfo; - if (androidInfo.version.sdkInt < 29) { - final status = await Permission.storage.status; - if (status.isDenied) { - final result = await Permission.storage.request(); - if (result.isDenied) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Storage permission is required to save PDF'), - duration: Duration(seconds: 3), - ), - ); - } - return; + // Step 1: Check if there are any transactions to export. + if (_transactions.isEmpty) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('No transactions to export.'), + ), + ); + } + return; + } + + // Step 2: Handle storage permissions for Android. + /*if (Platform.isAndroid) { + final androidInfo = await DeviceInfoPlugin().androidInfo; + if (androidInfo.version.sdkInt < 33) { // Target Android 12 & below + final status = await Permission.storage.status; + if (status.isDenied) { + final result = await Permission.storage.request(); + if (result.isDenied) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Storage permission is required to save PDF'), + ), + ); } + return; } } } + } + - String logoSvg = await rootBundle.loadString('assets/logos/logo.svg'); - var rubik = await rootBundle.load("assets/fonts/Rubik-Regular.ttf"); - try { - final statements = await accountStatementsFuture; - if (statements == null) return; - final pdf = pw.Document(); - pdf.addPage(pw.MultiPage( + // Step 3: Load assets for the PDF. + // IMPORTANT: The original path 'assets/logos/logo.svg' is incorrect. + // I've corrected it to 'assets/images/icon.svg' based on your project structure. + String logoSvg = await rootBundle.loadString('assets/images/icon.svg'); + var rubik = await rootBundle.load("assets/fonts/Rubik-Regular.ttf"); + + try { + final pdf = pw.Document(); + pdf.addPage( + pw.MultiPage( + margin: const pw.EdgeInsets.all(20), + build: (pw.Context context) { + return [ + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.start, + crossAxisAlignment: pw.CrossAxisAlignment.center, + children: [ + pw.SvgImage(svg: logoSvg, width: 50, height: 50), + pw.SizedBox(width: 20), + pw.Text( + "Account Statement - KCCB", + style: pw.TextStyle( + fontSize: 24, + fontWeight: pw.FontWeight.bold, + ), + ), + ], + ), + pw.SizedBox(height: 20), + pw.Row( + mainAxisAlignment: pw.MainAxisAlignment.spaceBetween, + children: [ + pw.Text( + 'Account Number: ${widget.accountNo}', + style: pw.TextStyle(font: pw.Font.ttf(rubik), fontSize: 15), + ), + pw.Text( + 'Account Type: ${widget.accountType}', + style: pw.TextStyle(font: pw.Font.ttf(rubik), fontSize: 15), + ), + ], + ), + pw.SizedBox(height: 20), + // Step 4: Build the table from the _transactions list. + pw.Table.fromTextArray( + border: pw.TableBorder.all(), + headerStyle: pw.TextStyle(fontWeight: pw.FontWeight.bold), + headerDecoration: const pw.BoxDecoration( + color: PdfColors.grey300, + ), + cellHeight: 30, + cellAlignments: { + 0: pw.Alignment.centerLeft, + 1: pw.Alignment.centerLeft, + 2: pw.Alignment.centerRight, + 3: pw.Alignment.center, + }, + headers: ['Date', 'Description', 'Amount', 'Type'], + data: _transactions.map((tx) => [ + tx.date ?? 'N/A', + tx.name ?? 'N/A', + '₹${tx.amount}', + tx.type ?? 'N/A', + ]).toList(), + ), + ]; + }, + footer: (pw.Context context) { + return pw.Container( + alignment: pw.Alignment.centerRight, + margin: const pw.EdgeInsets.only(top: 10), + child: pw.Text( + 'Kangra Central Co-Operative bank Pvt Ltd. ©. All rights reserved.', + style: pw.TextStyle( + font: pw.Font.ttf(rubik), + fontSize: 8, + ), + ), + ); + }, + ), + ); + + // Step 5: Save the PDF to the device. + Directory? directory = await getDownloadsDirectory(); + if (directory == null) { + throw Exception('Could not access downloads directory'); + } + final String timestamp = DateTime.now().millisecondsSinceEpoch.toString(); + final file = File('${directory.path}/account_statement_$timestamp.pdf'); + + await file.writeAsBytes(await pdf.save()); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('PDF saved to: ${file.path}'), + duration: const Duration(seconds: 3), + ), + ); + } + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error saving PDF: $e'), + ), + ); + } + }*/ + String logoSvg = await rootBundle.loadString('assets/images/kccb_logo.svg'); // Corrected asset path + var rubik = await rootBundle.load("assets/fonts/Rubik-Regular.ttf"); + final pdf = pw.Document(); + + pdf.addPage(pw.MultiPage( margin: const pw.EdgeInsets.all(20), build: (pw.Context context) { return [ @@ -363,7 +487,7 @@ class _AccountStatementScreen extends State { children: [ pw.SvgImage(svg: logoSvg, width: 50, height: 50), pw.SizedBox(width: 20), - pw.Text(AppLocalizations.of(context as BuildContext).kccBankFull, + pw.Text('Account Statement - KCCB', style: pw.TextStyle( fontSize: 24, fontWeight: pw.FontWeight.bold)), ]), @@ -374,7 +498,7 @@ class _AccountStatementScreen extends State { pw.Text('Account Number: ${widget.accountNo}', style: pw.TextStyle( font: pw.Font.ttf(rubik), fontSize: 15)), - pw.Text('Account Type: selectedAccountType', + pw.Text('Account Type: ${widget.accountType}', style: pw.TextStyle( fontSize: 15, font: pw.Font.ttf(rubik), @@ -403,40 +527,28 @@ class _AccountStatementScreen extends State { child: pw.Text('Balance')), ], ), - ...statements['accountStatement'].map((statement) { + ..._transactions.map((tx) { return pw.TableRow(children: [ - pw.Padding( - padding: const pw.EdgeInsets.all(10), - child: pw.Text( - '${statement['postDate']} ${statement['postTime']}', - style: pw.TextStyle( - fontSize: 12, - font: pw.Font.ttf(rubik), - ))), - pw.Padding( - padding: const pw.EdgeInsets.all(10), - child: pw.Text(statement['narration'], - style: pw.TextStyle( - fontSize: 12, font: pw.Font.ttf(rubik)))), - pw.Padding( - padding: const pw.EdgeInsets.all(10), - child: pw.Text("₹${statement['transactionAmount']}", - style: pw.TextStyle( - fontSize: 12, - font: pw.Font.ttf(rubik), - // color: - // statement['transactionAmount'].contains('-') - // ? const PdfColor.fromInt(0xFFB00020) - // : const PdfColor.fromInt(0xFF007B0D), - ))), - pw.Padding( - padding: const pw.EdgeInsets.all(10), - child: pw.Text("₹${statement['endBalance']}", - style: pw.TextStyle( - fontSize: 12, - font: pw.Font.ttf(rubik), - ))), - ]); + pw.Padding( + padding: const pw.EdgeInsets.all(10), + child: pw.Text(tx.date ?? '', + style: pw.TextStyle( + fontSize: 12, + font: pw.Font.ttf(rubik), + ))), + pw.Padding( + padding: const pw.EdgeInsets.all(10), + child: pw.Text(tx.name ?? '', + style: pw.TextStyle( + fontSize: 12, font: pw.Font.ttf(rubik)))), + pw.Padding( + padding: const pw.EdgeInsets.all(10), + child: pw.Text("₹${tx.amount}", + style: pw.TextStyle( + fontSize: 12, + font: pw.Font.ttf(rubik), + ))), +]); }).toList(), ]) ]; @@ -446,7 +558,7 @@ class _AccountStatementScreen extends State { alignment: pw.Alignment.centerRight, margin: const pw.EdgeInsets.only(top: 10), child: pw.Text( - 'Kangra Central Co-Operative bank Pvt Ltd. ©. All rights reserved.', + 'Kangra Central Co-Operative Bank Pvt Ltd. ©. All rights reserved.', style: pw.TextStyle( font: pw.Font.ttf(rubik), fontSize: 8, @@ -455,41 +567,81 @@ class _AccountStatementScreen extends State { ); })); - Directory? directory; - if (Platform.isAndroid) { - directory = Directory('/storage/emulated/0/Download'); - } else { - directory = await getDownloadsDirectory(); - } + //Logic For all platforms + try { + final Uint8List pdfBytes = await pdf.save(); + final String timestamp = DateTime.now().millisecondsSinceEpoch.toString(); + final String fileName = 'account_statement_$timestamp.pdf'; - if (directory == null) { - throw Exception('Could not access downloads directory'); - } - final String timestamp = DateTime.now().millisecondsSinceEpoch.toString(); - final file = File('${directory.path}/account_statement_$timestamp.pdf'); + // For Web + if (kIsWeb) { + final blob = html.Blob([pdfBytes], 'application/pdf'); + final url = html.Url.createObjectUrlFromBlob(blob); + html.Url.revokeObjectUrl(url); + print('Generated PDF Blob URL for web: $url'); + + final anchor = html.document.createElement('a') as html.AnchorElement + ..href = url + ..style.display = 'none' + ..download = fileName; - await file.writeAsBytes(await pdf.save()); +html.document.body!.children.add(anchor); +anchor.click(); +html.document.body!.children.remove(anchor); - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('PDF saved to: ${file.path}'), - duration: const Duration(seconds: 3), - ), - ); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Error saving PDF: $e'), - duration: const Duration(seconds: 3), - ), - ); - } - } - } +html.Url.revokeObjectUrl(url); + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('PDF download started: $fileName'), + ), + ); + } + } + // For Android + else if (Platform.isAndroid) { + final androidInfo = await DeviceInfoPlugin().androidInfo; + if (androidInfo.version.sdkInt < 29) { + final status = await Permission.storage.status; + if (status.isDenied) { + final result = await Permission.storage.request(); + if (result.isDenied) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar( + content: Text('Storage permission is required to save PDF'), + ), + ); + } + return; + } + } + } + final directory = Directory('/storage/emulated/0/Download'); + final file = File('${directory.path}/$fileName'); + await file.writeAsBytes(pdfBytes); + + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('PDF saved to: ${file.path}'), + ), + ); + } + } + // Add for IOS + + } catch (e) { + if (mounted) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text('Error saving PDF: $e'), + ), + ); + } + } +} Widget buildDateBox(String label, DateTime? date) { return Container( diff --git a/lib/features/dashboard/screens/dashboard_screen.dart b/lib/features/dashboard/screens/dashboard_screen.dart index 619cfcb..cf02b07 100644 --- a/lib/features/dashboard/screens/dashboard_screen.dart +++ b/lib/features/dashboard/screens/dashboard_screen.dart @@ -521,6 +521,8 @@ class _DashboardScreenState extends State { .accountNo!, balance: users[selectedAccountIndex] .availableBalance!, + accountType: users[selectedAccountIndex] + .accountType!, ))); }), _buildQuickLink(Symbols.checkbook, diff --git a/lib/features/profile/profile_screen.dart b/lib/features/profile/profile_screen.dart index 9462610..5f8642e 100644 --- a/lib/features/profile/profile_screen.dart +++ b/lib/features/profile/profile_screen.dart @@ -40,7 +40,13 @@ class ProfileScreen extends StatelessWidget { ); }, ), - // You can add more profile options here later + ListTile( + leading: const Icon(Icons.password), + title: Text(loc.changePassword), + onTap: () { + + }, + ), ListTile( leading: const Icon(Icons.logout), title: Text(AppLocalizations.of(context).logout), @@ -55,6 +61,7 @@ class ProfileScreen extends StatelessWidget { } }, ), + // You can add more profile options here later ], ), ); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c94772a..40b1e5b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -291,5 +291,6 @@ "search": "Search", "viewCardDeatils": "View Card Details", "logout": "Logout", -"logoutCheck": "Are you sure you want to logout?" +"logoutCheck": "Are you sure you want to logout?", +"changePassword": "Change Password" } diff --git a/lib/l10n/app_hi.arb b/lib/l10n/app_hi.arb index 67977c9..541e609 100644 --- a/lib/l10n/app_hi.arb +++ b/lib/l10n/app_hi.arb @@ -292,5 +292,6 @@ "search": "खोजें", "viewCardDeatils": "कार्ड विवरण देखें", "logout": "लॉग आउट", -"logoutCheck": "क्या आप लॉग आउट करना चाहते हैं?" +"logoutCheck": "क्या आप लॉग आउट करना चाहते हैं?", +"changePassword": "पासवर्ड बदलें" }