1050 lines
38 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import '../../core/theme/app_theme.dart';
import '../../models/fee.dart';
import '../../providers/fee_provider.dart';
class FeesScreen extends ConsumerStatefulWidget {
const FeesScreen({super.key});
@override
ConsumerState<FeesScreen> createState() => _FeesScreenState();
}
class _FeesScreenState extends ConsumerState<FeesScreen> with SingleTickerProviderStateMixin {
late TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
}
@override
void dispose() {
_tabController.dispose();
super.dispose();
}
String _formatCurrency(double amount, String symbol) {
return '$symbol${amount.toStringAsFixed(2)}';
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Fee Management'),
bottom: TabBar(
controller: _tabController,
labelColor: AppTheme.primaryColor,
unselectedLabelColor: AppTheme.textSecondary,
indicatorColor: AppTheme.primaryColor,
indicatorWeight: 3,
tabs: const [
Tab(text: 'Invoices & Summary'),
Tab(text: 'Payment History'),
],
),
),
body: TabBarView(
controller: _tabController,
children: [
_InvoicesAndSummaryTab(formatCurrency: _formatCurrency),
_PaymentHistoryTab(formatCurrency: _formatCurrency),
],
),
);
}
}
class _InvoicesAndSummaryTab extends ConsumerWidget {
final String Function(double, String) formatCurrency;
const _InvoicesAndSummaryTab({required this.formatCurrency});
@override
Widget build(BuildContext context, WidgetRef ref) {
final summaryAsync = ref.watch(feeSummaryProvider);
final invoicesAsync = ref.watch(feeInvoicesProvider);
final activeFilter = ref.watch(feeStatusFilterProvider);
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(feeSummaryProvider);
ref.invalidate(feeInvoicesProvider);
await Future.wait([
ref.read(feeSummaryProvider.future),
ref.read(feeInvoicesProvider.future),
]);
},
child: ListView(
padding: const EdgeInsets.all(16),
children: [
// Fee Summary Section
summaryAsync.when(
loading: () => const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 24),
child: CircularProgressIndicator(),
),
),
error: (err, _) => Card(
color: Colors.red.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Row(
children: [
const Icon(Icons.error_outline, color: AppTheme.errorColor),
const SizedBox(width: 12),
Expanded(
child: Text(
'Failed to load fee summary: $err',
style: const TextStyle(color: AppTheme.errorColor, fontSize: 13),
),
),
TextButton(
onPressed: () => ref.refresh(feeSummaryProvider),
child: const Text('Retry'),
),
],
),
),
),
data: (summary) => _buildSummaryCard(context, summary),
),
const SizedBox(height: 16),
// Invoices Header & Filter Chips
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Fee Invoices',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
if (activeFilter != null)
TextButton(
onPressed: () => ref.read(feeStatusFilterProvider.notifier).state = null,
child: const Text('Clear Filter', style: TextStyle(fontSize: 12)),
),
],
),
const SizedBox(height: 8),
// Filters
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
_buildFilterChip(ref, 'All', null, activeFilter == null),
const SizedBox(width: 8),
_buildFilterChip(ref, 'Unpaid', 'unpaid', activeFilter == 'unpaid'),
const SizedBox(width: 8),
_buildFilterChip(ref, 'Partial', 'partial', activeFilter == 'partial'),
const SizedBox(width: 8),
_buildFilterChip(ref, 'Overdue', 'overdue', activeFilter == 'overdue'),
const SizedBox(width: 8),
_buildFilterChip(ref, 'Paid', 'paid', activeFilter == 'paid'),
],
),
),
const SizedBox(height: 12),
// Invoices List
invoicesAsync.when(
loading: () => const Center(
child: Padding(
padding: EdgeInsets.symmetric(vertical: 40),
child: CircularProgressIndicator(),
),
),
error: (err, _) => Card(
color: Colors.red.shade50,
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
children: [
const Icon(Icons.error_outline, color: AppTheme.errorColor, size: 36),
const SizedBox(height: 8),
Text(
'Failed to load invoices: $err',
textAlign: TextAlign.center,
style: const TextStyle(color: AppTheme.errorColor, fontSize: 13),
),
const SizedBox(height: 12),
ElevatedButton(
onPressed: () => ref.refresh(feeInvoicesProvider),
child: const Text('Retry'),
),
],
),
),
),
data: (pageResult) {
if (pageResult.invoices.isEmpty) {
return _buildEmptyState(
icon: Icons.receipt_long_outlined,
message: activeFilter != null
? 'No invoices match the "$activeFilter" filter.'
: 'No fee invoices found for your account.',
);
}
return Column(
children: pageResult.invoices.map((inv) => _buildInvoiceCard(context, inv)).toList(),
);
},
),
],
),
);
}
Widget _buildFilterChip(WidgetRef ref, String label, String? value, bool isSelected) {
return ChoiceChip(
label: Text(label),
selected: isSelected,
selectedColor: AppTheme.primaryColor.withValues(alpha: 0.15),
labelStyle: TextStyle(
color: isSelected ? AppTheme.primaryColor : AppTheme.textSecondary,
fontWeight: isSelected ? FontWeight.bold : FontWeight.normal,
fontSize: 12,
),
onSelected: (_) {
ref.read(feeStatusFilterProvider.notifier).state = value;
},
);
}
Widget _buildSummaryCard(BuildContext context, FeeSummary summary) {
final sym = summary.currencySymbol;
Color statusColor;
String statusLabel;
switch (summary.paymentStatus) {
case 'fully_paid':
case 'paid':
statusColor = AppTheme.successColor;
statusLabel = 'Fully Paid';
break;
case 'overdue':
statusColor = AppTheme.errorColor;
statusLabel = 'Overdue';
break;
case 'partial':
statusColor = AppTheme.warningColor;
statusLabel = 'Partially Paid';
break;
default:
statusColor = summary.outstandingBalance > 0 ? AppTheme.primaryLight : AppTheme.successColor;
statusLabel = summary.outstandingBalance > 0 ? 'Pending' : 'No Dues';
}
return Container(
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
AppTheme.primaryColor,
AppTheme.primaryColor.withValues(alpha: 0.85),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
boxShadow: [
BoxShadow(
color: AppTheme.primaryColor.withValues(alpha: 0.25),
blurRadius: 10,
offset: const Offset(0, 4),
),
],
),
padding: const EdgeInsets.all(20),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'Financial Overview',
style: TextStyle(
color: Colors.white70,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.25),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: statusColor, width: 1),
),
child: Text(
statusLabel,
style: TextStyle(
color: statusColor == AppTheme.errorColor ? Colors.red.shade200 : Colors.white,
fontSize: 11,
fontWeight: FontWeight.bold,
),
),
),
],
),
const SizedBox(height: 12),
Text(
formatCurrency(summary.outstandingBalance, sym),
style: const TextStyle(
color: Colors.white,
fontSize: 32,
fontWeight: FontWeight.bold,
),
),
const Text(
'Outstanding Balance',
style: TextStyle(
color: Colors.white70,
fontSize: 13,
),
),
const Divider(color: Colors.white24, height: 24),
Row(
children: [
Expanded(
child: _buildMetricCol('Assessed', formatCurrency(summary.totalFees, sym)),
),
Expanded(
child: _buildMetricCol('Paid', formatCurrency(summary.totalPaid, sym)),
),
Expanded(
child: _buildMetricCol(
'Overdue',
formatCurrency(summary.overdueAmount, sym),
isAlert: summary.overdueAmount > 0,
),
),
],
),
if (summary.nextDueDate != null) ...[
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: Colors.white.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Icon(Icons.event_outlined, color: Colors.white70, size: 16),
const SizedBox(width: 8),
Text(
'Next Due Date: ${summary.nextDueDate}',
style: const TextStyle(color: Colors.white, fontSize: 12),
),
],
),
),
],
],
),
);
}
Widget _buildMetricCol(String label, String value, {bool isAlert = false}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(color: Colors.white60, fontSize: 11),
),
const SizedBox(height: 4),
Text(
value,
style: TextStyle(
color: isAlert ? Colors.amber.shade300 : Colors.white,
fontSize: 14,
fontWeight: FontWeight.w600,
),
),
],
);
}
Widget _buildInvoiceCard(BuildContext context, FeeInvoice invoice) {
final sym = invoice.currencySymbol;
Color statusColor;
String statusText;
switch (invoice.paymentStatus) {
case 'paid':
statusColor = AppTheme.successColor;
statusText = 'Paid';
break;
case 'overdue':
statusColor = AppTheme.errorColor;
statusText = 'Overdue';
break;
case 'partial':
statusColor = AppTheme.warningColor;
statusText = 'Partial';
break;
default:
statusColor = AppTheme.textSecondary;
statusText = 'Unpaid';
}
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppTheme.borderLight),
),
child: InkWell(
borderRadius: BorderRadius.circular(12),
onTap: () => _showInvoiceDetailSheet(context, invoice.id),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
invoice.invoiceNumber,
style: const TextStyle(
fontWeight: FontWeight.bold,
fontSize: 15,
color: AppTheme.textPrimary,
),
),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3),
decoration: BoxDecoration(
color: statusColor.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: statusColor.withValues(alpha: 0.5)),
),
child: Text(
statusText,
style: TextStyle(
color: statusColor,
fontWeight: FontWeight.bold,
fontSize: 11,
),
),
),
],
),
const SizedBox(height: 6),
Text(
invoice.title,
style: const TextStyle(
color: AppTheme.textSecondary,
fontSize: 13,
),
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'Total Amount',
style: TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
const SizedBox(height: 2),
Text(
formatCurrency(invoice.amount, sym),
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: AppTheme.textPrimary,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Text(
'Paid',
style: TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
const SizedBox(height: 2),
Text(
formatCurrency(invoice.amountPaid, sym),
style: const TextStyle(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppTheme.successColor,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
const Text(
'Due',
style: TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
const SizedBox(height: 2),
Text(
formatCurrency(invoice.amountDue, sym),
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: invoice.isOverdue ? AppTheme.errorColor : AppTheme.textPrimary,
),
),
],
),
],
),
if (invoice.dueDate != null) ...[
const Divider(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Icon(
Icons.schedule_rounded,
size: 14,
color: invoice.isOverdue ? AppTheme.errorColor : AppTheme.textMuted,
),
const SizedBox(width: 4),
Text(
'Due: ${invoice.dueDate}',
style: TextStyle(
fontSize: 12,
color: invoice.isOverdue ? AppTheme.errorColor : AppTheme.textSecondary,
fontWeight: invoice.isOverdue ? FontWeight.bold : FontWeight.normal,
),
),
],
),
const Row(
children: [
Text(
'View Details',
style: TextStyle(
fontSize: 12,
color: AppTheme.primaryLight,
fontWeight: FontWeight.w600,
),
),
Icon(Icons.chevron_right, size: 16, color: AppTheme.primaryLight),
],
),
],
),
],
],
),
),
),
);
}
void _showInvoiceDetailSheet(BuildContext context, int invoiceId) {
showModalBottomSheet(
context: context,
isScrollControlled: true,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(20)),
),
builder: (_) => _InvoiceDetailBottomSheet(invoiceId: invoiceId, formatCurrency: formatCurrency),
);
}
Widget _buildEmptyState({required IconData icon, required String message}) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 40, horizontal: 20),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(icon, size: 54, color: AppTheme.textMuted),
const SizedBox(height: 12),
Text(
message,
textAlign: TextAlign.center,
style: const TextStyle(color: AppTheme.textSecondary, fontSize: 14),
),
],
),
);
}
}
class _InvoiceDetailBottomSheet extends ConsumerWidget {
final int invoiceId;
final String Function(double, String) formatCurrency;
const _InvoiceDetailBottomSheet({
required this.invoiceId,
required this.formatCurrency,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
final detailAsync = ref.watch(feeInvoiceDetailProvider(invoiceId));
return DraggableScrollableSheet(
initialChildSize: 0.75,
minChildSize: 0.5,
maxChildSize: 0.95,
expand: false,
builder: (_, scrollController) {
return detailAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: AppTheme.errorColor),
const SizedBox(height: 12),
Text('Failed to load invoice details: $err', textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.refresh(feeInvoiceDetailProvider(invoiceId)),
child: const Text('Retry'),
),
],
),
),
data: (invoice) {
final sym = invoice.currencySymbol;
return ListView(
controller: scrollController,
padding: const EdgeInsets.all(20),
children: [
Center(
child: Container(
width: 40,
height: 4,
decoration: BoxDecoration(
color: AppTheme.borderLight,
borderRadius: BorderRadius.circular(2),
),
),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
invoice.invoiceNumber,
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(
invoice.title,
style: const TextStyle(color: AppTheme.textSecondary, fontSize: 14),
),
],
),
),
_buildStatusBadge(invoice.paymentStatus),
],
),
const Divider(height: 24),
// Metadata Grid
Row(
children: [
if (invoice.academicYear != null)
Expanded(
child: _buildMetaItem('Academic Year', invoice.academicYear!.name),
),
if (invoice.semester != null)
Expanded(
child: _buildMetaItem('Semester', invoice.semester!.name),
),
],
),
const SizedBox(height: 12),
Row(
children: [
if (invoice.assignmentDate != null)
Expanded(
child: _buildMetaItem('Issue Date', invoice.assignmentDate!),
),
if (invoice.dueDate != null)
Expanded(
child: _buildMetaItem('Due Date', invoice.dueDate!),
),
],
),
const Divider(height: 28),
// Itemized Breakdown
const Text(
'Fee Breakdown',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
if (invoice.lines.isEmpty)
const Text('No line items recorded', style: TextStyle(color: AppTheme.textMuted))
else
...invoice.lines.map((line) => Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
line.name,
style: const TextStyle(fontWeight: FontWeight.w500, fontSize: 14),
),
if (line.category != null)
Text(
line.category!,
style: const TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
],
),
),
Text(
formatCurrency(line.amount, sym),
style: const TextStyle(fontWeight: FontWeight.w600, fontSize: 14),
),
],
),
)),
const Divider(height: 24),
// Totals
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Total Amount', style: TextStyle(color: AppTheme.textSecondary)),
Text(formatCurrency(invoice.amount, sym), style: const TextStyle(fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Amount Paid', style: TextStyle(color: AppTheme.textSecondary)),
Text(
formatCurrency(invoice.amountPaid, sym),
style: const TextStyle(color: AppTheme.successColor, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 6),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('Balance Due', style: TextStyle(fontWeight: FontWeight.bold, fontSize: 15)),
Text(
formatCurrency(invoice.amountDue, sym),
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 16,
color: invoice.isOverdue ? AppTheme.errorColor : AppTheme.primaryColor,
),
),
],
),
// Payment History on this invoice
if (invoice.payments.isNotEmpty) ...[
const Divider(height: 28),
const Text(
'Payments Received',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 10),
...invoice.payments.map((p) => Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppTheme.surfaceColor,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: AppTheme.borderLight),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
p.receiptNumber,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
),
if (p.paymentDate != null)
Text(
p.paymentDate!,
style: const TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
],
),
Text(
formatCurrency(p.amount, sym),
style: const TextStyle(
fontWeight: FontWeight.bold,
color: AppTheme.successColor,
),
),
],
),
)),
],
const SizedBox(height: 24),
// Safe Payment Notice (Gateway not enabled)
Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.blue.shade50,
borderRadius: BorderRadius.circular(8),
border: Border.all(color: Colors.blue.shade200),
),
child: const Row(
children: [
Icon(Icons.info_outline, color: AppTheme.primaryLight, size: 20),
SizedBox(width: 10),
Expanded(
child: Text(
'To make fee payments, please visit the university accounts office or use the authorized institution bank transfer details.',
style: TextStyle(fontSize: 12, color: AppTheme.primaryDark),
),
),
],
),
),
],
);
},
);
},
);
}
Widget _buildMetaItem(String label, String value) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(label, style: const TextStyle(fontSize: 11, color: AppTheme.textMuted)),
const SizedBox(height: 2),
Text(value, style: const TextStyle(fontSize: 13, fontWeight: FontWeight.w500)),
],
);
}
Widget _buildStatusBadge(String status) {
Color color;
String label;
switch (status) {
case 'paid':
color = AppTheme.successColor;
label = 'Paid';
break;
case 'overdue':
color = AppTheme.errorColor;
label = 'Overdue';
break;
case 'partial':
color = AppTheme.warningColor;
label = 'Partial';
break;
default:
color = AppTheme.textSecondary;
label = 'Unpaid';
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.12),
borderRadius: BorderRadius.circular(6),
border: Border.all(color: color),
),
child: Text(
label,
style: TextStyle(color: color, fontWeight: FontWeight.bold, fontSize: 12),
),
);
}
}
class _PaymentHistoryTab extends ConsumerWidget {
final String Function(double, String) formatCurrency;
const _PaymentHistoryTab({required this.formatCurrency});
@override
Widget build(BuildContext context, WidgetRef ref) {
final paymentsAsync = ref.watch(feePaymentsProvider);
return RefreshIndicator(
onRefresh: () async {
ref.invalidate(feePaymentsProvider);
await ref.read(feePaymentsProvider.future);
},
child: paymentsAsync.when(
loading: () => const Center(child: CircularProgressIndicator()),
error: (err, _) => Center(
child: Padding(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error_outline, size: 48, color: AppTheme.errorColor),
const SizedBox(height: 12),
Text('Failed to load payment history: $err', textAlign: TextAlign.center),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => ref.refresh(feePaymentsProvider),
child: const Text('Retry'),
),
],
),
),
),
data: (pageResult) {
if (pageResult.payments.isEmpty) {
return const Center(
child: Padding(
padding: EdgeInsets.all(24),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.payment_outlined, size: 54, color: AppTheme.textMuted),
SizedBox(height: 12),
Text(
'No payment records found.',
style: TextStyle(color: AppTheme.textSecondary, fontSize: 14),
),
],
),
),
);
}
return ListView.builder(
padding: const EdgeInsets.all(16),
itemCount: pageResult.payments.length,
itemBuilder: (context, index) {
final payment = pageResult.payments[index];
final sym = payment.currencySymbol;
return Card(
margin: const EdgeInsets.only(bottom: 12),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppTheme.borderLight),
),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Row(
children: [
Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: AppTheme.successColor.withValues(alpha: 0.1),
shape: BoxShape.circle,
),
child: const Icon(
Icons.check_circle_outline,
color: AppTheme.successColor,
size: 20,
),
),
const SizedBox(width: 10),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
payment.receiptNumber,
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
),
if (payment.paymentDate != null)
Text(
payment.paymentDate!,
style: const TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
],
),
],
),
Text(
formatCurrency(payment.amount, sym),
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
color: AppTheme.successColor,
),
),
],
),
const SizedBox(height: 12),
const Divider(height: 1),
const SizedBox(height: 10),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Method: ${payment.paymentMethodLabel}',
style: const TextStyle(fontSize: 12, color: AppTheme.textSecondary),
),
if (payment.invoiceNumber != null)
Text(
'Invoice: ${payment.invoiceNumber}',
style: const TextStyle(fontSize: 12, color: AppTheme.primaryLight),
),
],
),
if (payment.referenceNumber != null && payment.referenceNumber!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'Ref: ${payment.referenceNumber}',
style: const TextStyle(fontSize: 11, color: AppTheme.textMuted),
),
],
if (payment.remarks != null && payment.remarks!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
'Note: ${payment.remarks}',
style: const TextStyle(fontSize: 11, color: AppTheme.textMuted, fontStyle: FontStyle.italic),
),
],
],
),
),
);
},
);
},
),
);
}
}