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/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<AccountStatementScreen> {
List<Transaction> _transactions = [];
final _minAmountController = TextEditingController();
final _maxAmountController = TextEditingController();
Future<Map<String, dynamic>?>? accountStatementsFuture;
//Future<Map<String, dynamic>?>? accountStatementsFuture;
@override
void initState() {
@@ -325,35 +329,155 @@ class _AccountStatementScreen extends State<AccountStatementScreen> {
}
Future<void> _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<AccountStatementScreen> {
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<AccountStatementScreen> {
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<AccountStatementScreen> {
child: pw.Text('Balance')),
],
),
...statements['accountStatement'].map<pw.TableRow>((statement) {
..._transactions.map<pw.TableRow>((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<AccountStatementScreen> {
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<AccountStatementScreen> {
);
}));
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(

View File

@@ -521,6 +521,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
.accountNo!,
balance: users[selectedAccountIndex]
.availableBalance!,
accountType: users[selectedAccountIndex]
.accountType!,
)));
}),
_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(
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
],
),
);