This commit is contained in:
sanjeev.p 2026-04-04 15:05:01 +05:30
parent 31914224ba
commit a1711cc0e0
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/StaffAttendance/attendanceAllDetails.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/POS/pos_list.dart';
import '../../presentation/screens/UserManagement/Profile/profile_mobile.dart';
@ -134,6 +134,14 @@ final GoRouter appRouter = GoRouter(
builder: (context, state) => const AgentList(),
),
GoRoute(
path: AppRoutes.agentDetailsParam,
builder: (context, state) {
final agentId = state.pathParameters['agentId'] ?? 'new';
return AgentDetailsScreen(agentId: agentId);
},
),
// GoRoute(
// path: '/agent/:id',
// builder: (context, state) {

View File

@ -22,6 +22,11 @@ class AppRoutes {
static const String profile = '/profile';
static const String agentLst = '/agentLst';
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 staff = '/staff/:id';
static const String salesExecutiveLst = '/salesExecutiveLst';

View File

@ -915,13 +915,18 @@ class ApiService {
}
// ----------------------------------- 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) {
await _initializeToken();
}
final roleTrimmed = role?.trim();
final fileIdTrimmed = fileId?.trim();
final loggedIdTrimmed = loggedId?.trim();
final endpoint = Uri.parse('${Env.apiUrl}grid');
final queryParameters = <String, String>{};
@ -931,6 +936,9 @@ class ApiService {
if (fileIdTrimmed != null && fileIdTrimmed.isNotEmpty) {
queryParameters['file_id'] = fileIdTrimmed;
}
if (loggedIdTrimmed != null && loggedIdTrimmed.isNotEmpty) {
queryParameters['logged_id'] = loggedIdTrimmed;
}
final url = endpoint.replace(queryParameters: queryParameters);
final headers = {
@ -1059,6 +1067,65 @@ class ApiService {
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 {
// print(_token);
if (_token == null) {

View File

@ -34,10 +34,12 @@ class Validators {
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)) {
return "Invalid $label"; // 👈 will show: "Invalid Email"
if (!RegExp(emailRegex).hasMatch(trimmed)) {
return "Invalid $label";
}
return null;
}

View File

@ -83,6 +83,10 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
bool _isUpdating = false;
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
void dispose() {
_searchController.dispose();
@ -107,8 +111,16 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
@override
void initState() {
super.initState();
_loadRetentionRateFromToken();
_fetch();
WidgetsBinding.instance.addPostFrameCallback((_) => _bootstrapGrid());
}
Future<void> _bootstrapGrid() async {
await Future.wait([
_loadRetentionRateFromToken(),
_loadPartnerRetentionByVehicle(),
]);
if (!mounted) return;
await _fetch();
}
Future<void> _loadRetentionRateFromToken() async {
@ -122,14 +134,17 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
final retentionRate = data['retention_rate']?.toString();
final loggedId = data['id']?.toString();
if (retentionRate == null || retentionRate.trim().isEmpty) return;
if (mounted) {
setState(() {
_loggedId = (loggedId == null || loggedId.trim().isEmpty)
? null
: loggedId.trim();
_retentionRate = retentionRate.trim();
if (retentionRate != null && retentionRate.trim().isNotEmpty) {
_retentionRate = retentionRate.trim();
} else {
_retentionRate = '-';
}
_grid = _applyRetentionToGridRows(_grid);
_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) {
if (role == null) return null;
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(
role: effectiveRole,
fileId: effectiveFileId,
loggedId: loggedIdForGrid,
);
if ((res['status'] ?? '').toString().toLowerCase() != 'success') {
ToastHelper.showErrorToast(
@ -352,7 +453,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
setState(() {
_filteredGrid = next;
if (_showAddButton && !_hasAnyFilterSelection) {
if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
_filteredGrid = [];
}
_currentPage = 1;
@ -398,33 +499,73 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
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 retention = _toNum(_retentionRate);
if (base == null || retention == null) return '-';
if (base == 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;
if (result <= 0) return '-';
if (result == result.roundToDouble()) {
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>> rows,
) {
if (!_showAddButton) {
if (!_showPartnerRetentionValues) {
return rows
.map((row) => Map<String, dynamic>.from(row)
..remove('partner_comp')
..remove('partner_tp')
..remove('partner_od'))
.map(
(row) => Map<String, dynamic>.from(row)
..remove('partner_comp')
..remove('partner_tp')
..remove('partner_od'),
)
.toList();
}
// Partner (agent): API already applies retention on comp/tp/od when logged_id is sent.
if (_showAddButton) {
return rows.map((row) {
final next = Map<String, dynamic>.from(row);
next['partner_comp'] = next['comp'];
next['partner_tp'] = next['tp'];
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'] = _partnerValueFrom(next['comp']);
next['partner_tp'] = _partnerValueFrom(next['tp']);
next['partner_od'] = _partnerValueFrom(next['od']);
next['partner_comp'] = _partnerValueForRowPreview(next, next['comp']);
next['partner_tp'] = _partnerValueForRowPreview(next, next['tp']);
next['partner_od'] = _partnerValueForRowPreview(next, next['od']);
return next;
}).toList();
}
@ -440,7 +581,16 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
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 =>
!_showAddButton ||
_selectedPlanTypeValue == 'comp';
@ -807,7 +957,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
icon: Icons.search,
message: 'Search',
onTap: () {
if (_showAddButton && !_hasAnyFilterSelection) {
if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
ToastHelper.showInfoToast(
context,
'Please select at least one filter to view/export data.',
@ -975,7 +1125,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
showCompColumn: _showCompColumnForAgent,
showTpColumn: _showTpColumnForAgent,
showOdColumn: _showOdColumnForAgent,
showPartnerCompValue: _showAddButton,
showPartnerCompValue: _showPartnerRetentionValues,
),
);
}
@ -1139,7 +1289,7 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
onTap: _isLoading
? () {}
: () async {
if (_showAddButton && !_hasAnyFilterSelection) {
if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
if (mounted) {
ToastHelper.showInfoToast(
context,
@ -1157,6 +1307,8 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
(roleLc == 'manager' || roleLc == 'accounts')
? fileIdFromQuery
: null;
final previewAgentId =
queryParams['agent_id']?.trim();
await _apiService.downloadGridExcel(
role: role,
fileId: fileIdToSend,
@ -1166,7 +1318,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
rto: _selectedRto,
planType: _selectedPlanTypeValue,
search: _searchController.text.trim(),
loggedId: _loggedId,
loggedId: previewAgentId != null && previewAgentId.isNotEmpty
? previewAgentId
: _loggedId,
);
} catch (e) {
if (mounted) {

View File

@ -15,7 +15,6 @@ import '../../../themes/indicators/filter_btn.dart';
import '../../../themes/indicators/search_field_theme.dart';
import '../../../themes/indicators/text_field_theme.dart';
import '../../../widgets/custom_action_popup.dart';
import 'agent.dart';
import 'agentIncentiveFile.dart';
class AgentList extends ConsumerStatefulWidget {
@ -163,7 +162,7 @@ class AgentListState extends ConsumerState<AgentList> {
Navigator.pop(context);
print('EDIT - ${data['id']}');
dynamic id = data['id'];
context.go('/agent/$id');
context.go(AppRoutes.agentDetailsFor(id.toString()));
},
child: Row(
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
Widget build(BuildContext context) {
managerId = ref.watch(managerIdProvider);
@ -352,8 +337,7 @@ class AgentListState extends ConsumerState<AgentList> {
SizedBox(width: 10),
InkWell(
onTap: () {
// context.go('/agent/create');
showAgent(id: 'Create');
context.go(AppRoutes.agentDetailsFor('new'));
},
child: Container(
padding: EdgeInsets.all(5.0),
@ -620,8 +604,7 @@ class AgentListState extends ConsumerState<AgentList> {
print('EDITStaff - ${item['id']}');
selected_id = item['id'];
showAgent(id: selected_id);
// context.go('/agent/$selected_id');
context.go(AppRoutes.agentDetailsFor(selected_id.toString()));
},
splashRadius: 28,
hoverColor: Colors.black12,

File diff suppressed because it is too large Load Diff