Description
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:intl/intl.dart';
import 'package:printing/printing.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:sd_link/utils/theme/app_colors.dart';
import 'package:sd_link/utils/theme/app_spacing.dart';
import 'package:sd_link/models/customer.dart';
import 'package:sd_link/utils/pdf_export.dart';
import 'package:flutter_phone_direct_caller/flutter_phone_direct_caller.dart';
import 'package:sd_link/services/notification_service.dart';
class CustomerLedgerScreen extends StatefulWidget {
final String customerId;
final Map<String, dynamic> customerData;
const CustomerLedgerScreen({
super.key,
required this.customerId,
required this.customerData,
});
@OverRide
State createState() => _CustomerLedgerScreenState();
}
class _CustomerLedgerScreenState extends State {
final TextEditingController _searchController = TextEditingController();
final FirebaseFirestore _firestore = FirebaseFirestore.instance;
late Customer _customer;
bool _isGeneratingPdf = false;
@OverRide
void initState() {
super.initState();
_customer = Customer(
id: widget.customerId,
name: widget.customerData['name'] ?? 'Unknown',
phone: widget.customerData['phone'] ?? '',
location: widget.customerData['location'] ?? 'Unknown Location',
balance: (widget.customerData['balance'] as num?)?.toDouble() ?? 0.0,
);
}
@OverRide
void dispose() {
_searchController.dispose();
super.dispose();
}
@OverRide
Widget build(BuildContext context) {
final spacing = Theme.of(context).extension()!;
final balance = _customer.balance;
final isInDebt = balance < 0;
final formattedBalance = '${isInDebt ? '-' : ''}${balance.abs().toStringAsFixed(2)} PKR';
return Scaffold(
appBar: AppBar(
title: Text(
"${_customer.name}'s Ledger",
style: Theme.of(context).textTheme.titleLarge?.copyWith(
color: AppColors.textPrimary,
),
),
actions: [
IconButton(
icon: const Icon(Icons.person),
onPressed: () => Navigator.pushNamed(
context,
'/customer_profile',
arguments: {
'customerId': widget.customerId,
'customerData': widget.customerData,
},
),
),
IconButton(
icon: _isGeneratingPdf
? const CircularProgressIndicator()
: const Icon(Icons.picture_as_pdf),
onPressed: _isGeneratingPdf ? null : _generatePdfReport,
),
],
),
body: Padding(
padding: EdgeInsets.all(spacing.md),
child: Column(
children: [
_buildCustomerSummary(spacing, formattedBalance, isInDebt),
SizedBox(height: spacing.md),
_buildSearchBar(spacing),
SizedBox(height: spacing.md),
_buildActionButtons(spacing),
SizedBox(height: spacing.lg),
Expanded(
child: _buildLedgerEntries(),
),
],
),
),
floatingActionButton: FloatingActionButton(
backgroundColor: AppColors.primary,
onPressed: _navigateToEntryCreation,
child: const Icon(Icons.add, color: Colors.white),
),
);
}
Widget _buildCustomerSummary(AppSpacing spacing, String formattedBalance, bool isInDebt) {
return Card(
elevation: 2,
margin: EdgeInsets.symmetric(horizontal: spacing.md),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
child: Padding(
padding: EdgeInsets.all(spacing.md),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Current Balance:',
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold,
),
),
Text(
formattedBalance,
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: isInDebt ? AppColors.error : AppColors.success,
),
),
],
),
SizedBox(height: spacing.sm),
const Divider(color: AppColors.dividerColor),
SizedBox(height: spacing.sm),
Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
_buildInfoItem(Icons.phone, _customer.phone),
_buildInfoItem(Icons.location_on, _customer.location),
],
),
],
),
),
);
}
Widget _buildInfoItem(IconData icon, String text) {
return Row(
children: [
Icon(icon, size: 16, color: AppColors.textSecondary),
const SizedBox(width: 4),
Text(
text,
style: Theme.of(context).textTheme.bodySmall,
),
],
);
}
Widget _buildSearchBar(AppSpacing spacing) {
return TextField(
controller: _searchController,
decoration: InputDecoration(
prefixIcon: const Icon(Icons.search, color: AppColors.textSecondary),
hintText: 'Search entries...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(20),
borderSide: const BorderSide(color: AppColors.dividerColor),
),
filled: true,
fillColor: AppColors.background,
),
onChanged: (value) => setState(() {}),
);
}
Widget _buildActionButtons(AppSpacing spacing) {
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildActionButton(
Icons.receipt,
'Add Bill',
spacing,
() => _handleAction('Add Bill'),
),
_buildActionButton(
Icons.phone,
'Call',
spacing,
() => _handleAction('Call'),
),
_buildActionButton(
Icons.message,
'WhatsApp',
spacing,
() => _handleAction('WhatsApp'),
),
_buildActionButton(
Icons.notifications,
'Reminder',
spacing,
() => _handleAction('Reminder'),
),
],
);
}
Widget _buildActionButton(
IconData icon,
String label,
AppSpacing spacing,
VoidCallback onPressed,
) {
return Column(
children: [
IconButton(
icon: Icon(icon),
color: AppColors.primary,
onPressed: onPressed,
),
Text(
label,
style: TextStyle(
fontSize: 12,
color: AppColors.textSecondary,
),
),
],
);
}
Widget _buildLedgerEntries() {
return StreamBuilder<DocumentSnapshot<Map<String, dynamic>>>(
stream: _firestore.collection('customers').doc(widget.customerId).snapshots(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text(
'Error loading transactions: ${snapshot.error}',
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
if (!snapshot.hasData || !snapshot.data!.exists) {
return Center(
child: Text(
'No transactions found',
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
final transactions = snapshot.data!.data()!['transactions'] as List<dynamic>? ?? [];
final filteredTransactions = transactions.where((entry) {
final query = _searchController.text.toLowerCase();
if (query.isEmpty) return true;
final description = (entry['description']?.toString() ?? '').toLowerCase();
final amount = entry['amount'].toString().toLowerCase();
final date = DateFormat('yyyy-MM-dd HH:mm')
.format((entry['date'] as Timestamp).toDate())
.toLowerCase();
return description.contains(query) ||
amount.contains(query) ||
date.contains(query);
}).toList();
if (filteredTransactions.isEmpty) {
return Center(
child: Text(
'No matching transactions found',
style: Theme.of(context).textTheme.bodyMedium,
),
);
}
return ListView.separated(
itemCount: filteredTransactions.length,
separatorBuilder: (context, index) =>
Divider(color: AppColors.dividerColor.withAlpha(100)),
itemBuilder: (context, index) {
final entry = filteredTransactions[index];
return _buildTransactionItem(entry, index);
},
);
},
);
}
Widget _buildTransactionItem(Map<String, dynamic> entry, int index) {
final date = (entry['date'] as Timestamp).toDate();
final amount = (entry['amount'] as num).toDouble();
final description = entry['description'] ?? 'Transaction ${index + 1}';
return ListTile(
leading: CircleAvatar(
backgroundColor: AppColors.primary.withAlpha(30),
child: Text(
'${index + 1}',
style: TextStyle(color: AppColors.primary),
),
),
title: Text(
description,
style: Theme.of(context).textTheme.bodyMedium,
),
subtitle: Text(
DateFormat('MMM dd, yyyy - hh:mm a').format(date),
style: Theme.of(context).textTheme.bodySmall,
),
trailing: Text(
'${amount >= 0 ? '+' : ''}${amount.toStringAsFixed(2)} PKR',
style: TextStyle(
fontWeight: FontWeight.bold,
color: amount >= 0 ? AppColors.success : AppColors.error,
),
),
onLongPress: () => _showEntryOptions(entry),
);
}
Future _navigateToEntryCreation() async {
if (!mounted) return;
await Navigator.of(context).pushNamed(
'/entry_creation',
arguments: {
'customerId': widget.customerId,
'customerName': _customer.name,
},
);
}
Future _handleAction(String action) async {
if (!mounted) return;
final context = this.context;
final messenger = ScaffoldMessenger.of(context);
final navigator = Navigator.of(context);
try {
switch (action) {
case 'Add Bill':
await navigator.pushNamed(
'/bill_creation',
arguments: {
'customerId': widget.customerId,
'customerName': _customer.name,
'phone': _customer.phone,
},
);
if (!mounted) return;
break;
case 'Call':
final formattedNumber = _customer.phone.startsWith('+')
? _customer.phone
: _customer.phone.startsWith('0')
? '+92${_customer.phone.substring(1)}'
: '+92$_customer.phone';
final callResult = await FlutterPhoneDirectCaller.callNumber(formattedNumber);
if (!mounted) return;
if (!(callResult ?? false)) {
throw 'Could not initiate call';
}
break;
case 'WhatsApp':
final cleanNumber = _customer.phone.replaceAll(RegExp(r'[^0-9]'), '');
final message = Uri.encodeComponent(
'Hello ${_customer.name},\n'
'Your current balance: ${_customer.balance >= 0 ? 'PKR ${_customer.balance}' : 'PKR ${_customer.balance.abs()} (Due)'}\n'
'Last updated: ${DateFormat('dd MMM yyyy').format(DateTime.now())}',
);
final urls = [
'whatsapp://send?phone=$cleanNumber&text=$message',
'https://wa.me/$cleanNumber?text=$message',
'whatsapp-business://send?phone=$cleanNumber&text=$message',
];
bool launched = false;
for (final url in urls) {
if (await canLaunchUrl(Uri.parse(url))) {
await launchUrl(Uri.parse(url));
launched = true;
break;
}
}
if (!mounted) return;
if (!launched) {
throw 'No WhatsApp client installed';
}
break;
case 'Reminder':
final selectedDate = await showDatePicker(
context: context,
initialDate: DateTime.now(),
firstDate: DateTime.now(),
lastDate: DateTime.now().add(const Duration(days: 365)),
);
if (!mounted || selectedDate == null) return;
final selectedTime = await showTimePicker(
context: context,
initialTime: TimeOfDay.now(),
);
if (!mounted || selectedTime == null) return;
final reminderTime = DateTime(
selectedDate.year,
selectedDate.month,
selectedDate.day,
selectedTime.hour,
selectedTime.minute,
);
await _firestore.collection('reminders').add({
'customerId': widget.customerId,
'customerName': _customer.name,
'phone': _customer.phone,
'time': Timestamp.fromDate(reminderTime),
'status': 'pending',
'createdAt': FieldValue.serverTimestamp(),
});
await NotificationService.scheduleReminder(
id: reminderTime.millisecondsSinceEpoch,
title: 'Follow up with ${_customer.name}',
body: 'Reminder for customer contact',
scheduledTime: reminderTime,
payload: 'customerId:${widget.customerId}',
);
if (!mounted) return;
messenger.showSnackBar(
SnackBar(
content: Text(
'Reminder set for ${DateFormat('MMM dd, hh:mm a').format(reminderTime)}'),
behavior: SnackBarBehavior.floating,
),
);
break;
}
} catch (e) {
if (!mounted) return;
messenger.showSnackBar(
SnackBar(
content: Text('Action failed: ${e.toString()}'),
backgroundColor: AppColors.error,
),
);
}
}
void _showEntryOptions(Map<String, dynamic> entry) {
showDialog(
context: context,
builder: (dialogContext) => AlertDialog(
title: const Text('Entry Options'),
actions: [
TextButton(
child: const Text('Edit'),
onPressed: () {
Navigator.pop(dialogContext);
if (mounted) {
Navigator.pushNamed(
context,
'/edit_entry',
arguments: {
'customerId': widget.customerId,
'entryData': entry,
},
);
}
},
),
TextButton(
child: const Text('Delete', style: TextStyle(color: AppColors.error)),
onPressed: () => _confirmDelete(entry, dialogContext),
),
],
),
);
}
void _confirmDelete(Map<String, dynamic> entry, BuildContext dialogContext) {
final messenger = ScaffoldMessenger.of(context);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Confirm Delete'),
content: const Text('Are you sure you want to delete this entry?'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () => Navigator.pop(context),
),
TextButton(
child: const Text('Delete', style: TextStyle(color: AppColors.error)),
onPressed: () async {
try {
await _deleteEntry(entry);
if (mounted) {
Navigator.pop(dialogContext);
}
} catch (e) {
if (mounted) {
messenger.showSnackBar(
SnackBar(content: Text('Error deleting entry: $e')),
);
}
}
},
),
],
),
);
}
Future _deleteEntry(Map<String, dynamic> entry) async {
await _firestore.runTransaction((transaction) async {
final customerRef = _firestore.collection('customers').doc(widget.customerId);
final amount = (entry['amount'] as num).toDouble();
transaction.update(customerRef, {
'balance': FieldValue.increment(-amount),
'transactions': FieldValue.arrayRemove([entry]),
});
});
}
Future _generatePdfReport() async {
if (!mounted) return;
setState(() => _isGeneratingPdf = true);
final messenger = ScaffoldMessenger.of(context);
try {
final customerDoc = await _firestore
.collection('customers')
.doc(widget.customerId)
.get();
final customerData = {
'name': _customer.name,
'phone': _customer.phone,
'location': _customer.location,
'balance': _customer.balance,
'transactions': customerDoc.data()?['transactions'] ?? []
};
final pdf = await PdfGenerator.generateCustomerPdf([customerData]);
if (!mounted) return;
await Printing.sharePdf(
bytes: await pdf.save(),
filename: '${_customer.name}_ledger_${DateTime.now().millisecondsSinceEpoch}.pdf'
);
} catch (e) {
if (mounted) {
messenger.showSnackBar(
SnackBar(content: Text('PDF Error: ${e.toString()}')),
);
}
} finally {
if (mounted) {
setState(() => _isGeneratingPdf = false);
}
}
}
}