1935 lines
68 KiB
Dart
1935 lines
68 KiB
Dart
import 'package:flutter/material.dart';
|
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|
import 'package:go_router/go_router.dart';
|
|
import 'package:google_fonts/google_fonts.dart';
|
|
import 'package:intl/intl.dart';
|
|
import 'package:nhance_partner/data/utils/toastNotification.dart';
|
|
|
|
import '../../../core/routing/routes.dart';
|
|
import '../../../core/services/api_service.dart';
|
|
import '../../../data/utils/Pagination.dart';
|
|
import '../../layouts/main_layout.dart';
|
|
import '../../layouts/responsive_layout.dart';
|
|
import '../../providers/manager_provider.dart';
|
|
import '../../providers/userRoleProvider.dart';
|
|
import '../../themes/indicators/export_btn.dart';
|
|
import '../../themes/indicators/search_field_theme.dart';
|
|
import '../../widgets/custom_Stdate_EnDate_Filter.dart';
|
|
|
|
class PayoutList extends ConsumerStatefulWidget {
|
|
const PayoutList({super.key});
|
|
|
|
@override
|
|
ConsumerState<PayoutList> createState() => _PayoutListState();
|
|
}
|
|
|
|
class _PayoutListState extends ConsumerState<PayoutList> {
|
|
static const double _payoutFilterFieldHeight = 35;
|
|
|
|
int currentPage = 1;
|
|
int itemsPerPage = 10;
|
|
bool isLoading = false;
|
|
List<Map<String, dynamic>> getPayoutData = [];
|
|
List<Map<String, dynamic>> originalData = [];
|
|
List<Map<String, dynamic>> filteredData = [];
|
|
dynamic roleId;
|
|
dynamic userId;
|
|
late ApiService apiService;
|
|
final _formKey = GlobalKey<FormState>();
|
|
Map<String, TextEditingController> controllers = {};
|
|
List<String> tabHeader = [
|
|
'startDate',
|
|
'endDate',
|
|
'agentName',
|
|
'agentCode',
|
|
'policyNo',
|
|
];
|
|
dynamic SelectedStatus;
|
|
dynamic SelectedStaffId;
|
|
DateTime _paymentDate = DateTime.now();
|
|
dynamic _hoveredHistoryInvoiceId;
|
|
static const double _actionIconSize = 12;
|
|
static const double _actionButtonSize = 28;
|
|
static const double _actionIconGap = 2;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
apiService = ApiService();
|
|
for (String field in tabHeader) {
|
|
controllers[field] = TextEditingController();
|
|
}
|
|
|
|
Future.microtask(() {
|
|
final id = ref.read(managerIdProvider);
|
|
roleId = ref.read(userRoleProvider);
|
|
userId = ref.read(userIdProvider);
|
|
|
|
print("PL 1 => r : $roleId | mId: $id | uId: $userId ");
|
|
getPayoutList();
|
|
});
|
|
}
|
|
|
|
// Get Payout List
|
|
Future<void> getPayoutList() async {
|
|
print('PL 2 => get Payout Listing Called');
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final from = controllers['startDate']?.text.trim() ?? '';
|
|
final to = controllers['endDate']?.text.trim() ?? '';
|
|
final response = await apiService.getPayoutList(
|
|
fromDate: from.isNotEmpty ? from : null,
|
|
toDate: to.isNotEmpty ? to : null,
|
|
agentName: controllers['agentName']?.text,
|
|
agentCode: controllers['agentCode']?.text,
|
|
policyNo: controllers['policyNo']?.text,
|
|
);
|
|
|
|
if (response['status'] == 'success') {
|
|
print('PL 3 => getPayoutList - ${response['data']}');
|
|
final data = response['data'];
|
|
print(' PL 4 => getPayoutList => ${response['data']}');
|
|
|
|
setState(() {
|
|
currentPage = 1;
|
|
|
|
if (data is List) {
|
|
// Already a list of maps
|
|
getPayoutData = List<Map<String, dynamic>>.from(data);
|
|
} else if (data is Map) {
|
|
// Single object, wrap in a list
|
|
getPayoutData = [Map<String, dynamic>.from(data)];
|
|
} else {
|
|
getPayoutData = [];
|
|
}
|
|
originalData = getPayoutData;
|
|
filteredData = List.from(originalData);
|
|
});
|
|
} else {
|
|
originalData = [];
|
|
filteredData = [];
|
|
}
|
|
} catch (e) {
|
|
print('PL 5 => Exception occurred: $e');
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
// Delete Invoice List
|
|
Future<void> deleteInvoice(ID) async {
|
|
setState(() {
|
|
isLoading = true;
|
|
});
|
|
|
|
try {
|
|
final response = await apiService.deleteInvoice(ID);
|
|
|
|
if (response['status'] == 'success') {
|
|
ToastHelper.showSuccessToast(context, response['data']);
|
|
getPayoutList();
|
|
} else {
|
|
ToastHelper.showSuccessToast(context, response['data']);
|
|
}
|
|
} catch (e) {
|
|
print('PL 6 => Exception occurred: $e');
|
|
} finally {
|
|
setState(() {
|
|
isLoading = false;
|
|
});
|
|
}
|
|
}
|
|
|
|
List<dynamic> get _paginatedData {
|
|
// Sort descending by id first
|
|
final sortedData = [...filteredData]
|
|
..sort((a, b) => int.parse(b['id']) - int.parse(a['id']));
|
|
|
|
if (sortedData.isEmpty) return [];
|
|
|
|
// Ensure currentPage is valid
|
|
final maxPage = (sortedData.length / itemsPerPage).ceil();
|
|
final safePage = currentPage.clamp(1, maxPage);
|
|
|
|
final startIndex = (safePage - 1) * itemsPerPage;
|
|
final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length);
|
|
|
|
return sortedData.sublist(startIndex, endIndex);
|
|
}
|
|
|
|
void filterDateRange() {
|
|
getPayoutList();
|
|
}
|
|
|
|
void refrshfilterDateRange() {
|
|
setState(() {
|
|
controllers['startDate']!.clear();
|
|
controllers['endDate']!.clear();
|
|
controllers['agentName']!.clear();
|
|
controllers['agentCode']!.clear();
|
|
controllers['policyNo']!.clear();
|
|
_formKey.currentState?.reset();
|
|
});
|
|
getPayoutList();
|
|
}
|
|
|
|
void filterData(String query) {
|
|
print("PL 7 =>FilterDAta - $query");
|
|
final q = query.toLowerCase();
|
|
setState(() {
|
|
filteredData = getPayoutData.where((item) {
|
|
// Search matches visible table columns only.
|
|
final invoiceDateSearch = _formatDateSafe(item['invoice_date'])
|
|
.toLowerCase();
|
|
final statusSearch =
|
|
_exportPayoutStatusLabel(item['payout_status']).toLowerCase();
|
|
|
|
return invoiceDateSearch.contains(q) ||
|
|
(item['agent_name'] ?? '-').toString().toLowerCase().contains(q) ||
|
|
(item['agent_code'] ?? '-').toString().toLowerCase().contains(q) ||
|
|
(item['policy_no'] ?? '-').toString().toLowerCase().contains(q) ||
|
|
(item['invoice_no'] ?? '-').toLowerCase().contains(q) ||
|
|
(item['invoice_amount_indian_format'] ?? '-')
|
|
.toLowerCase()
|
|
.contains(q) ||
|
|
(item['utr_numbers'] ?? '-').toLowerCase().contains(q) ||
|
|
(item['invoiced_amount'] ?? '0').toLowerCase().contains(q) ||
|
|
(item['payout_amount'] ?? '0').toLowerCase().contains(q) ||
|
|
(item['balance_amount'] ?? '0').toLowerCase().contains(q) ||
|
|
statusSearch.contains(q) ||
|
|
// Also match raw codes if user types "1" or "2"
|
|
(item['payout_status'] ?? '-').toString().toLowerCase().contains(q);
|
|
}).toList();
|
|
});
|
|
}
|
|
|
|
String _formatDateSafe(dynamic raw) {
|
|
if (raw == null) return '-';
|
|
try {
|
|
final dateTime = DateTime.parse(raw.toString());
|
|
return DateFormat('dd-MM-yyyy').format(dateTime);
|
|
} catch (_) {
|
|
return raw.toString();
|
|
}
|
|
}
|
|
|
|
final TextEditingController _searchStaffController = TextEditingController();
|
|
|
|
String _formatDate(String rawDate) {
|
|
try {
|
|
final dateTime = DateTime.parse(rawDate);
|
|
return DateFormat('dd-MM-yyyy').format(dateTime); // 24-hour format
|
|
} catch (e) {
|
|
return rawDate; // fallback if parsing fails
|
|
}
|
|
}
|
|
|
|
/// Excel export: match table (1 = Pending, 2 = Completed).
|
|
String _exportPayoutStatusLabel(dynamic v) {
|
|
if (v == null) return '-';
|
|
final s = v.toString().trim();
|
|
if (s.isEmpty || s.toLowerCase() == 'null') return '-';
|
|
if (s == '1') return 'Pending';
|
|
if (s == '2') return 'Completed';
|
|
return 'Completed';
|
|
}
|
|
|
|
String _displayText(dynamic value) {
|
|
if (value == null) return '-';
|
|
final text = value.toString().trim();
|
|
if (text.isEmpty || text.toLowerCase() == 'null') return '-';
|
|
return text;
|
|
}
|
|
|
|
String _truncateText(String value, {int maxChars = 25}) {
|
|
if (value.length <= maxChars) return value;
|
|
return '${value.substring(0, maxChars)}...';
|
|
}
|
|
|
|
String _utrTooltipText(dynamic value) {
|
|
final text = _displayText(value);
|
|
if (text == '-') return text;
|
|
final items = text
|
|
.split(',')
|
|
.map((e) => e.trim())
|
|
.where((e) => e.isNotEmpty)
|
|
.toList();
|
|
if (items.isEmpty) return text;
|
|
return items.join('\n');
|
|
}
|
|
|
|
String _displayAmount(dynamic value) {
|
|
if (value == null) return '0';
|
|
final text = value.toString().trim();
|
|
if (text.isEmpty || text.toLowerCase() == 'null') return '0';
|
|
return text;
|
|
}
|
|
|
|
double _toDouble(dynamic value) {
|
|
if (value == null) return 0;
|
|
return double.tryParse(value.toString()) ?? 0;
|
|
}
|
|
|
|
String _generateUtrNumber() {
|
|
final now = DateTime.now();
|
|
return 'UTR${DateFormat('yyyyMMddHHmmss').format(now)}';
|
|
}
|
|
|
|
Future<void> _showAddPaymentDialog(Map<String, dynamic> item) async {
|
|
final TextEditingController amountController = TextEditingController();
|
|
_paymentDate = DateTime.now();
|
|
dynamic selectedUtrId;
|
|
List<Map<String, dynamic>> utrDetails = [];
|
|
String generatedUtrNo = _generateUtrNumber();
|
|
double totalAmount = _toDouble(item['invoiced_amount']);
|
|
double paidToDate = _toDouble(item['payout_amount']);
|
|
double currentBalance = _toDouble(item['balance_amount']);
|
|
double selectedUtrExistingPaid = 0;
|
|
double maxAllowedForSelectedUtr = 0;
|
|
|
|
try {
|
|
final utrResponse = await apiService.getInvoiceUtrDetails(item['id']);
|
|
if (utrResponse['status'] == 'success') {
|
|
final dataRoot = utrResponse['data'] is Map<String, dynamic>
|
|
? Map<String, dynamic>.from(utrResponse['data'])
|
|
: <String, dynamic>{};
|
|
final invoiceSummary =
|
|
dataRoot['invoice'] is Map<String, dynamic>
|
|
? Map<String, dynamic>.from(dataRoot['invoice'])
|
|
: <String, dynamic>{};
|
|
totalAmount = _toDouble(invoiceSummary['total_amount']);
|
|
paidToDate = _toDouble(invoiceSummary['paid_to_date']);
|
|
currentBalance = _toDouble(invoiceSummary['balance_amount']);
|
|
utrDetails = List<Map<String, dynamic>>.from(dataRoot['utrs_details'] ?? []);
|
|
if (utrDetails.isEmpty) {
|
|
generatedUtrNo = _generateUtrNumber();
|
|
}
|
|
}
|
|
} catch (e) {
|
|
ToastHelper.showWarningToast(
|
|
context,
|
|
'Failed to load UTR list. Please refresh and try again.',
|
|
);
|
|
print('PL 8 => UTR load error: $e');
|
|
}
|
|
|
|
final result = await showDialog<bool>(
|
|
context: context,
|
|
barrierDismissible: false,
|
|
builder: (ctx) {
|
|
return StatefulBuilder(
|
|
builder: (ctx, setLocalState) {
|
|
return AlertDialog(
|
|
title: Text(
|
|
'Add Paid Amount',
|
|
style: GoogleFonts.poppins(fontSize: 16, fontWeight: FontWeight.w600),
|
|
),
|
|
content: SizedBox(
|
|
width: 360,
|
|
child: Column(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'Invoice number #${_displayText(item['invoice_no'])}',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 13,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Total Amount',
|
|
style: GoogleFonts.inter(fontSize: 12),
|
|
),
|
|
),
|
|
Text(': ${totalAmount.toStringAsFixed(2)}',
|
|
style: GoogleFonts.inter(fontSize: 12)),
|
|
],
|
|
),
|
|
const SizedBox(height: 6),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Amount Paid Till Date',
|
|
style: GoogleFonts.inter(fontSize: 12),
|
|
),
|
|
),
|
|
Text(': ${paidToDate.toStringAsFixed(2)}',
|
|
style: GoogleFonts.inter(fontSize: 12)),
|
|
],
|
|
),
|
|
const SizedBox(height: 8),
|
|
const Divider(thickness: 1),
|
|
const SizedBox(height: 8),
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'Balance Amount',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
': ${currentBalance.toStringAsFixed(2)}',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const SizedBox(height: 12),
|
|
if (currentBalance > 0) ...[
|
|
if (utrDetails.isNotEmpty)
|
|
DropdownButtonFormField<dynamic>(
|
|
initialValue: selectedUtrId,
|
|
isExpanded: true,
|
|
decoration: const InputDecoration(
|
|
labelText: 'UTR Number',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
items: utrDetails.map((utr) {
|
|
return DropdownMenuItem<dynamic>(
|
|
value: utr['id'],
|
|
child: Text(_displayText(utr['utr_no'])),
|
|
);
|
|
}).toList(),
|
|
onChanged: (value) {
|
|
final selected = utrDetails.firstWhere(
|
|
(row) => '${row['id']}' == '$value',
|
|
orElse: () => <String, dynamic>{},
|
|
);
|
|
final selectedPaid = _toDouble(selected['paid_amount']);
|
|
final selectedDate = selected['utr_date'];
|
|
setLocalState(() {
|
|
selectedUtrId = value;
|
|
selectedUtrExistingPaid = selectedPaid;
|
|
maxAllowedForSelectedUtr =
|
|
currentBalance + selectedUtrExistingPaid;
|
|
amountController.text = selectedPaid > 0
|
|
? selectedPaid.toStringAsFixed(2)
|
|
: '';
|
|
if (selectedDate != null &&
|
|
selectedDate.toString().trim().isNotEmpty) {
|
|
_paymentDate =
|
|
DateTime.tryParse(selectedDate.toString()) ??
|
|
_paymentDate;
|
|
}
|
|
});
|
|
},
|
|
)
|
|
else
|
|
TextFormField(
|
|
readOnly: true,
|
|
initialValue: generatedUtrNo,
|
|
decoration: const InputDecoration(
|
|
labelText: 'Generated UTR Number',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
controller: amountController,
|
|
keyboardType: const TextInputType.numberWithOptions(decimal: true),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Paid Amount',
|
|
border: OutlineInputBorder(),
|
|
),
|
|
),
|
|
const SizedBox(height: 12),
|
|
TextFormField(
|
|
readOnly: true,
|
|
controller: TextEditingController(
|
|
text: DateFormat('dd-MM-yyyy').format(_paymentDate),
|
|
),
|
|
decoration: const InputDecoration(
|
|
labelText: 'Paid Date',
|
|
border: OutlineInputBorder(),
|
|
suffixIcon: Icon(Icons.calendar_today_outlined, size: 18),
|
|
),
|
|
onTap: () async {
|
|
final now = DateTime.now();
|
|
final minDate = DateTime(now.year, now.month, now.day);
|
|
final picked = await showDatePicker(
|
|
context: ctx,
|
|
initialDate: _paymentDate.isBefore(minDate)
|
|
? minDate
|
|
: _paymentDate,
|
|
firstDate: minDate,
|
|
lastDate: DateTime(2100),
|
|
);
|
|
if (picked != null) {
|
|
setLocalState(() {
|
|
_paymentDate = picked;
|
|
});
|
|
}
|
|
},
|
|
),
|
|
] else
|
|
Text(
|
|
'Invoice fully paid. No payment entry required.',
|
|
style: GoogleFonts.inter(
|
|
fontSize: 12,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF2E7D6E),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
actions: [
|
|
TextButton(
|
|
onPressed: () => Navigator.pop(ctx, false),
|
|
child: const Text('Cancel'),
|
|
),
|
|
if (currentBalance > 0)
|
|
ElevatedButton(
|
|
onPressed: () async {
|
|
final enteredAmount = _toDouble(amountController.text.trim());
|
|
if (enteredAmount <= 0) {
|
|
ToastHelper.showWarningToast(context, 'Enter valid paid amount');
|
|
return;
|
|
}
|
|
if (utrDetails.isNotEmpty && selectedUtrId == null) {
|
|
ToastHelper.showWarningToast(context, 'Please choose the UTR number');
|
|
return;
|
|
}
|
|
if (enteredAmount < selectedUtrExistingPaid) {
|
|
ToastHelper.showWarningToast(
|
|
context,
|
|
'Paid amount cannot be less than existing paid amount',
|
|
);
|
|
return;
|
|
}
|
|
final maxAllowed = maxAllowedForSelectedUtr > 0
|
|
? maxAllowedForSelectedUtr
|
|
: currentBalance;
|
|
if (enteredAmount > maxAllowed) {
|
|
ToastHelper.showWarningToast(
|
|
context,
|
|
'Paid amount cannot exceed balance amount',
|
|
);
|
|
return;
|
|
}
|
|
Navigator.pop(ctx, true);
|
|
},
|
|
child: const Text('Save'),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
);
|
|
},
|
|
);
|
|
|
|
if (result != true) return;
|
|
|
|
try {
|
|
setState(() => isLoading = true);
|
|
final response = await apiService.updateInvoiceUtrDetails({
|
|
'invoice_id': int.tryParse(item['id'].toString()) ?? 0,
|
|
'paid_amount': _toDouble(amountController.text.trim()),
|
|
'utr_id': selectedUtrId != null ? int.tryParse('$selectedUtrId') : null,
|
|
'utr_no': selectedUtrId == null ? generatedUtrNo : null,
|
|
'paid_date': DateFormat('yyyy-MM-dd').format(_paymentDate),
|
|
'created_by': userId,
|
|
'updated_by': userId,
|
|
});
|
|
|
|
if (response['status'] == 'success') {
|
|
ToastHelper.showSuccessToast(context, 'UTR details updated successfully');
|
|
await getPayoutList();
|
|
} else {
|
|
ToastHelper.showWarningToast(
|
|
context,
|
|
response['message']?.toString() ?? 'Failed to update UTR details',
|
|
);
|
|
}
|
|
} catch (e) {
|
|
ToastHelper.showErrorToast(context, e.toString());
|
|
} finally {
|
|
setState(() => isLoading = false);
|
|
}
|
|
}
|
|
|
|
void _showAccountHistoryModal(Map<String, dynamic> item) {
|
|
showDialog(
|
|
context: context,
|
|
builder: (_) => AccountHistoryModal(
|
|
invoiceId: item['id'],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return MainLayout(
|
|
title: "Pay Out",
|
|
body: SelectionArea(
|
|
child: Container(
|
|
// height: 30,
|
|
// color: Colors.yellow.shade50,
|
|
width: MediaQuery.of(context).size.width,
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
// Container(
|
|
// height: 30,
|
|
// width: MediaQuery.of(context).size.width,
|
|
// child: GestureDetector(
|
|
// onTap: () {
|
|
// context.go(AppRoutes.dashboard);
|
|
// },
|
|
// child: Row(
|
|
// crossAxisAlignment: CrossAxisAlignment.center,
|
|
// mainAxisAlignment: MainAxisAlignment.start,
|
|
// children: [
|
|
// // Tooltip(
|
|
// // message: 'Back',
|
|
// // child: IconButton(
|
|
// // icon: const Icon(
|
|
// // Icons.arrow_left_sharp,
|
|
// // size: 25,
|
|
// // color: Color(0xFF425B5B),
|
|
// // ),
|
|
// // onPressed: () {
|
|
// // context.go(AppRoutes.dashboard);
|
|
// // },
|
|
// // splashRadius: 18,
|
|
// // hoverColor: Colors.black12,
|
|
// // padding: const EdgeInsets.all(4),
|
|
// // constraints: const BoxConstraints(),
|
|
// // ),
|
|
// // ),
|
|
// // const SizedBox(width: 5), // spacing between icon and text
|
|
// Text(
|
|
// "Invoice",
|
|
// style: GoogleFonts.inter(
|
|
// fontSize: 14,
|
|
// fontWeight: FontWeight.w500,
|
|
// ),
|
|
// ),
|
|
// ],
|
|
// ),
|
|
// ),
|
|
// ),
|
|
//
|
|
// SizedBox(height: 5),
|
|
Expanded(
|
|
child: Container(
|
|
width: MediaQuery.of(context).size.width,
|
|
padding: EdgeInsets.all(8.0),
|
|
child: _buildContent(context),
|
|
),
|
|
),
|
|
|
|
Container(
|
|
// height: 20,
|
|
width: MediaQuery.of(context).size.width,
|
|
// color: Colors.green.shade50,
|
|
child: PaginationControls(
|
|
currentPage: currentPage,
|
|
itemsPerPage: itemsPerPage,
|
|
// totalItems: dataVal.length,
|
|
totalItems: filteredData.length,
|
|
// activeColor: layoutColor, // your theme color
|
|
onPageChanged: (page) {
|
|
setState(() {
|
|
currentPage = page;
|
|
});
|
|
},
|
|
onItemsPerPageChanged: (items) {
|
|
setState(() {
|
|
itemsPerPage = items;
|
|
currentPage = 1;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
) ,) ,
|
|
);
|
|
}
|
|
|
|
Widget _payoutFilterField({
|
|
required String hint,
|
|
required TextEditingController controller,
|
|
required double width,
|
|
}) {
|
|
return Container(
|
|
width: width,
|
|
height: _payoutFilterFieldHeight,
|
|
padding: const EdgeInsets.symmetric(horizontal: 15),
|
|
alignment: Alignment.center,
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(10),
|
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
|
),
|
|
child: TextField(
|
|
controller: controller,
|
|
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black),
|
|
decoration: InputDecoration(
|
|
isDense: true,
|
|
hintText: hint,
|
|
hintStyle: GoogleFonts.poppins(
|
|
fontSize: 11,
|
|
color: const Color(0xFF64748B),
|
|
),
|
|
border: InputBorder.none,
|
|
enabledBorder: InputBorder.none,
|
|
focusedBorder: InputBorder.none,
|
|
contentPadding: EdgeInsets.zero,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _payoutFilterActions() {
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
Tooltip(
|
|
message: 'Filter',
|
|
child: IconButton(
|
|
icon: const Icon(
|
|
Icons.search_rounded,
|
|
size: 18,
|
|
color: Color(0xFF94A3B8),
|
|
),
|
|
onPressed: () {
|
|
if (_formKey.currentState!.validate()) {
|
|
filterDateRange();
|
|
}
|
|
},
|
|
),
|
|
),
|
|
Tooltip(
|
|
message: 'Refresh',
|
|
child: IconButton(
|
|
onPressed: refrshfilterDateRange,
|
|
icon: const Icon(
|
|
Icons.refresh,
|
|
size: 18,
|
|
color: Color(0xFF94A3B8),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _payoutTextFilters(BuildContext context) {
|
|
final fieldWidth = ResponsiveLayout.isMobile(context) ? 120.0 : 140.0;
|
|
final isMobile = ResponsiveLayout.isMobile(context);
|
|
final fields = [
|
|
_payoutFilterField(
|
|
hint: 'Agent Name',
|
|
controller: controllers['agentName']!,
|
|
width: fieldWidth,
|
|
),
|
|
_payoutFilterField(
|
|
hint: 'Agent Code',
|
|
controller: controllers['agentCode']!,
|
|
width: fieldWidth,
|
|
),
|
|
_payoutFilterField(
|
|
hint: 'Policy No',
|
|
controller: controllers['policyNo']!,
|
|
width: fieldWidth,
|
|
),
|
|
_payoutFilterActions(),
|
|
];
|
|
|
|
if (isMobile) {
|
|
return Wrap(
|
|
spacing: 8,
|
|
runSpacing: 8,
|
|
crossAxisAlignment: WrapCrossAlignment.center,
|
|
children: fields,
|
|
);
|
|
}
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
for (var i = 0; i < fields.length; i++) ...[
|
|
if (i > 0) const SizedBox(width: 8),
|
|
fields[i],
|
|
],
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _payoutInvoiceDateFilter(BuildContext context) {
|
|
final isMobile = ResponsiveLayout.isMobile(context);
|
|
final dateFilter = DateFilterRow(
|
|
compactDateFiltersOnly: true,
|
|
hideFilterActions: true,
|
|
dataFrom: 'Payout',
|
|
role: roleId,
|
|
id: userId,
|
|
selectedStaffId: null,
|
|
startController: controllers['startDate']!,
|
|
endController: controllers['endDate']!,
|
|
onStatusChanged: (_) {},
|
|
formKey: _formKey,
|
|
isMobile: isMobile,
|
|
onFilter: filterDateRange,
|
|
onRefresh: refrshfilterDateRange,
|
|
);
|
|
|
|
if (isMobile) {
|
|
return Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
dateFilter,
|
|
const SizedBox(height: 8),
|
|
_payoutTextFilters(context),
|
|
],
|
|
);
|
|
}
|
|
|
|
return Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
dateFilter,
|
|
const SizedBox(width: 8),
|
|
_payoutTextFilters(context),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildContent(BuildContext context) {
|
|
return Column(
|
|
children: [
|
|
if (ResponsiveLayout.isMobile(context))
|
|
Padding(
|
|
padding: const EdgeInsets.only(bottom: 8),
|
|
child: _payoutInvoiceDateFilter(context),
|
|
),
|
|
Container(
|
|
// height: 40,
|
|
// color: Colors.pink,
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.center,
|
|
mainAxisAlignment: MainAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
"Payout",
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
fontWeight: FontWeight.w400,
|
|
),
|
|
),
|
|
if (!ResponsiveLayout.isMobile(context)) ...[
|
|
const SizedBox(width: 16),
|
|
Expanded(
|
|
child: Align(
|
|
alignment: Alignment.centerLeft,
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: _payoutInvoiceDateFilter(context),
|
|
),
|
|
),
|
|
),
|
|
] else ...[
|
|
const Spacer(),
|
|
],
|
|
ThemedSearchField(
|
|
hintText: 'Search',
|
|
// backgroundColor: Color(0xFFF6F8F8),
|
|
onChanged: filterData,
|
|
controller: _searchStaffController,
|
|
txtHeight: 30,
|
|
txtwidth: ResponsiveLayout.isMobile(context)
|
|
? MediaQuery.of(context).size.width * 0.7
|
|
: MediaQuery.of(context).size.width * 0.15,
|
|
),
|
|
SizedBox(width: 10),
|
|
Tooltip(
|
|
message: 'Add Payout Details',
|
|
child: InkWell(
|
|
onTap: () async {
|
|
context.go(AppRoutes.payoutDetails);
|
|
},
|
|
child: Container(
|
|
padding: const EdgeInsets.all(4.0),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFF2E7D6E),
|
|
// color: const Color(0xFF425B5B),
|
|
borderRadius: BorderRadius.circular(8.0),
|
|
),
|
|
child: Row(
|
|
mainAxisSize: MainAxisSize.min,
|
|
children: [
|
|
// Image.asset(
|
|
// "assets/miscellaneous/export.png",
|
|
// height: 25,
|
|
// width: 25,
|
|
// ),
|
|
Icon(Icons.add, color: Colors.white, size: 18),
|
|
// Image.asset("assets/miscellaneous/export", height: 15, width: 15),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 10),
|
|
ExportBtn(
|
|
sheetName: "Payout",
|
|
fileName: "Payout_List",
|
|
data: filteredData,
|
|
txt: true,
|
|
displayHeaders: [
|
|
"S.No",
|
|
"Invoice No",
|
|
"Invoice Date",
|
|
"UTR Number",
|
|
"Invoiced Amount",
|
|
"Payout Amount",
|
|
"Balance Amount",
|
|
"Status",
|
|
],
|
|
keys: [
|
|
"id",
|
|
"invoice_no",
|
|
"invoice_date",
|
|
"utr_numbers",
|
|
"invoiced_amount",
|
|
"payout_amount",
|
|
"balance_amount",
|
|
"payout_status",
|
|
],
|
|
valueFormatters: {
|
|
'payout_status': (v, _) => _exportPayoutStatusLabel(v),
|
|
},
|
|
),
|
|
],
|
|
),
|
|
),
|
|
SizedBox(height: 5),
|
|
Container(
|
|
decoration: BoxDecoration(
|
|
color: Color(0xFFF1F5F9),
|
|
borderRadius: BorderRadius.circular(6),
|
|
),
|
|
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
|
child: Row(
|
|
children: [
|
|
Expanded(flex: 1, child: Text('S.No', style: _headerStyle)),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text('Invoice Date', style: _headerStyle),
|
|
),
|
|
Expanded(
|
|
flex: 3,
|
|
child: Text('Invoice No', style: _headerStyle),
|
|
),
|
|
Expanded(flex: 2, child: Text('UTR Number', style: _headerStyle)),
|
|
Expanded(flex: 2, child: Text('Invoiced Amount', style: _headerStyle),),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text('Payout Amount', style: _headerStyle),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text('Balance Amount', style: _headerStyle),
|
|
),
|
|
Expanded(flex: 2, child: Text('Status', style: _headerStyle)),
|
|
Expanded(flex: 2, child: Text('Action', style: _headerStyle)),
|
|
],
|
|
),
|
|
),
|
|
Expanded(child: _buildDataTable(context)),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildDataTable(BuildContext context) {
|
|
if (filteredData.isEmpty) {
|
|
return const SizedBox(
|
|
height: 50,
|
|
child: Center(child: Text('No available data')),
|
|
);
|
|
}
|
|
|
|
final sortedData = [..._paginatedData];
|
|
|
|
// Desktop: keep ListView.builder
|
|
return ListView.builder(
|
|
itemCount: sortedData.length + 1,
|
|
itemBuilder: (context, index) {
|
|
if (index == 0) return _buildHeader();
|
|
final startIndex = (currentPage - 1) * itemsPerPage;
|
|
final item = sortedData[index - 1];
|
|
final sno = startIndex + index;
|
|
return _buildDataRow(item, sno);
|
|
},
|
|
);
|
|
}
|
|
|
|
Widget _buildHeader() {
|
|
return SizedBox.shrink();
|
|
}
|
|
|
|
Widget _buildDataRow(Map<String, dynamic> item, sno) {
|
|
return Container(
|
|
padding: const EdgeInsets.symmetric(vertical: 5, horizontal: 16),
|
|
// margin: const EdgeInsets.only(top: 10),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
// color: Color(0xFFE0F7F9),
|
|
border: const Border(
|
|
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
|
|
),
|
|
borderRadius: BorderRadius.circular(8),
|
|
),
|
|
child: Row(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Expanded(
|
|
flex: 1,
|
|
child: Text('$sno', style: _dataBold),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
item['invoice_date'] != null
|
|
? _formatDate(item['invoice_date'])
|
|
: '-',
|
|
style: _dataBold,
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 3,
|
|
child: Text(
|
|
_displayText(item['invoice_no']),
|
|
style: _dataBold,
|
|
softWrap: true,
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Tooltip(
|
|
message: _utrTooltipText(item['utr_numbers']),
|
|
waitDuration: const Duration(milliseconds: 250),
|
|
child: Text(
|
|
_truncateText(_displayText(item['utr_numbers']), maxChars: 10),
|
|
style: _dataBold,
|
|
softWrap: false,
|
|
overflow: TextOverflow.ellipsis,
|
|
maxLines: 1,
|
|
),
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
_displayAmount(item['invoiced_amount']),
|
|
style: _dataBold,
|
|
softWrap: true,
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
_displayAmount(item['payout_amount']),
|
|
style: _dataBold,
|
|
softWrap: true,
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
_displayAmount(item['balance_amount']),
|
|
style: _dataBold,
|
|
softWrap: true,
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
Expanded(
|
|
flex: 2,
|
|
child: Text(
|
|
// item['payout_status'] != null ? item['payout_status'] : '-',
|
|
item['payout_status'] == '1'
|
|
? 'Pending'
|
|
: item['payout_status'] != null
|
|
? 'Completed'
|
|
: '-',
|
|
style: _dataBold,
|
|
softWrap: true,
|
|
maxLines: 3,
|
|
),
|
|
),
|
|
|
|
// ACTION ICONS (Edit + Delete)
|
|
Expanded(
|
|
flex: 2,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.start,
|
|
children: [
|
|
// Step 1: Open payout/view screen for selected invoice.
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.remove_red_eye_outlined,
|
|
color: Colors.blue,
|
|
size: _actionIconSize,
|
|
),
|
|
tooltip: 'View Payout',
|
|
onPressed: () {
|
|
context.go(AppRoutes.payoutDetailsView, extra: item);
|
|
print("PL 9 => View clicked for ${item['invoice_no']}");
|
|
},
|
|
splashRadius: 16,
|
|
hoverColor: Colors.black12,
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints.tightFor(
|
|
width: _actionButtonSize,
|
|
height: _actionButtonSize,
|
|
),
|
|
visualDensity: VisualDensity.compact,
|
|
),
|
|
const SizedBox(width: _actionIconGap),
|
|
|
|
// Step 2: Add payment only when balance amount is pending.
|
|
// IconButton(
|
|
// icon: const Icon(
|
|
// Icons.currency_rupee,
|
|
// color: Colors.green,
|
|
// size: _actionIconSize,
|
|
// ),
|
|
// tooltip: 'Add payment',
|
|
// onPressed: () {
|
|
// final balance = _toDouble(item['balance_amount']);
|
|
// if (balance <= 0) {
|
|
// ToastHelper.showWarningToast(context, 'Invoice already fully paid');
|
|
// return;
|
|
// }
|
|
// _showAddPaymentDialog(item);
|
|
// },
|
|
// splashRadius: 16,
|
|
// hoverColor: Colors.black12,
|
|
// padding: EdgeInsets.zero,
|
|
// constraints: const BoxConstraints.tightFor(
|
|
// width: _actionButtonSize,
|
|
// height: _actionButtonSize,
|
|
// ),
|
|
// visualDensity: VisualDensity.compact,
|
|
// ),
|
|
// const SizedBox(width: _actionIconGap),
|
|
|
|
// Step 3: Show account history modal for this invoice.
|
|
IconButton(
|
|
icon: const Icon(
|
|
Icons.history,
|
|
color: Colors.green,
|
|
size: _actionIconSize,
|
|
),
|
|
tooltip: 'Account details',
|
|
onPressed: () => _showAccountHistoryModal(item),
|
|
splashRadius: 16,
|
|
hoverColor: Colors.black12,
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints.tightFor(
|
|
width: _actionButtonSize,
|
|
height: _actionButtonSize,
|
|
),
|
|
visualDensity: VisualDensity.compact,
|
|
),
|
|
const SizedBox(width: _actionIconGap),
|
|
|
|
// Step 4: Delete invoice entry.
|
|
IconButton(
|
|
icon: const Icon(Icons.delete, color: Colors.red, size: _actionIconSize),
|
|
tooltip: 'Delete Payout',
|
|
onPressed: () {
|
|
// TODO: handle delete
|
|
deleteInvoice(item['id']);
|
|
print("PL 10 => Delete clicked for ${item['invoice_no']}");
|
|
},
|
|
splashRadius: 16,
|
|
hoverColor: Colors.black12,
|
|
padding: EdgeInsets.zero,
|
|
constraints: const BoxConstraints.tightFor(
|
|
width: _actionButtonSize,
|
|
height: _actionButtonSize,
|
|
),
|
|
visualDensity: VisualDensity.compact,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
static final _dataBold = GoogleFonts.inter(
|
|
fontSize: 11.5,
|
|
fontWeight: FontWeight.w400,
|
|
color: Color(0xFF000000),
|
|
);
|
|
|
|
static final _headerStyle = GoogleFonts.poppins(
|
|
fontSize: 11.2,
|
|
fontWeight: FontWeight.w500,
|
|
color: Color(0xFF1E293B),
|
|
);
|
|
}
|
|
|
|
class AccountHistoryModal extends StatefulWidget {
|
|
final dynamic invoiceId;
|
|
const AccountHistoryModal({super.key, required this.invoiceId});
|
|
|
|
@override
|
|
State<AccountHistoryModal> createState() => _AccountHistoryModalState();
|
|
}
|
|
|
|
class _AccountHistoryModalState extends State<AccountHistoryModal> {
|
|
late ApiService apiService;
|
|
bool isLoading = true;
|
|
List<Map<String, dynamic>> accountHistoryData = [];
|
|
Map<String, dynamic>? invoiceSummary;
|
|
final TextStyle _historyHeaderStyle = GoogleFonts.poppins(
|
|
fontSize: 11.2,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF1E293B),
|
|
);
|
|
final TextStyle _historyCellStyle = GoogleFonts.inter(
|
|
fontSize: 11.5,
|
|
fontWeight: FontWeight.w400,
|
|
color: const Color(0xFF000000),
|
|
);
|
|
|
|
String _formatINR(dynamic value) {
|
|
final amount = double.tryParse(value?.toString() ?? '') ?? 0;
|
|
return NumberFormat.currency(
|
|
locale: 'en_IN',
|
|
symbol: '₹',
|
|
decimalDigits: 0,
|
|
).format(amount);
|
|
}
|
|
|
|
String _formatTimelineDate(dynamic rawDate) {
|
|
if (rawDate == null) return '-';
|
|
try {
|
|
final dt = DateTime.parse(rawDate.toString());
|
|
return DateFormat('d MMM yyyy').format(dt);
|
|
} catch (_) {
|
|
return rawDate.toString();
|
|
}
|
|
}
|
|
|
|
double _toDouble(dynamic value) {
|
|
if (value == null) return 0;
|
|
return double.tryParse(value.toString()) ?? 0;
|
|
}
|
|
|
|
Widget _buildWebActivityBody() {
|
|
// Step 1: Sort payments by paid date so we can take "latest" balance.
|
|
final sortedPayments = [...accountHistoryData]..sort((a, b) {
|
|
DateTime da = DateTime.tryParse((a['paid_date'] ?? '').toString()) ??
|
|
DateTime.fromMillisecondsSinceEpoch(0);
|
|
DateTime db = DateTime.tryParse((b['paid_date'] ?? '').toString()) ??
|
|
DateTime.fromMillisecondsSinceEpoch(0);
|
|
return da.compareTo(db);
|
|
});
|
|
|
|
final latestBalance = sortedPayments.isEmpty
|
|
? 0.0
|
|
: _toDouble(sortedPayments.last['balance_amount']);
|
|
final paidToDate = sortedPayments.fold<double>(
|
|
0.0,
|
|
(sum, item) => sum + _toDouble(item['paid_amount']),
|
|
);
|
|
final totalAmount = invoiceSummary != null
|
|
? _toDouble(invoiceSummary?['total_amount'])
|
|
: paidToDate + latestBalance;
|
|
|
|
// Step 2: Build UI sections (responsive and cleaner typography).
|
|
final invoiceNo = _displayText(invoiceSummary?['invoice_no']);
|
|
final invoiceLabel =
|
|
invoiceNo.startsWith('#') ? invoiceNo : '#$invoiceNo';
|
|
final width = MediaQuery.of(context).size.width;
|
|
final bool isCompact = width < 1200;
|
|
final double titleFont = isCompact ? 17 : 20;
|
|
final double amountBigFont = isCompact ? 22 : 26;
|
|
final double sectionTitleFont = isCompact ? 12 : 14;
|
|
final double bodyFont = isCompact ? 11 : 12;
|
|
final double valueFont = isCompact ? 14 : 16;
|
|
|
|
return Padding(
|
|
padding: EdgeInsets.all(isCompact ? 10 : 12),
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
// Step 2a: Top row (Invoice ID only).
|
|
Row(
|
|
children: [
|
|
Text(
|
|
invoiceLabel,
|
|
style: GoogleFonts.inter(
|
|
fontSize: titleFont,
|
|
fontWeight: FontWeight.w800,
|
|
color: const Color(0xFF0F172A),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Step 2b: Total amount card.
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 18),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFF8FAFC),
|
|
borderRadius: BorderRadius.circular(14),
|
|
border: Border.all(color: const Color(0xFFE5E7EB)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
'TOTAL AMOUNT',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: bodyFont,
|
|
fontWeight: FontWeight.w600,
|
|
letterSpacing: 0.2,
|
|
color: const Color(0xFF334155),
|
|
),
|
|
),
|
|
const SizedBox(height: 6),
|
|
Text(
|
|
_formatINR(totalAmount),
|
|
style: GoogleFonts.inter(
|
|
fontSize: amountBigFont,
|
|
fontWeight: FontWeight.w900,
|
|
color: const Color(0xFF0B3A6B),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
const Spacer(),
|
|
Container(
|
|
width: 54,
|
|
height: 54,
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFEAF2FF),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: const Icon(
|
|
Icons.receipt_long_rounded,
|
|
color: Color(0xFF2563EB),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 16),
|
|
|
|
// Step 2c: Payment timeline header.
|
|
Row(
|
|
children: [
|
|
Text(
|
|
'PAYMENT TIMELINE',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: sectionTitleFont,
|
|
fontWeight: FontWeight.w700,
|
|
color: const Color(0xFF111827),
|
|
),
|
|
),
|
|
const Spacer(),
|
|
Text(
|
|
'${sortedPayments.length} TRANSACTIONS',
|
|
style: GoogleFonts.inter(
|
|
fontSize: bodyFont,
|
|
fontWeight: FontWeight.w600,
|
|
color: const Color(0xFF334155),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
// Step 2d: Timeline list.
|
|
Expanded(
|
|
child: ListView.builder(
|
|
padding: const EdgeInsets.only(top: 2),
|
|
itemCount: sortedPayments.length,
|
|
itemBuilder: (context, index) {
|
|
// Show latest first on the UI.
|
|
final data =
|
|
sortedPayments[sortedPayments.length - 1 - index];
|
|
final name = _displayText(data['createdby_name']);
|
|
final paidAmount = _formatINR(data['paid_amount']);
|
|
final date = _formatTimelineDate(data['paid_date']);
|
|
|
|
return Container(
|
|
margin: const EdgeInsets.only(bottom: 10),
|
|
padding: const EdgeInsets.all(12),
|
|
decoration: BoxDecoration(
|
|
color: Colors.white,
|
|
borderRadius: BorderRadius.circular(12),
|
|
border: Border.all(color: const Color(0xFFE5E7EB)),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Container(
|
|
width: 40,
|
|
height: 40,
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFECFDF5),
|
|
shape: BoxShape.circle,
|
|
),
|
|
child: const Icon(
|
|
Icons.check_circle,
|
|
size: 22,
|
|
color: Color(0xFF16A34A),
|
|
),
|
|
),
|
|
const SizedBox(width: 12),
|
|
Expanded(
|
|
child: Column(
|
|
crossAxisAlignment: CrossAxisAlignment.start,
|
|
children: [
|
|
Text(
|
|
name,
|
|
style: GoogleFonts.inter(
|
|
fontSize: isCompact ? 12 : 13,
|
|
fontWeight: FontWeight.w700,
|
|
color: const Color(0xFF0F172A),
|
|
),
|
|
),
|
|
const SizedBox(height: 4),
|
|
Text(
|
|
date,
|
|
style: GoogleFonts.inter(
|
|
fontSize: bodyFont,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFF475569),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Column(
|
|
crossAxisAlignment: CrossAxisAlignment.end,
|
|
children: [
|
|
Text(
|
|
paidAmount,
|
|
style: GoogleFonts.inter(
|
|
fontSize: isCompact ? 14 : 15,
|
|
fontWeight: FontWeight.w800,
|
|
color: const Color(0xFF0B3A6B),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
],
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
|
|
const SizedBox(height: 12),
|
|
|
|
// Step 2e: Bottom summary.
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'TOTAL AMOUNT',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: bodyFont,
|
|
fontWeight: FontWeight.w700,
|
|
color: const Color(0xFF334155),
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
_formatINR(totalAmount),
|
|
style: GoogleFonts.inter(
|
|
fontSize: valueFont,
|
|
fontWeight: FontWeight.w900,
|
|
color: const Color(0xFF0F172A),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 8),
|
|
|
|
Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'PAID TO DATE',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: bodyFont,
|
|
fontWeight: FontWeight.w700,
|
|
color: const Color(0xFF334155),
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
_formatINR(paidToDate),
|
|
style: GoogleFonts.inter(
|
|
fontSize: valueFont,
|
|
fontWeight: FontWeight.w900,
|
|
color: const Color(0xFF0F172A),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
|
|
const SizedBox(height: 10),
|
|
|
|
Container(
|
|
width: double.infinity,
|
|
padding: const EdgeInsets.symmetric(vertical: 14, horizontal: 16),
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFBFDBFE).withOpacity(0.45),
|
|
borderRadius: BorderRadius.circular(10),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Expanded(
|
|
child: Text(
|
|
'BALANCE',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: bodyFont,
|
|
fontWeight: FontWeight.w800,
|
|
color: const Color(0xFF0F172A),
|
|
),
|
|
),
|
|
),
|
|
Text(
|
|
_formatINR(latestBalance),
|
|
style: GoogleFonts.inter(
|
|
fontSize: isCompact ? 16 : 18,
|
|
fontWeight: FontWeight.w900,
|
|
color: const Color(0xFF0B3A6B),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
apiService = ApiService();
|
|
fetchAccountHistory();
|
|
}
|
|
|
|
String _displayText(dynamic value) {
|
|
if (value == null) return '-';
|
|
final text = value.toString().trim();
|
|
if (text.isEmpty || text.toLowerCase() == 'null') return '-';
|
|
return text;
|
|
}
|
|
|
|
String _displayAmount(dynamic value) {
|
|
if (value == null) return '0.00';
|
|
final parsed = double.tryParse(value.toString()) ?? 0;
|
|
return parsed.toStringAsFixed(2);
|
|
}
|
|
|
|
String _formatIndianDate(dynamic rawDate) {
|
|
if (rawDate == null) return '-';
|
|
try {
|
|
final dateTime = DateTime.parse(rawDate.toString());
|
|
return DateFormat('dd-MM-yyyy hh:mm a').format(dateTime);
|
|
} catch (_) {
|
|
return rawDate.toString();
|
|
}
|
|
}
|
|
|
|
DateTime _parseDateTime(dynamic rawDate) {
|
|
if (rawDate == null) {
|
|
return DateTime.fromMillisecondsSinceEpoch(0);
|
|
}
|
|
return DateTime.tryParse(rawDate.toString()) ??
|
|
DateTime.fromMillisecondsSinceEpoch(0);
|
|
}
|
|
|
|
DateTime _rowEffectiveDate(Map<String, dynamic> row) {
|
|
final paidAt = _parseDateTime(row['paid_date']);
|
|
if (paidAt.millisecondsSinceEpoch > 0) return paidAt;
|
|
final createdAt = _parseDateTime(row['created_at']);
|
|
if (createdAt.millisecondsSinceEpoch > 0) return createdAt;
|
|
return _parseDateTime(row['updated_at']);
|
|
}
|
|
|
|
String _formatPaidDateTime(Map<String, dynamic> row) {
|
|
final paidAt = _parseDateTime(row['paid_date']);
|
|
if (paidAt.millisecondsSinceEpoch > 0 && paidAt.hour + paidAt.minute > 0) {
|
|
return DateFormat('dd-MM-yyyy hh:mm a').format(paidAt);
|
|
}
|
|
final createdAt = _parseDateTime(row['created_at']);
|
|
if (createdAt.millisecondsSinceEpoch > 0) {
|
|
return DateFormat('dd-MM-yyyy hh:mm a').format(createdAt);
|
|
}
|
|
if (paidAt.millisecondsSinceEpoch > 0) {
|
|
return DateFormat('dd-MM-yyyy').format(paidAt);
|
|
}
|
|
return '-';
|
|
}
|
|
|
|
List<Map<String, dynamic>> _buildMeaningfulHistoryRows() {
|
|
final sortedRows = [...accountHistoryData]..sort((a, b) {
|
|
final dateCompare = _rowEffectiveDate(a).compareTo(_rowEffectiveDate(b));
|
|
if (dateCompare != 0) return dateCompare;
|
|
final idA = int.tryParse('${a['id'] ?? 0}') ?? 0;
|
|
final idB = int.tryParse('${b['id'] ?? 0}') ?? 0;
|
|
return idA.compareTo(idB);
|
|
});
|
|
|
|
double runningPaid = 0;
|
|
final rowsWithMetrics = sortedRows.map((row) {
|
|
final paid = _toDouble(row['paid_amount']);
|
|
runningPaid += paid;
|
|
return <String, dynamic>{
|
|
...row,
|
|
'total_paid_till_date': runningPaid,
|
|
};
|
|
}).toList();
|
|
|
|
return rowsWithMetrics.reversed.toList();
|
|
}
|
|
|
|
Future<void> fetchAccountHistory() async {
|
|
setState(() => isLoading = true);
|
|
try {
|
|
final response = await apiService.listInvoiceUtrDetails(widget.invoiceId);
|
|
if (response['status'] == 'success') {
|
|
setState(() {
|
|
accountHistoryData = List<Map<String, dynamic>>.from(
|
|
response['data'] ?? [],
|
|
);
|
|
invoiceSummary = response['invoice'] is Map<String, dynamic>
|
|
? Map<String, dynamic>.from(response['invoice'])
|
|
: null;
|
|
});
|
|
} else {
|
|
setState(() {
|
|
accountHistoryData = [];
|
|
invoiceSummary = null;
|
|
});
|
|
}
|
|
} catch (_) {
|
|
setState(() {
|
|
accountHistoryData = [];
|
|
invoiceSummary = null;
|
|
});
|
|
} finally {
|
|
if (mounted) setState(() => isLoading = false);
|
|
}
|
|
}
|
|
|
|
String _safeFileNamePart(String value) {
|
|
return value.replaceAll(RegExp(r'[\\/:*?"<>|]'), '_').trim();
|
|
}
|
|
|
|
Future<void> _exportAccountHistoryExcel() async {
|
|
if (accountHistoryData.isEmpty) {
|
|
ToastHelper.showWarningToast(context, 'No account details data to export');
|
|
return;
|
|
}
|
|
|
|
final invoiceNo = _displayText(invoiceSummary?['invoice_no']);
|
|
final cleanedInvoiceNo = _safeFileNamePart(
|
|
invoiceNo == '-' ? 'Invoice number' : invoiceNo,
|
|
);
|
|
|
|
final exportRows = _buildMeaningfulHistoryRows();
|
|
// ExcelExporter internally reverses incoming rows before writing.
|
|
// So we pass oldest->latest to get latest->oldest in the final file.
|
|
final exportInputRows = exportRows.reversed.toList();
|
|
final exportData = exportInputRows.asMap().entries.map((entry) {
|
|
final index = entry.key;
|
|
final row = entry.value;
|
|
return <String, dynamic>{
|
|
's_no': index + 1,
|
|
'invoice_no': _displayText(row['invoice_no']),
|
|
'utr_no': _displayText(row['utr_no']),
|
|
'total_amount': _displayAmount(row['total_amount']),
|
|
'paid_date_time': _formatPaidDateTime(row),
|
|
'createdby_name': _displayText(row['createdby_name']),
|
|
'paid_amount': _displayAmount(row['paid_amount']),
|
|
'total_paid_till_date': _displayAmount(row['total_paid_till_date']),
|
|
'balance_amount': _displayAmount(row['balance_amount']),
|
|
};
|
|
}).toList();
|
|
|
|
await ExcelExporter.exportToExcel(
|
|
sheetName: 'Account Details',
|
|
fileName: 'Account details $cleanedInvoiceNo',
|
|
data: exportData,
|
|
displayHeaders: const [
|
|
'S.No',
|
|
'Invoice Number',
|
|
'UTR Number',
|
|
'Total Amount',
|
|
'Paid Date & Time',
|
|
'Updated By',
|
|
'Paid Amount',
|
|
'Total Paid Till Date',
|
|
'Balance',
|
|
],
|
|
keys: const [
|
|
's_no',
|
|
'invoice_no',
|
|
'utr_no',
|
|
'total_amount',
|
|
'paid_date_time',
|
|
'createdby_name',
|
|
'paid_amount',
|
|
'total_paid_till_date',
|
|
'balance_amount',
|
|
],
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final bool isMobile = ResponsiveLayout.isMobile(context);
|
|
final media = MediaQuery.of(context).size;
|
|
|
|
return Dialog(
|
|
insetPadding: EdgeInsets.symmetric(
|
|
horizontal: isMobile ? 12 : 24,
|
|
vertical: isMobile ? 18 : 24,
|
|
),
|
|
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(14)),
|
|
clipBehavior: Clip.antiAlias,
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
maxWidth: isMobile ? media.width : 1100,
|
|
maxHeight: media.height * (isMobile ? 0.86 : 0.82),
|
|
),
|
|
child: Column(
|
|
children: [
|
|
Container(
|
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
|
decoration: const BoxDecoration(
|
|
color: Color(0xFFF8FAFC),
|
|
borderRadius: BorderRadius.only(
|
|
topLeft: Radius.circular(14),
|
|
topRight: Radius.circular(14),
|
|
),
|
|
border: Border(bottom: BorderSide(color: Color(0xFFE5E7EB))),
|
|
),
|
|
child: Row(
|
|
children: [
|
|
Text(
|
|
'Account Details',
|
|
style: GoogleFonts.poppins(
|
|
fontSize: 14,
|
|
color: const Color(0xFF2E7D6E),
|
|
fontWeight: FontWeight.w600,
|
|
),
|
|
),
|
|
const Spacer(),
|
|
TextButton.icon(
|
|
onPressed: isLoading ? null : _exportAccountHistoryExcel,
|
|
icon: const Icon(Icons.file_download_outlined, size: 18),
|
|
label: const Text('Export Excel'),
|
|
style: TextButton.styleFrom(
|
|
foregroundColor: const Color(0xFF2E7D6E),
|
|
),
|
|
),
|
|
IconButton(
|
|
onPressed: () => Navigator.pop(context),
|
|
icon: const Icon(
|
|
Icons.close,
|
|
size: 18,
|
|
color: Color(0xFF2E7D6E),
|
|
),
|
|
splashRadius: 18,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Expanded(
|
|
child: isLoading
|
|
? const Center(child: CircularProgressIndicator())
|
|
: accountHistoryData.isEmpty
|
|
? const Center(child: Text('No Account Details Data Found'))
|
|
: Padding(
|
|
padding: const EdgeInsets.all(12),
|
|
child: Container(
|
|
color: Colors.white,
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final historyRows = _buildMeaningfulHistoryRows();
|
|
return Scrollbar(
|
|
thumbVisibility: true,
|
|
child: SingleChildScrollView(
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
child: ConstrainedBox(
|
|
constraints: BoxConstraints(
|
|
minWidth: constraints.maxWidth,
|
|
),
|
|
child: DataTable(
|
|
headingRowColor: WidgetStateProperty.all(
|
|
const Color(0xFFF1F5F9),
|
|
),
|
|
dataRowMinHeight: 42,
|
|
dataRowMaxHeight: 52,
|
|
columnSpacing: isMobile ? 14 : 20,
|
|
columns: [
|
|
DataColumn(
|
|
label: Text('S.No', style: _historyHeaderStyle),
|
|
),
|
|
DataColumn(
|
|
label: Text(
|
|
'Invoice Number',
|
|
style: _historyHeaderStyle,
|
|
),
|
|
),
|
|
DataColumn(
|
|
label: Text(
|
|
'Total Amount',
|
|
style: _historyHeaderStyle,
|
|
),
|
|
),
|
|
DataColumn(
|
|
label: Text(
|
|
'UTR Number',
|
|
style: _historyHeaderStyle,
|
|
),
|
|
),
|
|
DataColumn(
|
|
label: Text(
|
|
'Paid Date & Time',
|
|
style: _historyHeaderStyle,
|
|
),
|
|
),
|
|
DataColumn(
|
|
label: Text('Updated By', style: _historyHeaderStyle),
|
|
),
|
|
DataColumn(
|
|
label: Text(
|
|
'Paid Amount',
|
|
style: _historyHeaderStyle,
|
|
),
|
|
),
|
|
DataColumn(
|
|
label: Text(
|
|
'Total Paid Till Date',
|
|
style: _historyHeaderStyle,
|
|
),
|
|
),
|
|
DataColumn(
|
|
label: Text('Balance', style: _historyHeaderStyle),
|
|
),
|
|
],
|
|
rows: List.generate(historyRows.length, (index) {
|
|
final data = historyRows[index];
|
|
return DataRow(
|
|
cells: [
|
|
DataCell(
|
|
Text('${index + 1}', style: _historyCellStyle),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_displayText(data['invoice_no']),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_displayAmount(data['total_amount']),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_displayText(data['utr_no']),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_formatPaidDateTime(data),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_displayText(data['createdby_name']),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_displayAmount(data['paid_amount']),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_displayAmount(data['total_paid_till_date']),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
DataCell(
|
|
Text(
|
|
_displayAmount(data['balance_amount']),
|
|
style: _historyCellStyle,
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|