Download PDF #1
This commit is contained in:
@@ -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(
|
||||
|
@@ -521,6 +521,8 @@ class _DashboardScreenState extends State<DashboardScreen> {
|
||||
.accountNo!,
|
||||
balance: users[selectedAccountIndex]
|
||||
.availableBalance!,
|
||||
accountType: users[selectedAccountIndex]
|
||||
.accountType!,
|
||||
)));
|
||||
}),
|
||||
_buildQuickLink(Symbols.checkbook,
|
||||
|
@@ -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
|
||||
],
|
||||
),
|
||||
);
|
||||
|
Reference in New Issue
Block a user