Skip to content

Lib/screen/customer #1187

Open
Open
@ahmadkhan-420

Description

@ahmadkhan-420

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);
  }
}

}
}

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions