diff --git a/lib/presentation/screens/UserManagement/Agent/agent_details.dart b/lib/presentation/screens/UserManagement/Agent/agent_details.dart index def3a0d..e3a5a0b 100644 --- a/lib/presentation/screens/UserManagement/Agent/agent_details.dart +++ b/lib/presentation/screens/UserManagement/Agent/agent_details.dart @@ -103,6 +103,21 @@ class _AgentDetailsScreenState extends ConsumerState { _bootstrap(); } + /// After create, [context.go] to `/agentDetails/:id` can update [widget.agentId] while + /// reusing this [State] — [initState] does not run again, so we must load the new id here. + @override + void didUpdateWidget(covariant AgentDetailsScreen oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.agentId != widget.agentId && !widget.isCreate) { + _reloadAgentForRouteIdChange(); + } + } + + Future _reloadAgentForRouteIdChange() async { + await _loadAgent(); + if (mounted) setState(() {}); + } + Future _bootstrap() async { _token = await AuthService.getToken(); await _loadSalesExecutives(); @@ -186,7 +201,11 @@ class _AgentDetailsScreenState extends ConsumerState { return; } final data = Map.from(res['data'] as Map); - _loadedAgentPk = data['id']?.toString(); + final pk = data['id'] ?? data['agent_id']; + _loadedAgentPk = pk?.toString().trim(); + if (_loadedAgentPk != null && _loadedAgentPk!.isEmpty) { + _loadedAgentPk = null; + } _nameCtrl.text = (data['name'] ?? '').toString(); _emailCtrl.text = (data['email'] ?? '').toString(); @@ -317,6 +336,30 @@ class _AgentDetailsScreenState extends ConsumerState { } } + /// Backend may return the new row id under [data.id], [data.agent_id], or top-level keys. + String? _newAgentIdFromCreateBody(dynamic body) { + if (body is! Map) return null; + final m = Map.from(body); + final data = m['data']; + if (data is Map) { + final dm = Map.from(data); + for (final key in ['agent_id', 'id', 'agentId']) { + final v = dm[key]; + final s = v?.toString().trim(); + if (s != null && s.isNotEmpty && s != 'null') return s; + } + } else if (data != null && data is! List) { + final s = data.toString().trim(); + if (s.isNotEmpty && s != 'null') return s; + } + for (final key in ['agent_id', 'id']) { + final v = m[key]; + final s = v?.toString().trim(); + if (s != null && s.isNotEmpty && s != 'null') return s; + } + return null; + } + Future _saveRowRetention(int vehicleTypeId) async { if (!_retentionTableEnabled) { ToastHelper.showWarningToast( @@ -466,12 +509,9 @@ class _AgentDetailsScreenState extends ConsumerState { return; } if (!isUpdate && mounted) { - final data = body['data']; - String? newId; - if (data is Map && data['agent_id'] != null) { - newId = data['agent_id'].toString(); - } + final newId = _newAgentIdFromCreateBody(body); if (newId != null && newId.isNotEmpty) { + setState(() => _loadedAgentPk = newId); context.go(AppRoutes.agentDetailsFor(newId)); return; } diff --git a/lib/presentation/screens/dashboard/partner_portal_dashboard.dart b/lib/presentation/screens/dashboard/partner_portal_dashboard.dart index 9382d2e..558c7c3 100644 --- a/lib/presentation/screens/dashboard/partner_portal_dashboard.dart +++ b/lib/presentation/screens/dashboard/partner_portal_dashboard.dart @@ -5,6 +5,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:intl/intl.dart'; import '../../../../core/services/api_service.dart'; import '../../providers/manager_provider.dart'; @@ -137,12 +138,13 @@ class _State extends ConsumerState { double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0; + /// Indian-grouped full rupee amount (no L/Cr/K shorthand). String _inr(dynamic v) { final n = _d(v); - if (n >= 10000000) return 'Rs.${(n/10000000).toStringAsFixed(1)}Cr'; - if (n >= 100000) return 'Rs.${(n/100000).toStringAsFixed(1)}L'; - if (n >= 1000) return 'Rs.${(n/1000).toStringAsFixed(1)}K'; - return 'Rs.${n.toStringAsFixed(0)}'; + if (n == n.roundToDouble()) { + return 'Rs.${NumberFormat('#,##,###', 'en_IN').format(n.toInt())}'; + } + return 'Rs.${NumberFormat('#,##,##0.00', 'en_IN').format(n)}'; } String _initials(String name) { final p = name.trim().split(' '); @@ -218,8 +220,8 @@ class _State extends ConsumerState { // ───────────────────────────────────────────────────────────────────────────── // PARTNER PROFILE CARD -// Fields: agent_name, agent_code, mobile, email, manager_name, status -// All come from partnerDetails() → partner_agent table +// Fields: agent_name, agent_code, mobile, email, manager_name, manager_mobile, status +// All come from GET partner/{id}/details (partner_agent + manager join) // ───────────────────────────────────────────────────────────────────────────── class _PartnerProfileCard extends StatelessWidget { final Map details; final String initials; @@ -231,6 +233,8 @@ class _PartnerProfileCard extends StatelessWidget { final mobile = details['mobile'] ?? '--'; final email = details['email'] ?? '--'; final manager = details['manager_name'] ?? '--'; + final managerMobile = details['manager_mobile'] ?? '--'; + final managerEmail = details['manager_email'] ?? '--'; final status = details['status'] ?? 'Active'; return Container( padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18), @@ -249,6 +253,8 @@ class _PartnerProfileCard extends StatelessWidget { _m('Mobile', mobile.toString()), _m('Email', email.toString()), _m('Manager', manager.toString()), + _m('Manager Mobile', managerMobile), + _m('Manager Email', managerEmail), ]), ])), const SizedBox(width: 12), @@ -438,20 +444,20 @@ class _RenewalAlertsCard extends StatelessWidget { children: [ Text('Renewal Alerts', style: _chartHeader), Text( - 'At-risk policies', + '', style: GoogleFonts.inter(fontSize: 9.5, color: const Color(0xFF8AABB5)), ), ], ), const Spacer(), - if (totalCount > 0) - Padding( - padding: const EdgeInsets.only(right: 8), - child: Text( - '$totalCount total', - style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5)), - ), - ), + // if (totalCount > 0) + // Padding( + // padding: const EdgeInsets.only(right: 8), + // child: Text( + // '$totalCount total', + // style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5)), + // ), + // ), Container( padding: const EdgeInsets.all(3), decoration: BoxDecoration( @@ -501,13 +507,13 @@ class _RenewalAlertsCard extends StatelessWidget { color: const Color(0xFFE3F9F8), child: Row( children: [ - SizedBox(width: 88, child: Text('Due', style: _tableHeadStyle)), + SizedBox(width: 40, child: Text('S.No', style: _tableHeadStyle)), Expanded(flex: 2, child: Text('Policy No.', style: _tableHeadStyle)), - Expanded(flex: 2, child: Text('Holder', style: _tableHeadStyle)), - Expanded(flex: 2, child: Text('Expires', style: _tableHeadStyle)), + Expanded(flex: 2, child: Text('Policy Holder Name', style: _tableHeadStyle)), + Expanded(flex: 2, child: Text('Expiry Date', style: _tableHeadStyle)), Expanded( flex: 2, - child: Text('Premium', textAlign: TextAlign.right, style: _tableHeadStyle), + child: Text('Premium Amount', textAlign: TextAlign.right, style: _tableHeadStyle), ), ], ), @@ -518,7 +524,11 @@ class _RenewalAlertsCard extends StatelessWidget { physics: const ClampingScrollPhysics(), itemCount: renewals.length, separatorBuilder: (_, __) => const Divider(height: 1, color: Color(0xFFD9F0ED)), - itemBuilder: (_, i) => _RenewalTableRow(renewal: renewals[i], inr: inr), + itemBuilder: (_, i) => _RenewalTableRow( + serialNo: i + 1, + renewal: renewals[i], + inr: inr, + ), ), ), ], @@ -527,52 +537,42 @@ class _RenewalAlertsCard extends StatelessWidget { } class _RenewalTableRow extends StatelessWidget { + final int serialNo; final Map renewal; final String Function(dynamic) inr; - const _RenewalTableRow({required this.renewal, required this.inr}); + const _RenewalTableRow({ + required this.serialNo, + required this.renewal, + required this.inr, + }); @override Widget build(BuildContext context) { final d = int.tryParse(renewal['days_left']?.toString() ?? '0') ?? 0; - Color chipBg, chipFg; + final Color chipFg; if (d <= 10) { - chipBg = const Color(0xFFFFEDED); chipFg = const Color(0xFFEF4444); } else if (d <= 20) { - chipBg = const Color(0xFFFEF0E7); chipFg = const Color(0xFFF97316); } else { - chipBg = const Color(0xFFF1F5F9); chipFg = const Color(0xFF64748B); } + final endDate = renewal['end_date']?.toString() ?? '--'; return SizedBox( height: _RenewalAlertsCard.kRowHeight, child: Padding( padding: const EdgeInsets.symmetric(horizontal: 12), child: Row( + crossAxisAlignment: CrossAxisAlignment.center, children: [ SizedBox( - width: 88, - child: Align( - alignment: Alignment.centerLeft, - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 3), - decoration: BoxDecoration( - color: chipBg, - borderRadius: BorderRadius.circular(4), - ), - child: Text( - '$d d', - style: GoogleFonts.inter( - fontSize: 9.5, - fontWeight: FontWeight.w700, - color: chipFg, - letterSpacing: 0.2, - ), - ), - ), + width: 40, + child: Text( + '$serialNo', + style: _tableData, + textAlign: TextAlign.center, ), ), Expanded( @@ -593,18 +593,32 @@ class _RenewalTableRow extends StatelessWidget { ), Expanded( flex: 2, - child: Text( - renewal['end_date']?.toString() ?? '--', - style: _tableData, + child: Text.rich( + TextSpan( + style: _tableData, + children: [ + TextSpan(text: endDate), + TextSpan( + text: ' ($d d)', + style: GoogleFonts.inter( + fontSize: 9, + fontWeight: FontWeight.w600, + color: chipFg, + ), + ), + ], + ), overflow: TextOverflow.ellipsis, ), ), Expanded( flex: 2, child: Text( - inr(renewal['premium'] ?? 0), + inr(renewal['premium'] ?? renewal['premium_amount'] ?? 0), style: _tableDataBold, textAlign: TextAlign.right, + maxLines: 2, + overflow: TextOverflow.ellipsis, ), ), ], diff --git a/lib/presentation/screens/payout/payout_details.dart b/lib/presentation/screens/payout/payout_details.dart index 97082f1..fd0b252 100644 --- a/lib/presentation/screens/payout/payout_details.dart +++ b/lib/presentation/screens/payout/payout_details.dart @@ -25,13 +25,23 @@ class _PayOutDetailsState extends ConsumerState { static final List _commissionInputFormatter = [ FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')), ]; + static const int _utrMinLength = 12; + static const int _utrMaxLength = 22; + /// Alphanumeric only, length validated on submit (12–22). + static final RegExp _utrFullValuePattern = + RegExp(r'^[a-zA-Z0-9]{12,22}$'); static final List _utrInputFormatter = [ FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')), + LengthLimitingTextInputFormatter(_utrMaxLength), ]; + + static bool _isValidUtr(String raw) => + _utrFullValuePattern.hasMatch(raw.trim()); // -------------------------- // Invoice Fields // -------------------------- final _formKey = GlobalKey(); + final _utrFormKey = GlobalKey(); // final TextEditingController invoiceNoController; DateTime invoiceDate = DateTime.now(); // dynamic selectedAgentId; @@ -83,6 +93,8 @@ class _PayOutDetailsState extends ConsumerState { bool isLoading = false; bool hasFetchedTableData = false; final Set _inlineSavingIds = {}; + /// Per-policy payout editors so [setState] rebuilds do not reset focus/text. + final Map _commissionControllers = {}; late ApiService apiService; dynamic managerId; dynamic userId; @@ -114,6 +126,89 @@ class _PayOutDetailsState extends ConsumerState { }); } + int _policyRowId(Map p) { + return int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0; + } + + void _rebuildCommissionControllers(List> policies) { + for (final c in _commissionControllers.values) { + c.dispose(); + } + _commissionControllers.clear(); + for (final p in policies) { + final id = _policyRowId(p); + if (id == 0) continue; + _commissionControllers[id] = TextEditingController( + text: (p["commission_amount"] ?? '').toString(), + ); + } + } + + void _disposeCommissionControllers() { + for (final c in _commissionControllers.values) { + c.dispose(); + } + _commissionControllers.clear(); + } + + TextEditingController? _commissionControllerFor(Map p) { + final id = _policyRowId(p); + if (id == 0) return null; + return _commissionControllers[id]; + } + + Widget _commissionAmountField( + Map p, + TextEditingController? controller, + ) { + final fieldKey = ValueKey('commission_${p["id"] ?? p["policy_id"]}'); + final decoration = const InputDecoration( + isDense: true, + contentPadding: EdgeInsets.symmetric( + horizontal: 6, + vertical: 6, + ), + border: OutlineInputBorder(), + ); + const fieldStyle = TextStyle( + fontSize: 11, + fontWeight: FontWeight.w600, + color: Color(0xFF009B77), + ); + void onChanged(String value) { + p["commission_amount"] = value.trim(); + _calculateTotalCommission(); + } + + if (controller != null) { + return TextFormField( + key: fieldKey, + controller: controller, + style: fieldStyle, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: _commissionInputFormatter, + decoration: decoration, + onChanged: onChanged, + onFieldSubmitted: (value) => _updateInlineCommission(p, value), + ); + } + return TextFormField( + key: fieldKey, + initialValue: (p["commission_amount"] ?? '').toString(), + style: fieldStyle, + keyboardType: const TextInputType.numberWithOptions(decimal: true), + inputFormatters: _commissionInputFormatter, + decoration: decoration, + onChanged: onChanged, + onFieldSubmitted: (value) => _updateInlineCommission(p, value), + ); + } + + @override + void dispose() { + _disposeCommissionControllers(); + super.dispose(); + } void filterPolicyData(String query) { final lowerQuery = query.toLowerCase(); @@ -167,6 +262,7 @@ class _PayOutDetailsState extends ConsumerState { totalPolicies = ''; totalCommission = ''; filteredPolicies = []; + _disposeCommissionControllers(); hasFetchedTableData = false; // dropDownKeyPartner.currentState?.clear(); }); @@ -302,6 +398,7 @@ class _PayOutDetailsState extends ConsumerState { print('PD =>API Data response - $response'); setState(() { filteredPolicies = List>.from(response['data']); + _rebuildCommissionControllers(filteredPolicies); hasFetchedTableData = true; print('PD =>API Data - $filteredPolicies'); // filteredPolicies = allPolicies.where((p) { @@ -315,6 +412,7 @@ class _PayOutDetailsState extends ConsumerState { } else { setState(() { filteredPolicies = []; + _rebuildCommissionControllers(filteredPolicies); hasFetchedTableData = true; }); } @@ -322,6 +420,7 @@ class _PayOutDetailsState extends ConsumerState { print('PD =>Exception occurred: $e'); setState(() { filteredPolicies = []; + _rebuildCommissionControllers(filteredPolicies); hasFetchedTableData = false; }); } finally { @@ -367,6 +466,11 @@ class _PayOutDetailsState extends ConsumerState { return; } + final utrFormState = _utrFormKey.currentState; + if (utrFormState != null && !utrFormState.validate()) { + return; + } + final String utrInput = (controllers['utrNumber']?.text ?? '').trim(); if (utrInput.isEmpty) { ToastHelper.showWarningToast( @@ -375,6 +479,14 @@ class _PayOutDetailsState extends ConsumerState { ); return; } + if (!_isValidUtr(utrInput)) { + ToastHelper.showWarningToast( + context, + "UTR must be $_utrMinLength–$_utrMaxLength letters or numbers only, " + "with no spaces or special characters.", + ); + return; + } // Format dates for API final String formattedInvoiceDate = DateFormat( @@ -822,11 +934,11 @@ class _PayOutDetailsState extends ConsumerState { color: Color(0xFFf9fafb), // color: Colors.white, child: Row( - // mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.start, children: [ // ---------------- SELECTED COUNT ---------------- Row( - // crossAxisAlignment: CrossAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, children: [ Text( // selectedPolicies.length.toString(), @@ -883,7 +995,9 @@ class _PayOutDetailsState extends ConsumerState { buildUTRNumber(context), SizedBox(width: 20), // ---------------- BUTTONS ---------------- - Row( + Padding( + padding: const EdgeInsets.only(top: 2), + child: Row( children: [ ElevatedButton( onPressed: saveInvoice, @@ -917,6 +1031,7 @@ class _PayOutDetailsState extends ConsumerState { ), ], ), + ), ], ), ), @@ -1022,7 +1137,6 @@ class _PayOutDetailsState extends ConsumerState { final int id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0; final bool isSelected = selectedPolicies.contains(id); - return InkWell( onTap: () { setState(() { @@ -1087,35 +1201,9 @@ class _PayOutDetailsState extends ConsumerState { ), const SizedBox(width: 2), Expanded( - child: TextFormField( - key: ValueKey( - 'commission_${p["policy_id"]}_${p["commission_amount"]}', - ), - initialValue: (p["commission_amount"] ?? '') - .toString(), - style: const TextStyle( - fontSize: 11, - fontWeight: FontWeight.w600, - color: Color(0xFF009B77), - ), - keyboardType: const TextInputType.numberWithOptions( - decimal: true, - ), - inputFormatters: _commissionInputFormatter, - decoration: const InputDecoration( - isDense: true, - contentPadding: EdgeInsets.symmetric( - horizontal: 6, - vertical: 6, - ), - border: OutlineInputBorder(), - ), - onChanged: (value) { - p["commission_amount"] = value.trim(); - _calculateTotalCommission(); - }, - onFieldSubmitted: (value) => - _updateInlineCommission(p, value), + child: _commissionAmountField( + p, + _commissionControllerFor(p), ), ), ], @@ -1186,6 +1274,14 @@ class _PayOutDetailsState extends ConsumerState { ); if (response['status'] == 'success' || response['status'] == 200) { row['commission_amount'] = trimmed; + final cid = _policyRowId(row); + final ctrl = _commissionControllers[cid]; + if (ctrl != null && ctrl.text != trimmed) { + ctrl.value = TextEditingValue( + text: trimmed, + selection: TextSelection.collapsed(offset: trimmed.length), + ); + } _calculateTotalCommission(); } else { ToastHelper.showErrorToast( @@ -1237,20 +1333,56 @@ class _PayOutDetailsState extends ConsumerState { } Widget buildUTRNumber(BuildContext context) { - + final fieldWidth = MediaQuery.of(context).size.width * 0.15; + final utrDecoration = commonInputDecoration( + hint: 'UTR ($_utrMinLength–$_utrMaxLength chars)', + ).copyWith( + hintStyle: GoogleFonts.poppins( + fontSize: 9, + fontWeight: FontWeight.w400, + color: const Color(0xFF9CA3AF), + ), + isDense: true, + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), + errorStyle: GoogleFonts.poppins( + fontSize: 10, + fontWeight: FontWeight.w500, + color: const Color(0xFFB91C1C), + height: 1.25, + ), + errorMaxLines: 3, + ); + return Row( - crossAxisAlignment: CrossAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text('UTR Number', style: _textStyle), - SizedBox(width: 5), + Padding( + padding: const EdgeInsets.only(top: 10), + child: Text('UTR Number', style: _textStyle), + ), + const SizedBox(width: 5), SizedBox( - height: 35, - width: MediaQuery.of(context).size.width * 0.15, - child: TextFormField( - controller: controllers['utrNumber']!, - inputFormatters: _utrInputFormatter, - decoration: commonInputDecoration(hint: 'UTR Number'), - ), + width: fieldWidth.clamp(160.0, 280.0), + child: Form( + key: _utrFormKey, + child: TextFormField( + controller: controllers['utrNumber']!, + keyboardType: TextInputType.text, + autocorrect: false, + inputFormatters: _utrInputFormatter, + autovalidateMode: AutovalidateMode.onUserInteraction, + validator: (value) { + final v = (value ?? '').trim(); + if (v.isEmpty) return null; + if (!_isValidUtr(v)) { + return 'Use $_utrMinLength–$_utrMaxLength letters or numbers only. ' + 'No spaces or symbols.'; + } + return null; + }, + decoration: utrDecoration, + ), + ), ), ], ); diff --git a/lib/presentation/themes/indicators/text_field_theme.dart b/lib/presentation/themes/indicators/text_field_theme.dart index 43eb001..2de6bbc 100644 --- a/lib/presentation/themes/indicators/text_field_theme.dart +++ b/lib/presentation/themes/indicators/text_field_theme.dart @@ -5,6 +5,11 @@ import 'package:google_fonts/google_fonts.dart'; // import 'package:uae_stat/config/my_theme.dart'; +/// Characters allowed while typing an email (matches typical local-part + domain). +final List kEmailAddressInputFormatters = [ + FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9@._%+-]')), +]; + class ThemedFormField extends HookWidget { ThemedFormField({ super.key, @@ -202,9 +207,10 @@ class ThemedFormField extends HookWidget { maxLength: maxLength, // ✅ max length showCursor: isShowCursor ?? true, enableInteractiveSelection: isShowCursor ?? true, - inputFormatters: - inputFormatters ?? - [FilteringTextInputFormatter.allow(RegExp(r'[a-z A-Z]'))], + inputFormatters: inputFormatters ?? + (keyboardType == TextInputType.emailAddress + ? kEmailAddressInputFormatters + : null), // ✅ allow multiline if user sets maxLines / minLines minLines: (keyboardType == TextInputType.multiline) ? 3 : 1, maxLines: (keyboardType == TextInputType.multiline) ? null : 1,