This commit is contained in:
Surendiran 2026-04-04 15:08:42 +05:30
commit c35d42f39c
8 changed files with 1528 additions and 48 deletions

File diff suppressed because one or more lines are too long

View File

@ -21,7 +21,7 @@ import '../../presentation/screens/Masters/EndorsementType/endorsementList.dart'
import '../../presentation/screens/Masters/VehicleType/vehicleList.dart'; import '../../presentation/screens/Masters/VehicleType/vehicleList.dart';
import '../../presentation/screens/StaffAttendance/attendanceAllDetails.dart'; import '../../presentation/screens/StaffAttendance/attendanceAllDetails.dart';
import '../../presentation/screens/StaffAttendance/individual_Attendance.dart'; import '../../presentation/screens/StaffAttendance/individual_Attendance.dart';
import '../../presentation/screens/UserManagement/Agent/agent.dart'; import '../../presentation/screens/UserManagement/Agent/agent_details.dart';
import '../../presentation/screens/UserManagement/Agent/agentList.dart'; import '../../presentation/screens/UserManagement/Agent/agentList.dart';
import '../../presentation/screens/UserManagement/POS/pos_list.dart'; import '../../presentation/screens/UserManagement/POS/pos_list.dart';
import '../../presentation/screens/UserManagement/Profile/profile_mobile.dart'; import '../../presentation/screens/UserManagement/Profile/profile_mobile.dart';
@ -134,6 +134,14 @@ final GoRouter appRouter = GoRouter(
builder: (context, state) => const AgentList(), builder: (context, state) => const AgentList(),
), ),
GoRoute(
path: AppRoutes.agentDetailsParam,
builder: (context, state) {
final agentId = state.pathParameters['agentId'] ?? 'new';
return AgentDetailsScreen(agentId: agentId);
},
),
// GoRoute( // GoRoute(
// path: '/agent/:id', // path: '/agent/:id',
// builder: (context, state) { // builder: (context, state) {

View File

