Download PDF #1

This commit is contained in:
2025-09-08 16:10:56 +05:30
parent d8d87e8da4
commit b513664a47
5 changed files with 259 additions and 96 deletions

View File

@@ -2,7 +2,6 @@ import 'dart:io';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart';
import 'package:path_provider/path_provider.dart';
import 'package:shimmer/shimmer.dart'; import 'package:shimmer/shimmer.dart';
import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/models/transaction.dart';
import 'package:kmobile/data/repositories/transaction_repository.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:pdf/widgets.dart' as pw;
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
import 'package:device_info_plus/device_info_plus.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 { class AccountStatementScreen extends StatefulWidget {
final String accountNo; final String accountNo;
final String balance; final String balance;
final String accountType;
const AccountStatementScreen({ const AccountStatementScreen({
super.key, super.key,
required this.accountNo, required this.accountNo,
required this.balance, required this.balance,
required this.accountType,
}); });
@override @override
@@ -33,7 +37,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
List<Transaction> _transactions = []; List<Transaction> _transactions = [];
final _minAmountController = TextEditingController(); final _minAmountController = TextEditingController();
final _maxAmountController = TextEditingController(); final _maxAmountController = TextEditingController();
Future<Map<String, dynamic>?>? accountStatementsFuture; //Future<Map<String, dynamic>?>? accountStatementsFuture;
@override @override
void initState() { void initState() {
@@ -325,35 +329,155 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
} }
Future<void> _exportToPdf() async { Future<void> _exportToPdf() async {
if (accountStatementsFuture == null) return; // Step 1: Check if there are any transactions to export.
if (Platform.isAndroid) { if (_transactions.isEmpty) {
final androidInfo = await DeviceInfoPlugin().androidInfo; if (mounted) {
if (androidInfo.version.sdkInt < 29) { ScaffoldMessenger.of(context).showSnackBar(
final status = await Permission.storage.status; const SnackBar(
if (status.isDenied) { content: Text('No transactions to export.'),
final result = await Permission.storage.request(); ),
if (result.isDenied) { );
if (mounted) { }
ScaffoldMessenger.of(context).showSnackBar( return;
const SnackBar( }
content: Text('Storage permission is required to save PDF'),
duration: Duration(seconds: 3), // Step 2: Handle storage permissions for Android.
), /*if (Platform.isAndroid) {
); final androidInfo = await DeviceInfoPlugin().androidInfo;
} if (androidInfo.version.sdkInt < 33) { // Target Android 12 & below
return; 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'); // Step 3: Load assets for the PDF.
var rubik = await rootBundle.load("assets/fonts/Rubik-Regular.ttf"); // IMPORTANT: The original path 'assets/logos/logo.svg' is incorrect.
try { // I've corrected it to 'assets/images/icon.svg' based on your project structure.
final statements = await accountStatementsFuture; String logoSvg = await rootBundle.loadString('assets/images/icon.svg');
if (statements == null) return; var rubik = await rootBundle.load("assets/fonts/Rubik-Regular.ttf");
final pdf = pw.Document();
pdf.addPage(pw.MultiPage( 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), margin: const pw.EdgeInsets.all(20),
build: (pw.Context context) { build: (pw.Context context) {
return [ return [
@@ -363,7 +487,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
children: [ children: [
pw.SvgImage(svg: logoSvg, width: 50, height: 50), pw.SvgImage(svg: logoSvg, width: 50, height: 50),
pw.SizedBox(width: 20), pw.SizedBox(width: 20),
pw.Text(AppLocalizations.of(context as BuildContext).kccBankFull, pw.Text('Account Statement - KCCB',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: 24, fontWeight: pw.FontWeight.bold)), fontSize: 24, fontWeight: pw.FontWeight.bold)),
]), ]),
@@ -374,7 +498,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
pw.Text('Account Number: ${widget.accountNo}', pw.Text('Account Number: ${widget.accountNo}',
style: pw.TextStyle( style: pw.TextStyle(
font: pw.Font.ttf(rubik), fontSize: 15)), font: pw.Font.ttf(rubik), fontSize: 15)),
pw.Text('Account Type: selectedAccountType', pw.Text('Account Type: ${widget.accountType}',
style: pw.TextStyle( style: pw.TextStyle(
fontSize: 15, fontSize: 15,
font: pw.Font.ttf(rubik), font: pw.Font.ttf(rubik),
@@ -403,40 +527,28 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
child: pw.Text('Balance')), child: pw.Text('Balance')),
], ],
), ),
...statements['accountStatement'].map<pw.TableRow>((statement) { ..._transactions.map<pw.TableRow>((tx) {
return pw.TableRow(children: [ return pw.TableRow(children: [
pw.Padding( pw.Padding(
padding: const pw.EdgeInsets.all(10), padding: const pw.EdgeInsets.all(10),
child: pw.Text( child: pw.Text(tx.date ?? '',
'${statement['postDate']} ${statement['postTime']}', style: pw.TextStyle(
style: pw.TextStyle( fontSize: 12,
fontSize: 12, font: pw.Font.ttf(rubik),
font: pw.Font.ttf(rubik), ))),
))), pw.Padding(
pw.Padding( padding: const pw.EdgeInsets.all(10),
padding: const pw.EdgeInsets.all(10), child: pw.Text(tx.name ?? '',
child: pw.Text(statement['narration'], style: pw.TextStyle(
style: pw.TextStyle( fontSize: 12, font: pw.Font.ttf(rubik)))),
fontSize: 12, font: pw.Font.ttf(rubik)))), pw.Padding(
pw.Padding( padding: const pw.EdgeInsets.all(10),
padding: const pw.EdgeInsets.all(10), child: pw.Text("${tx.amount}",
child: pw.Text("${statement['transactionAmount']}", style: pw.TextStyle(
style: pw.TextStyle( fontSize: 12,
fontSize: 12, font: pw.Font.ttf(rubik),
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),
))),
]);
}).toList(), }).toList(),
]) ])
]; ];
@@ -446,7 +558,7 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
alignment: pw.Alignment.centerRight, alignment: pw.Alignment.centerRight,
margin: const pw.EdgeInsets.only(top: 10), margin: const pw.EdgeInsets.only(top: 10),
child: pw.Text( 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( style: pw.TextStyle(
font: pw.Font.ttf(rubik), font: pw.Font.ttf(rubik),
fontSize: 8, fontSize: 8,
@@ -455,41 +567,81 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
); );
})); }));
Directory? directory; //Logic For all platforms
if (Platform.isAndroid) { try {
directory = Directory('/storage/emulated/0/Download'); final Uint8List pdfBytes = await pdf.save();
} else { final String timestamp = DateTime.now().millisecondsSinceEpoch.toString();
directory = await getDownloadsDirectory(); final String fileName = 'account_statement_$timestamp.pdf';
}
if (directory == null) { // For Web
throw Exception('Could not access downloads directory'); if (kIsWeb) {
} final blob = html.Blob([pdfBytes], 'application/pdf');
final String timestamp = DateTime.now().millisecondsSinceEpoch.toString(); final url = html.Url.createObjectUrlFromBlob(blob);
final file = File('${directory.path}/account_statement_$timestamp.pdf'); 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) { html.Url.revokeObjectUrl(url);
ScaffoldMessenger.of(context).showSnackBar( if (mounted) {
SnackBar( ScaffoldMessenger.of(context).showSnackBar(
content: Text('PDF saved to: ${file.path}'), SnackBar(
duration: const Duration(seconds: 3), content: Text('PDF download started: $fileName'),
), ),
); );
} }
} catch (e) { }
if (mounted) { // For Android
ScaffoldMessenger.of(context).showSnackBar( else if (Platform.isAndroid) {
SnackBar( final androidInfo = await DeviceInfoPlugin().androidInfo;
content: Text('Error saving PDF: $e'), if (androidInfo.version.sdkInt < 29) {
duration: const Duration(seconds: 3), 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) { Widget buildDateBox(String label, DateTime? date) {
return Container( return Container(

View File

@@ -521,6 +521,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
.accountNo!, .accountNo!,
balance: users[selectedAccountIndex] balance: users[selectedAccountIndex]
.availableBalance!, .availableBalance!,
accountType: users[selectedAccountIndex]
.accountType!,
))); )));
}), }),
_buildQuickLink(Symbols.checkbook, _buildQuickLink(Symbols.checkbook,

View File

@@ -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( ListTile(
leading: const Icon(Icons.logout), leading: const Icon(Icons.logout),
title: Text(AppLocalizations.of(context).logout), title: Text(AppLocalizations.of(context).logout),
@@ -55,6 +61,7 @@ class ProfileScreen extends StatelessWidget {
} }
}, },
), ),
// You can add more profile options here later
], ],
), ),
); );

View File

@@ -291,5 +291,6 @@
"search": "Search", "search": "Search",
"viewCardDeatils": "View Card Details", "viewCardDeatils": "View Card Details",
"logout": "Logout", "logout": "Logout",
"logoutCheck": "Are you sure you want to logout?" "logoutCheck": "Are you sure you want to logout?",
"changePassword": "Change Password"
} }

View File

@@ -292,5 +292,6 @@
"search": "खोजें", "search": "खोजें",
"viewCardDeatils": "कार्ड विवरण देखें", "viewCardDeatils": "कार्ड विवरण देखें",
"logout": "लॉग आउट", "logout": "लॉग आउट",
"logoutCheck": "क्या आप लॉग आउट करना चाहते हैं?" "logoutCheck": "क्या आप लॉग आउट करना चाहते हैं?",
"changePassword": "पासवर्ड बदलें"
} }