bug fix
This commit is contained in:
commit
c2bdd3b556
@ -103,6 +103,21 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
_bootstrap();
|
_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<void> _reloadAgentForRouteIdChange() async {
|
||||||
|
await _loadAgent();
|
||||||
|
if (mounted) setState(() {});
|
||||||
|
}
|
||||||
|
|
||||||
Future<void> _bootstrap() async {
|
Future<void> _bootstrap() async {
|
||||||
_token = await AuthService.getToken();
|
_token = await AuthService.getToken();
|
||||||
await _loadSalesExecutives();
|
await _loadSalesExecutives();
|
||||||
@ -186,7 +201,11 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
final data = Map<String, dynamic>.from(res['data'] as Map);
|
final data = Map<String, dynamic>.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();
|
_nameCtrl.text = (data['name'] ?? '').toString();
|
||||||
_emailCtrl.text = (data['email'] ?? '').toString();
|
_emailCtrl.text = (data['email'] ?? '').toString();
|
||||||
@ -317,6 +336,30 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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<String, dynamic>.from(body);
|
||||||
|
final data = m['data'];
|
||||||
|
if (data is Map) {
|
||||||
|
final dm = Map<String, dynamic>.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<void> _saveRowRetention(int vehicleTypeId) async {
|
Future<void> _saveRowRetention(int vehicleTypeId) async {
|
||||||
if (!_retentionTableEnabled) {
|
if (!_retentionTableEnabled) {
|
||||||
ToastHelper.showWarningToast(
|
ToastHelper.showWarningToast(
|
||||||
@ -466,12 +509,9 @@ class _AgentDetailsScreenState extends ConsumerState<AgentDetailsScreen> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!isUpdate && mounted) {
|
if (!isUpdate && mounted) {
|
||||||
final data = body['data'];
|
final newId = _newAgentIdFromCreateBody(body);
|
||||||
String? newId;
|
|
||||||
if (data is Map && data['agent_id'] != null) {
|
|
||||||
newId = data['agent_id'].toString();
|
|
||||||
}
|
|
||||||
if (newId != null && newId.isNotEmpty) {
|
if (newId != null && newId.isNotEmpty) {
|
||||||
|
setState(() => _loadedAgentPk = newId);
|
||||||
context.go(AppRoutes.agentDetailsFor(newId));
|
context.go(AppRoutes.agentDetailsFor(newId));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,6 +5,7 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||||
import 'package:google_fonts/google_fonts.dart';
|
import 'package:google_fonts/google_fonts.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import '../../../../core/services/api_service.dart';
|
import '../../../../core/services/api_service.dart';
|
||||||
import '../../providers/manager_provider.dart';
|
import '../../providers/manager_provider.dart';
|
||||||
@ -137,12 +138,13 @@ class _State extends ConsumerState<PartnerPortalDashboard> {
|
|||||||
|
|
||||||
double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0;
|
double _d(dynamic v) => double.tryParse(v?.toString() ?? '0') ?? 0;
|
||||||
|
|
||||||
|
/// Indian-grouped full rupee amount (no L/Cr/K shorthand).
|
||||||
String _inr(dynamic v) {
|
String _inr(dynamic v) {
|
||||||
final n = _d(v);
|
final n = _d(v);
|
||||||
if (n >= 10000000) return 'Rs.${(n/10000000).toStringAsFixed(1)}Cr';
|
if (n == n.roundToDouble()) {
|
||||||
if (n >= 100000) return 'Rs.${(n/100000).toStringAsFixed(1)}L';
|
return 'Rs.${NumberFormat('#,##,###', 'en_IN').format(n.toInt())}';
|
||||||
if (n >= 1000) return 'Rs.${(n/1000).toStringAsFixed(1)}K';
|
}
|
||||||
return 'Rs.${n.toStringAsFixed(0)}';
|
return 'Rs.${NumberFormat('#,##,##0.00', 'en_IN').format(n)}';
|
||||||
}
|
}
|
||||||
String _initials(String name) {
|
String _initials(String name) {
|
||||||
final p = name.trim().split(' ');
|
final p = name.trim().split(' ');
|
||||||
@ -218,8 +220,8 @@ class _State extends ConsumerState<PartnerPortalDashboard> {
|
|||||||
|
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
// PARTNER PROFILE CARD
|
// PARTNER PROFILE CARD
|
||||||
// Fields: agent_name, agent_code, mobile, email, manager_name, status
|
// Fields: agent_name, agent_code, mobile, email, manager_name, manager_mobile, status
|
||||||
// All come from partnerDetails() → partner_agent table
|
// All come from GET partner/{id}/details (partner_agent + manager join)
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
class _PartnerProfileCard extends StatelessWidget {
|
class _PartnerProfileCard extends StatelessWidget {
|
||||||
final Map<String, dynamic> details; final String initials;
|
final Map<String, dynamic> details; final String initials;
|
||||||
@ -231,6 +233,8 @@ class _PartnerProfileCard extends StatelessWidget {
|
|||||||
final mobile = details['mobile'] ?? '--';
|
final mobile = details['mobile'] ?? '--';
|
||||||
final email = details['email'] ?? '--';
|
final email = details['email'] ?? '--';
|
||||||
final manager = details['manager_name'] ?? '--';
|
final manager = details['manager_name'] ?? '--';
|
||||||
|
final managerMobile = details['manager_mobile'] ?? '--';
|
||||||
|
final managerEmail = details['manager_email'] ?? '--';
|
||||||
final status = details['status'] ?? 'Active';
|
final status = details['status'] ?? 'Active';
|
||||||
return Container(
|
return Container(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 18),
|
||||||
@ -249,6 +253,8 @@ class _PartnerProfileCard extends StatelessWidget {
|
|||||||
_m('Mobile', mobile.toString()),
|
_m('Mobile', mobile.toString()),
|
||||||
_m('Email', email.toString()),
|
_m('Email', email.toString()),
|
||||||
_m('Manager', manager.toString()),
|
_m('Manager', manager.toString()),
|
||||||
|
_m('Manager Mobile', managerMobile),
|
||||||
|
_m('Manager Email', managerEmail),
|
||||||
]),
|
]),
|
||||||
])),
|
])),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
@ -438,20 +444,20 @@ class _RenewalAlertsCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
Text('Renewal Alerts', style: _chartHeader),
|
Text('Renewal Alerts', style: _chartHeader),
|
||||||
Text(
|
Text(
|
||||||
'At-risk policies',
|
'',
|
||||||
style: GoogleFonts.inter(fontSize: 9.5, color: const Color(0xFF8AABB5)),
|
style: GoogleFonts.inter(fontSize: 9.5, color: const Color(0xFF8AABB5)),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
if (totalCount > 0)
|
// if (totalCount > 0)
|
||||||
Padding(
|
// Padding(
|
||||||
padding: const EdgeInsets.only(right: 8),
|
// padding: const EdgeInsets.only(right: 8),
|
||||||
child: Text(
|
// child: Text(
|
||||||
'$totalCount total',
|
// '$totalCount total',
|
||||||
style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5)),
|
// style: GoogleFonts.inter(fontSize: 10, color: const Color(0xFF8AABB5)),
|
||||||
),
|
// ),
|
||||||
),
|
// ),
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(3),
|
padding: const EdgeInsets.all(3),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
@ -501,13 +507,13 @@ class _RenewalAlertsCard extends StatelessWidget {
|
|||||||
color: const Color(0xFFE3F9F8),
|
color: const Color(0xFFE3F9F8),
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
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('Policy No.', style: _tableHeadStyle)),
|
||||||
Expanded(flex: 2, child: Text('Holder', style: _tableHeadStyle)),
|
Expanded(flex: 2, child: Text('Policy Holder Name', style: _tableHeadStyle)),
|
||||||
Expanded(flex: 2, child: Text('Expires', style: _tableHeadStyle)),
|
Expanded(flex: 2, child: Text('Expiry Date', style: _tableHeadStyle)),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
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(),
|
physics: const ClampingScrollPhysics(),
|
||||||
itemCount: renewals.length,
|
itemCount: renewals.length,
|
||||||
separatorBuilder: (_, __) => const Divider(height: 1, color: Color(0xFFD9F0ED)),
|
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 {
|
class _RenewalTableRow extends StatelessWidget {
|
||||||
|
final int serialNo;
|
||||||
final Map<String, dynamic> renewal;
|
final Map<String, dynamic> renewal;
|
||||||
final String Function(dynamic) inr;
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final d = int.tryParse(renewal['days_left']?.toString() ?? '0') ?? 0;
|
final d = int.tryParse(renewal['days_left']?.toString() ?? '0') ?? 0;
|
||||||
Color chipBg, chipFg;
|
final Color chipFg;
|
||||||
if (d <= 10) {
|
if (d <= 10) {
|
||||||
chipBg = const Color(0xFFFFEDED);
|
|
||||||
chipFg = const Color(0xFFEF4444);
|
chipFg = const Color(0xFFEF4444);
|
||||||
} else if (d <= 20) {
|
} else if (d <= 20) {
|
||||||
chipBg = const Color(0xFFFEF0E7);
|
|
||||||
chipFg = const Color(0xFFF97316);
|
chipFg = const Color(0xFFF97316);
|
||||||
} else {
|
} else {
|
||||||
chipBg = const Color(0xFFF1F5F9);
|
|
||||||
chipFg = const Color(0xFF64748B);
|
chipFg = const Color(0xFF64748B);
|
||||||
}
|
}
|
||||||
|
final endDate = renewal['end_date']?.toString() ?? '--';
|
||||||
|
|
||||||
return SizedBox(
|
return SizedBox(
|
||||||
height: _RenewalAlertsCard.kRowHeight,
|
height: _RenewalAlertsCard.kRowHeight,
|
||||||
child: Padding(
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: 88,
|
width: 40,
|
||||||
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(
|
child: Text(
|
||||||
'$d d',
|
'$serialNo',
|
||||||
style: GoogleFonts.inter(
|
style: _tableData,
|
||||||
fontSize: 9.5,
|
textAlign: TextAlign.center,
|
||||||
fontWeight: FontWeight.w700,
|
|
||||||
color: chipFg,
|
|
||||||
letterSpacing: 0.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
@ -593,18 +593,32 @@ class _RenewalTableRow extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(
|
child: Text.rich(
|
||||||
renewal['end_date']?.toString() ?? '--',
|
TextSpan(
|
||||||
style: _tableData,
|
style: _tableData,
|
||||||
|
children: [
|
||||||
|
TextSpan(text: endDate),
|
||||||
|
TextSpan(
|
||||||
|
text: ' ($d d)',
|
||||||
|
style: GoogleFonts.inter(
|
||||||
|
fontSize: 9,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: chipFg,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Text(
|
child: Text(
|
||||||
inr(renewal['premium'] ?? 0),
|
inr(renewal['premium'] ?? renewal['premium_amount'] ?? 0),
|
||||||
style: _tableDataBold,
|
style: _tableDataBold,
|
||||||
textAlign: TextAlign.right,
|
textAlign: TextAlign.right,
|
||||||
|
maxLines: 2,
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -25,13 +25,23 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
static final List<TextInputFormatter> _commissionInputFormatter = [
|
static final List<TextInputFormatter> _commissionInputFormatter = [
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'^\d*\.?\d{0,2}$')),
|
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<TextInputFormatter> _utrInputFormatter = [
|
static final List<TextInputFormatter> _utrInputFormatter = [
|
||||||
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9]')),
|
||||||
|
LengthLimitingTextInputFormatter(_utrMaxLength),
|
||||||
];
|
];
|
||||||
|
|
||||||
|
static bool _isValidUtr(String raw) =>
|
||||||
|
_utrFullValuePattern.hasMatch(raw.trim());
|
||||||
// --------------------------
|
// --------------------------
|
||||||
// Invoice Fields
|
// Invoice Fields
|
||||||
// --------------------------
|
// --------------------------
|
||||||
final _formKey = GlobalKey<FormState>();
|
final _formKey = GlobalKey<FormState>();
|
||||||
|
final _utrFormKey = GlobalKey<FormState>();
|
||||||
// final TextEditingController invoiceNoController;
|
// final TextEditingController invoiceNoController;
|
||||||
DateTime invoiceDate = DateTime.now();
|
DateTime invoiceDate = DateTime.now();
|
||||||
// dynamic selectedAgentId;
|
// dynamic selectedAgentId;
|
||||||
@ -83,6 +93,8 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
bool hasFetchedTableData = false;
|
bool hasFetchedTableData = false;
|
||||||
final Set<int> _inlineSavingIds = <int>{};
|
final Set<int> _inlineSavingIds = <int>{};
|
||||||
|
/// Per-policy payout editors so [setState] rebuilds do not reset focus/text.
|
||||||
|
final Map<int, TextEditingController> _commissionControllers = {};
|
||||||
late ApiService apiService;
|
late ApiService apiService;
|
||||||
dynamic managerId;
|
dynamic managerId;
|
||||||
dynamic userId;
|
dynamic userId;
|
||||||
@ -114,6 +126,89 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int _policyRowId(Map<String, dynamic> p) {
|
||||||
|
return int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _rebuildCommissionControllers(List<Map<String, dynamic>> 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<String, dynamic> p) {
|
||||||
|
final id = _policyRowId(p);
|
||||||
|
if (id == 0) return null;
|
||||||
|
return _commissionControllers[id];
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _commissionAmountField(
|
||||||
|
Map<String, dynamic> 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) {
|
void filterPolicyData(String query) {
|
||||||
final lowerQuery = query.toLowerCase();
|
final lowerQuery = query.toLowerCase();
|
||||||
@ -167,6 +262,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
totalPolicies = '';
|
totalPolicies = '';
|
||||||
totalCommission = '';
|
totalCommission = '';
|
||||||
filteredPolicies = [];
|
filteredPolicies = [];
|
||||||
|
_disposeCommissionControllers();
|
||||||
hasFetchedTableData = false;
|
hasFetchedTableData = false;
|
||||||
// dropDownKeyPartner.currentState?.clear();
|
// dropDownKeyPartner.currentState?.clear();
|
||||||
});
|
});
|
||||||
@ -302,6 +398,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
print('PD =>API Data response - $response');
|
print('PD =>API Data response - $response');
|
||||||
setState(() {
|
setState(() {
|
||||||
filteredPolicies = List<Map<String, dynamic>>.from(response['data']);
|
filteredPolicies = List<Map<String, dynamic>>.from(response['data']);
|
||||||
|
_rebuildCommissionControllers(filteredPolicies);
|
||||||
hasFetchedTableData = true;
|
hasFetchedTableData = true;
|
||||||
print('PD =>API Data - $filteredPolicies');
|
print('PD =>API Data - $filteredPolicies');
|
||||||
// filteredPolicies = allPolicies.where((p) {
|
// filteredPolicies = allPolicies.where((p) {
|
||||||
@ -315,6 +412,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
} else {
|
} else {
|
||||||
setState(() {
|
setState(() {
|
||||||
filteredPolicies = [];
|
filteredPolicies = [];
|
||||||
|
_rebuildCommissionControllers(filteredPolicies);
|
||||||
hasFetchedTableData = true;
|
hasFetchedTableData = true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -322,6 +420,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
print('PD =>Exception occurred: $e');
|
print('PD =>Exception occurred: $e');
|
||||||
setState(() {
|
setState(() {
|
||||||
filteredPolicies = [];
|
filteredPolicies = [];
|
||||||
|
_rebuildCommissionControllers(filteredPolicies);
|
||||||
hasFetchedTableData = false;
|
hasFetchedTableData = false;
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
@ -367,6 +466,11 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final utrFormState = _utrFormKey.currentState;
|
||||||
|
if (utrFormState != null && !utrFormState.validate()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
final String utrInput = (controllers['utrNumber']?.text ?? '').trim();
|
final String utrInput = (controllers['utrNumber']?.text ?? '').trim();
|
||||||
if (utrInput.isEmpty) {
|
if (utrInput.isEmpty) {
|
||||||
ToastHelper.showWarningToast(
|
ToastHelper.showWarningToast(
|
||||||
@ -375,6 +479,14 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
);
|
);
|
||||||
return;
|
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
|
// Format dates for API
|
||||||
final String formattedInvoiceDate = DateFormat(
|
final String formattedInvoiceDate = DateFormat(
|
||||||
@ -822,11 +934,11 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
color: Color(0xFFf9fafb),
|
color: Color(0xFFf9fafb),
|
||||||
// color: Colors.white,
|
// color: Colors.white,
|
||||||
child: Row(
|
child: Row(
|
||||||
// mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// ---------------- SELECTED COUNT ----------------
|
// ---------------- SELECTED COUNT ----------------
|
||||||
Row(
|
Row(
|
||||||
// crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
// selectedPolicies.length.toString(),
|
// selectedPolicies.length.toString(),
|
||||||
@ -883,7 +995,9 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
buildUTRNumber(context),
|
buildUTRNumber(context),
|
||||||
SizedBox(width: 20),
|
SizedBox(width: 20),
|
||||||
// ---------------- BUTTONS ----------------
|
// ---------------- BUTTONS ----------------
|
||||||
Row(
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(top: 2),
|
||||||
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
ElevatedButton(
|
ElevatedButton(
|
||||||
onPressed: saveInvoice,
|
onPressed: saveInvoice,
|
||||||
@ -917,6 +1031,7 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@ -1022,7 +1137,6 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
|
|
||||||
final int id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0;
|
final int id = int.tryParse(p["policy_id"]?.toString() ?? "0") ?? 0;
|
||||||
final bool isSelected = selectedPolicies.contains(id);
|
final bool isSelected = selectedPolicies.contains(id);
|
||||||
|
|
||||||
return InkWell(
|
return InkWell(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
setState(() {
|
setState(() {
|
||||||
@ -1087,35 +1201,9 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
),
|
),
|
||||||
const SizedBox(width: 2),
|
const SizedBox(width: 2),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextFormField(
|
child: _commissionAmountField(
|
||||||
key: ValueKey(
|
p,
|
||||||
'commission_${p["policy_id"]}_${p["commission_amount"]}',
|
_commissionControllerFor(p),
|
||||||
),
|
|
||||||
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),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -1186,6 +1274,14 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
);
|
);
|
||||||
if (response['status'] == 'success' || response['status'] == 200) {
|
if (response['status'] == 'success' || response['status'] == 200) {
|
||||||
row['commission_amount'] = trimmed;
|
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();
|
_calculateTotalCommission();
|
||||||
} else {
|
} else {
|
||||||
ToastHelper.showErrorToast(
|
ToastHelper.showErrorToast(
|
||||||
@ -1237,19 +1333,55 @@ class _PayOutDetailsState extends ConsumerState<PayOutDetails> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Widget buildUTRNumber(BuildContext context) {
|
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(
|
return Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text('UTR Number', style: _textStyle),
|
Padding(
|
||||||
SizedBox(width: 5),
|
padding: const EdgeInsets.only(top: 10),
|
||||||
|
child: Text('UTR Number', style: _textStyle),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 5),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
height: 35,
|
width: fieldWidth.clamp(160.0, 280.0),
|
||||||
width: MediaQuery.of(context).size.width * 0.15,
|
child: Form(
|
||||||
|
key: _utrFormKey,
|
||||||
child: TextFormField(
|
child: TextFormField(
|
||||||
controller: controllers['utrNumber']!,
|
controller: controllers['utrNumber']!,
|
||||||
|
keyboardType: TextInputType.text,
|
||||||
|
autocorrect: false,
|
||||||
inputFormatters: _utrInputFormatter,
|
inputFormatters: _utrInputFormatter,
|
||||||
decoration: commonInputDecoration(hint: 'UTR Number'),
|
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,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
@ -5,6 +5,11 @@ import 'package:google_fonts/google_fonts.dart';
|
|||||||
|
|
||||||
// import 'package:uae_stat/config/my_theme.dart';
|
// import 'package:uae_stat/config/my_theme.dart';
|
||||||
|
|
||||||
|
/// Characters allowed while typing an email (matches typical local-part + domain).
|
||||||
|
final List<TextInputFormatter> kEmailAddressInputFormatters = [
|
||||||
|
FilteringTextInputFormatter.allow(RegExp(r'[a-zA-Z0-9@._%+-]')),
|
||||||
|
];
|
||||||
|
|
||||||
class ThemedFormField extends HookWidget {
|
class ThemedFormField extends HookWidget {
|
||||||
ThemedFormField({
|
ThemedFormField({
|
||||||
super.key,
|
super.key,
|
||||||
@ -202,9 +207,10 @@ class ThemedFormField extends HookWidget {
|
|||||||
maxLength: maxLength, // ✅ max length
|
maxLength: maxLength, // ✅ max length
|
||||||
showCursor: isShowCursor ?? true,
|
showCursor: isShowCursor ?? true,
|
||||||
enableInteractiveSelection: isShowCursor ?? true,
|
enableInteractiveSelection: isShowCursor ?? true,
|
||||||
inputFormatters:
|
inputFormatters: inputFormatters ??
|
||||||
inputFormatters ??
|
(keyboardType == TextInputType.emailAddress
|
||||||
[FilteringTextInputFormatter.allow(RegExp(r'[a-z A-Z]'))],
|
? kEmailAddressInputFormatters
|
||||||
|
: null),
|
||||||
// ✅ allow multiline if user sets maxLines / minLines
|
// ✅ allow multiline if user sets maxLines / minLines
|
||||||
minLines: (keyboardType == TextInputType.multiline) ? 3 : 1,
|
minLines: (keyboardType == TextInputType.multiline) ? 3 : 1,
|
||||||
maxLines: (keyboardType == TextInputType.multiline) ? null : 1,
|
maxLines: (keyboardType == TextInputType.multiline) ? null : 1,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user