nhance_partner/lib/presentation/screens/payout/payout_list.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,
),
),
],
);
}),
),
),
),
),
);
},
),
),
),
),
],
),
),
);
}
}