@ -22,6 +22,11 @@ class AppRoutes {
static const String profile = '/profile'; static const String profile = '/profile';
static const String agentLst = '/agentLst'; static const String agentLst = '/agentLst';
static const agent = '/agent/:id'; static const agent = '/agent/:id';
/// Partner details page: use [agentDetailsFor] with `'new'` or agent id.
static const String agentDetailsParam = '/agentDetails/:agentId';
static String agentDetailsFor(String agentId) =>
'/agentDetails/${Uri.encodeComponent(agentId)}';
static const String staffLst = '/staffLst'; static const String staffLst = '/staffLst';
static const staff = '/staff/:id'; static const staff = '/staff/:id';
static const String salesExecutiveLst = '/salesExecutiveLst'; static const String salesExecutiveLst = '/salesExecutiveLst';

View File

@ -915,13 +915,18 @@ class ApiService {
} }
// ----------------------------------- PAYOUT GRID ---------------------------------------------- // ----------------------------------- PAYOUT GRID ----------------------------------------------
Future<Map<String, dynamic>> loadPayoutGrid({String? role, String? fileId}) async { Future<Map<String, dynamic>> loadPayoutGrid({
String? role,
String? fileId,
String? loggedId,
}) async {
if (_token == null) { if (_token == null) {
await _initializeToken(); await _initializeToken();
} }
final roleTrimmed = role?.trim(); final roleTrimmed = role?.trim();
final fileIdTrimmed = fileId?.trim(); final fileIdTrimmed = fileId?.trim();
final loggedIdTrimmed = loggedId?.trim();
final endpoint = Uri.parse('${Env.apiUrl}grid'); final endpoint = Uri.parse('${Env.apiUrl}grid');
final queryParameters = <String, String>{}; final queryParameters = <String, String>{};
@ -931,6 +936,9 @@ class ApiService {
if (fileIdTrimmed != null && fileIdTrimmed.isNotEmpty) { if (fileIdTrimmed != null && fileIdTrimmed.isNotEmpty) {
queryParameters['file_id'] = fileIdTrimmed; queryParameters['file_id'] = fileIdTrimmed;
} }
if (loggedIdTrimmed != null && loggedIdTrimmed.isNotEmpty) {
queryParameters['logged_id'] = loggedIdTrimmed;
}
final url = endpoint.replace(queryParameters: queryParameters); final url = endpoint.replace(queryParameters: queryParameters);
final headers = { final headers = {
@ -1059,6 +1067,65 @@ class ApiService {
return response; return response;
} }
Future<Map<String, dynamic>> fetchPartnerVehicleTypes() async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}agent/partnerVehicleTypeList');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'app-signature': Env.App_Signature,
};
return _makeGetRequest(url, headers);
}
Future<Map<String, dynamic>> updateAgentVehicleRetention({
required String agentId,
required String vehicleTypeId,
required String retentionRate,
required String updatedBy,
}) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}agent/updateAgentVehicleRetention');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'Content-Type': 'application/json',
'app-signature': Env.App_Signature,
};
final body = {
'agent_id': agentId,
'vehicle_type_id': vehicleTypeId,
'retention_rate': retentionRate,
'updated_by': updatedBy,
};
return _makePostRequestJson(url, body, headers);
}
/// Bulk upsert retention rows for an agent (Save all in retention table).
Future<Map<String, dynamic>> saveAgentRetentionRatesBulk({
required String agentId,
required List<Map<String, dynamic>> retentionRates,
required String updatedBy,
}) async {
if (_token == null) {
await _initializeToken();
}
final url = Uri.parse('${Env.apiUrl}agent/saveAgentRetentionRatesBulk');
final headers = {
'Authorization': 'Bearer $_token' ?? '',
'Content-Type': 'application/json',
'app-signature': Env.App_Signature,
};
final body = {
'agent_id': agentId,
'updated_by': updatedBy,
'retention_rates': retentionRates,
};
return _makePostRequestJson(url, body, headers);
}
Future<Map<String, dynamic>> deleteAgentIncentiveFile(id) async { Future<Map<String, dynamic>> deleteAgentIncentiveFile(id) async {
// print(_token); // print(_token);
if (_token == null) { if (_token == null) {

View File

@ -34,10 +34,12 @@ class Validators {
return "Required"; return "Required";
} }
final emailRegex = r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$'; final trimmed = value.trim();
final emailRegex =
r'^[a-zA-Z0-9](?:[a-zA-Z0-9._%+-]*[a-zA-Z0-9])?@(?:[a-zA-Z0-9](?:[a-zA-Z0-9-]*[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$';
if (!RegExp(emailRegex).hasMatch(value)) { if (!RegExp(emailRegex).hasMatch(trimmed)) {
return "Invalid $label"; // 👈 will show: "Invalid Email" return "Invalid $label";
} }
return null; return null;
} }

View File

@ -83,6 +83,10 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
bool _isUpdating = false; bool _isUpdating = false;
String _retentionRate = '-'; String _retentionRate = '-';
/// Normalized vehicle type key retention % from [partner_retention_rate] (via findAgent `retention_by_vehicle`).
/// Also keys `id:<vehicle_type_id>` when the API returns `vehicle_type_id`.
final Map<String, num> _retentionByVehicleKey = {};
@override @override
void dispose() { void dispose() {
_searchController.dispose(); _searchController.dispose();
@ -107,8 +111,16 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_loadRetentionRateFromToken(); WidgetsBinding.instance.addPostFrameCallback((_) => _bootstrapGrid());
_fetch(); }
Future<void> _bootstrapGrid() async {
await Future.wait([
_loadRetentionRateFromToken(),
_loadPartnerRetentionByVehicle(),
]);
if (!mounted) return;
await _fetch();
} }
Future<void> _loadRetentionRateFromToken() async { Future<void> _loadRetentionRateFromToken() async {
@ -122,14 +134,17 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
final retentionRate = data['retention_rate']?.toString(); final retentionRate = data['retention_rate']?.toString();
final loggedId = data['id']?.toString(); final loggedId = data['id']?.toString();
if (retentionRate == null || retentionRate.trim().isEmpty) return;
if (mounted) { if (mounted) {
setState(() { setState(() {
_loggedId = (loggedId == null || loggedId.trim().isEmpty) _loggedId = (loggedId == null || loggedId.trim().isEmpty)
? null ? null
: loggedId.trim(); : loggedId.trim();
if (retentionRate != null && retentionRate.trim().isNotEmpty) {
_retentionRate = retentionRate.trim(); _retentionRate = retentionRate.trim();
} else {
_retentionRate = '-';
}
_grid = _applyRetentionToGridRows(_grid); _grid = _applyRetentionToGridRows(_grid);
_filteredGrid = _applyRetentionToGridRows(_filteredGrid); _filteredGrid = _applyRetentionToGridRows(_filteredGrid);
}); });
@ -139,6 +154,88 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
} }
} }
String _normalizeVehicleTypeKey(String raw) {
return raw.toLowerCase().trim().replaceAll(RegExp(r'\s+'), ' ');
}
/// Loads pervehicle-type retention for the logged-in agent, or for Accounts with `?agent_id=` on grid URL.
Future<void> _loadPartnerRetentionByVehicle() async {
final appRole =
(ref.read(userRoleProvider) ?? '').toString().trim().toLowerCase();
String? agentPk;
if (appRole == 'agent') {
try {
final token = await AuthService.getToken();
if (token == null || token.isEmpty) return;
final data = Jwt.parseJwt(token)['data'];
if (data is Map) {
agentPk = (data['id']?.toString() ?? '').trim();
if (agentPk.isEmpty) agentPk = null;
}
} catch (_) {
return;
}
} else if (appRole == 'accounts') {
agentPk = Uri.base.queryParameters['agent_id']?.trim();
if (agentPk != null && agentPk.isEmpty) agentPk = null;
}
if (agentPk == null || agentPk.isEmpty) {
if (mounted) {
setState(() {
_retentionByVehicleKey.clear();
_grid = _applyRetentionToGridRows(_grid);
_filteredGrid = _applyRetentionToGridRows(_filteredGrid);
});
}
return;
}
try {
final res = await _apiService.findSingleAgentData(agentPk);
if ((res['status'] ?? '').toString().toLowerCase() != 'success' ||
res['data'] == null) {
return;
}
final data = Map<String, dynamic>.from(res['data'] as Map);
final raw = data['retention_by_vehicle'] ?? data['retentionByVehicle'];
final next = <String, num>{};
if (raw is List) {
for (final item in raw) {
if (item is! Map) continue;
final m = Map<String, dynamic>.from(item);
final rate = _toNum(m['retention_rate'] ?? m['retentionRate']);
if (rate == null) continue;
final name = (m['vehicle_type'] ?? '').toString().trim();
if (name.isNotEmpty) {
next[_normalizeVehicleTypeKey(name)] = rate;
}
final vid =
int.tryParse((m['vehicle_type_id'] ?? m['vehicleTypeId'] ?? '')
.toString());
if (vid != null) {
next['id:$vid'] = rate;
}
}
}
if (mounted) {
setState(() {
_retentionByVehicleKey
..clear()
..addAll(next);
_grid = _applyRetentionToGridRows(_grid);
_filteredGrid = _applyRetentionToGridRows(_filteredGrid);
});
}
} catch (_) {
if (mounted) {
setState(() {
_retentionByVehicleKey.clear();
});
}
}
}
String? _apiRoleFromAppRole(String? role) { String? _apiRoleFromAppRole(String? role) {
if (role == null) return null; if (role == null) return null;
final r = role.trim(); final r = role.trim();
@ -194,9 +291,13 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
} }
} }
final loggedIdForGrid =
(effectiveRole ?? '').toLowerCase() == 'agent' ? _loggedId : null;
final res = await _apiService.loadPayoutGrid( final res = await _apiService.loadPayoutGrid(
role: effectiveRole, role: effectiveRole,
fileId: effectiveFileId, fileId: effectiveFileId,
loggedId: loggedIdForGrid,
); );
if ((res['status'] ?? '').toString().toLowerCase() != 'success') { if ((res['status'] ?? '').toString().toLowerCase() != 'success') {
ToastHelper.showErrorToast( ToastHelper.showErrorToast(
@ -352,7 +453,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
setState(() { setState(() {
_filteredGrid = next; _filteredGrid = next;
if (_showAddButton && !_hasAnyFilterSelection) { if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
_filteredGrid = []; _filteredGrid = [];
} }
_currentPage = 1; _currentPage = 1;
@ -398,33 +499,73 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
return num.tryParse(normalized); return num.tryParse(normalized);
} }
String _partnerValueFrom(dynamic baseValue) { num? _retentionPercentForGridRowPreview(Map<String, dynamic> row) {
final vt = row['vehicle_type']?.toString().trim() ?? '';
if (vt.isNotEmpty) {
final byName = _retentionByVehicleKey[_normalizeVehicleTypeKey(vt)];
if (byName != null) return byName;
}
final vid =
int.tryParse(row['vehicle_type_id']?.toString() ?? '');
if (vid != null) {
final byId = _retentionByVehicleKey['id:$vid'];
if (byId != null) return byId;
}
return null;
}
/// Accounts preview: subtract loaded pervehicle retention; 0 '-'; no match show base.
String _partnerValueForRowPreview(Map<String, dynamic> row, dynamic baseValue) {
final base = _toNum(baseValue); final base = _toNum(baseValue);
final retention = _toNum(_retentionRate); if (base == null) return '-';
if (base == null || retention == null) return '-'; final retention = _retentionPercentForGridRowPreview(row);
if (retention == null) {
if (base == base.roundToDouble()) return base.toInt().toString();
return base
.toStringAsFixed(2)
.replaceFirst(RegExp(r'0+$'), '')
.replaceFirst(RegExp(r'\.$'), '');
}
final result = base - retention; final result = base - retention;
if (result <= 0) return '-';
if (result == result.roundToDouble()) { if (result == result.roundToDouble()) {
return result.toInt().toString(); return result.toInt().toString();
} }
return result.toStringAsFixed(2).replaceFirst(RegExp(r'0+$'), '').replaceFirst(RegExp(r'\.$'), ''); return result
.toStringAsFixed(2)
.replaceFirst(RegExp(r'0+$'), '')
.replaceFirst(RegExp(r'\.$'), '');
} }
List<Map<String, dynamic>> _applyRetentionToGridRows( List<Map<String, dynamic>> _applyRetentionToGridRows(
List<Map<String, dynamic>> rows, List<Map<String, dynamic>> rows,
) { ) {
if (!_showAddButton) { if (!_showPartnerRetentionValues) {
return rows return rows
.map((row) => Map<String, dynamic>.from(row) .map(
(row) => Map<String, dynamic>.from(row)
..remove('partner_comp') ..remove('partner_comp')
..remove('partner_tp') ..remove('partner_tp')
..remove('partner_od')) ..remove('partner_od'),
)
.toList(); .toList();
} }
// Partner (agent): API already applies retention on comp/tp/od when logged_id is sent.
if (_showAddButton) {
return rows.map((row) { return rows.map((row) {
final next = Map<String, dynamic>.from(row); final next = Map<String, dynamic>.from(row);
next['partner_comp'] = _partnerValueFrom(next['comp']); next['partner_comp'] = next['comp'];
next['partner_tp'] = _partnerValueFrom(next['tp']); next['partner_tp'] = next['tp'];
next['partner_od'] = _partnerValueFrom(next['od']); next['partner_od'] = next['od'];
return next;
}).toList();
}
// Accounts + ?agent_id= preview: client-side using findAgent retention map.
return rows.map((row) {
final next = Map<String, dynamic>.from(row);
next['partner_comp'] = _partnerValueForRowPreview(next, next['comp']);
next['partner_tp'] = _partnerValueForRowPreview(next, next['tp']);
next['partner_od'] = _partnerValueForRowPreview(next, next['od']);
return next; return next;
}).toList(); }).toList();
} }
@ -440,7 +581,16 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
return role == 'agent'; return role == 'agent';
} }
bool get _showPartnerRetentionValues => _showAddButton; /// Partner (agent) login, or Accounts opening grid with `?agent_id=` to preview that partners payouts.
bool get _accountsAgentPreview {
final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase();
if (role != 'accounts') return false;
final aid = Uri.base.queryParameters['agent_id']?.trim();
return aid != null && aid.isNotEmpty;
}
bool get _showPartnerRetentionValues =>
_showAddButton || _accountsAgentPreview;
bool get _showCompColumnForAgent => bool get _showCompColumnForAgent =>
!_showAddButton || !_showAddButton ||
_selectedPlanTypeValue == 'comp'; _selectedPlanTypeValue == 'comp';
@ -807,7 +957,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
icon: Icons.search, icon: Icons.search,
message: 'Search', message: 'Search',
onTap: () { onTap: () {
if (_showAddButton && !_hasAnyFilterSelection) { if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
ToastHelper.showInfoToast( ToastHelper.showInfoToast(
context, context,
'Please select at least one filter to view/export data.', 'Please select at least one filter to view/export data.',
@ -975,7 +1125,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
showCompColumn: _showCompColumnForAgent, showCompColumn: _showCompColumnForAgent,
showTpColumn: _showTpColumnForAgent, showTpColumn: _showTpColumnForAgent,
showOdColumn: _showOdColumnForAgent, showOdColumn: _showOdColumnForAgent,
showPartnerCompValue: _showAddButton, showPartnerCompValue: _showPartnerRetentionValues,
), ),
); );
} }
@ -1139,7 +1289,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
onTap: _isLoading onTap: _isLoading
? () {} ? () {}
: () async { : () async {
if (_showAddButton && !_hasAnyFilterSelection) { if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
if (mounted) { if (mounted) {
ToastHelper.showInfoToast( ToastHelper.showInfoToast(
context, context,
@ -1157,6 +1307,8 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
(roleLc == 'manager' || roleLc == 'accounts') (roleLc == 'manager' || roleLc == 'accounts')
? fileIdFromQuery ? fileIdFromQuery
: null; : null;
final previewAgentId =
queryParams['agent_id']?.trim();
await _apiService.downloadGridExcel( await _apiService.downloadGridExcel(
role: role, role: role,
fileId: fileIdToSend, fileId: fileIdToSend,
@ -1166,7 +1318,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
rto: _selectedRto, rto: _selectedRto,
planType: _selectedPlanTypeValue, planType: _selectedPlanTypeValue,
search: _searchController.text.trim(), search: _searchController.text.trim(),
loggedId: _loggedId, loggedId: previewAgentId != null && previewAgentId.isNotEmpty
? previewAgentId
: _loggedId,
); );
} catch (e) { } catch (e) {
if (mounted) { if (mounted) {

View File

@ -15,7 +15,6 @@ import '../../../themes/indicators/filter_btn.dart';
import '../../../themes/indicators/search_field_theme.dart'; import '../../../themes/indicators/search_field_theme.dart';
import '../../../themes/indicators/text_field_theme.dart'; import '../../../themes/indicators/text_field_theme.dart';
import '../../../widgets/custom_action_popup.dart'; import '../../../widgets/custom_action_popup.dart';
import 'agent.dart';
import 'agentIncentiveFile.dart'; import 'agentIncentiveFile.dart';
class AgentList extends ConsumerStatefulWidget { class AgentList extends ConsumerStatefulWidget {
@ -163,7 +162,7 @@ class AgentListState extends ConsumerState<AgentList> {
Navigator.pop(context); Navigator.pop(context);
print('EDIT - ${data['id']}'); print('EDIT - ${data['id']}');
dynamic id = data['id']; dynamic id = data['id'];
context.go('/agent/$id'); context.go(AppRoutes.agentDetailsFor(id.toString()));
}, },
child: Row( child: Row(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -177,20 +176,6 @@ class AgentListState extends ConsumerState<AgentList> {
]; ];
} }
Future<void> showAgent({required String id}) {
print('showAgent : $id');
return showDialog(
context: context,
builder: (ctx) => Agent(
id: id,
onSubmit: (value) {
debugPrint("New Claims: $value");
refresh();
},
),
);
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
managerId = ref.watch(managerIdProvider); managerId = ref.watch(managerIdProvider);
@ -352,8 +337,7 @@ class AgentListState extends ConsumerState<AgentList> {
SizedBox(width: 10), SizedBox(width: 10),
InkWell( InkWell(
onTap: () { onTap: () {
// context.go('/agent/create'); context.go(AppRoutes.agentDetailsFor('new'));
showAgent(id: 'Create');
}, },
child: Container( child: Container(
padding: EdgeInsets.all(5.0), padding: EdgeInsets.all(5.0),
@ -620,8 +604,7 @@ class AgentListState extends ConsumerState<AgentList> {
print('EDITStaff - ${item['id']}'); print('EDITStaff - ${item['id']}');
selected_id = item['id']; selected_id = item['id'];
showAgent(id: selected_id); context.go(AppRoutes.agentDetailsFor(selected_id.toString()));
// context.go('/agent/$selected_id');
}, },
splashRadius: 28, splashRadius: 28,
hoverColor: Colors.black12, hoverColor: Colors.black12,

File diff suppressed because it is too large Load Diff