1076 lines
39 KiB
Dart
1076 lines
39 KiB
Dart
import 'dart:io';
|
|
import 'dart:typed_data';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:material_symbols_icons/material_symbols_icons.dart';
|
|
import 'package:pdf/pdf.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:path_provider/path_provider.dart';
|
|
import 'package:flutter_local_notifications/flutter_local_notifications.dart';
|
|
import 'package:open_filex/open_filex.dart';
|
|
import 'package:kmobile/data/models/user.dart';
|
|
|
|
class AccountStatementScreen extends StatefulWidget {
|
|
final List<User> users;
|
|
final int selectedIndex;
|
|
|
|
const AccountStatementScreen({
|
|
super.key,
|
|
required this.users,
|
|
required this.selectedIndex,
|
|
});
|
|
|
|
@override
|
|
State<AccountStatementScreen> createState() => _AccountStatementScreen();
|
|
}
|
|
|
|
class _AccountStatementScreen extends State<AccountStatementScreen> {
|
|
late User selectedUser;
|
|
DateTime? fromDate;
|
|
DateTime? toDate;
|
|
bool _txLoading = true;
|
|
List<Transaction> _transactions = [];
|
|
final _minAmountController = TextEditingController();
|
|
final _maxAmountController = TextEditingController();
|
|
late FlutterLocalNotificationsPlugin flutterLocalNotificationsPlugin;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
selectedUser = widget.users[widget.selectedIndex];
|
|
_loadTransactions();
|
|
_initializeNotifications();
|
|
}
|
|
|
|
void _initializeNotifications() async {
|
|
flutterLocalNotificationsPlugin = FlutterLocalNotificationsPlugin();
|
|
|
|
const AndroidInitializationSettings initializationSettingsAndroid =
|
|
AndroidInitializationSettings('notification_icon');
|
|
|
|
const InitializationSettings initializationSettings =
|
|
InitializationSettings(android: initializationSettingsAndroid);
|
|
|
|
await flutterLocalNotificationsPlugin.initialize(
|
|
initializationSettings,
|
|
onDidReceiveNotificationResponse: _onDidReceiveNotificationResponse,
|
|
);
|
|
_requestNotificationPermission();
|
|
}
|
|
|
|
void _onDidReceiveNotificationResponse(
|
|
NotificationResponse notificationResponse) async {
|
|
final String? payload = notificationResponse.payload;
|
|
if (payload != null && payload.isNotEmpty) {
|
|
await OpenFilex.open(payload);
|
|
}
|
|
}
|
|
|
|
void _requestNotificationPermission() async {
|
|
if (Platform.isAndroid) {
|
|
final AndroidFlutterLocalNotificationsPlugin? androidImplementation =
|
|
flutterLocalNotificationsPlugin.resolvePlatformSpecificImplementation<
|
|
AndroidFlutterLocalNotificationsPlugin>();
|
|
|
|
if (androidImplementation != null) {
|
|
await androidImplementation.requestNotificationsPermission();
|
|
}
|
|
}
|
|
}
|
|
|
|
Future<void> _loadTransactions() async {
|
|
setState(() {
|
|
_txLoading = true;
|
|
_transactions = [];
|
|
});
|
|
try {
|
|
final repo = getIt<TransactionRepository>();
|
|
final txs = await repo.fetchTransactions(
|
|
selectedUser.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<void> _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<void> _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: 183)).isBefore(now)
|
|
? fromDate!.add(const Duration(days: 183))
|
|
: now;
|
|
final initialToDate = toDate ?? now;
|
|
final clampedInitialToDate =
|
|
initialToDate.isBefore(fromDate!) ? fromDate! : initialToDate;
|
|
final picked = await showDatePicker(
|
|
context: context,
|
|
initialDate: clampedInitialToDate,
|
|
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: Stack(
|
|
children: [
|
|
Padding(
|
|
padding: const EdgeInsets.all(12.0),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Card(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
AppLocalizations.of(context).accountNumber,
|
|
style: const TextStyle(
|
|
fontWeight: FontWeight.w500, fontSize: 17),
|
|
),
|
|
const VerticalDivider(
|
|
width: 20,
|
|
thickness: 1,
|
|
indent: 5,
|
|
endIndent: 5,
|
|
color: Colors.grey),
|
|
DropdownButton<User>(
|
|
value: selectedUser,
|
|
onChanged: (User? newUser) {
|
|
if (newUser != null) {
|
|
setState(() {
|
|
selectedUser = newUser;
|
|
});
|
|
_loadTransactions();
|
|
}
|
|
},
|
|
items: widget.users.map((user) {
|
|
return DropdownMenuItem<User>(
|
|
value: user,
|
|
child: Text(user.accountNo.toString()),
|
|
);
|
|
}).toList(),
|
|
underline: Container(), // Remove the underline
|
|
),
|
|
Spacer(),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Card(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
child: Padding(
|
|
padding: const EdgeInsets.all(16.0),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
"${AppLocalizations.of(context).availableBalance}: ",
|
|
style: const TextStyle(
|
|
fontSize: 17,
|
|
),
|
|
),
|
|
Text(' ₹ ${selectedUser.availableBalance}',
|
|
style: const TextStyle(fontSize: 17)),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
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:
|
|
Theme.of(context).colorScheme.surfaceVariant,
|
|
highlightColor: Theme.of(context)
|
|
.colorScheme
|
|
.onSurfaceVariant,
|
|
child: CircleAvatar(
|
|
radius: 12,
|
|
backgroundColor:
|
|
Theme.of(context).scaffoldBackgroundColor,
|
|
),
|
|
),
|
|
title: Shimmer.fromColors(
|
|
baseColor:
|
|
Theme.of(context).colorScheme.surfaceVariant,
|
|
highlightColor: Theme.of(context)
|
|
.colorScheme
|
|
.onSurfaceVariant,
|
|
child: Container(
|
|
height: 10,
|
|
width: 100,
|
|
color:
|
|
Theme.of(context).scaffoldBackgroundColor,
|
|
),
|
|
),
|
|
subtitle: Shimmer.fromColors(
|
|
baseColor:
|
|
Theme.of(context).colorScheme.surfaceVariant,
|
|
highlightColor: Theme.of(context)
|
|
.colorScheme
|
|
.onSurfaceVariant,
|
|
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.builder(
|
|
itemCount: _transactions.length,
|
|
itemBuilder: (context, index) {
|
|
final tx = _transactions[index];
|
|
return Card(
|
|
margin: const EdgeInsets.symmetric(
|
|
horizontal: 0, vertical: 4),
|
|
child: ListTile(
|
|
leading: Icon(
|
|
tx.type == 'CR'
|
|
? Symbols.call_received
|
|
: Symbols.call_made,
|
|
color: tx.type == 'CR'
|
|
? const Color(0xFF10BB10)
|
|
: Theme.of(context).colorScheme.error,
|
|
),
|
|
title: Text(
|
|
tx.date ?? '',
|
|
style: const TextStyle(fontSize: 15),
|
|
),
|
|
subtitle: Text(
|
|
tx.name != null
|
|
? (tx.name!.length > 22
|
|
? tx.name!.substring(0, 22)
|
|
: tx.name!)
|
|
: '',
|
|
style: const TextStyle(fontSize: 12),
|
|
),
|
|
trailing: Column(
|
|
crossAxisAlignment:
|
|
CrossAxisAlignment.end,
|
|
mainAxisAlignment:
|
|
MainAxisAlignment.center,
|
|
children: [
|
|
Text(
|
|
"₹${tx.amount}",
|
|
style: const TextStyle(fontSize: 17),
|
|
),
|
|
Text(
|
|
"Bal: ₹${tx.balance}",
|
|
style: const TextStyle(
|
|
fontSize:
|
|
12), // Style matches tx.name
|
|
),
|
|
],
|
|
),
|
|
onTap: () {
|
|
Navigator.push(
|
|
context,
|
|
MaterialPageRoute(
|
|
builder: (_) =>
|
|
TransactionDetailsScreen(
|
|
transaction: tx),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
IgnorePointer(
|
|
child: Center(
|
|
child: Opacity(
|
|
opacity: 0.07, // Reduced opacity
|
|
child: ClipOval(
|
|
child: Image.asset(
|
|
'assets/images/logo.png',
|
|
width: 200, // Adjust size as needed
|
|
height: 200, // Adjust size as needed
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
floatingActionButton: FloatingActionButton(
|
|
onPressed: () {
|
|
_exportToPdf();
|
|
},
|
|
child: const Icon(Icons.download),
|
|
),
|
|
);
|
|
}
|
|
|
|
Future<void> _exportToPdf() async {
|
|
if (_transactions.isEmpty) {
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
const SnackBar(content: Text('No transactions to export.')),
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
|
|
await flutterLocalNotificationsPlugin.show(
|
|
0,
|
|
'Downloading PDF',
|
|
'Your account statement is being downloaded...',
|
|
const NotificationDetails(
|
|
android: AndroidNotificationDetails(
|
|
'download_channel',
|
|
'Download Notifications',
|
|
channelDescription: 'Notifications for PDF downloads',
|
|
importance: Importance.low,
|
|
priority: Priority.low,
|
|
showProgress: true,
|
|
maxProgress: 0,
|
|
ongoing: true,
|
|
icon: 'notification_icon',
|
|
),
|
|
),
|
|
);
|
|
|
|
// --- 1. LOAD ASSETS ---
|
|
final logoImage = pw.MemoryImage(
|
|
(await rootBundle.load('assets/images/logo.png')).buffer.asUint8List());
|
|
final timesFont = await rootBundle.load("assets/fonts/Rubik-Regular.ttf");
|
|
final timesBoldFont = await rootBundle.load("assets/fonts/Rubik-Bold.ttf");
|
|
final ttf = pw.Font.ttf(timesFont);
|
|
final ttfBold = pw.Font.ttf(timesBoldFont);
|
|
|
|
// --- 2. DEFINE COLORS ---
|
|
final primaryColor = PdfColor.fromHex("#1a5f3a");
|
|
final secondaryColor = PdfColor.fromHex("#2e7d32");
|
|
final debitColor = PdfColor.fromHex("#d32f2f");
|
|
final lightGreyColor = PdfColor.fromHex("#666");
|
|
final tableBorderColor = PdfColor.fromHex("#d0d0d0");
|
|
final lightBgColor = PdfColor.fromHex("#f9f9f9");
|
|
final warningBgColor = PdfColor.fromHex("#f8d7da");
|
|
final warningBorderColor = PdfColor.fromHex("#f5c6cb");
|
|
final warningTextColor = PdfColor.fromHex("#721c24");
|
|
|
|
// --- 3. CREATE PDF ---
|
|
final pdf = pw.Document(
|
|
theme: pw.ThemeData.withFont(base: ttf, bold: ttfBold),
|
|
);
|
|
|
|
// --- 4. BUILD PAGES ---
|
|
pdf.addPage(
|
|
pw.MultiPage(
|
|
pageFormat: PdfPageFormat.a4.copyWith(
|
|
marginTop: 15 * PdfPageFormat.mm,
|
|
marginLeft: 10 * PdfPageFormat.mm,
|
|
marginRight: 10 * PdfPageFormat.mm,
|
|
marginBottom: 20 * PdfPageFormat.mm,
|
|
),
|
|
header: (context) =>
|
|
_buildHeader(logoImage, primaryColor, lightGreyColor),
|
|
footer: (context) {
|
|
return pw.Center(
|
|
child: pw.Text(
|
|
'** This is only for information purpose and not for legal use **',
|
|
style: pw.TextStyle(
|
|
fontSize: 9,
|
|
color: lightGreyColor,
|
|
fontStyle: pw.FontStyle.italic)));
|
|
},
|
|
build: (context) => [
|
|
_buildAccountDetails(
|
|
customerName: selectedUser.name ?? '',
|
|
branchCode: selectedUser.branchId ?? '',
|
|
accountNo: selectedUser.accountNo ?? '',
|
|
cifNumber: selectedUser.cifNumber ?? '',
|
|
address: selectedUser.address ?? '',
|
|
lightGreyColor: lightGreyColor,
|
|
tableBorderColor: tableBorderColor,
|
|
lightBgColor: lightBgColor,
|
|
),
|
|
_buildWarning(
|
|
warningBgColor, warningBorderColor, debitColor, warningTextColor),
|
|
_buildPeriodHeader(
|
|
primaryColor: primaryColor,
|
|
fromDate: fromDate,
|
|
toDate: toDate,
|
|
),
|
|
_buildTransactionsTable(
|
|
transactions: _transactions,
|
|
primaryColor: primaryColor,
|
|
secondaryColor: secondaryColor,
|
|
debitColor: debitColor,
|
|
tableBorderColor: tableBorderColor,
|
|
),
|
|
pw.SizedBox(height: 20),
|
|
pw.Text('END OF STATEMENT', style: const pw.TextStyle(fontSize: 12)),
|
|
],
|
|
),
|
|
);
|
|
|
|
pdf.addPage(
|
|
pw.Page(
|
|
pageFormat: PdfPageFormat.a4.copyWith(
|
|
marginTop: 15 * PdfPageFormat.mm,
|
|
marginLeft: 10 * PdfPageFormat.mm,
|
|
marginRight: 10 * PdfPageFormat.mm,
|
|
marginBottom: 20 * PdfPageFormat.mm,
|
|
),
|
|
build: (context) => _buildLastPage(),
|
|
),
|
|
);
|
|
|
|
// --- 5. SAVE AND NOTIFY ---
|
|
try {
|
|
final Uint8List pdfBytes = await pdf.save();
|
|
final String timestamp =
|
|
DateFormat("ddMMyyyy_HHmm").format(DateTime.now());
|
|
final String fileName =
|
|
'Statement_${selectedUser.accountNo}_$timestamp.pdf';
|
|
|
|
String? filePath;
|
|
|
|
if (Platform.isAndroid) {
|
|
final directory = Directory('/storage/emulated/0/Download');
|
|
if (!await directory.exists()) {
|
|
await directory.create(recursive: true);
|
|
}
|
|
final file = File('${directory.path}/$fileName');
|
|
await file.writeAsBytes(pdfBytes);
|
|
filePath = file.path;
|
|
} else {
|
|
final tempDir = await getTemporaryDirectory();
|
|
final file = await File('${tempDir.path}/$fileName').create();
|
|
await file.writeAsBytes(pdfBytes);
|
|
filePath = file.path;
|
|
}
|
|
|
|
await flutterLocalNotificationsPlugin.show(
|
|
0,
|
|
'PDF Download Complete',
|
|
'Your account statement has been saved.',
|
|
const NotificationDetails(
|
|
android: AndroidNotificationDetails(
|
|
'download_channel',
|
|
'Download Notifications',
|
|
channelDescription: 'Notifications for PDF downloads',
|
|
importance: Importance.high,
|
|
priority: Priority.high,
|
|
showProgress: false,
|
|
ongoing: false,
|
|
autoCancel: true,
|
|
icon: 'notification_icon',
|
|
actions: [
|
|
AndroidNotificationAction('open_pdf', 'Open PDF',
|
|
showsUserInterface: true)
|
|
],
|
|
),
|
|
),
|
|
payload: filePath,
|
|
);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('PDF saved to: $filePath'),
|
|
),
|
|
);
|
|
}
|
|
} catch (e) {
|
|
await flutterLocalNotificationsPlugin.show(
|
|
0,
|
|
'PDF Download Failed',
|
|
'Error saving PDF: $e',
|
|
const NotificationDetails(
|
|
android: AndroidNotificationDetails(
|
|
'download_channel', 'Download Notifications',
|
|
channelDescription: 'Notifications for PDF downloads',
|
|
importance: Importance.high,
|
|
priority: Priority.high,
|
|
showProgress: false,
|
|
ongoing: false,
|
|
icon: 'notification_icon'),
|
|
),
|
|
);
|
|
|
|
if (mounted) {
|
|
ScaffoldMessenger.of(context).showSnackBar(
|
|
SnackBar(
|
|
content: Text('Error saving PDF: $e'),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
pw.Widget _buildHeader(pw.MemoryImage logoImage, PdfColor primaryColor,
|
|
PdfColor lightGreyColor) {
|
|
return pw.Container(
|
|
margin: const pw.EdgeInsets.only(bottom: 15),
|
|
padding: const pw.EdgeInsets.only(bottom: 12),
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border(bottom: pw.BorderSide(color: primaryColor, width: 2)),
|
|
),
|
|
child: pw.Row(
|
|
children: [
|
|
pw.Image(logoImage, height: 55, width: 55),
|
|
pw.SizedBox(width: 12),
|
|
pw.Expanded(
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text(
|
|
'THE KANGRA CENTRAL CO-OPERATIVE BANK LTD.',
|
|
style: pw.TextStyle(
|
|
fontSize: 18,
|
|
color: primaryColor,
|
|
fontWeight: pw.FontWeight.bold,
|
|
letterSpacing: 0.3),
|
|
),
|
|
pw.Text(
|
|
'Head Office: Dharmsala, District Kangra (H.P.), Pin. 176215',
|
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.end,
|
|
children: [
|
|
pw.Text(
|
|
'e-Statement Service',
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
color: primaryColor,
|
|
fontWeight: pw.FontWeight.bold),
|
|
),
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
'Generated: ${DateFormat("dd/MM/yyyy HH:mm").format(DateTime.now())}',
|
|
style: pw.TextStyle(fontSize: 9, color: lightGreyColor),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _buildAccountDetails({
|
|
required String customerName,
|
|
required String branchCode,
|
|
required String accountNo,
|
|
required String cifNumber,
|
|
required String address,
|
|
required PdfColor lightGreyColor,
|
|
required PdfColor tableBorderColor,
|
|
required PdfColor lightBgColor,
|
|
}) {
|
|
const cellPadding = pw.EdgeInsets.symmetric(horizontal: 12, vertical: 8);
|
|
final border = pw.BorderSide(color: tableBorderColor, width: 1);
|
|
|
|
return pw.Table(
|
|
border: pw.TableBorder(
|
|
top: border,
|
|
bottom: border,
|
|
left: border,
|
|
right: border,
|
|
horizontalInside: border,
|
|
verticalInside: border,
|
|
),
|
|
columnWidths: {
|
|
0: const pw.FlexColumnWidth(1),
|
|
1: const pw.FlexColumnWidth(1),
|
|
},
|
|
children: [
|
|
pw.TableRow(
|
|
children: [
|
|
_buildDetailCell(
|
|
'Customer Name', customerName, cellPadding, lightGreyColor),
|
|
_buildDetailCell(
|
|
'CIF Number', cifNumber, cellPadding, lightGreyColor),
|
|
],
|
|
),
|
|
pw.TableRow(
|
|
children: [
|
|
_buildDetailCell(
|
|
'Account Number', accountNo, cellPadding, lightGreyColor),
|
|
_buildDetailCell(
|
|
'Branch Code', branchCode, cellPadding, lightGreyColor),
|
|
],
|
|
),
|
|
pw.TableRow(
|
|
children: [
|
|
pw.Container(
|
|
padding: cellPadding,
|
|
// Using a Column inside a single cell to potentially wrap long address
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text(
|
|
'Customer Address',
|
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
|
),
|
|
pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
address,
|
|
style: pw.TextStyle(
|
|
fontSize: 11, fontWeight: pw.FontWeight.bold),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
// Empty container for the second column in this row, as it's a single spanning column
|
|
pw.Container(),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
pw.Widget _buildDetailCell(String label, String value, pw.EdgeInsets padding,
|
|
PdfColor lightGreyColor,
|
|
{bool vertical = false}) {
|
|
final children = [
|
|
pw.Text(
|
|
label,
|
|
style: pw.TextStyle(fontSize: 10, color: lightGreyColor),
|
|
),
|
|
if (vertical) pw.SizedBox(height: 2),
|
|
pw.Text(
|
|
value,
|
|
style: pw.TextStyle(fontSize: 11, fontWeight: pw.FontWeight.bold),
|
|
),
|
|
];
|
|
return pw.Padding(
|
|
padding: padding,
|
|
child: vertical
|
|
? pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
mainAxisAlignment: pw.MainAxisAlignment.start,
|
|
children: children)
|
|
: pw.Row(
|
|
mainAxisAlignment: pw.MainAxisAlignment.spaceBetween,
|
|
children: children),
|
|
);
|
|
}
|
|
|
|
pw.Widget _buildWarning(PdfColor warningBgColor, PdfColor warningBorderColor,
|
|
PdfColor debitColor, PdfColor warningTextColor) {
|
|
return pw.Container(
|
|
margin: const pw.EdgeInsets.symmetric(vertical: 15),
|
|
padding: const pw.EdgeInsets.all(10),
|
|
decoration: pw.BoxDecoration(
|
|
color: warningBgColor,
|
|
border: pw.Border.all(color: warningBorderColor, width: 1),
|
|
borderRadius: pw.BorderRadius.circular(4),
|
|
),
|
|
child: pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.SizedBox(width: 10),
|
|
pw.Expanded(
|
|
child: pw.RichText(
|
|
text: pw.TextSpan(
|
|
style: pw.TextStyle(
|
|
fontSize: 10, color: warningTextColor, lineSpacing: 1.5),
|
|
children: [
|
|
pw.TextSpan(
|
|
text: 'NEVER SHARE ',
|
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
|
const pw.TextSpan(
|
|
text:
|
|
'your Card number, CVV, PIN, OTP, Internet Banking User ID, Password or URB with anyone even if the caller claims to be a bank employee. Sharing these details can lead to unauthorized access to your account.'),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _buildPeriodHeader(
|
|
{required PdfColor primaryColor, DateTime? fromDate, DateTime? toDate}) {
|
|
String from = fromDate != null
|
|
? DateFormat('dd/MM/yyyy').format(fromDate)
|
|
: 'the beginning';
|
|
String to =
|
|
toDate != null ? DateFormat('dd/MM/yyyy').format(toDate) : 'today';
|
|
|
|
return pw.Container(
|
|
margin: const pw.EdgeInsets.only(bottom: 12),
|
|
padding: const pw.EdgeInsets.all(10),
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.symmetric(
|
|
horizontal: pw.BorderSide(color: primaryColor, width: 2)),
|
|
color: PdfColor.fromHex("#f5f5f5"),
|
|
),
|
|
alignment: pw.Alignment.center,
|
|
child: pw.RichText(
|
|
text: pw.TextSpan(
|
|
style: pw.TextStyle(
|
|
fontSize: 13,
|
|
color: primaryColor,
|
|
fontWeight: pw.FontWeight.bold),
|
|
children: [
|
|
const pw.TextSpan(text: 'Account statement from '),
|
|
pw.TextSpan(
|
|
text: from,
|
|
style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
|
const pw.TextSpan(text: ' to '),
|
|
pw.TextSpan(
|
|
text: to, style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
pw.Widget _buildTransactionsTable({
|
|
required List<Transaction> transactions,
|
|
required PdfColor primaryColor,
|
|
required PdfColor secondaryColor,
|
|
required PdfColor debitColor,
|
|
required PdfColor tableBorderColor,
|
|
}) {
|
|
//final border = pw.BorderSide(color: tableBorderColor, width: 1);
|
|
|
|
return pw.Table(
|
|
border: pw.TableBorder.all(color: tableBorderColor, width: 1),
|
|
columnWidths: {
|
|
0: const pw.FlexColumnWidth(1.5),
|
|
1: const pw.FlexColumnWidth(4),
|
|
2: const pw.FlexColumnWidth(2.2),
|
|
3: const pw.FlexColumnWidth(2.2),
|
|
},
|
|
children: [
|
|
pw.TableRow(
|
|
decoration: pw.BoxDecoration(color: secondaryColor),
|
|
children: [
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(10),
|
|
child: pw.Text('Date',
|
|
textAlign: pw.TextAlign.center,
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: pw.FontWeight.bold,
|
|
color: PdfColors.white)),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(10),
|
|
child: pw.Text('Mode / Particulars',
|
|
textAlign: pw.TextAlign.left,
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: pw.FontWeight.bold,
|
|
color: PdfColors.white)),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(10),
|
|
child: pw.Text('Withdrawals / Deposits',
|
|
textAlign: pw.TextAlign.center,
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: pw.FontWeight.bold,
|
|
color: PdfColors.white)),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(10),
|
|
child: pw.Text('Balance',
|
|
textAlign: pw.TextAlign.center,
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: pw.FontWeight.bold,
|
|
color: PdfColors.white)),
|
|
),
|
|
],
|
|
),
|
|
...transactions.map((tx) {
|
|
final amount = double.tryParse(tx.amount ?? '0') ?? 0;
|
|
final isDebit = tx.type == 'DR';
|
|
return pw.TableRow(
|
|
decoration: const pw.BoxDecoration(color: PdfColors.white),
|
|
children: [
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(8),
|
|
child: pw.Text(tx.date ?? '',
|
|
textAlign: pw.TextAlign.center,
|
|
style: const pw.TextStyle(fontSize: 11)),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(8),
|
|
child: pw.Text(tx.name ?? '',
|
|
textAlign: pw.TextAlign.left,
|
|
style: const pw.TextStyle(fontSize: 11)),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(8),
|
|
child: pw.Text(
|
|
'${NumberFormat.currency(locale: 'en_IN', symbol: '₹').format(amount)} ${tx.type}',
|
|
textAlign: pw.TextAlign.right,
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
color: isDebit ? debitColor : secondaryColor,
|
|
fontWeight: pw.FontWeight.bold,
|
|
),
|
|
),
|
|
),
|
|
pw.Padding(
|
|
padding: const pw.EdgeInsets.all(8),
|
|
child: pw.Text(
|
|
'${NumberFormat.currency(locale: 'en_IN', symbol: '₹').format(double.tryParse(tx.balance ?? '0') ?? 0)} ${tx.balanceType ?? ''}',
|
|
textAlign: pw.TextAlign.right,
|
|
style: pw.TextStyle(
|
|
fontSize: 11, fontWeight: pw.FontWeight.bold),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
],
|
|
);
|
|
}
|
|
|
|
pw.Widget _buildLastPage() {
|
|
return pw.Container(
|
|
padding: const pw.EdgeInsets.all(20),
|
|
decoration: pw.BoxDecoration(
|
|
border: pw.Border.all(color: PdfColors.black, width: 2),
|
|
color: PdfColor.fromHex("#fafafa"),
|
|
borderRadius: pw.BorderRadius.circular(4),
|
|
),
|
|
child: pw.Column(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Text(
|
|
'IMPORTANT INFORMATION:',
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
fontWeight: pw.FontWeight.bold,
|
|
color: PdfColors.black,
|
|
),
|
|
),
|
|
pw.SizedBox(height: 15),
|
|
..._buildInfoPoints(PdfColors.black, [
|
|
'The Kangra Central Cooperative Bank Officials or representatives will NEVER ask you for your personal information i.e. your card details, passwords, PIN, CVV, OTP etc. Do not share such details with anyone over phone, SMS or email.',
|
|
'Always stay vigilant to suspicious emails. Do not open any suspicious emails.',
|
|
'Always stay vigilant when giving out sensitive personal or account information.',
|
|
'Beware of messages that instill a sense of urgency (e.g., account will expire unless you "verify" your information). Contact the Bank directly if unsure.',
|
|
'Always log out of secondary devices and reset your passwords frequently.',
|
|
'Use strong passwords: Create strong passwords that are difficult for hackers to guess.',
|
|
'Use public Wi-Fi with caution: Be careful when using public Wi-Fi networks.',
|
|
'Back up your data regularly to a secure, encrypted, off-site location.',
|
|
'Follow corporate security policies: Adhere to your company\'s security guidelines.',
|
|
'Assess third-party app permissions carefully before granting access.',
|
|
'Lock your computer and mobile phone when not in use.',
|
|
'Don\'t leave devices unattended. Keep all mobile devices, such as laptops and cell phones, physically secured.',
|
|
'Don\'t leave Bluetooth / Wireless turned on when not in use. Enable them only when needed and in a safe environment.',
|
|
]),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
List<pw.Widget> _buildInfoPoints(PdfColor primaryColor, List<String> points) {
|
|
return points.map((point) {
|
|
return pw.Padding(
|
|
padding: const pw.EdgeInsets.only(bottom: 10),
|
|
child: pw.Row(
|
|
crossAxisAlignment: pw.CrossAxisAlignment.start,
|
|
children: [
|
|
pw.Container(
|
|
width: 15,
|
|
child: pw.Text('*',
|
|
style: pw.TextStyle(
|
|
color: primaryColor,
|
|
fontSize: 12,
|
|
fontWeight: pw.FontWeight.bold))),
|
|
pw.Expanded(
|
|
child: pw.Text(
|
|
point,
|
|
style: pw.TextStyle(
|
|
fontSize: 11,
|
|
lineSpacing: 1.6,
|
|
fontWeight: pw.FontWeight.bold),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}).toList();
|
|
}
|
|
|
|
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),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|