This commit is contained in:
Surendiran 2026-04-07 12:05:45 +05:30
commit 81327bbc1b
4 changed files with 153 additions and 286 deletions

File diff suppressed because one or more lines are too long

View File

@ -922,7 +922,6 @@ class ApiService {
String? vehicleType,
String? segment,
String? rto,
String? planType,
String? search,
String? loggedId,
}) async {
@ -936,7 +935,6 @@ class ApiService {
final vehicleTypeTrimmed = vehicleType?.trim();
final segmentTrimmed = segment?.trim();
final rtoTrimmed = rto?.trim();
final planTypeTrimmed = planType?.trim();
final searchTrimmed = search?.trim();
final loggedIdTrimmed = loggedId?.trim();
final endpoint = Uri.parse('${Env.apiUrl}grid');
@ -963,9 +961,6 @@ class ApiService {
if (rtoTrimmed != null && rtoTrimmed.isNotEmpty) {
queryParameters['rto'] = rtoTrimmed;
}
if (planTypeTrimmed != null && planTypeTrimmed.isNotEmpty) {
queryParameters['plan_type'] = planTypeTrimmed;
}
if (searchTrimmed != null && searchTrimmed.isNotEmpty) {
queryParameters['search'] = searchTrimmed;
}
@ -988,7 +983,6 @@ class ApiService {
String? vehicleType,
String? segment,
String? rto,
String? planType,
String? search,
String? loggedId,
}) async {
@ -1007,13 +1001,17 @@ class ApiService {
}
}
final downloadRoleLc = role?.trim().toLowerCase();
addQuery('role', role);
addQuery('file_id', fileId);
// Manager / Accounts use file_id; Agent uses logged_id only (same as loadPayoutGrid).
if (downloadRoleLc != 'agent') {
addQuery('file_id', fileId);
}
addQuery('insurer', insurer);
addQuery('vehicle_type', vehicleType);
addQuery('segment', segment);
addQuery('rto', rto);
addQuery('plan_type', planType);
addQuery('search', search);
addQuery('logged_id', loggedId);

View File

@ -33,9 +33,14 @@ class gridViewScreen extends ConsumerStatefulWidget {
}
class _gridViewScreenState extends ConsumerState<gridViewScreen> {
static const String _defaultVehicleTypeLabel = 'TWO WHEELER NEW';
final ApiService _apiService = ApiService();
String? _loggedId;
/// `file_id` from the last successful Manager/Accounts grid load (export matches this).
String? _gridFileIdUsed;
bool _isLoading = false;
// API response
@ -43,7 +48,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
List<Map<String, dynamic>> _filteredGrid = [];
List<String> _insurers = [];
List<Map<String, dynamic>> _planTypes = [];
List<String> _rtos = [];
List<String> _vehicleTypes = [];
List<String> _segments = [];
@ -53,7 +57,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
String? _selectedRto;
String? _selectedVehicleType;
String? _selectedSegment;
String? _selectedPlanTypeValue; // comp/tp/od
final TextEditingController _searchController = TextEditingController();
final TextEditingController _editVehicleTypeController =
@ -267,32 +270,63 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
final queryFileId = queryParams['file_id']?.trim();
final effectiveRole =
(queryRole != null && queryRole.isNotEmpty) ? queryRole : apiRole;
String? effectiveFileId =
(queryFileId != null && queryFileId.isNotEmpty) ? queryFileId : null;
final roleLc = (effectiveRole ?? '').toLowerCase();
if (effectiveFileId == null &&
(effectiveRole?.toLowerCase() == 'manager' ||
effectiveRole?.toLowerCase() == 'agent' ||
effectiveRole?.toLowerCase() == 'accounts')) {
final fileListRes = await _apiService.fetchGridFileList();
if ((fileListRes['status'] ?? '').toString().toLowerCase() ==
'success') {
final files = List<Map<String, dynamic>>.from(
fileListRes['data'] ?? const [],
);
if (files.isNotEmpty) {
files.sort((a, b) {
final left = int.tryParse(a['id']?.toString() ?? '') ?? 0;
final right = int.tryParse(b['id']?.toString() ?? '') ?? 0;
return right.compareTo(left);
});
effectiveFileId = files.first['id']?.toString();
// Manager / Accounts: require file_id (URL or latest from file list).
// Agent: never send file_id; require logged-in user id for retention.
String? effectiveFileId;
if (roleLc == 'agent') {
effectiveFileId = null;
} else {
effectiveFileId =
(queryFileId != null && queryFileId.isNotEmpty) ? queryFileId : null;
if (effectiveFileId == null &&
(roleLc == 'manager' || roleLc == 'accounts')) {
final fileListRes = await _apiService.fetchGridFileList();
if ((fileListRes['status'] ?? '').toString().toLowerCase() ==
'success') {
final files = List<Map<String, dynamic>>.from(
fileListRes['data'] ?? const [],
);
if (files.isNotEmpty) {
files.sort((a, b) {
final left = int.tryParse(a['id']?.toString() ?? '') ?? 0;
final right = int.tryParse(b['id']?.toString() ?? '') ?? 0;
return right.compareTo(left);
});
effectiveFileId = files.first['id']?.toString();
}
}
}
if (roleLc == 'manager' || roleLc == 'accounts') {
if (effectiveFileId == null || effectiveFileId.trim().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(
context,
'No grid file found. Open grid from a file or upload a payout grid first.',
);
}
return;
}
}
}
final loggedIdForGrid =
(effectiveRole ?? '').toLowerCase() == 'agent' ? _loggedId : null;
if (roleLc == 'agent') {
final lid = _loggedId?.trim();
if (lid == null || lid.isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(
context,
'Unable to load grid: missing logged-in user id.',
);
}
return;
}
}
final loggedIdForGrid = roleLc == 'agent' ? _loggedId?.trim() : null;
final res = await _apiService.loadPayoutGrid(
role: effectiveRole,
@ -300,6 +334,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
loggedId: loggedIdForGrid,
);
if ((res['status'] ?? '').toString().toLowerCase() != 'success') {
if (mounted) {
setState(() => _gridFileIdUsed = null);
}
ToastHelper.showErrorToast(
context,
res['data']?.toString() ?? 'Failed to load payout grid',
@ -310,7 +347,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
final payload = _extractPayload(res);
final grid = payload['grid'];
final insurers = payload['insurers'];
final planTypes = payload['plan_types'];
final rtos = payload['rtos'];
final vehicleTypes = payload['vehicle_types'];
final segments = payload['segments'];
@ -325,13 +361,19 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
_filteredGrid = List<Map<String, dynamic>>.from(_grid);
_insurers = _toStringList(insurers);
_planTypes = _toPlanTypeList(planTypes);
_rtos = _toStringList(rtos);
_vehicleTypes = _toStringList(vehicleTypes);
_segments = _toStringList(segments);
_gridFileIdUsed =
(roleLc == 'manager' || roleLc == 'accounts') ? effectiveFileId : null;
_applyDefaultVehicleTypeSelection();
_applyFilters();
} catch (e) {
if (mounted) {
setState(() => _gridFileIdUsed = null);
}
ToastHelper.showErrorToast(context, e.toString());
} finally {
if (mounted) setState(() => _isLoading = false);
@ -381,25 +423,18 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
.toList();
}
List<Map<String, dynamic>> _toPlanTypeList(dynamic raw) {
if (raw is! List) return <Map<String, dynamic>>[];
return raw.map<Map<String, dynamic>>((item) {
if (item is Map) {
final map = item.cast<String, dynamic>();
final value = map['value']?.toString() ??
map['id']?.toString() ??
map['key']?.toString() ??
'';
final label = map['label']?.toString() ??
map['name']?.toString() ??
map['title']?.toString() ??
map['value']?.toString() ??
value;
return {'value': value, 'label': label};
/// Sets [_selectedVehicleType] to the dropdown spelling that matches
/// [_defaultVehicleTypeLabel], or prepends that label if the API omits it.
void _applyDefaultVehicleTypeSelection() {
final desiredLower = _defaultVehicleTypeLabel.toLowerCase();
for (final v in _vehicleTypes) {
if (v.trim().toLowerCase() == desiredLower) {
_selectedVehicleType = v;
return;
}
final v = item?.toString() ?? '';
return {'value': v, 'label': v.toUpperCase()};
}).where((e) => (e['value']?.toString().trim().isNotEmpty ?? false)).toList();
}
_vehicleTypes = [_defaultVehicleTypeLabel, ..._vehicleTypes];
_selectedVehicleType = _defaultVehicleTypeLabel;
}
void _applyFilters() {
@ -411,20 +446,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
return raw?.toString().toLowerCase() == selected.toLowerCase();
}
bool matchesPlanType() {
if (_selectedPlanTypeValue == null) return true;
final raw = row[_selectedPlanTypeValue];
if (raw == null) return false;
final s = raw.toString().trim().toLowerCase();
if (s.isEmpty || s == 'null' || s == '-') return false;
// Treat 0 payout as "not configured" so it doesn't match plan filters.
final n = num.tryParse(s);
if (n != null) return n != 0;
return s != '0';
}
bool matchesSearch() {
if (q.isEmpty) return true;
final values = <String>[
@ -447,7 +468,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
equalsOrAll(_selectedVehicleType, row['vehicle_type']) &&
equalsOrAll(_selectedSegment, row['segment']) &&
equalsOrAll(_selectedRto, row['rto']) &&
matchesPlanType() &&
matchesSearch();
}).toList();
@ -591,24 +611,13 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
bool get _showPartnerRetentionValues =>
_showAddButton || _accountsAgentPreview;
bool get _showCompColumnForAgent =>
!_showAddButton ||
_selectedPlanTypeValue == 'comp';
bool get _showTpColumnForAgent =>
!_showAddButton ||
_selectedPlanTypeValue == 'tp';
bool get _showOdColumnForAgent =>
!_showAddButton ||
_selectedPlanTypeValue == 'od';
bool get _hasAnyFilterSelection {
return (_selectedInsurer != null && _selectedInsurer!.trim().isNotEmpty) ||
(_selectedRto != null && _selectedRto!.trim().isNotEmpty) ||
(_selectedVehicleType != null &&
_selectedVehicleType!.trim().isNotEmpty) ||
(_selectedSegment != null && _selectedSegment!.trim().isNotEmpty) ||
(_selectedPlanTypeValue != null &&
_selectedPlanTypeValue!.trim().isNotEmpty);
(_selectedSegment != null && _selectedSegment!.trim().isNotEmpty);
}
String _toEditText(dynamic value) {
@ -766,17 +775,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
Widget _buildFilterBar(BuildContext context) {
final isMobile = ResponsiveLayout.isMobile(context);
final planTypeItems = List<Map<String, dynamic>>.from(_planTypes);
Map<String, dynamic>? selectedPlanType;
if (_selectedPlanTypeValue != null) {
for (final e in planTypeItems) {
if ((e['value'] as String?) == _selectedPlanTypeValue) {
selectedPlanType = e;
break;
}
}
}
Widget dropdownInsurer() {
return SizedBox(
width: isMobile ? double.infinity : 200,
@ -811,46 +809,6 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
);
}
Widget dropdownPlanType() {
return SizedBox(
width: isMobile ? double.infinity : 220,
height: 40,
child: DropdownSearch<Map<String, dynamic>>(
items: (filter, infiniteScrollProps) {
final items = planTypeItems;
if (filter.isEmpty) return items;
return items
.where((e) =>
(e['label']?.toString() ?? '')
.toLowerCase()
.contains(filter.toLowerCase()))
.toList();
},
selectedItem: selectedPlanType,
itemAsString: (item) => item['label']?.toString() ?? '',
compareFn: (a, b) => (a['value'] == b['value']),
popupProps: const PopupProps.menu(
showSearchBox: true,
fit: FlexFit.loose,
),
dropdownBuilder: (context, selectedItem) => Padding(
padding: const EdgeInsets.only(left: 10),
child: Text(
selectedItem?['label']?.toString() ?? '',
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
overflow: TextOverflow.ellipsis,
),
),
onChanged: (val) => _setFilter(() {
_selectedPlanTypeValue = val?['value'] as String?;
}),
decoratorProps: DropDownDecoratorProps(
decoration: _dropdownDecoration('Plan Type'),
),
),
);
}
Widget dropdownRto() {
return SizedBox(
width: isMobile ? double.infinity : 180,
@ -974,11 +932,10 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
onTap: () {
setState(() {
_selectedInsurer = null;
_selectedPlanTypeValue = null;
_selectedRto = null;
_selectedVehicleType = null;
_selectedSegment = null;
_searchController.clear();
_applyDefaultVehicleTypeSelection();
});
_applyFilters();
},
@ -996,12 +953,10 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
child: isMobile
? Column(
children: [
dropdownInsurer(),
const SizedBox(height: 10),
dropdownPlanType(),
const SizedBox(height: 10),
dropdownVehicleType(),
const SizedBox(height: 10),
dropdownInsurer(),
const SizedBox(height: 10),
dropdownSegment(),
const SizedBox(height: 10),
dropdownRto(),
@ -1018,12 +973,10 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
scrollDirection: Axis.horizontal,
child: Row(
children: [
dropdownInsurer(),
const SizedBox(width: 12),
dropdownPlanType(),
const SizedBox(width: 12),
dropdownVehicleType(),
const SizedBox(width: 12),
dropdownInsurer(),
const SizedBox(width: 12),
dropdownSegment(),
const SizedBox(width: 12),
dropdownRto(),
@ -1107,13 +1060,8 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
itemCount: _paginatedGrid.length,
separatorBuilder: (context, index) => const SizedBox(height: 10),
itemBuilder: (context, index) => _gridViewCard(
row: _paginatedGrid[index],
showCompColumn: _showCompColumnForAgent,
showTpColumn: _showTpColumnForAgent,
showOdColumn: _showOdColumnForAgent,
showPartnerCompValue: _showPartnerRetentionValues,
),
itemBuilder: (context, index) =>
_gridViewCard(row: _paginatedGrid[index]),
);
}
@ -1139,12 +1087,9 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
Expanded(flex: 2, child: Text('Vehicle', style: _headerStyle)),
Expanded(flex: 3, child: Text('Segment', style: _headerStyle)),
Expanded(flex: 2, child: Text('RTO', style: _headerStyle)),
if (_showCompColumnForAgent)
Expanded(flex: 2, child: Text('Comp', style: _headerStyle)),
if (_showTpColumnForAgent)
Expanded(flex: 2, child: Text('TP', style: _headerStyle)),
if (_showOdColumnForAgent)
Expanded(flex: 2, child: Text('OD', style: _headerStyle)),
Expanded(flex: 2, child: Text('Comp', style: _headerStyle)),
Expanded(flex: 2, child: Text('TP', style: _headerStyle)),
Expanded(flex: 2, child: Text('OD', style: _headerStyle)),
Expanded(flex: 3, child: Text('Remarks', style: _headerStyle)),
],
),
@ -1181,48 +1126,18 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
),
),
Expanded(flex: 2, child: Text(_nullableString(row['rto']), style: _dataBold)),
if (_showCompColumnForAgent)
Expanded(
flex: 2,
child: Text(
_showPartnerRetentionValues
? _nullableString(row['partner_comp'])
: _nullableString(row['comp']),
style: _dataBold,
),
),
if (_showTpColumnForAgent)
Expanded(flex: 2, child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${_nullableString(row['tp'])}', style: _dataBold),
if (_showPartnerRetentionValues) ...[
const SizedBox(height: 2),
Text(
'${_nullableString(row['partner_tp'])}',
style: _dataBold,
),
],
],
),
),
if (_showOdColumnForAgent)
Expanded(
flex: 2,
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('${_nullableString(row['od'])}', style: _dataBold),
if (_showPartnerRetentionValues) ...[
const SizedBox(height: 2),
Text(
'${_nullableString(row['partner_od'])}',
style: _dataBold,
),
],
],
),
),
Expanded(
flex: 2,
child: Text(_nullableString(row['comp']), style: _dataBold),
),
Expanded(
flex: 2,
child: Text(_nullableString(row['tp']), style: _dataBold),
),
Expanded(
flex: 2,
child: Text(_nullableString(row['od']), style: _dataBold),
),
Expanded(
flex: 3,
child: Text(
@ -1287,15 +1202,23 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
}
try {
final queryParams = Uri.base.queryParameters;
final fileIdFromQuery = queryParams['file_id']?.trim();
final role = _apiRoleFromAppRole(ref.read(userRoleProvider));
final roleLc = (role ?? '').toLowerCase();
final fileIdFromQuery = queryParams['file_id']?.trim();
final fileIdToSend =
(roleLc == 'manager' || roleLc == 'accounts')
? fileIdFromQuery
? (_gridFileIdUsed ??
(fileIdFromQuery != null && fileIdFromQuery.isNotEmpty
? fileIdFromQuery
: null))
: null;
final previewAgentId =
queryParams['agent_id']?.trim();
final loggedIdForExport = roleLc == 'agent'
? _loggedId?.trim()
: (previewAgentId != null && previewAgentId.isNotEmpty
? previewAgentId
: null);
await _apiService.downloadGridExcel(
role: role,
fileId: fileIdToSend,
@ -1303,11 +1226,8 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
vehicleType: _selectedVehicleType,
segment: _selectedSegment,
rto: _selectedRto,
planType: _selectedPlanTypeValue,
search: _searchController.text.trim(),
loggedId: previewAgentId != null && previewAgentId.isNotEmpty
? previewAgentId
: _loggedId,
loggedId: loggedIdForExport,
);
} catch (e) {
if (mounted) {
@ -1340,18 +1260,8 @@ class _gridViewScreenState extends ConsumerState<gridViewScreen> {
class _gridViewCard extends StatelessWidget {
final Map<String, dynamic> row;
final bool showCompColumn;
final bool showTpColumn;
final bool showOdColumn;
final bool showPartnerCompValue;
const _gridViewCard({
required this.row,
required this.showCompColumn,
required this.showTpColumn,
required this.showOdColumn,
required this.showPartnerCompValue,
});
const _gridViewCard({required this.row});
String _nullableString(dynamic v) {
if (v == null) return '-';
@ -1405,21 +1315,11 @@ class _gridViewCard extends StatelessWidget {
const SizedBox(height: 10),
Row(
children: [
if (showCompColumn)
Expanded(
child: _upDownMetric(
title: 'Comp',
value: showPartnerCompValue ? row['partner_comp'] : row['comp'],
),
),
if (showCompColumn && (showTpColumn || showOdColumn))
const SizedBox(width: 10),
if (showTpColumn)
Expanded(child: _metric('TP', row['tp'])),
if (showTpColumn && showOdColumn)
const SizedBox(width: 10),
if (showOdColumn)
Expanded(child: _metric('OD', row['od'])),
Expanded(child: _metric('Comp', row['comp'])),
const SizedBox(width: 10),
Expanded(child: _metric('TP', row['tp'])),
const SizedBox(width: 10),
Expanded(child: _metric('OD', row['od'])),
],
),
const SizedBox(height: 10),
@ -1460,40 +1360,6 @@ class _gridViewCard extends StatelessWidget {
),
);
}
Widget _upDownMetric({
required String title,
required dynamic value,
}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFC),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: const Color(0xFFE2E8F0)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.poppins(
fontSize: 10,
color: const Color(0xFF64748B),
),
),
const SizedBox(height: 2),
Text(
_nullableString(value),
style: GoogleFonts.poppins(
fontSize: 11,
fontWeight: FontWeight.w600,
),
),
],
),
);
}
}
/* import 'package:dropdown_search/dropdown_search.dart';
import 'package:flutter/material.dart';

View File

@ -156,30 +156,32 @@ class AgentListState extends ConsumerState<AgentList> {
}
Future<void> _exportRetentionRateExcel() async {
final proceed = await showDialog<bool>(
context: context,
builder: (ctx) {
return AlertDialog(
title: const Text('Export Rentation Rate'),
content: const Text(
'The downloaded Excel will include input validation.\n\n'
'Allowed values: only numbers from 0 to 100.\n'
'Not allowed: negative values, values above 100, text/special characters.',
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx, false),
child: const Text('Cancel'),
),
ElevatedButton(
onPressed: () => Navigator.pop(ctx, true),
child: const Text('Download'),
),
],
);
},
);
if (proceed != true) return;
/****** Export confirmation dialog kept for future use ********/
// final proceed = await showDialog<bool>(
// context: context,
// builder: (ctx) {
// return AlertDialog(
// title: const Text('Export Rentation Rate'),
// content: const Text(
// 'The downloaded Excel will include input validation.\n\n'
// 'Allowed values: only numbers from 0 to 100.\n'
// 'Not allowed: negative values, values above 100, text/special characters.',
// ),
// actions: [
// TextButton(
// onPressed: () => Navigator.pop(ctx, false),
// child: const Text('Cancel'),
// ),
// ElevatedButton(
// onPressed: () => Navigator.pop(ctx, true),
// child: const Text('Download'),
// ),
// ],
// );
// },
// );
// if (proceed != true) return;
/****** *************************************************** ********/
try {
await apiService.downloadAgentRetentionRateExcel();
@ -241,7 +243,8 @@ class AgentListState extends ConsumerState<AgentList> {
'Import done',
);
}
await _showRetentionImportReportDialog(data, report);
// doN'T Delete This function And Related Codes
// await _showRetentionImportReportDialog(data, report);
refresh();
} else {
if (mounted) {