enrollment-app/lib/presentation/pendingDependentApproval.dart

1028 lines
31 KiB
Dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhancepolicy/customAppBar/base_layout.dart';
import 'package:nhancepolicy/customAppBar/toastHelper.dart';
import 'package:nhancepolicy/logger.dart';
import 'package:nhancepolicy/service/api_service.dart';
import 'package:nhancepolicy/service/secure_pop_scope.dart';
import 'package:nhancepolicy/service/token_storage_service.dart';
class PendingDependentApproval extends StatefulWidget {
const PendingDependentApproval({super.key});
@override
State<PendingDependentApproval> createState() =>
_PendingDependentApprovalState();
}
class _PendingDependentApprovalState extends State<PendingDependentApproval> {
final tokenService = TokenStorageService();
late ApiService apiService;
final TextEditingController searchController = TextEditingController();
Timer? _searchDebounce;
int _searchRequestId = 0;
List<Map<String, dynamic>> listData = [];
bool isLoading = false;
bool _sessionReady = false;
bool _isActing = false;
int? _actingEmployeeId;
bool _isPost = true;
String? _token;
String? _clientId;
String? _branchId;
String? _hrId;
int _currentPage = 1;
int _rowsPerPage = 10;
List<Map<String, dynamic>> get _paginatedData {
if (listData.isEmpty) return [];
final startIndex = (_currentPage - 1) * _rowsPerPage;
if (startIndex >= listData.length) return [];
final endIndex = (_currentPage * _rowsPerPage).clamp(0, listData.length);
return listData.sublist(startIndex, endIndex);
}
@override
void initState() {
super.initState();
apiService = ApiService(context);
_loadData();
}
@override
void dispose() {
_searchDebounce?.cancel();
searchController.dispose();
super.dispose();
}
Future<void> _ensureSessionContext() async {
if (_sessionReady && _token != null && _token!.isNotEmpty) return;
_token = tokenService.getCurrentToken();
// Always use post client / branch / hr for pending approval APIs.
_isPost = true;
_clientId = await tokenService.readValue('empClientId');
_branchId = await tokenService.readValue('empClientBranchId');
_hrId = await tokenService.readValue('empHrId');
_sessionReady = true;
}
Future<void> _loadData({String? search}) async {
final requestId = ++_searchRequestId;
final searchQuery = (search ?? searchController.text).trim();
setState(() => isLoading = true);
try {
await _ensureSessionContext();
final response = await apiService.getPendingApprovalDependents(
clientId: _clientId,
branchId: _branchId,
search: searchQuery.isEmpty ? null : searchQuery,
token: _token,
);
if (!mounted || requestId != _searchRequestId) return;
if (response['status'] == 'success') {
final raw = response['data'];
final list = raw is List
? raw
.whereType<Map>()
.map((e) => Map<String, dynamic>.from(e))
.toList()
: <Map<String, dynamic>>[];
setState(() {
listData = _sortByPendingFirst(list);
_currentPage = 1;
isLoading = false;
});
} else {
setState(() {
listData = [];
isLoading = false;
});
final message =
response['message']?.toString() ?? 'Failed to load dependents';
ToastHelper.showWarningToast(context, message);
}
} catch (e) {
logDebug('getPendingApprovalDependents error: $e');
if (!mounted || requestId != _searchRequestId) return;
setState(() {
listData = [];
isLoading = false;
});
ToastHelper.showErrorToast(context, 'Failed to load pending dependents');
}
}
void _onSearchChanged(String query) {
_searchDebounce?.cancel();
_searchDebounce = Timer(const Duration(milliseconds: 450), () {
_loadData(search: query);
});
}
void _onRefresh() {
_searchDebounce?.cancel();
searchController.clear();
_loadData(search: '');
}
String _field(Map<String, dynamic> row, List<String> keys) {
for (final key in keys) {
final value = row[key];
if (value != null && value.toString().trim().isNotEmpty) {
return value.toString().trim();
}
}
// Compose first + last when present.
if (keys.contains('first_name') || keys.contains('emp_name')) {
final first = row['first_name']?.toString().trim() ?? '';
final last = row['last_name']?.toString().trim() ?? '';
final combined = '$first $last'.trim();
if (combined.isNotEmpty) return combined;
}
return '-';
}
/// Normalize API dates to `dd-MM-yyyy`.
String _formatDisplayDate(dynamic raw) {
if (raw == null) return '-';
final value = raw.toString().trim();
if (value.isEmpty || value == '-') return '-';
final cleaned = value
.replaceAll('/', '-')
.replaceAll('.', '-')
.split(' ')
.first
.split('T')
.first;
// Already dd-MM-yyyy
final dmy = RegExp(r'^(\d{2})-(\d{2})-(\d{4})$').firstMatch(cleaned);
if (dmy != null) return cleaned;
// yyyy-MM-dd → dd-MM-yyyy
final ymd = RegExp(r'^(\d{4})-(\d{2})-(\d{2})$').firstMatch(cleaned);
if (ymd != null) {
return '${ymd.group(3)}-${ymd.group(2)}-${ymd.group(1)}';
}
// dd-MM-yy → leave as-is if not parseable further
return cleaned;
}
dynamic _employeeId(Map<String, dynamic> row) {
return row['employee_id'] ??
row['emp_id'] ??
row['id'] ??
row['dependent_id'];
}
dynamic _clientPolicyId(Map<String, dynamic> row) {
return row['client_policy_id'] ??
row['policy_id'] ??
row['clientPolicyId'];
}
Future<void> _confirmAndProcess(
Map<String, dynamic> row,
String status,
) async {
final employeeId = _employeeId(row);
final clientPolicyId = _clientPolicyId(row);
if (employeeId == null || clientPolicyId == null) {
ToastHelper.showWarningToast(
context,
'Missing employee or policy id for this record',
);
return;
}
final isApprove = status == 'approved';
final name = _field(row, const [
'emp_name',
'name',
'dependent_name',
'first_name',
'last_name',
]);
if (isApprove) {
final confirmed = await showDialog<bool>(
context: context,
builder: (ctx) => AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Text(
'Confirm Approval',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
),
content: Text(
'Are you sure you want to approve the dependent "$name"?\n\n'
'This will activate their policy coverage.',
style: GoogleFonts.poppins(fontSize: 14, height: 1.4),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(false),
child: Text(
'Cancel',
style: GoogleFonts.poppins(color: const Color(0xFF009195)),
),
),
ElevatedButton(
onPressed: () => Navigator.of(ctx).pop(true),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Approve',
style: GoogleFonts.poppins(color: Colors.white),
),
),
],
),
) ??
false;
if (!confirmed || !mounted) return;
await _processDependent(row, status);
return;
}
// Reject flow — require reason.
final reasonController = TextEditingController();
final rejectResult = await showDialog<String>(
context: context,
builder: (ctx) {
String? errorText;
return StatefulBuilder(
builder: (ctx, setDialogState) {
return AlertDialog(
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
title: Text(
'Confirm Rejection',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600),
),
content: SizedBox(
width: 420,
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Are you sure you want to reject the dependent "$name"?\n'
'Please provide a reason for rejection.',
style: GoogleFonts.poppins(fontSize: 14, height: 1.4),
),
const SizedBox(height: 14),
Text(
'Reason',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
TextField(
controller: reasonController,
maxLines: 4,
autofocus: true,
style: GoogleFonts.poppins(fontSize: 13),
decoration: InputDecoration(
hintText: 'Enter rejection reason...',
hintStyle: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF94A3B8),
),
errorText: errorText,
filled: true,
fillColor: const Color(0xFFF8FAFC),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: Color(0xFF009195)),
),
),
),
],
),
),
actions: [
TextButton(
onPressed: () => Navigator.of(ctx).pop(),
child: Text(
'Cancel',
style: GoogleFonts.poppins(color: const Color(0xFF009195)),
),
),
ElevatedButton(
onPressed: () {
final reason = reasonController.text.trim();
if (reason.isEmpty) {
setDialogState(() {
errorText = 'Rejection reason is required';
});
return;
}
Navigator.of(ctx).pop(reason);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
),
child: Text(
'Reject',
style: GoogleFonts.poppins(color: Colors.white),
),
),
],
);
},
);
},
);
reasonController.dispose();
if (rejectResult == null || !mounted) return;
await _processDependent(row, status, rejectReason: rejectResult);
}
Future<void> _processDependent(
Map<String, dynamic> row,
String status, {
String? rejectReason,
}) async {
final employeeId = _employeeId(row);
final clientPolicyId = _clientPolicyId(row);
setState(() {
_isActing = true;
_actingEmployeeId = int.tryParse(employeeId.toString());
});
try {
final response = await apiService.processDependentAdd(
isPost: _isPost,
employeeId: employeeId is num
? employeeId
: int.tryParse(employeeId.toString()) ?? employeeId,
clientPolicyId: clientPolicyId is num
? clientPolicyId
: int.tryParse(clientPolicyId.toString()) ?? clientPolicyId,
status: status,
hrId: _hrId,
rejectReason: rejectReason,
token: _token,
);
if (!mounted) return;
if (response['status'] == 'success') {
ToastHelper.showSuccessToast(
context,
status == 'approved'
? 'Dependent approved successfully'
: 'Dependent rejected successfully',
);
setState(() {
final idx = listData.indexWhere(
(r) => _employeeId(r)?.toString() == employeeId.toString(),
);
if (idx != -1) {
listData[idx] = {
...listData[idx],
'status': status,
if (rejectReason != null) 'reject_reason': rejectReason,
};
listData = _sortByPendingFirst(listData);
}
});
} else {
final message = response['message']?.toString() ??
response['data']?.toString() ??
'Action failed';
ToastHelper.showWarningToast(context, message);
}
} catch (e) {
logDebug('processDependentAdd error: $e');
if (mounted) {
ToastHelper.showErrorToast(context, 'Failed to process dependent');
}
} finally {
if (mounted) {
setState(() {
_isActing = false;
_actingEmployeeId = null;
});
}
}
}
@override
Widget build(BuildContext context) {
return BaseLayout(
child: SecurePopScope(
child: _buildContent(context),
),
);
}
Widget _buildContent(BuildContext context) {
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
),
padding: const EdgeInsets.all(16),
child: Column(
children: [
Row(
children: [
Text(
'Approvals',
style: GoogleFonts.poppins(
fontSize: 22,
fontWeight: FontWeight.w500,
color: Colors.black,
),
),
const Spacer(),
Container(
width: 360,
height: 40,
decoration: BoxDecoration(
color: const Color(0xFFF0F0F0),
borderRadius: BorderRadius.circular(8),
),
child: TextField(
controller: searchController,
onChanged: _onSearchChanged,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: 'Search by name, emp code, mobile, email...',
hintStyle: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF94A3B8),
),
prefixIcon: const Icon(Icons.search, size: 18),
border: InputBorder.none,
contentPadding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
),
),
),
const SizedBox(width: 10),
IconButton(
tooltip: 'Refresh',
onPressed: isLoading ? null : _onRefresh,
icon: const Icon(Icons.refresh_rounded, color: Color(0xFF009195)),
),
],
),
const SizedBox(height: 16),
Expanded(
child: isLoading
? Center(
child: Image.asset(
'assets/nhance-loader.gif',
height: 60,
width: 60,
),
)
: _buildTable(context),
),
],
),
);
}
Widget _buildTable(BuildContext context) {
if (listData.isEmpty) {
return Center(
child: Text(
searchController.text.trim().isEmpty
? 'No pending approval dependents found'
: 'No matching dependents',
style: GoogleFonts.poppins(
fontSize: 14,
color: const Color(0xFF64748B),
),
),
);
}
return Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: const Color(0xFFE2E8F0)),
boxShadow: [
BoxShadow(
color: Colors.black.withValues(alpha: 0.04),
blurRadius: 10,
offset: const Offset(0, 2),
),
],
),
clipBehavior: Clip.antiAlias,
child: Column(
children: [
_buildHeaderRow(),
Expanded(
child: ListView.builder(
itemCount: _paginatedData.length,
itemBuilder: (context, index) {
return _buildDataRow(_paginatedData[index], index);
},
),
),
const Divider(height: 1, thickness: 1, color: Color(0xFFE2E8F0)),
_buildPagination(context),
],
),
);
}
Widget _buildHeaderRow() {
return Container(
height: 55,
color: const Color(0xFFE6F5F6),
padding: const EdgeInsets.symmetric(horizontal: 16),
alignment: Alignment.centerLeft,
child: Row(
children: [
_headerCell('Employee', flex: 3),
_headerCell('Dependent', flex: 3),
_headerCell('Relationship', flex: 2),
_headerCell('Date Of Birth', flex: 2),
_headerCell('Dependent Effective Date', flex: 3),
_headerCell('Policy', flex: 3),
_headerCell('Status', flex: 2),
_headerCell('Action', flex: 2, align: TextAlign.center),
],
),
);
}
Widget _headerCell(
String label, {
required int flex,
TextAlign align = TextAlign.left,
}) {
return Expanded(
flex: flex,
child: Text(
label,
textAlign: align,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
color: const Color(0xFF0F172A),
),
),
);
}
String _rowStatus(Map<String, dynamic> item) {
return _field(item, const ['status', 'emp_status', 'approval_status'])
.toLowerCase()
.trim();
}
int _statusSortOrder(String status) {
switch (status) {
case 'pending_approval':
return 0;
case 'rejected':
return 1;
default:
return 2;
}
}
/// `pending_approval` first, then `rejected`, then other statuses.
List<Map<String, dynamic>> _sortByPendingFirst(
List<Map<String, dynamic>> rows,
) {
final sorted = List<Map<String, dynamic>>.from(rows);
sorted.sort((a, b) {
return _statusSortOrder(_rowStatus(a))
.compareTo(_statusSortOrder(_rowStatus(b)));
});
return sorted;
}
String _statusLabel(String status) {
if (status.isEmpty || status == '-') return 'Pending';
if (status == 'approved' || status == 'active') return 'Approved';
if (status == 'rejected') return 'Rejected';
if (status == 'pending' || status == 'pending_approval') return 'Pending';
return status[0].toUpperCase() + status.substring(1);
}
Color _statusChipColor(String status) {
final normalized = status.toLowerCase();
if (normalized == 'approved' || normalized == 'active') {
return const Color(0xFFDCFCE7);
}
if (normalized == 'rejected') {
return const Color(0xFFFEE2E2);
}
return const Color(0xFFFFF7ED);
}
Color _statusTextColor(String status) {
final normalized = status.toLowerCase();
if (normalized == 'approved' || normalized == 'active') {
return const Color(0xFF166534);
}
if (normalized == 'rejected') {
return const Color(0xFFB91C1C);
}
return const Color(0xFFC2410C);
}
Widget _buildDataRow(Map<String, dynamic> item, int index) {
final employeeId = _employeeId(item);
final isRowActing =
_isActing && _actingEmployeeId?.toString() == employeeId?.toString();
final employeeName = _field(item, const ['self_name']);
final empCode = _field(item, const ['emp_code', 'employee_code', 'code']);
final dependentName = _field(item, const [
'emp_name',
'name',
'dependent_name',
'first_name',
'last_name',
]);
final relation = _field(item, const [
'relationship',
'relation',
'relation_name',
'relationship_name',
]);
final dob = _formatDisplayDate(_field(item, const [
'dob',
'date_of_birth',
'dateOfBirth',
'birth_date',
'formatted_dob',
]));
final effectiveDate = _formatDisplayDate(_field(item, const [
'date_coverage',
'coverage_date',
'dependent_effective_date',
]));
final policy = _field(item, const [
'policy_no',
'policy_name',
'policy_type_name',
'policy_type',
]);
final statusRaw = _rowStatus(item);
final statusLabel = _statusLabel(statusRaw);
final isApproved =
statusRaw == 'approved' || statusRaw == 'active';
final isRejected = statusRaw == 'rejected';
final rejectReason = _field(item, const [
'reject_reason',
'rejection_reason',
'reason',
]);
final showRejectReasonTooltip =
isRejected && rejectReason != '-' && rejectReason.isNotEmpty;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
decoration: BoxDecoration(
color: index.isEven ? Colors.white : const Color(0xFFF8FFFE),
border: const Border(
bottom: BorderSide(color: Color(0xFFE2E8F0)),
),
),
child: Row(
children: [
Expanded(
flex: 3,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(employeeName, style: _dataBold),
Text(empCode, style: _dataSub),
],
),
),
_dataCell(dependentName, flex: 3),
_dataCell(relation, flex: 2),
_dataCell(dob, flex: 2),
_dataCell(effectiveDate, flex: 3),
_dataCell(policy, flex: 3),
Expanded(
flex: 2,
child: Align(
alignment: Alignment.centerLeft,
child: Builder(
builder: (context) {
final chip = Container(
padding: const EdgeInsets.symmetric(
horizontal: 10,
vertical: 4,
),
decoration: BoxDecoration(
color: _statusChipColor(statusRaw),
borderRadius: BorderRadius.circular(10),
),
child: Text(
statusLabel,
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w600,
color: _statusTextColor(statusRaw),
),
),
);
if (!showRejectReasonTooltip) return chip;
return Tooltip(
message: 'Reason: $rejectReason',
waitDuration: const Duration(milliseconds: 250),
child: chip,
);
},
),
),
),
Expanded(
flex: 2,
child: isRowActing
? const Center(
child: SizedBox(
width: 22,
height: 22,
child: CircularProgressIndicator(strokeWidth: 2),
),
)
: isApproved
? Center(child: Text('-', style: _dataBold))
: Center(
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
_buildActionIconSlot(
tooltip: 'Approve',
icon: const Icon(
Icons.check_circle_rounded,
color: Color(0xFF16A34A),
size: 26,
),
onPressed: _isActing
? null
: () =>
_confirmAndProcess(item, 'approved'),
),
_buildActionIconSlot(
tooltip: 'Reject',
icon: const Icon(
Icons.cancel_rounded,
color: Color(0xFFE26728),
size: 26,
),
onPressed: _isActing || isRejected
? null
: () =>
_confirmAndProcess(item, 'rejected'),
visible: !isRejected,
),
],
),
),
),
],
),
);
}
static final _dataBold = GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w400,
color: Color(0xFF000000),
);
static final _dataSub = GoogleFonts.poppins(
fontSize: 10,
fontWeight: FontWeight.w300,
color: Color(0xFF585757),
);
Widget _dataCell(String text, {required int flex}) {
return Expanded(
flex: flex,
child: Text(
text,
maxLines: 2,
overflow: TextOverflow.ellipsis,
style: _dataBold,
),
);
}
Widget _buildActionIconSlot({
required String tooltip,
required Widget icon,
required VoidCallback? onPressed,
bool visible = true,
}) {
return SizedBox(
width: 40,
height: 40,
child: visible
? IconButton(
tooltip: tooltip,
onPressed: onPressed,
padding: EdgeInsets.zero,
constraints: const BoxConstraints(),
icon: icon,
)
: null,
);
}
Widget _buildPagination(BuildContext context) {
final totalItems = listData.length;
final startEntry =
totalItems == 0 ? 0 : ((_currentPage - 1) * _rowsPerPage) + 1;
var endEntry = _currentPage * _rowsPerPage;
if (endEntry > totalItems) endEntry = totalItems;
final totalPages =
totalItems == 0 ? 1 : (totalItems / _rowsPerPage).ceil();
List<int> getVisiblePages() {
const visiblePageCount = 5;
if (totalPages <= visiblePageCount) {
return List.generate(totalPages, (i) => i + 1);
}
if (_currentPage <= 3) return [1, 2, 3, 4, 5];
if (_currentPage >= totalPages - 2) {
return [
totalPages - 4,
totalPages - 3,
totalPages - 2,
totalPages - 1,
totalPages,
];
}
return [
_currentPage - 2,
_currentPage - 1,
_currentPage,
_currentPage + 1,
_currentPage + 2,
];
}
final visiblePages = getVisiblePages();
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 12),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'Showing $startEntry to $endEntry of $totalItems entries',
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF585757),
),
),
Row(
children: [
Text(
'Rows',
style: GoogleFonts.poppins(
fontSize: 13,
color: const Color(0xFF585757),
),
),
const SizedBox(width: 8),
DropdownButton<int>(
value: _rowsPerPage,
underline: const SizedBox.shrink(),
items: [5, 10, 15, 20, 50].map((value) {
return DropdownMenuItem<int>(
value: value,
child: Text(
'$value',
style: GoogleFonts.poppins(fontSize: 14),
),
);
}).toList(),
onChanged: (value) {
if (value == null) return;
setState(() {
_rowsPerPage = value;
_currentPage = 1;
});
},
),
IconButton(
onPressed: _currentPage > 1
? () => setState(() => _currentPage--)
: null,
icon: const Icon(Icons.chevron_left),
),
if (!visiblePages.contains(1) && totalPages > 0) ...[
_buildPageButton(1),
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text('...'),
),
],
for (final page in visiblePages) _buildPageButton(page),
if (!visiblePages.contains(totalPages) && totalPages > 0) ...[
const Padding(
padding: EdgeInsets.symmetric(horizontal: 4),
child: Text('...'),
),
_buildPageButton(totalPages),
],
IconButton(
onPressed: _currentPage < totalPages
? () => setState(() => _currentPage++)
: null,
icon: const Icon(Icons.chevron_right),
),
],
),
],
),
);
}
Widget _buildPageButton(int page) {
final isActive = _currentPage == page;
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 3),
child: SizedBox(
width: 36,
height: 36,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor:
isActive ? const Color(0xFF00A6A6) : Colors.grey[300],
foregroundColor: isActive ? Colors.white : Colors.black,
elevation: 0,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(18),
),
),
onPressed: () => setState(() => _currentPage = page),
child: Text(
'$page',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
),
);
}
}