|
|
|
@@ -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(
|
|
|
|
|