import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:material_symbols_icons/material_symbols_icons.dart'; import 'package:shimmer/shimmer.dart'; import 'package:kmobile/data/models/transaction.dart'; import 'package:kmobile/data/repositories/transaction_repository.dart'; import 'package:kmobile/di/injection.dart'; import '../../../l10n/app_localizations.dart'; 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 State createState() => _AccountStatementScreen(); } class _AccountStatementScreen extends State { DateTime? fromDate; DateTime? toDate; bool _txLoading = true; List _transactions = []; final _minAmountController = TextEditingController(); final _maxAmountController = TextEditingController(); //Future?>? accountStatementsFuture; @override void initState() { super.initState(); _loadTransactions(); } Future _loadTransactions() async { setState(() { _txLoading = true; _transactions = []; }); try { final repo = getIt(); final txs = await repo.fetchTransactions( widget.accountNo, fromDate: fromDate, toDate: toDate, ); setState(() => _transactions = txs); } catch (e) { if (!mounted) return; ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text( '${AppLocalizations.of(context).failedToLoadTransactions} $e', ), ), ); } finally { setState(() => _txLoading = false); } } Future _selectFromDate(BuildContext context) async { final now = DateTime.now(); final picked = await showDatePicker( context: context, initialDate: fromDate ?? now, firstDate: DateTime(2020), lastDate: now, ); if (picked != null) { setState(() { fromDate = picked; toDate = null; }); } } Future _selectToDate(BuildContext context) async { if (fromDate == null) { ScaffoldMessenger.of(context).showSnackBar( SnackBar( content: Text(AppLocalizations.of(context).pleaseSelectDateFirst), ), ); return; } final now = DateTime.now(); final maxToDate = fromDate!.add(const Duration(days: 31)).isBefore(now) ? fromDate!.add(const Duration(days: 31)) : now; final picked = await showDatePicker( context: context, initialDate: toDate ?? fromDate!, firstDate: fromDate!, lastDate: maxToDate, ); if (picked != null) { setState(() => toDate = picked); } } String _formatDate(DateTime date) { return "${date.day.toString().padLeft(2, '0')}-" "${date.month.toString().padLeft(2, '0')}-" "${date.year}"; } @override void dispose() { _minAmountController.dispose(); _maxAmountController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar( title: Text( AppLocalizations.of(context).accountStatement, ), centerTitle: false, ), body: Padding( padding: const EdgeInsets.all(12.0), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Text( "${AppLocalizations.of(context).accountNumber}: ", style: const TextStyle( fontSize: 17, fontWeight: FontWeight.bold, ), ), Text(widget.accountNo, style: const TextStyle(fontSize: 17)), ], ), const SizedBox(height: 15), Row( children: [ Text( "${AppLocalizations.of(context).availableBalance}: ", style: const TextStyle( fontSize: 17, ), ), Text(' ₹ ${widget.balance}', style: const TextStyle(fontSize: 17)), ], ), const SizedBox(height: 15), Text( AppLocalizations.of(context).filters, style: const TextStyle(fontSize: 17), ), const SizedBox(height: 15), Row( children: [ Expanded( child: GestureDetector( onTap: () => _selectFromDate(context), child: buildDateBox( AppLocalizations.of(context).fromDate, fromDate, ), ), ), const SizedBox(width: 10), Expanded( child: GestureDetector( onTap: () => _selectToDate(context), child: buildDateBox( AppLocalizations.of(context).toDate, toDate, ), ), ), ], ), const SizedBox(height: 20), SizedBox( width: double.infinity, child: ElevatedButton( onPressed: _loadTransactions, style: ElevatedButton.styleFrom( backgroundColor: Theme.of(context).colorScheme.primaryContainer, padding: const EdgeInsets.symmetric(vertical: 16), ), child: Text( AppLocalizations.of(context).search, style: TextStyle( color: Theme.of(context).colorScheme.onPrimaryContainer, fontSize: 16, ), ), ), ), const SizedBox(height: 15), if (!_txLoading && _transactions.isNotEmpty && fromDate == null && toDate == null) Padding( padding: const EdgeInsets.only(bottom: 12.0), child: Text( AppLocalizations.of(context).lastTenTransactions, style: TextStyle( fontSize: 16, fontWeight: FontWeight.w500, color: Theme.of(context).colorScheme.onSurfaceVariant, ), ), ), Expanded( child: _txLoading ? ListView.builder( itemCount: 3, itemBuilder: (_, __) => ListTile( leading: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: CircleAvatar( radius: 12, backgroundColor: Theme.of(context).scaffoldBackgroundColor, ), ), title: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( height: 10, width: 100, color: Theme.of(context).scaffoldBackgroundColor, ), ), subtitle: Shimmer.fromColors( baseColor: Colors.grey[300]!, highlightColor: Colors.grey[100]!, child: Container( height: 8, width: 60, color: Theme.of(context).scaffoldBackgroundColor, ), ), ), ) : _transactions.isEmpty ? Center( child: Text( AppLocalizations.of(context).noTransactions, style: TextStyle( fontSize: 16, color: Theme.of(context).colorScheme.onSurface, )), ) : ListView.separated( itemCount: _transactions.length, itemBuilder: (context, index) { final tx = _transactions[index]; return ListTile( leading: Icon( tx.type == 'CR' ? Symbols.call_received : Symbols.call_made, color: tx.type == 'CR' ? Colors.green : Theme.of(context).colorScheme.error, ), title: Text( tx.date ?? '', style: const TextStyle(fontSize: 15), ), subtitle: Text( tx.name != null ? (tx.name!.length > 18 ? tx.name!.substring(0, 22) : tx.name!) : '', style: const TextStyle(fontSize: 12), ), trailing: Text( "₹${tx.amount}", style: const TextStyle(fontSize: 17), ), onTap: () { Navigator.push( context, MaterialPageRoute( builder: (_) => TransactionDetailsScreen( transaction: tx), ), ); }, ); }, separatorBuilder: (context, index) { return const Divider(); }, ), ), ], ), ), floatingActionButton: FloatingActionButton( onPressed: () { _exportToPdf(); }, child: const Icon(Icons.download), ), ); } Future _exportToPdf() async { // 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; } } } } // 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 [ 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( fontSize: 15, font: pw.Font.ttf(rubik), )), ]), pw.SizedBox(height: 20), pw.Table(border: pw.TableBorder.all(), columnWidths: { 0: const pw.FractionColumnWidth(0.2), 1: const pw.FractionColumnWidth(0.5), 2: const pw.FractionColumnWidth(0.15), 3: const pw.FractionColumnWidth(0.15), }, children: [ pw.TableRow( children: [ pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('Date')), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('Description', softWrap: true)), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('Amount')), pw.Padding( padding: const pw.EdgeInsets.all(4), child: pw.Text('Balance')), ], ), ..._transactions.map((tx) { return pw.TableRow(children: [ 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(), ]) ]; }, 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, ), ), ); })); //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'; // 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; html.document.body!.children.add(anchor); anchor.click(); html.document.body!.children.remove(anchor); 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( padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 16), decoration: BoxDecoration( border: Border.all(color: Theme.of(context).colorScheme.onSurfaceVariant), borderRadius: BorderRadius.circular(6), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [ Text( date != null ? _formatDate(date) : label, style: TextStyle( fontSize: 16, color: date != null ? Theme.of(context).colorScheme.onSurface : Theme.of(context).colorScheme.onSurfaceVariant, ), ), const Icon(Icons.arrow_drop_down), ], ), ); } }