5394 lines
174 KiB
Dart
5394 lines
174 KiB
Dart
import 'package:dropdown_search/dropdown_search.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:jwt_decode/jwt_decode.dart';
|
||
import 'package:nhance_partner/core/routing/routes.dart';
|
||
import 'package:nhance_partner/core/services/api_service.dart';
|
||
import 'package:nhance_partner/data/services/auth_service.dart';
|
||
import 'package:nhance_partner/data/utils/Pagination.dart';
|
||
import 'package:nhance_partner/data/utils/toastNotification.dart';
|
||
import 'package:nhance_partner/presentation/layouts/main_layout.dart';
|
||
import 'package:nhance_partner/presentation/layouts/responsive_layout.dart';
|
||
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
|
||
// import 'package:nhance_partner/presentation/screens/Grid/Gridupload.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart';
|
||
|
||
class GridViewScreen extends StatelessWidget {
|
||
const GridViewScreen({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return const gridViewScreen();
|
||
}
|
||
}
|
||
|
||
class gridViewScreen extends ConsumerStatefulWidget {
|
||
const gridViewScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<gridViewScreen> createState() => _gridViewScreenState();
|
||
}
|
||
|
||
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
|
||
List<Map<String, dynamic>> _grid = [];
|
||
List<Map<String, dynamic>> _filteredGrid = [];
|
||
|
||
List<String> _insurers = [];
|
||
List<String> _rtos = [];
|
||
List<String> _vehicleTypes = [];
|
||
List<String> _segments = [];
|
||
|
||
// Filters
|
||
String? _selectedInsurer;
|
||
String? _selectedRto;
|
||
String? _selectedVehicleType;
|
||
String? _selectedSegment;
|
||
|
||
final TextEditingController _searchController = TextEditingController();
|
||
final TextEditingController _editVehicleTypeController =
|
||
TextEditingController();
|
||
final TextEditingController _editFuelController = TextEditingController();
|
||
final TextEditingController _editRtoController = TextEditingController();
|
||
final TextEditingController _editSegmentController = TextEditingController();
|
||
final TextEditingController _editBrokerController = TextEditingController();
|
||
final TextEditingController _editCompController = TextEditingController();
|
||
final TextEditingController _editTpController = TextEditingController();
|
||
final TextEditingController _editOdController = TextEditingController();
|
||
final TextEditingController _editBrokerCompController =
|
||
TextEditingController();
|
||
final TextEditingController _editBrokerTpController = TextEditingController();
|
||
final TextEditingController _editBrokerOdController = TextEditingController();
|
||
final TextEditingController _editPartnerCompController =
|
||
TextEditingController();
|
||
final TextEditingController _editPartnerTpController =
|
||
TextEditingController();
|
||
final TextEditingController _editPartnerOdController =
|
||
TextEditingController();
|
||
final TextEditingController _editRemarksController = TextEditingController();
|
||
|
||
// Pagination
|
||
int _currentPage = 1;
|
||
int _itemsPerPage = 10;
|
||
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();
|
||
_editVehicleTypeController.dispose();
|
||
_editFuelController.dispose();
|
||
_editRtoController.dispose();
|
||
_editSegmentController.dispose();
|
||
_editBrokerController.dispose();
|
||
_editCompController.dispose();
|
||
_editTpController.dispose();
|
||
_editOdController.dispose();
|
||
_editBrokerCompController.dispose();
|
||
_editBrokerTpController.dispose();
|
||
_editBrokerOdController.dispose();
|
||
_editPartnerCompController.dispose();
|
||
_editPartnerTpController.dispose();
|
||
_editPartnerOdController.dispose();
|
||
_editRemarksController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
WidgetsBinding.instance.addPostFrameCallback((_) => _bootstrapGrid());
|
||
}
|
||
|
||
Future<void> _bootstrapGrid() async {
|
||
await Future.wait([
|
||
_loadRetentionRateFromToken(),
|
||
_loadPartnerRetentionByVehicle(),
|
||
]);
|
||
if (!mounted) return;
|
||
await _fetch();
|
||
}
|
||
|
||
Future<void> _loadRetentionRateFromToken() async {
|
||
try {
|
||
final token = await AuthService.getToken();
|
||
if (token == null || token.isEmpty) return;
|
||
|
||
final decodedToken = Jwt.parseJwt(token);
|
||
final data = decodedToken['data'];
|
||
if (data is! Map) return;
|
||
|
||
final retentionRate = data['retention_rate']?.toString();
|
||
final loggedId = data['id']?.toString();
|
||
|
||
if (mounted) {
|
||
setState(() {
|
||
_loggedId = (loggedId == null || loggedId.trim().isEmpty)
|
||
? null
|
||
: loggedId.trim();
|
||
if (retentionRate != null && retentionRate.trim().isNotEmpty) {
|
||
_retentionRate = retentionRate.trim();
|
||
} else {
|
||
_retentionRate = '-';
|
||
}
|
||
_grid = _applyRetentionToGridRows(_grid);
|
||
_filteredGrid = _applyRetentionToGridRows(_filteredGrid);
|
||
});
|
||
}
|
||
} catch (_) {
|
||
// Keep fallback "-" when token parsing fails.
|
||
}
|
||
}
|
||
|
||
String _normalizeVehicleTypeKey(String raw) {
|
||
return raw.toLowerCase().trim().replaceAll(RegExp(r'\s+'), ' ');
|
||
}
|
||
|
||
/// Loads per–vehicle-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();
|
||
if (r.isEmpty) return null;
|
||
|
||
switch (r.toLowerCase()) {
|
||
case 'manager':
|
||
return 'Manager';
|
||
case 'handler':
|
||
return 'Handler';
|
||
case 'staff':
|
||
return 'Staff';
|
||
case 'agent':
|
||
return 'Agent';
|
||
case 'accounts':
|
||
return 'Accounts';
|
||
default:
|
||
return r;
|
||
}
|
||
}
|
||
|
||
Future<void> _fetch({bool applyRemoteFilters = false}) async {
|
||
setState(() => _isLoading = true);
|
||
try {
|
||
final appRole = ref.read(userRoleProvider);
|
||
final apiRole = _apiRoleFromAppRole(appRole);
|
||
final queryParams = Uri.base.queryParameters;
|
||
final queryRole = queryParams['role']?.trim();
|
||
final queryFileId = queryParams['file_id']?.trim();
|
||
final effectiveRole =
|
||
(queryRole != null && queryRole.isNotEmpty) ? queryRole : apiRole;
|
||
final roleLc = (effectiveRole ?? '').toLowerCase();
|
||
|
||
// 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;
|
||
}
|
||
}
|
||
}
|
||
|
||
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,
|
||
fileId: effectiveFileId,
|
||
insurer: applyRemoteFilters ? _selectedInsurer : null,
|
||
vehicleType: applyRemoteFilters ? _selectedVehicleType : null,
|
||
segment: applyRemoteFilters ? _selectedSegment : null,
|
||
rto: applyRemoteFilters ? _selectedRto : null,
|
||
search: applyRemoteFilters ? _searchController.text.trim() : null,
|
||
loggedId: loggedIdForGrid,
|
||
);
|
||
if ((res['status'] ?? '').toString().toLowerCase() != 'success') {
|
||
if (mounted) {
|
||
setState(() => _gridFileIdUsed = null);
|
||
}
|
||
ToastHelper.showErrorToast(
|
||
context,
|
||
res['data']?.toString() ?? 'Failed to load payout grid',
|
||
);
|
||
return;
|
||
}
|
||
|
||
final payload = _extractPayload(res);
|
||
final grid = payload['grid'];
|
||
final insurers = payload['insurers'];
|
||
final rtos = payload['rtos'];
|
||
final vehicleTypes = payload['vehicle_types'];
|
||
final segments = payload['segments'];
|
||
|
||
final rawGrid = (grid is List)
|
||
? grid
|
||
.map<Map<String, dynamic>>(
|
||
(e) => (e as Map).cast<String, dynamic>())
|
||
.toList()
|
||
: <Map<String, dynamic>>[];
|
||
_grid = _applyRetentionToGridRows(rawGrid);
|
||
_filteredGrid = List<Map<String, dynamic>>.from(_grid);
|
||
|
||
_insurers = _toStringList(insurers);
|
||
_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);
|
||
}
|
||
}
|
||
|
||
void _setFilter(VoidCallback fn) {
|
||
fn();
|
||
_applyFilters();
|
||
}
|
||
|
||
Map<String, dynamic> _extractPayload(Map<String, dynamic> res) {
|
||
final data = res['data'];
|
||
if (data is Map<String, dynamic>) return data;
|
||
if (data is Map) return data.cast<String, dynamic>();
|
||
return res;
|
||
}
|
||
|
||
List<String> _toStringList(dynamic raw) {
|
||
if (raw is! List) return <String>[];
|
||
return raw
|
||
.map((item) {
|
||
if (item is Map) {
|
||
for (final key in const [
|
||
'short_name',
|
||
'name',
|
||
'label',
|
||
'title',
|
||
'value',
|
||
'rto',
|
||
'segment',
|
||
'vehicle_type',
|
||
]) {
|
||
final val = item[key]?.toString().trim();
|
||
if (val != null && val.isNotEmpty && val.toLowerCase() != 'null') {
|
||
return val;
|
||
}
|
||
}
|
||
final firstVal = item.values.isNotEmpty ? item.values.first : null;
|
||
return firstVal?.toString() ?? '';
|
||
}
|
||
return item?.toString() ?? '';
|
||
})
|
||
.map((e) => e.trim())
|
||
.where((e) => e.isNotEmpty && e.toLowerCase() != 'null')
|
||
.toSet()
|
||
.toList();
|
||
}
|
||
|
||
/// 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;
|
||
}
|
||
}
|
||
_vehicleTypes = [_defaultVehicleTypeLabel, ..._vehicleTypes];
|
||
_selectedVehicleType = _defaultVehicleTypeLabel;
|
||
}
|
||
|
||
void _applyFilters() {
|
||
final q = _searchController.text.trim().toLowerCase();
|
||
|
||
final next = _grid.where((row) {
|
||
bool equalsOrAll(String? selected, dynamic raw) {
|
||
if (selected == null) return true;
|
||
return raw?.toString().toLowerCase() == selected.toLowerCase();
|
||
}
|
||
|
||
bool matchesSearch() {
|
||
if (q.isEmpty) return true;
|
||
final values = <String>[
|
||
row['id']?.toString() ?? '',
|
||
row['insurer']?.toString() ?? '',
|
||
row['vehicle_type']?.toString() ?? '',
|
||
row['segment']?.toString() ?? '',
|
||
row['rto']?.toString() ?? '',
|
||
row['remarks']?.toString() ?? '',
|
||
row['comp']?.toString() ?? '',
|
||
row['partner_comp']?.toString() ?? '',
|
||
row['tp']?.toString() ?? '',
|
||
row['od']?.toString() ?? '',
|
||
].map((e) => e.toLowerCase());
|
||
|
||
return values.any((v) => v.contains(q));
|
||
}
|
||
|
||
return equalsOrAll(_selectedInsurer, row['insurer']) &&
|
||
equalsOrAll(_selectedVehicleType, row['vehicle_type']) &&
|
||
equalsOrAll(_selectedSegment, row['segment']) &&
|
||
equalsOrAll(_selectedRto, row['rto']) &&
|
||
matchesSearch();
|
||
}).toList();
|
||
|
||
setState(() {
|
||
_filteredGrid = next;
|
||
if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
|
||
_filteredGrid = [];
|
||
}
|
||
_currentPage = 1;
|
||
});
|
||
}
|
||
|
||
List<Map<String, dynamic>> get _sortedGrid {
|
||
final next = [..._filteredGrid];
|
||
next.sort((a, b) {
|
||
final ai = int.tryParse(a['id']?.toString() ?? '') ?? 0;
|
||
final bi = int.tryParse(b['id']?.toString() ?? '') ?? 0;
|
||
return bi.compareTo(ai);
|
||
});
|
||
return next;
|
||
}
|
||
|
||
List<Map<String, dynamic>> get _paginatedGrid {
|
||
final sorted = _sortedGrid;
|
||
if (sorted.isEmpty) return [];
|
||
|
||
final maxPage = (sorted.length / _itemsPerPage).ceil();
|
||
final safePage =
|
||
_currentPage.clamp(1, maxPage == 0 ? 1 : maxPage) as int;
|
||
|
||
final startIndex = (safePage - 1) * _itemsPerPage;
|
||
final endIndex = (startIndex + _itemsPerPage).clamp(0, sorted.length);
|
||
return sorted.sublist(startIndex, endIndex);
|
||
}
|
||
|
||
String _nullableString(dynamic v) {
|
||
if (v == null) return '-';
|
||
final s = v.toString().trim();
|
||
if (s.isEmpty || s.toLowerCase() == 'null') return '-';
|
||
return s;
|
||
}
|
||
|
||
num? _toNum(dynamic value) {
|
||
if (value == null) return null;
|
||
final raw = value.toString().trim();
|
||
if (raw.isEmpty || raw.toLowerCase() == 'null' || raw == '-') return null;
|
||
final normalized = raw.replaceAll(RegExp(r'[^0-9\.\-]'), '');
|
||
if (normalized.isEmpty || normalized == '-' || normalized == '.') return null;
|
||
return num.tryParse(normalized);
|
||
}
|
||
|
||
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 per–vehicle retention; ≤0 → '-'; no match → show base.
|
||
String _partnerValueForRowPreview(Map<String, dynamic> row, dynamic baseValue) {
|
||
final base = _toNum(baseValue);
|
||
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'\.$'), '');
|
||
}
|
||
|
||
List<Map<String, dynamic>> _applyRetentionToGridRows(
|
||
List<Map<String, dynamic>> rows,
|
||
) {
|
||
if (!_showPartnerRetentionValues) {
|
||
return rows
|
||
.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['oa'] ?? next['comp'];
|
||
next['partner_tp'] = next['ob'] ?? next['tp'];
|
||
next['partner_od'] = next['oc'] ?? 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;
|
||
}).toList();
|
||
}
|
||
|
||
/// Edit column: Manager & Accounts only (not Agent).
|
||
bool get _showActionColumn {
|
||
final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase();
|
||
return role == 'manager' || role == 'accounts';
|
||
}
|
||
|
||
bool get _showAddButton {
|
||
final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase();
|
||
return role == 'agent';
|
||
}
|
||
|
||
/// Partner (agent) login, or Accounts opening grid with `?agent_id=` to preview that partner’s 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 _hasAnyFilterSelection {
|
||
return (_selectedInsurer != null && _selectedInsurer!.trim().isNotEmpty) ||
|
||
(_selectedRto != null && _selectedRto!.trim().isNotEmpty) ||
|
||
(_selectedVehicleType != null &&
|
||
_selectedVehicleType!.trim().isNotEmpty) ||
|
||
(_selectedSegment != null && _selectedSegment!.trim().isNotEmpty);
|
||
}
|
||
|
||
String _toEditText(dynamic value) {
|
||
if (value == null) return '';
|
||
final s = value.toString().trim();
|
||
if (s.isEmpty || s.toLowerCase() == 'null') return '';
|
||
return s;
|
||
}
|
||
|
||
void _setEditControllersFromRow(Map<String, dynamic> row) {
|
||
_editVehicleTypeController.text = _toEditText(row['vehicle_type']);
|
||
_editFuelController.text = _toEditText(row['fuel']);
|
||
_editRtoController.text = _toEditText(row['rto']);
|
||
_editSegmentController.text = _toEditText(row['segment']);
|
||
_editBrokerController.text = _toEditText(row['broker_name']);
|
||
_editCompController.text = _toEditText(row['comp']);
|
||
_editTpController.text = _toEditText(row['tp']);
|
||
_editOdController.text = _toEditText(row['od']);
|
||
_editBrokerCompController.text = _toEditText(row['broker_comp']);
|
||
_editBrokerTpController.text = _toEditText(row['broker_tp']);
|
||
_editBrokerOdController.text = _toEditText(row['broker_od']);
|
||
_editPartnerCompController.text = _toEditText(row['partner_comp']);
|
||
_editPartnerTpController.text = _toEditText(row['partner_tp']);
|
||
_editPartnerOdController.text = _toEditText(row['partner_od']);
|
||
_editRemarksController.text = _toEditText(row['remarks']);
|
||
}
|
||
|
||
void _clearEditControllers() {
|
||
_editVehicleTypeController.clear();
|
||
_editFuelController.clear();
|
||
_editRtoController.clear();
|
||
_editSegmentController.clear();
|
||
_editBrokerController.clear();
|
||
_editCompController.clear();
|
||
_editTpController.clear();
|
||
_editOdController.clear();
|
||
_editBrokerCompController.clear();
|
||
_editBrokerTpController.clear();
|
||
_editBrokerOdController.clear();
|
||
_editPartnerCompController.clear();
|
||
_editPartnerTpController.clear();
|
||
_editPartnerOdController.clear();
|
||
_editRemarksController.clear();
|
||
}
|
||
|
||
InputDecoration _editFieldDecoration(String label) {
|
||
return InputDecoration(
|
||
labelText: label,
|
||
labelStyle: GoogleFonts.poppins(fontSize: 12),
|
||
isDense: true,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _modalField(
|
||
String label,
|
||
TextEditingController controller, {
|
||
int maxLines = 1,
|
||
}) {
|
||
return TextField(
|
||
controller: controller,
|
||
maxLines: maxLines,
|
||
style: GoogleFonts.inter(fontSize: 12),
|
||
decoration: _editFieldDecoration(label),
|
||
);
|
||
}
|
||
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MainLayout(
|
||
title: 'Payout Details',
|
||
body: SelectionArea(
|
||
child: Container(
|
||
width: MediaQuery.of(context).size.width,
|
||
padding: const EdgeInsets.all(8),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
_buildTopBar(context),
|
||
const SizedBox(height: 10),
|
||
_buildFilterBar(context),
|
||
const SizedBox(height: 10),
|
||
if (!ResponsiveLayout.isMobile(context)) _buildDesktopHeader(),
|
||
Expanded(
|
||
child: _isLoading
|
||
? const Center(child: CircularProgressIndicator())
|
||
: _buildGridOrCards(context),
|
||
),
|
||
SizedBox(
|
||
width: double.infinity,
|
||
child: Align(
|
||
alignment: Alignment.centerRight,
|
||
child: PaginationControls(
|
||
currentPage: _currentPage,
|
||
itemsPerPage: _itemsPerPage,
|
||
totalItems: _filteredGrid.length,
|
||
onPageChanged: (page) => setState(() => _currentPage = page),
|
||
onItemsPerPageChanged: (items) {
|
||
setState(() {
|
||
_itemsPerPage = items;
|
||
_currentPage = 1;
|
||
});
|
||
},
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTopBar(BuildContext context) {
|
||
final isAgent =
|
||
(ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() ==
|
||
'agent';
|
||
final isMobile = ResponsiveLayout.isMobile(context);
|
||
return Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
if (!isAgent) ...[
|
||
Tooltip(
|
||
message: 'Back',
|
||
child: IconButton(
|
||
icon: const Icon(Icons.arrow_left_sharp, size: 25),
|
||
onPressed: () => context.go(AppRoutes.gridList),
|
||
splashRadius: 18,
|
||
padding: const EdgeInsets.all(4),
|
||
hoverColor: Colors.black12,
|
||
constraints: const BoxConstraints(),
|
||
),
|
||
),
|
||
const SizedBox(width: 5),
|
||
],
|
||
Text(
|
||
'Payout Details',
|
||
style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400),
|
||
),
|
||
const Spacer(),
|
||
if (isMobile)
|
||
Expanded(child: _buildSearch(context, true))
|
||
else
|
||
_buildSearch(context, false),
|
||
const SizedBox(width: 12),
|
||
_buildExportButtonWidget(),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildFilterBar(BuildContext context) {
|
||
final isMobile = ResponsiveLayout.isMobile(context);
|
||
|
||
Widget dropdownInsurer() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 200,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _insurers;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedInsurer,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? '',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedInsurer = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: _dropdownDecoration('Insurer'),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownRto() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 180,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _rtos;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedRto,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? '',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedRto = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: _dropdownDecoration('RTO'),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownVehicleType() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 200,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _vehicleTypes;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedVehicleType,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? '',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedVehicleType = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: _dropdownDecoration('Vehicle Type'),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownSegment() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 220,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _segments;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedSegment,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? '',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedSegment = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: _dropdownDecoration('Segment'),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget searchBtn() => _filterIconButton(
|
||
icon: Icons.search,
|
||
message: 'Search',
|
||
onTap: () async {
|
||
if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
|
||
ToastHelper.showInfoToast(
|
||
context,
|
||
'Please select at least one filter to view/export data.',
|
||
);
|
||
return;
|
||
}
|
||
await _fetch(applyRemoteFilters: true);
|
||
},
|
||
);
|
||
|
||
Widget resetBtn() => _filterIconButton(
|
||
icon: Icons.refresh,
|
||
message: 'Reset',
|
||
onTap: () async {
|
||
setState(() {
|
||
_selectedInsurer = null;
|
||
_selectedRto = null;
|
||
_selectedSegment = null;
|
||
_searchController.clear();
|
||
_applyDefaultVehicleTypeSelection();
|
||
});
|
||
await _fetch();
|
||
},
|
||
);
|
||
|
||
return SizedBox(
|
||
width: double.infinity,
|
||
child: Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
child: isMobile
|
||
? Column(
|
||
children: [
|
||
dropdownVehicleType(),
|
||
const SizedBox(height: 10),
|
||
dropdownInsurer(),
|
||
const SizedBox(height: 10),
|
||
dropdownSegment(),
|
||
const SizedBox(height: 10),
|
||
dropdownRto(),
|
||
const SizedBox(height: 10),
|
||
searchBtn(),
|
||
const SizedBox(height: 10),
|
||
resetBtn(),
|
||
],
|
||
)
|
||
: Row(
|
||
children: [
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
scrollDirection: Axis.horizontal,
|
||
child: Row(
|
||
children: [
|
||
dropdownVehicleType(),
|
||
const SizedBox(width: 12),
|
||
dropdownInsurer(),
|
||
const SizedBox(width: 12),
|
||
dropdownSegment(),
|
||
const SizedBox(width: 12),
|
||
dropdownRto(),
|
||
const SizedBox(width: 12),
|
||
searchBtn(),
|
||
const SizedBox(width: 8),
|
||
resetBtn(),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSearch(BuildContext context, bool isMobile) {
|
||
return ThemedSearchField(
|
||
hintText: 'Search...',
|
||
controller: _searchController,
|
||
txtHeight: 40,
|
||
txtwidth: isMobile ? null : 240,
|
||
backgroundColor: Colors.white,
|
||
onChanged: (_) => _applyFilters(),
|
||
);
|
||
}
|
||
|
||
InputDecoration _dropdownDecoration(String label) {
|
||
return AppInputDecorations.dropdownDecoration(label: label).copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(8),
|
||
borderSide: const BorderSide(color: Color(0xFF2E7D6E)),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _filterIconButton({
|
||
required IconData icon,
|
||
required String message,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
return Tooltip(
|
||
message: message,
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
borderRadius: BorderRadius.circular(8),
|
||
child: Container(
|
||
width: 34,
|
||
height: 34,
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||
),
|
||
child: Icon(icon, size: 18, color: const Color(0xFF334155)),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildGridOrCards(BuildContext context) {
|
||
if (_paginatedGrid.isEmpty) {
|
||
return const Center(child: Text('No available data'));
|
||
}
|
||
|
||
if (ResponsiveLayout.isMobile(context)) {
|
||
return ListView.separated(
|
||
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]),
|
||
);
|
||
}
|
||
|
||
return ListView.builder(
|
||
itemCount: _paginatedGrid.length,
|
||
itemBuilder: (context, index) {
|
||
return _buildTableRow(_paginatedGrid[index], rowIndex: index);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildDesktopHeader() {
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF1F5F9),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
|
||
child: Row(
|
||
children: [
|
||
Expanded(flex: 1, child: Text('S.No', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('Insurer', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('Vehicle', style: _headerStyle)),
|
||
Expanded(flex: 3, child: Text('Segment', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('RTO', 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)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTableRow(Map<String, dynamic> row, {required int rowIndex}) {
|
||
final sno = (_currentPage - 1) * _itemsPerPage + rowIndex + 1;
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 16),
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(flex: 1, child: Text('$sno', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(_nullableString(row['insurer']), style: _dataBold),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(_nullableString(row['vehicle_type']), style: _dataBold),
|
||
),
|
||
Expanded(
|
||
flex: 3,
|
||
child: Text(
|
||
_nullableString(row['segment']),
|
||
style: _dataBold,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
Expanded(flex: 2, child: Text(_nullableString(row['rto']), 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(
|
||
_nullableString(row['remarks']),
|
||
style: _dataBold,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
static final _headerStyle = GoogleFonts.poppins(
|
||
fontSize: 11.2,
|
||
fontWeight: FontWeight.w500,
|
||
color: const Color(0xFF1E293B),
|
||
);
|
||
|
||
static final _dataBold = GoogleFonts.inter(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w400,
|
||
color: const Color(0xFF000000),
|
||
);
|
||
|
||
Widget _toolbarIconButton({
|
||
required IconData icon,
|
||
required String message,
|
||
required VoidCallback onTap,
|
||
}) {
|
||
return Tooltip(
|
||
message: message,
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
borderRadius: BorderRadius.circular(8),
|
||
child: Container(
|
||
width: 30,
|
||
height: 30,
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFF2E7D6E),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Icon(icon, size: 16, color: Colors.white),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildExportButtonWidget() {
|
||
return InkWell(
|
||
onTap: _isLoading
|
||
? () {}
|
||
: () async {
|
||
if (_showPartnerRetentionValues && !_hasAnyFilterSelection) {
|
||
if (mounted) {
|
||
ToastHelper.showInfoToast(
|
||
context,
|
||
'Please select at least one filter to view/export data.',
|
||
);
|
||
}
|
||
return;
|
||
}
|
||
try {
|
||
final queryParams = Uri.base.queryParameters;
|
||
final role = _apiRoleFromAppRole(ref.read(userRoleProvider));
|
||
final roleLc = (role ?? '').toLowerCase();
|
||
final fileIdFromQuery = queryParams['file_id']?.trim();
|
||
final fileIdToSend =
|
||
(roleLc == 'manager' || roleLc == 'accounts')
|
||
? (_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,
|
||
insurer: _selectedInsurer,
|
||
vehicleType: _selectedVehicleType,
|
||
segment: _selectedSegment,
|
||
rto: _selectedRto,
|
||
search: _searchController.text.trim(),
|
||
loggedId: loggedIdForExport,
|
||
);
|
||
} catch (e) {
|
||
if (mounted) {
|
||
ToastHelper.showErrorToast(
|
||
context,
|
||
e.toString().replaceFirst('Exception: ', ''),
|
||
);
|
||
}
|
||
}
|
||
},
|
||
child: Container(
|
||
width: 40,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(8),
|
||
color: const Color(0xFF2E7D6E),
|
||
),
|
||
child: Tooltip(
|
||
message: 'Export',
|
||
child: Image.asset(
|
||
"assets/miscellaneous/export.png",
|
||
height: 13,
|
||
width: 13,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _gridViewCard extends StatelessWidget {
|
||
final Map<String, dynamic> row;
|
||
|
||
const _gridViewCard({required this.row});
|
||
|
||
String _nullableString(dynamic v) {
|
||
if (v == null) return '-';
|
||
final s = v.toString().trim();
|
||
if (s.isEmpty || s.toLowerCase() == 'null') return '-';
|
||
return s;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final id = row['id']?.toString() ?? '-';
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFEAEAEA)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'${_nullableString(row['insurer'])} | ${_nullableString(row['vehicle_type'])}',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
Text(
|
||
'ID: $id',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 10,
|
||
color: const Color(0xFF6E6E6E),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text('Segment: ${_nullableString(row['segment'])}',
|
||
style: GoogleFonts.inter(fontSize: 12)),
|
||
const SizedBox(height: 6),
|
||
Text('RTO: ${_nullableString(row['rto'])}',
|
||
style: GoogleFonts.inter(fontSize: 12)),
|
||
const SizedBox(height: 10),
|
||
Row(
|
||
children: [
|
||
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),
|
||
Text('Remarks: ${_nullableString(row['remarks'])}',
|
||
style: GoogleFonts.inter(fontSize: 12)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _metric(String label, 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(
|
||
label,
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 10,
|
||
color: const Color(0xFF64748B),
|
||
),
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
_nullableString(value),
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
/* import 'package:dropdown_search/dropdown_search.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:nhance_partner/core/services/api_service.dart';
|
||
import 'package:nhance_partner/data/utils/Pagination.dart';
|
||
import 'package:nhance_partner/data/utils/toastNotification.dart';
|
||
import 'package:nhance_partner/presentation/layouts/main_layout.dart';
|
||
import 'package:nhance_partner/presentation/layouts/responsive_layout.dart';
|
||
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
|
||
import 'package:nhance_partner/presentation/screens/UserManagement/Agent/agentGridFile.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/export_btn.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart';
|
||
|
||
class gridViewScreen extends ConsumerStatefulWidget {
|
||
const gridViewScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<gridViewScreen> createState() => _gridViewScreenState();
|
||
}
|
||
|
||
class _gridViewScreenState extends ConsumerState<gridViewScreen> {
|
||
final ApiService _apiService = ApiService();
|
||
|
||
bool _isLoading = false;
|
||
|
||
// API response
|
||
List<Map<String, dynamic>> _grid = [];
|
||
List<Map<String, dynamic>> _filteredGrid = [];
|
||
|
||
List<String> _insurers = [];
|
||
List<Map<String, dynamic>> _planTypes = [];
|
||
List<String> _rtos = [];
|
||
List<String> _vehicleTypes = [];
|
||
List<String> _segments = [];
|
||
|
||
// Filters
|
||
String? _selectedInsurer;
|
||
String? _selectedRto;
|
||
String? _selectedVehicleType;
|
||
String? _selectedSegment;
|
||
String? _selectedPlanTypeValue; // comp/tp/od
|
||
|
||
final TextEditingController _searchController = TextEditingController();
|
||
|
||
// Pagination
|
||
int _currentPage = 1;
|
||
int _itemsPerPage = 10;
|
||
|
||
@override
|
||
void dispose() {
|
||
_searchController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_fetch();
|
||
}
|
||
|
||
String? _apiRoleFromAppRole(String? role) {
|
||
if (role == null) return null;
|
||
final r = role.trim();
|
||
if (r.isEmpty) return null;
|
||
|
||
switch (r.toLowerCase()) {
|
||
case 'manager':
|
||
return 'Manager';
|
||
case 'handler':
|
||
return 'Handler';
|
||
case 'staff':
|
||
return 'Staff';
|
||
case 'agent':
|
||
return 'Agent';
|
||
case 'accounts':
|
||
return 'Accounts';
|
||
default:
|
||
return r;
|
||
}
|
||
}
|
||
|
||
Future<void> _fetch() async {
|
||
setState(() => _isLoading = true);
|
||
try {
|
||
final appRole = ref.read(userRoleProvider);
|
||
final apiRole = _apiRoleFromAppRole(appRole);
|
||
final queryParams = Uri.base.queryParameters;
|
||
final queryRole = queryParams['role']?.trim();
|
||
final queryFileId = queryParams['file_id']?.trim();
|
||
final effectiveRole =
|
||
(queryRole != null && queryRole.isNotEmpty) ? queryRole : apiRole;
|
||
String? effectiveFileId =
|
||
(queryFileId != null && queryFileId.isNotEmpty) ? queryFileId : null;
|
||
|
||
if (effectiveFileId == null &&
|
||
(effectiveRole?.toLowerCase() == 'manager' ||
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
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(
|
||
context,
|
||
res['data']?.toString() ?? 'Failed to load payout grid',
|
||
);
|
||
return;
|
||
}
|
||
|
||
final grid = res['grid'];
|
||
final insurers = res['insurers'];
|
||
final planTypes = res['plan_types'];
|
||
final rtos = res['rtos'];
|
||
final vehicleTypes = res['vehicle_types'];
|
||
final segments = res['segments'];
|
||
|
||
_grid = (grid is List)
|
||
? grid
|
||
.map<Map<String, dynamic>>(
|
||
(e) => (e as Map).cast<String, dynamic>())
|
||
.toList()
|
||
: <Map<String, dynamic>>[];
|
||
_filteredGrid = List<Map<String, dynamic>>.from(_grid);
|
||
|
||
_insurers = insurers is List ? insurers.map((e) => e.toString()).toList() : [];
|
||
_planTypes = planTypes is List
|
||
? planTypes
|
||
.map<Map<String, dynamic>>(
|
||
(e) => (e as Map).cast<String, dynamic>())
|
||
.toList()
|
||
: [];
|
||
_rtos = rtos is List ? rtos.map((e) => e.toString()).toList() : [];
|
||
_vehicleTypes =
|
||
vehicleTypes is List ? vehicleTypes.map((e) => e.toString()).toList() : [];
|
||
_segments =
|
||
segments is List ? segments.map((e) => e.toString()).toList() : [];
|
||
|
||
_applyFilters();
|
||
} catch (e) {
|
||
ToastHelper.showErrorToast(context, e.toString());
|
||
} finally {
|
||
if (mounted) setState(() => _isLoading = false);
|
||
}
|
||
}
|
||
|
||
void _setFilter(VoidCallback fn) {
|
||
fn();
|
||
_applyFilters();
|
||
}
|
||
|
||
void _applyFilters() {
|
||
final q = _searchController.text.trim().toLowerCase();
|
||
|
||
final next = _grid.where((row) {
|
||
bool equalsOrAll(String? selected, dynamic raw) {
|
||
if (selected == null) return true;
|
||
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();
|
||
return s.isNotEmpty && s != 'null';
|
||
}
|
||
|
||
bool matchesSearch() {
|
||
if (q.isEmpty) return true;
|
||
final values = <String>[
|
||
row['id']?.toString() ?? '',
|
||
row['insurer']?.toString() ?? '',
|
||
row['vehicle_type']?.toString() ?? '',
|
||
row['segment']?.toString() ?? '',
|
||
row['rto']?.toString() ?? '',
|
||
row['remarks']?.toString() ?? '',
|
||
row['comp']?.toString() ?? '',
|
||
row['tp']?.toString() ?? '',
|
||
row['od']?.toString() ?? '',
|
||
].map((e) => e.toLowerCase());
|
||
|
||
return values.any((v) => v.contains(q));
|
||
}
|
||
|
||
return equalsOrAll(_selectedInsurer, row['insurer']) &&
|
||
equalsOrAll(_selectedVehicleType, row['vehicle_type']) &&
|
||
equalsOrAll(_selectedSegment, row['segment']) &&
|
||
equalsOrAll(_selectedRto, row['rto']) &&
|
||
matchesPlanType() &&
|
||
matchesSearch();
|
||
}).toList();
|
||
|
||
setState(() {
|
||
_filteredGrid = next;
|
||
_currentPage = 1;
|
||
});
|
||
}
|
||
|
||
List<Map<String, dynamic>> get _sortedGrid {
|
||
final next = [..._filteredGrid];
|
||
next.sort((a, b) {
|
||
final ai = int.tryParse(a['id']?.toString() ?? '') ?? 0;
|
||
final bi = int.tryParse(b['id']?.toString() ?? '') ?? 0;
|
||
return bi.compareTo(ai);
|
||
});
|
||
return next;
|
||
}
|
||
|
||
List<Map<String, dynamic>> get _paginatedGrid {
|
||
final sorted = _sortedGrid;
|
||
if (sorted.isEmpty) return [];
|
||
|
||
final maxPage = (sorted.length / _itemsPerPage).ceil();
|
||
final safePage =
|
||
_currentPage.clamp(1, maxPage == 0 ? 1 : maxPage) as int;
|
||
|
||
final startIndex = (safePage - 1) * _itemsPerPage;
|
||
final endIndex = (startIndex + _itemsPerPage).clamp(0, sorted.length);
|
||
return sorted.sublist(startIndex, endIndex);
|
||
}
|
||
|
||
String _nullableString(dynamic v) {
|
||
if (v == null) return '-';
|
||
final s = v.toString().trim();
|
||
if (s.isEmpty || s.toLowerCase() == 'null') return '-';
|
||
return s;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MainLayout(
|
||
title: 'Grid Details',
|
||
body: SelectionArea(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(8),
|
||
child: Column(
|
||
children: [
|
||
_buildTopBar(context),
|
||
const SizedBox(height: 10),
|
||
_buildFilterBar(context),
|
||
const SizedBox(height: 10),
|
||
Expanded(
|
||
child: _isLoading
|
||
? const Center(child: CircularProgressIndicator())
|
||
: _buildGridOrCards(context),
|
||
),
|
||
PaginationControls(
|
||
currentPage: _currentPage,
|
||
itemsPerPage: _itemsPerPage,
|
||
totalItems: _filteredGrid.length,
|
||
onPageChanged: (page) => setState(() => _currentPage = page),
|
||
onItemsPerPageChanged: (items) {
|
||
setState(() {
|
||
_itemsPerPage = items;
|
||
_currentPage = 1;
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTopBar(BuildContext context) {
|
||
final isAgent =
|
||
(ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() ==
|
||
'agent';
|
||
return Row(
|
||
children: [
|
||
if (!isAgent) ...[
|
||
Tooltip(
|
||
message: 'Back',
|
||
child: IconButton(
|
||
icon: const Icon(Icons.arrow_left_sharp, size: 25),
|
||
onPressed: () => context.go(AppRoutes.gridList),
|
||
splashRadius: 18,
|
||
padding: const EdgeInsets.all(4),
|
||
hoverColor: Colors.black12,
|
||
constraints: const BoxConstraints(),
|
||
),
|
||
),
|
||
const SizedBox(width: 5),
|
||
],
|
||
Text(
|
||
'Grid Details',
|
||
style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400),
|
||
),
|
||
const Spacer(),
|
||
ExportBtn(
|
||
sheetName: 'Grid Details',
|
||
fileName: 'payout_grid',
|
||
data: _filteredGrid,
|
||
displayHeaders: const [
|
||
'ID',
|
||
'Insurer',
|
||
'Vehicle Type',
|
||
'Segment',
|
||
'RTO',
|
||
'Comp',
|
||
'TP',
|
||
'OD',
|
||
'Remarks',
|
||
],
|
||
keys: const [
|
||
'id',
|
||
'insurer',
|
||
'vehicle_type',
|
||
'segment',
|
||
'rto',
|
||
'comp',
|
||
'tp',
|
||
'od',
|
||
'remarks',
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildFilterBar(BuildContext context) {
|
||
final isMobile = ResponsiveLayout.isMobile(context);
|
||
final screenWidth = MediaQuery.of(context).size.width;
|
||
final isCompactFilter = !isMobile && screenWidth <= 1400;
|
||
final fieldHeight = isCompactFilter ? 36.0 : 40.0;
|
||
final fieldFontSize = isCompactFilter ? 11.0 : 12.0;
|
||
final fieldSpacing = isCompactFilter ? 8.0 : 12.0;
|
||
final filterHorizontalPadding = isCompactFilter ? 10.0 : 12.0;
|
||
final filterVerticalPadding = isCompactFilter ? 8.0 : 10.0;
|
||
|
||
final planTypeItems = <Map<String, dynamic>>[
|
||
{'value': null, 'label': 'All'},
|
||
..._planTypes,
|
||
];
|
||
|
||
Widget dropdownInsurer() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 200,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _insurers;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedInsurer,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? 'All',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedInsurer = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'Insurer')
|
||
.copyWith(filled: true, fillColor: Colors.white, isDense: true),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
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: planTypeItems.firstWhere(
|
||
(e) => (e['value'] as String?) == _selectedPlanTypeValue,
|
||
orElse: () => planTypeItems.first,
|
||
),
|
||
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() ?? 'All',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() {
|
||
_selectedPlanTypeValue = val?['value'] as String?;
|
||
}),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration:
|
||
AppInputDecorations.dropdownDecoration(label: 'Plan Type')
|
||
.copyWith(filled: true, fillColor: Colors.white, isDense: true),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownRto() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 180,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _rtos;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedRto,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? 'All',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedRto = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'RTO')
|
||
.copyWith(filled: true, fillColor: Colors.white, isDense: true),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownVehicleType() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 200,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _vehicleTypes;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedVehicleType,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? 'All',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedVehicleType = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration:
|
||
AppInputDecorations.dropdownDecoration(label: 'Vehicle Type')
|
||
.copyWith(filled: true, fillColor: Colors.white, isDense: true),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownSegment() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : 220,
|
||
height: 40,
|
||
child: DropdownSearch<String>(
|
||
items: (filter, infiniteScrollProps) {
|
||
final items = _segments;
|
||
if (filter.isEmpty) return items;
|
||
return items
|
||
.where((e) => e.toLowerCase().contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
selectedItem: _selectedSegment,
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? 'All',
|
||
style: GoogleFonts.poppins(fontSize: 12, color: Colors.black),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedSegment = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'Segment')
|
||
.copyWith(filled: true, fillColor: Colors.white, isDense: true),
|
||
),
|
||
compareFn: (a, b) => a == b,
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget resetBtn() {
|
||
return SizedBox(
|
||
height: 40,
|
||
child: ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF2E7D6E),
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
),
|
||
onPressed: () => setState(() {
|
||
_selectedInsurer = null;
|
||
_selectedPlanTypeValue = null;
|
||
_selectedRto = null;
|
||
_selectedVehicleType = null;
|
||
_selectedSegment = null;
|
||
_searchController.clear();
|
||
_applyFilters();
|
||
}),
|
||
icon: const Icon(Icons.refresh, size: 18),
|
||
label: Text(
|
||
'Reset',
|
||
style: GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w500),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||
),
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10),
|
||
child: isMobile
|
||
? Column(
|
||
children: [
|
||
_buildSearch(context, isMobile),
|
||
const SizedBox(height: 10),
|
||
dropdownInsurer(),
|
||
const SizedBox(height: 10),
|
||
dropdownPlanType(),
|
||
const SizedBox(height: 10),
|
||
dropdownVehicleType(),
|
||
const SizedBox(height: 10),
|
||
dropdownSegment(),
|
||
const SizedBox(height: 10),
|
||
dropdownRto(),
|
||
const SizedBox(height: 10),
|
||
resetBtn(),
|
||
],
|
||
)
|
||
: SingleChildScrollView(
|
||
scrollDirection: Axis.horizontal,
|
||
child: Row(
|
||
children: [
|
||
_buildSearch(context, isMobile),
|
||
const SizedBox(width: 12),
|
||
dropdownInsurer(),
|
||
const SizedBox(width: 12),
|
||
dropdownPlanType(),
|
||
const SizedBox(width: 12),
|
||
dropdownVehicleType(),
|
||
const SizedBox(width: 12),
|
||
dropdownSegment(),
|
||
const SizedBox(width: 12),
|
||
dropdownRto(),
|
||
const SizedBox(width: 12),
|
||
resetBtn(),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSearch(BuildContext context, bool isMobile) {
|
||
return ThemedSearchField(
|
||
hintText: 'Search...',
|
||
controller: _searchController,
|
||
txtHeight: 40,
|
||
txtwidth: isMobile ? null : 240,
|
||
backgroundColor: Colors.white,
|
||
onChanged: (_) => _applyFilters(),
|
||
);
|
||
}
|
||
|
||
Widget _buildGridOrCards(BuildContext context) {
|
||
if (_paginatedGrid.isEmpty) {
|
||
return const Center(child: Text('No available data'));
|
||
}
|
||
|
||
if (ResponsiveLayout.isMobile(context)) {
|
||
return ListView.separated(
|
||
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]),
|
||
);
|
||
}
|
||
|
||
return ListView.builder(
|
||
itemCount: _paginatedGrid.length + 1,
|
||
itemBuilder: (context, index) {
|
||
if (index == 0) return _buildTableHeader();
|
||
final rowIndex = index - 1;
|
||
return _buildTableRow(_paginatedGrid[rowIndex], rowIndex: rowIndex);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildTableHeader() {
|
||
return Container(
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
|
||
),
|
||
),
|
||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||
child: Row(
|
||
children: [
|
||
Expanded(flex: 1, child: Text('S.No', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('Insurer', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('Vehicle', style: _headerStyle)),
|
||
Expanded(flex: 3, child: Text('Segment', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('RTO', 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)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTableRow(Map<String, dynamic> row, {required int rowIndex}) {
|
||
final sno = (_currentPage - 1) * _itemsPerPage + rowIndex + 1;
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(flex: 1, child: Text('$sno', style: _dataBold)),
|
||
Expanded(flex: 2, child: Text(_nullableString(row['insurer']), style: _dataBold)),
|
||
Expanded(flex: 2, child: Text(_nullableString(row['vehicle_type']), style: _dataBold)),
|
||
Expanded(
|
||
flex: 3,
|
||
child: Text(
|
||
_nullableString(row['segment']),
|
||
style: _dataBold,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
Expanded(flex: 2, child: Text(_nullableString(row['rto']), 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(
|
||
_nullableString(row['remarks']),
|
||
style: _dataBold,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
static final _headerStyle = GoogleFonts.poppins(
|
||
fontSize: 11.2,
|
||
fontWeight: FontWeight.w500,
|
||
color: const Color(0xFF1E293B),
|
||
);
|
||
|
||
static final _dataBold = GoogleFonts.inter(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w400,
|
||
color: const Color(0xFF000000),
|
||
);
|
||
}
|
||
|
||
class _gridViewCard extends StatelessWidget {
|
||
final Map<String, dynamic> row;
|
||
|
||
const _gridViewCard({required this.row});
|
||
|
||
String _nullableString(dynamic v) {
|
||
if (v == null) return '-';
|
||
final s = v.toString().trim();
|
||
if (s.isEmpty || s.toLowerCase() == 'null') return '-';
|
||
return s;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final id = row['id']?.toString() ?? '-';
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFEAEAEA)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'${_nullableString(row['insurer'])} | ${_nullableString(row['vehicle_type'])}',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
Text(
|
||
'ID: $id',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 10,
|
||
color: const Color(0xFF6E6E6E),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text('Segment: ${_nullableString(row['segment'])}',
|
||
style: GoogleFonts.inter(fontSize: 12)),
|
||
const SizedBox(height: 6),
|
||
Text('RTO: ${_nullableString(row['rto'])}',
|
||
style: GoogleFonts.inter(fontSize: 12)),
|
||
const SizedBox(height: 10),
|
||
Row(
|
||
children: [
|
||
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),
|
||
Text('Remarks: ${_nullableString(row['remarks'])}',
|
||
style: GoogleFonts.inter(fontSize: 12)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _metric(String label, 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(label,
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 10,
|
||
color: const Color(0xFF64748B),
|
||
)),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
_nullableString(value),
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w600,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
import 'package:dropdown_search/dropdown_search.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:nhance_partner/core/services/api_service.dart';
|
||
import 'package:nhance_partner/data/utils/toastNotification.dart';
|
||
import 'package:nhance_partner/data/utils/Pagination.dart';
|
||
import 'package:nhance_partner/presentation/layouts/main_layout.dart';
|
||
import 'package:nhance_partner/presentation/layouts/responsive_layout.dart';
|
||
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/input_field_decoration.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/export_btn.dart';
|
||
import 'package:nhance_partner/presentation/screens/UserManagement/Agent/agentGridFile.dart';
|
||
|
||
class gridViewScreen extends ConsumerStatefulWidget {
|
||
const gridViewScreen({super.key});
|
||
|
||
@override
|
||
ConsumerState<gridViewScreen> createState() => _gridViewScreenState();
|
||
}
|
||
|
||
class _gridViewScreenState extends ConsumerState<gridViewScreen> {
|
||
final ApiService _apiService = ApiService();
|
||
|
||
bool _isLoading = false;
|
||
|
||
// API response
|
||
List<Map<String, dynamic>> _grid = [];
|
||
List<Map<String, dynamic>> _filteredGrid = [];
|
||
|
||
List<String> _insurers = [];
|
||
List<Map<String, dynamic>> _planTypes = [];
|
||
List<String> _rtos = [];
|
||
List<String> _vehicleTypes = [];
|
||
List<String> _segments = [];
|
||
|
||
// Filters
|
||
String? _selectedInsurer;
|
||
String? _selectedRto;
|
||
String? _selectedVehicleType;
|
||
String? _selectedSegment;
|
||
String? _selectedPlanTypeValue; // comp/tp/od
|
||
|
||
final TextEditingController _searchController = TextEditingController();
|
||
|
||
// Pagination
|
||
int _currentPage = 1;
|
||
int _itemsPerPage = 10;
|
||
|
||
@override
|
||
void dispose() {
|
||
_searchController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_fetch();
|
||
}
|
||
|
||
String? _apiRoleFromAppRole(String? role) {
|
||
if (role == null) return null;
|
||
final r = role.trim();
|
||
if (r.isEmpty) return null;
|
||
|
||
// API expects capitalized role names (example: role=Manager)
|
||
switch (r.toLowerCase()) {
|
||
case 'manager':
|
||
return 'Manager';
|
||
case 'handler':
|
||
return 'Handler';
|
||
case 'staff':
|
||
return 'Staff';
|
||
case 'agent':
|
||
return 'Agent';
|
||
case 'accounts':
|
||
return 'Accounts';
|
||
default:
|
||
// fallback: keep original capitalization
|
||
return r;
|
||
}
|
||
}
|
||
|
||
Future<void> _fetch() async {
|
||
setState(() => _isLoading = true);
|
||
try {
|
||
final appRole = ref.read(userRoleProvider);
|
||
final apiRole = _apiRoleFromAppRole(appRole);
|
||
final queryParams = Uri.base.queryParameters;
|
||
final queryRole = queryParams['role']?.trim();
|
||
final queryFileId = queryParams['file_id']?.trim();
|
||
final effectiveRole =
|
||
(queryRole != null && queryRole.isNotEmpty) ? queryRole : apiRole;
|
||
String? effectiveFileId =
|
||
(queryFileId != null && queryFileId.isNotEmpty) ? queryFileId : null;
|
||
|
||
if (effectiveFileId == null &&
|
||
(effectiveRole?.toLowerCase() == 'manager' ||
|
||
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();
|
||
}
|
||
}
|
||
}
|
||
|
||
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(
|
||
context,
|
||
res['data']?.toString() ?? 'Failed to load payout grid',
|
||
);
|
||
return;
|
||
}
|
||
|
||
final grid = res['grid'];
|
||
final insurers = res['insurers'];
|
||
final planTypes = res['plan_types'];
|
||
final rtos = res['rtos'];
|
||
final vehicleTypes = res['vehicle_types'];
|
||
final segments = res['segments'];
|
||
|
||
_grid = (grid is List)
|
||
? grid.map<Map<String, dynamic>>((e) => (e as Map).cast<String, dynamic>()).toList()
|
||
: <Map<String, dynamic>>[];
|
||
_filteredGrid = List<Map<String, dynamic>>.from(_grid);
|
||
|
||
_insurers = insurers is List ? insurers.map((e) => e.toString()).toList() : [];
|
||
_planTypes = planTypes is List
|
||
? planTypes
|
||
.map<Map<String, dynamic>>((e) => (e as Map).cast<String, dynamic>())
|
||
.toList()
|
||
: [];
|
||
_rtos = rtos is List ? rtos.map((e) => e.toString()).toList() : [];
|
||
_vehicleTypes = vehicleTypes is List ? vehicleTypes.map((e) => e.toString()).toList() : [];
|
||
_segments = segments is List ? segments.map((e) => e.toString()).toList() : [];
|
||
|
||
// Ensure filters apply after fresh data load.
|
||
_applyFilters();
|
||
} catch (e) {
|
||
ToastHelper.showErrorToast(context, e.toString());
|
||
} finally {
|
||
if (mounted) setState(() => _isLoading = false);
|
||
}
|
||
}
|
||
|
||
void _applyFilters() {
|
||
final q = _searchController.text.trim().toLowerCase();
|
||
|
||
List<Map<String, dynamic>> next = _grid.where((row) {
|
||
bool matchesInsurer() {
|
||
if (_selectedInsurer == null) return true;
|
||
return row['insurer']?.toString().toLowerCase() == _selectedInsurer!.toLowerCase();
|
||
}
|
||
|
||
bool matchesRto() {
|
||
if (_selectedRto == null) return true;
|
||
return row['rto']?.toString().toLowerCase() == _selectedRto!.toLowerCase();
|
||
}
|
||
|
||
bool matchesVehicleType() {
|
||
if (_selectedVehicleType == null) return true;
|
||
return row['vehicle_type']?.toString().toLowerCase() == _selectedVehicleType!.toLowerCase();
|
||
}
|
||
|
||
bool matchesSegment() {
|
||
if (_selectedSegment == null) return true;
|
||
return row['segment']?.toString().toLowerCase() == _selectedSegment!.toLowerCase();
|
||
}
|
||
|
||
bool matchesPlanType() {
|
||
if (_selectedPlanTypeValue == null) return true;
|
||
final v = _selectedPlanTypeValue!;
|
||
final raw = row[v];
|
||
if (raw == null) return false;
|
||
final s = raw.toString().trim().toLowerCase();
|
||
if (s.isEmpty || s == 'null') return false;
|
||
// Treat 0 as "not configured" only when backend returns 0.
|
||
return s != '0';
|
||
}
|
||
|
||
bool matchesSearch() {
|
||
if (q.isEmpty) return true;
|
||
|
||
final id = row['id']?.toString().toLowerCase() ?? '';
|
||
final insurer = row['insurer']?.toString().toLowerCase() ?? '';
|
||
final vehicleType = row['vehicle_type']?.toString().toLowerCase() ?? '';
|
||
final segment = row['segment']?.toString().toLowerCase() ?? '';
|
||
final rto = row['rto']?.toString().toLowerCase() ?? '';
|
||
final remarks = row['remarks']?.toString().toLowerCase() ?? '';
|
||
final comp = row['comp']?.toString().toLowerCase() ?? '';
|
||
final tp = row['tp']?.toString().toLowerCase() ?? '';
|
||
final od = row['od']?.toString().toLowerCase() ?? '';
|
||
|
||
return [id, insurer, vehicleType, segment, rto, remarks, comp, tp, od].any((s) => s.contains(q));
|
||
}
|
||
|
||
return matchesInsurer() &&
|
||
matchesRto() &&
|
||
matchesVehicleType() &&
|
||
matchesSegment() &&
|
||
matchesPlanType() &&
|
||
matchesSearch();
|
||
}).toList();
|
||
|
||
setState(() {
|
||
_filteredGrid = next;
|
||
_currentPage = 1;
|
||
});
|
||
}
|
||
|
||
List<Map<String, dynamic>> get _sortedGrid {
|
||
final next = [..._filteredGrid];
|
||
next.sort((a, b) {
|
||
final ai = int.tryParse(a['id']?.toString() ?? '') ?? 0;
|
||
final bi = int.tryParse(b['id']?.toString() ?? '') ?? 0;
|
||
return bi.compareTo(ai);
|
||
});
|
||
return next;
|
||
}
|
||
|
||
List<Map<String, dynamic>> get _paginatedGrid {
|
||
final sorted = _sortedGrid;
|
||
if (sorted.isEmpty) return [];
|
||
|
||
final maxPage = (sorted.length / _itemsPerPage).ceil();
|
||
final safePage = _currentPage.clamp(1, maxPage == 0 ? 1 : maxPage);
|
||
|
||
final startIndex = (safePage - 1) * _itemsPerPage;
|
||
final endIndex = (startIndex + _itemsPerPage).clamp(0, sorted.length);
|
||
return sorted.sublist(startIndex, endIndex);
|
||
}
|
||
|
||
void _setFilter(VoidCallback fn) {
|
||
fn();
|
||
_applyFilters();
|
||
}
|
||
|
||
String _nullableString(dynamic v) {
|
||
if (v == null) return '-';
|
||
final s = v.toString();
|
||
if (s.trim().isEmpty || s.trim().toLowerCase() == 'null') return '-';
|
||
return s;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MainLayout(
|
||
title: 'Grid Details',
|
||
body: SelectionArea(
|
||
child: Container(
|
||
padding: const EdgeInsets.all(8),
|
||
child: Column(
|
||
children: [
|
||
_buildTopBar(context),
|
||
const SizedBox(height: 10),
|
||
_buildFilterBar(context),
|
||
const SizedBox(height: 10),
|
||
Expanded(
|
||
child: _isLoading
|
||
? const Center(child: CircularProgressIndicator())
|
||
: _buildGridOrCards(context),
|
||
),
|
||
PaginationControls(
|
||
currentPage: _currentPage,
|
||
itemsPerPage: _itemsPerPage,
|
||
totalItems: _filteredGrid.length,
|
||
onPageChanged: (page) => setState(() => _currentPage = page),
|
||
onItemsPerPageChanged: (items) {
|
||
setState(() {
|
||
_itemsPerPage = items;
|
||
_currentPage = 1;
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTopBar(BuildContext context) {
|
||
final isMobile = ResponsiveLayout.isMobile(context);
|
||
final isAgent =
|
||
(ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase() ==
|
||
'agent';
|
||
|
||
return Row(
|
||
children: [
|
||
if (!isAgent) ...[
|
||
Tooltip(
|
||
message: 'Back',
|
||
child: IconButton(
|
||
icon: const Icon(Icons.arrow_left_sharp, size: 25),
|
||
onPressed: () => context.go(AppRoutes.gridList),
|
||
splashRadius: 18,
|
||
padding: const EdgeInsets.all(4),
|
||
hoverColor: Colors.black12,
|
||
constraints: const BoxConstraints(),
|
||
),
|
||
),
|
||
const SizedBox(width: 5),
|
||
],
|
||
Text(
|
||
'Grid Details',
|
||
style: GoogleFonts.poppins(fontSize: 14, fontWeight: FontWeight.w400),
|
||
),
|
||
const Spacer(),
|
||
if (!isMobile)
|
||
ExportBtn(
|
||
sheetName: 'Grid Details',
|
||
fileName: 'payout_grid',
|
||
data: _filteredGrid,
|
||
displayHeaders: const [
|
||
'ID',
|
||
'Insurer',
|
||
'Vehicle Type',
|
||
'Segment',
|
||
'RTO',
|
||
'Comp',
|
||
'TP',
|
||
'OD',
|
||
'Remarks',
|
||
],
|
||
keys: const [
|
||
'id',
|
||
'insurer',
|
||
'vehicle_type',
|
||
'segment',
|
||
'rto',
|
||
'comp',
|
||
'tp',
|
||
'od',
|
||
'remarks',
|
||
],
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildFilterBar(BuildContext context) {
|
||
final isMobile = ResponsiveLayout.isMobile(context);
|
||
|
||
final planTypeItems = <Map<String, dynamic>>[
|
||
{'value': null, 'label': 'All'},
|
||
..._planTypes,
|
||
];
|
||
|
||
Widget dropdownInsurer() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : (isCompactFilter ? 180 : 200),
|
||
height: fieldHeight,
|
||
child: DropdownSearch<String>(
|
||
items: _insurers,
|
||
selectedItem: _selectedInsurer,
|
||
dropdownSearchTextInputDecoration: InputDecoration(
|
||
hintText: 'Search insurer...',
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
isDense: true,
|
||
),
|
||
dropdownBuilder: (context, selectedItem) => Padding(
|
||
padding: const EdgeInsets.only(left: 10),
|
||
child: Text(
|
||
selectedItem ?? 'All',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: fieldFontSize,
|
||
color: Colors.black,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
popupProps: const PopupProps.menu(
|
||
showSearchBox: true,
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedInsurer = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'Insurer')
|
||
.copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownPlanType() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : (isCompactFilter ? 200 : 220),
|
||
height: fieldHeight,
|
||
child: DropdownSearch<Map<String, dynamic>>(
|
||
items: planTypeItems,
|
||
selectedItem: planTypeItems.firstWhere(
|
||
(e) => (e['value'] as String?) == _selectedPlanTypeValue,
|
||
orElse: () => planTypeItems.first,
|
||
),
|
||
itemAsString: (v) => (v['label'] ?? '').toString(),
|
||
compareFn: (a, b) => (a['value'] == b['value']),
|
||
popupProps: const PopupProps.menu(showSearchBox: true),
|
||
onChanged: (val) => _setFilter(() => _selectedPlanTypeValue = val?['value'] as String?),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'Plan Type')
|
||
.copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownRto() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : (isCompactFilter ? 160 : 180),
|
||
height: fieldHeight,
|
||
child: DropdownSearch<String>(
|
||
items: _rtos,
|
||
selectedItem: _selectedRto,
|
||
popupProps: const PopupProps.menu(showSearchBox: true),
|
||
dropdownSearchTextInputDecoration: InputDecoration(
|
||
hintText: 'Search RTO...',
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
isDense: true,
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedRto = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'RTO')
|
||
.copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownVehicleType() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : (isCompactFilter ? 180 : 200),
|
||
height: fieldHeight,
|
||
child: DropdownSearch<String>(
|
||
items: _vehicleTypes,
|
||
selectedItem: _selectedVehicleType,
|
||
popupProps: const PopupProps.menu(showSearchBox: true),
|
||
dropdownSearchTextInputDecoration: InputDecoration(
|
||
hintText: 'Search vehicle type...',
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
isDense: true,
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedVehicleType = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'Vehicle Type')
|
||
.copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget dropdownSegment() {
|
||
return SizedBox(
|
||
width: isMobile ? double.infinity : (isCompactFilter ? 200 : 220),
|
||
height: fieldHeight,
|
||
child: DropdownSearch<String>(
|
||
items: _segments,
|
||
selectedItem: _selectedSegment,
|
||
popupProps: const PopupProps.menu(showSearchBox: true),
|
||
dropdownSearchTextInputDecoration: InputDecoration(
|
||
hintText: 'Search segment...',
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
isDense: true,
|
||
),
|
||
onChanged: (val) => _setFilter(() => _selectedSegment = val),
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(label: 'Segment')
|
||
.copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
labelStyle: GoogleFonts.poppins(fontSize: fieldFontSize),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
return Container(
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||
),
|
||
padding: EdgeInsets.symmetric(
|
||
horizontal: filterHorizontalPadding,
|
||
vertical: filterVerticalPadding,
|
||
),
|
||
child: isMobile
|
||
? Column(
|
||
children: [
|
||
_buildSearch(context, isCompact: isCompactFilter),
|
||
const SizedBox(height: 10),
|
||
dropdownInsurer(),
|
||
const SizedBox(height: 10),
|
||
dropdownPlanType(),
|
||
const SizedBox(height: 10),
|
||
dropdownVehicleType(),
|
||
const SizedBox(height: 10),
|
||
dropdownSegment(),
|
||
const SizedBox(height: 10),
|
||
dropdownRto(),
|
||
const SizedBox(height: 10),
|
||
_buildResetButton(),
|
||
],
|
||
)
|
||
: SingleChildScrollView(
|
||
scrollDirection: Axis.horizontal,
|
||
child: Row(
|
||
children: [
|
||
_buildSearch(context, isCompact: isCompactFilter),
|
||
SizedBox(width: fieldSpacing),
|
||
dropdownInsurer(),
|
||
SizedBox(width: fieldSpacing),
|
||
dropdownPlanType(),
|
||
SizedBox(width: fieldSpacing),
|
||
dropdownVehicleType(),
|
||
SizedBox(width: fieldSpacing),
|
||
dropdownSegment(),
|
||
SizedBox(width: fieldSpacing),
|
||
dropdownRto(),
|
||
SizedBox(width: fieldSpacing),
|
||
_buildResetButton(isCompact: isCompactFilter),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildSearch(BuildContext context, {bool isCompact = false}) {
|
||
return ThemedSearchField(
|
||
hintText: 'Search...',
|
||
controller: _searchController,
|
||
txtHeight: isCompact ? 36 : 40,
|
||
txtwidth: ResponsiveLayout.isMobile(context)
|
||
? null
|
||
: (isCompact ? 210 : 240),
|
||
onChanged: (_) => _applyFilters(),
|
||
backgroundColor: Colors.white,
|
||
);
|
||
}
|
||
|
||
Widget _buildResetButton({bool isCompact = false}) {
|
||
return SizedBox(
|
||
height: isCompact ? 36 : 40,
|
||
child: ElevatedButton.icon(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF2E7D6E),
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
),
|
||
onPressed: () {
|
||
_setFilter(() {
|
||
_selectedInsurer = null;
|
||
_selectedPlanTypeValue = null;
|
||
_selectedRto = null;
|
||
_selectedVehicleType = null;
|
||
_selectedSegment = null;
|
||
_searchController.clear();
|
||
});
|
||
},
|
||
icon: Icon(Icons.refresh, size: isCompact ? 16 : 18),
|
||
label: Text(
|
||
'Reset',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: isCompact ? 11 : 12,
|
||
fontWeight: FontWeight.w500,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildGridOrCards(BuildContext context) {
|
||
if (_paginatedGrid.isEmpty) {
|
||
return const Center(child: Text('No available data'));
|
||
}
|
||
|
||
if (ResponsiveLayout.isMobile(context)) {
|
||
return ListView.separated(
|
||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||
itemCount: _paginatedGrid.length,
|
||
separatorBuilder: (context, index) => const SizedBox(height: 10),
|
||
itemBuilder: (context, index) {
|
||
final row = _paginatedGrid[index];
|
||
return _gridViewCard(row: row);
|
||
},
|
||
);
|
||
}
|
||
|
||
return ListView.builder(
|
||
itemCount: _paginatedGrid.length + 1, // + header
|
||
itemBuilder: (context, index) {
|
||
if (index == 0) return _buildTableHeader();
|
||
final rowIndex = index - 1;
|
||
final row = _paginatedGrid[rowIndex];
|
||
return _buildTableRow(row, rowIndex: rowIndex);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildTableHeader() {
|
||
return Container(
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
|
||
),
|
||
),
|
||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||
child: Row(
|
||
children: [
|
||
Expanded(flex: 1, child: Text('S.No', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('Insurer', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('Vehicle', style: _headerStyle)),
|
||
Expanded(flex: 3, child: Text('Segment', style: _headerStyle)),
|
||
Expanded(flex: 2, child: Text('RTO', 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)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildTableRow(
|
||
Map<String, dynamic> row, {
|
||
required int rowIndex,
|
||
}) {
|
||
final sno = (_currentPage - 1) * _itemsPerPage + rowIndex + 1;
|
||
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 12),
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(color: Color(0xFFEAEAEA), width: 1),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(flex: 1, child: Text('$sno', style: _dataBold)),
|
||
Expanded(flex: 2, child: Text(_nullableString(row['insurer']), style: _dataBold)),
|
||
Expanded(flex: 2, child: Text(_nullableString(row['vehicle_type']), style: _dataBold)),
|
||
Expanded(flex: 3, child: Text(_nullableString(row['segment']), style: _dataBold, overflow: TextOverflow.ellipsis)),
|
||
Expanded(flex: 2, child: Text(_nullableString(row['rto']), 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(_nullableString(row['remarks']), style: _dataBold, overflow: TextOverflow.ellipsis)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
static final _headerStyle = GoogleFonts.poppins(
|
||
fontSize: 11.2,
|
||
fontWeight: FontWeight.w500,
|
||
color: const Color(0xFF1E293B),
|
||
);
|
||
|
||
static final _dataBold = GoogleFonts.inter(
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.w400,
|
||
color: const Color(0xFF000000),
|
||
);
|
||
}
|
||
|
||
class _gridViewCard extends StatelessWidget {
|
||
final Map<String, dynamic> row;
|
||
|
||
const _gridViewCard({required this.row});
|
||
|
||
String _nullableString(dynamic v) {
|
||
if (v == null) return '-';
|
||
final s = v.toString();
|
||
if (s.trim().isEmpty || s.trim().toLowerCase() == 'null') return '-';
|
||
return s;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final id = row['id']?.toString() ?? '-';
|
||
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(horizontal: 8),
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFEAEAEA)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
'${_nullableString(row['insurer'])} | ${_nullableString(row['vehicle_type'])}',
|
||
style: GoogleFonts.poppins(fontSize: 13, fontWeight: FontWeight.w500),
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
),
|
||
Text(
|
||
'ID: $id',
|
||
style: GoogleFonts.poppins(fontSize: 10, color: const Color(0xFF6E6E6E)),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 10),
|
||
Text('Segment: ${_nullableString(row['segment'])}', style: GoogleFonts.inter(fontSize: 12)),
|
||
const SizedBox(height: 6),
|
||
Text('RTO: ${_nullableString(row['rto'])}', style: GoogleFonts.inter(fontSize: 12)),
|
||
const SizedBox(height: 10),
|
||
Row(
|
||
children: [
|
||
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),
|
||
Text('Remarks: ${_nullableString(row['remarks'])}', style: GoogleFonts.inter(fontSize: 12)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _metric(String label, 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(label, style: GoogleFonts.poppins(fontSize: 10, color: const Color(0xFF64748B))),
|
||
const SizedBox(height: 2),
|
||
Text(_nullableString(value), style: GoogleFonts.poppins(fontSize: 12, fontWeight: FontWeight.w600)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
import 'package:file_picker/file_picker.dart';
|
||
import 'package:flutter/material.dart';
|
||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||
import 'package:google_fonts/google_fonts.dart';
|
||
import 'package:go_router/go_router.dart';
|
||
import 'package:http/http.dart' as http;
|
||
import 'package:shared_preferences/shared_preferences.dart';
|
||
import 'dart:convert';
|
||
import 'package:nhance_partner/core/services/api_service.dart';
|
||
import 'package:nhance_partner/core/config/env.dart';
|
||
import 'package:nhance_partner/data/utils/toastNotification.dart';
|
||
import 'package:nhance_partner/data/services/auth_service.dart';
|
||
import 'package:nhance_partner/presentation/layouts/main_layout.dart';
|
||
import 'package:nhance_partner/presentation/providers/manager_provider.dart';
|
||
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/date_field_theme.dart';
|
||
import 'package:nhance_partner/presentation/themes/indicators/search_field_theme.dart';
|
||
import 'package:nhance_partner/presentation/screens/UserManagement/Agent/agentGridFile.dart';
|
||
|
||
class gridViewScreen extends ConsumerStatefulWidget {
|
||
const gridViewScreen({super.key});
|
||
@override
|
||
ConsumerState<gridViewScreen> createState() => gridViewScreenState();
|
||
}
|
||
|
||
class gridViewScreenState extends ConsumerState<gridViewScreen> {
|
||
int currentPage = 1;
|
||
int itemsPerPage = 10;
|
||
late ApiService apiService;
|
||
|
||
// List<Map<String, dynamic>> dataVal = [];
|
||
List<Map<String, dynamic>> getStaffData = [];
|
||
List<Map<String, dynamic>> originalData = [];
|
||
List<Map<String, dynamic>> filteredData = [];
|
||
bool isLoading = false;
|
||
dynamic userId;
|
||
dynamic roleId;
|
||
dynamic managerId;
|
||
|
||
// bool isDrawerOpen = false;
|
||
Map<String, dynamic>? selectedRow;
|
||
List<Map<String, dynamic>> endorsementNumbers = [];
|
||
|
||
List<Map<String, dynamic>> getEndrosmentType = [];
|
||
List<Map<String, dynamic>> filteredEndrosmentData = [];
|
||
String? hoveredRowId;
|
||
|
||
String? selectedFileNames;
|
||
PlatformFile? docUploadedFile;
|
||
String? lastPickedFile;
|
||
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
|
||
apiService = ApiService();
|
||
|
||
Future.microtask(() async {
|
||
final id = ref.read(managerIdProvider);
|
||
managerId = ref.read(managerIdProvider);
|
||
roleId = ref.read(userRoleProvider);
|
||
userId = ref.read(userIdProvider);
|
||
print("F46 => r : $roleId | mId: $id | uId: $userId ");
|
||
/// ⭐ DEFAULT FILTER FOR ACCOUNTS
|
||
if (roleId == 'Accounts') {
|
||
selectedVerificationVal = 0; // 0 = To Verify
|
||
} else {
|
||
selectedStatusVal = 'Open';
|
||
}
|
||
|
||
if (userId != null) {
|
||
await getStaffList(managerId, userId, roleId);
|
||
}
|
||
getEnroementType();
|
||
getInsurerDetails();
|
||
});
|
||
}
|
||
|
||
Future<void> getStaffList(int managerId, userId, role) async {
|
||
setState(() {
|
||
isLoading = true;
|
||
});
|
||
|
||
print('fetchEndorsementList first $selectedVerificationVal');
|
||
|
||
try {
|
||
final response = await apiService.fetchEndorsementList(
|
||
managerId,
|
||
userId,
|
||
role,
|
||
|
||
fromDate: startController.text.isNotEmpty
|
||
? DateFormat('yyyy-MM-dd')
|
||
.format(DateFormat('dd-MM-yyyy')
|
||
.parse(startController.text))
|
||
: null,
|
||
|
||
toDate: endController.text.isNotEmpty
|
||
? DateFormat('yyyy-MM-dd')
|
||
.format(DateFormat('dd-MM-yyyy')
|
||
.parse(endController.text))
|
||
: null,
|
||
|
||
endorsementType:
|
||
selectedEndorsementTypeVal?.toString(),
|
||
|
||
insurerId:
|
||
selectedInsurerVal?.toString(),
|
||
|
||
status: selectedStatusVal == 'All'
|
||
? ''
|
||
: selectedStatusVal == 'Open'
|
||
? 'Open'
|
||
: selectedStatusVal == 'Closed'
|
||
? 'Closed'
|
||
: null,
|
||
|
||
|
||
verification: selectedVerificationVal == 2
|
||
? ''
|
||
: selectedVerificationVal == 0
|
||
? 'To Verify'
|
||
: selectedVerificationVal == 1
|
||
? 'Verified'
|
||
: null,
|
||
// selectedVerificationVal?.toString(),
|
||
);
|
||
print('fetchEndorsementList');
|
||
|
||
if (response['status'] == 'success') {
|
||
final data = response['data'];
|
||
|
||
setState(() {
|
||
if (data is List) {
|
||
getStaffData =
|
||
List<Map<String, dynamic>>.from(data);
|
||
} else if (data is Map) {
|
||
getStaffData = [
|
||
Map<String, dynamic>.from(data)
|
||
];
|
||
} else {
|
||
getStaffData = [];
|
||
}
|
||
|
||
originalData = getStaffData;
|
||
filteredData = List.from(originalData);
|
||
});
|
||
} else {
|
||
getStaffData = [];
|
||
originalData = [];
|
||
}
|
||
} catch (e) {
|
||
print('Exception occurred: $e');
|
||
} finally {
|
||
setState(() {
|
||
isLoading = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
List<dynamic> get _paginatedData {
|
||
// Sort descending by id first
|
||
final sortedData = [...filteredData]
|
||
..sort((a, b) => int.parse(b['id']) - int.parse(a['id']));
|
||
|
||
if (sortedData.isEmpty) return [];
|
||
|
||
// Ensure currentPage is valid
|
||
final maxPage = (sortedData.length / itemsPerPage).ceil();
|
||
final safePage = currentPage.clamp(1, maxPage);
|
||
|
||
final startIndex = (safePage - 1) * itemsPerPage;
|
||
final endIndex = (startIndex + itemsPerPage).clamp(0, sortedData.length);
|
||
|
||
return sortedData.sublist(startIndex, endIndex);
|
||
}
|
||
|
||
void refresh() {
|
||
getStaffList(managerId, userId, roleId);
|
||
}
|
||
|
||
void filterData(String query) {
|
||
final lowerQuery = query.toLowerCase();
|
||
|
||
setState(() {
|
||
filteredData = getStaffData.where((item) {
|
||
|
||
/// 🔹 Search all raw values
|
||
final matchesRawValues = item.values.any(
|
||
(value) =>
|
||
value != null &&
|
||
value.toString().toLowerCase().contains(lowerQuery),
|
||
);
|
||
|
||
/// 🔹 Add computed verification text
|
||
final verificationText =
|
||
item['is_data_accuracy_checked'] == '0'
|
||
? 'to verify'
|
||
: 'verified';
|
||
|
||
final matchesVerification =
|
||
verificationText.contains(lowerQuery);
|
||
|
||
return matchesRawValues || matchesVerification;
|
||
|
||
}).toList();
|
||
});
|
||
}
|
||
|
||
void _confirmDelete(BuildContext context, String id) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (ctx) => AlertDialog(
|
||
backgroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
|
||
title: Row(
|
||
children: [
|
||
Icon(Icons.warning_amber_rounded, color: Colors.red.shade600, size: 22),
|
||
const SizedBox(width: 8),
|
||
Text(
|
||
'Delete Endorsement',
|
||
style: GoogleFonts.poppins(fontSize: 15, fontWeight: FontWeight.w600),
|
||
),
|
||
],
|
||
),
|
||
content: Text(
|
||
'Are you sure you want to delete this endorsement?\nThis action cannot be undone.',
|
||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade700),
|
||
),
|
||
actions: [
|
||
// CANCEL
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(ctx),
|
||
child: Text(
|
||
'Cancel',
|
||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade600),
|
||
),
|
||
),
|
||
|
||
// CONFIRM DELETE
|
||
ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: Colors.red.shade600,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||
),
|
||
onPressed: () async {
|
||
Navigator.pop(ctx);
|
||
await _deleteEndorsement(id);
|
||
},
|
||
child: Text(
|
||
'Delete',
|
||
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _deleteEndorsement(String id) async {
|
||
try {
|
||
setState(() => isLoading = true);
|
||
|
||
final response = await apiService.deleteEndorsement(id);
|
||
|
||
if (response['status'] == 'success' || response['code'] == 200) {
|
||
refresh();
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
'Endorsement deleted successfully.',
|
||
style: GoogleFonts.poppins(fontSize: 13),
|
||
),
|
||
backgroundColor: Colors.green.shade600,
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||
duration: const Duration(seconds: 2),
|
||
),
|
||
);
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text(
|
||
response['message'] ?? 'Failed to delete endorsement.',
|
||
style: GoogleFonts.poppins(fontSize: 13),
|
||
),
|
||
backgroundColor: Colors.red.shade600,
|
||
behavior: SnackBarBehavior.floating,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
|
||
duration: const Duration(seconds: 2),
|
||
),
|
||
);
|
||
}
|
||
} catch (e) {
|
||
ScaffoldMessenger.of(context).showSnackBar(
|
||
SnackBar(
|
||
content: Text('Error: $e', style: GoogleFonts.poppins(fontSize: 13)),
|
||
backgroundColor: Colors.red.shade600,
|
||
behavior: SnackBarBehavior.floating,
|
||
),
|
||
);
|
||
} finally {
|
||
setState(() => isLoading = false);
|
||
}
|
||
}
|
||
|
||
final TextEditingController rightSearchController = TextEditingController();
|
||
final TextEditingController startController = TextEditingController();
|
||
final TextEditingController endController = TextEditingController();
|
||
|
||
dynamic selectedStatusVal;
|
||
dynamic selectedVerificationVal;
|
||
dynamic selectedInsurerVal;
|
||
dynamic selectedInsurer;
|
||
dynamic selectedEndorsementTypeVal;
|
||
|
||
List<Map<String, dynamic>> getInsurerDetailsData = [];
|
||
List<Map<String, dynamic>> filteredInsurerData = [];
|
||
bool isLoadingA = false;
|
||
|
||
String _formatDate(String rawDate) {
|
||
try {
|
||
final dateTime = DateTime.parse(rawDate);
|
||
return DateFormat('dd-MM-yyyy HH:mm').format(dateTime); // 24-hour format
|
||
} catch (e) {
|
||
return rawDate; // fallback if parsing fails
|
||
}
|
||
}
|
||
|
||
String _truncateText(String? text, int maxLength) {
|
||
if (text == null || text.length <= maxLength) {
|
||
return text ?? '-';
|
||
}
|
||
return '${text.substring(0, maxLength)}...';
|
||
}
|
||
|
||
List<Widget> _buildPopupMenuActions(BuildContext context, Map<String, dynamic> item) {
|
||
final String Id = item['id']?.toString() ?? '';
|
||
final String? pdfPath = item["endorsement_completion_file"];
|
||
final String? fileName = (pdfPath != null && pdfPath.isNotEmpty)
|
||
? pdfPath.split('/').last
|
||
: null;
|
||
|
||
return [
|
||
// --- EDIT BUTTON ---
|
||
InkWell(
|
||
onTap: () {
|
||
Navigator.pop(context); // Close the menu first
|
||
showDialog(
|
||
context: context,
|
||
barrierDismissible: true,
|
||
builder: (BuildContext dialogContext) {
|
||
return UpdateEndorsementDialog(
|
||
item: item,
|
||
userId: userId, // Ensure these variables are accessible
|
||
managerId: managerId,
|
||
onSubmit: (val) => refresh(),
|
||
);
|
||
},
|
||
);
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
|
||
child: Row(
|
||
children: const [
|
||
Icon(Icons.edit_sharp, color: Color(0xFF319718), size: 18),
|
||
SizedBox(width: 12),
|
||
Text('Edit', style: TextStyle(fontSize: 14)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
const Divider(height: 1, thickness: 0.5), // Subtle separator
|
||
|
||
// --- DOWNLOAD BUTTON ---
|
||
InkWell(
|
||
onTap: () {
|
||
Navigator.pop(context); // Close the menu first
|
||
apiService.downloadFile(
|
||
apiUrl: 'endorsement/downloadEndorsementCompletionFile?id=$Id',
|
||
apiId: Id,
|
||
localFile: null,
|
||
fileName: fileName,
|
||
);
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
|
||
child: Row(
|
||
children: const [
|
||
Icon(Icons.download_rounded, color: Colors.blue, size: 18),
|
||
SizedBox(width: 12),
|
||
Text('Download', style: TextStyle(fontSize: 14)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
// --- DELETE BUTTON ---
|
||
if (roleId == 'Accounts') ...[
|
||
const Divider(height: 1, thickness: 0.5),
|
||
InkWell(
|
||
onTap: () {
|
||
Navigator.pop(context);
|
||
_confirmDelete(context, Id);
|
||
},
|
||
child: Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 4.0),
|
||
child: Row(
|
||
children: const [
|
||
Icon(Icons.delete_outline, color: Colors.red, size: 18),
|
||
SizedBox(width: 12),
|
||
Text('Delete', style: TextStyle(fontSize: 14, color: Colors.red)),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
],
|
||
];
|
||
}
|
||
|
||
Future<void> fetchEndorsementExcelReport() async {
|
||
final String searchValue = rightSearchController.text;
|
||
final response = await apiService.generateEndorsementExcel(managerId,searchValue);
|
||
// Map your controllers and IDs to the API parameters
|
||
// final String fromDate = controllers['startDate']!.text; // "11-01-2025"
|
||
// final String toDate = controllers['endDate']!.text; // "20-01-2025"
|
||
// print("fetchPolicyExcelReport function called here.");
|
||
//
|
||
// try {
|
||
// // Assuming you have a generic _makeGetRequest method
|
||
// final response = await apiService.fetchPolicyExcel(managerId,fromDate,toDate);
|
||
//
|
||
// if (response != null && response['status'] == 200) {
|
||
// setState(() {
|
||
// // Store the list of data in your variable
|
||
// excelResponseData = response['data'];
|
||
// });
|
||
// print("Data fetched. Ready to export.");
|
||
// }
|
||
// } catch (e) {
|
||
// debugPrint("Export Error: $e");
|
||
// print("Failed to fetch report data.");
|
||
// }
|
||
}
|
||
|
||
Future<void> getEnroementType() async {
|
||
print('Insurers called');
|
||
setState(() {
|
||
isLoading = true;
|
||
});
|
||
|
||
try {
|
||
final response = await apiService.fetchMasterDropDown('Endorsement');
|
||
|
||
if (response['status'] == 200) {
|
||
print('getEnroementType - ${response['data']}');
|
||
setState(() {
|
||
getEndrosmentType = List<Map<String, dynamic>>.from(response['data']);
|
||
print('API Data - $getEndrosmentType');
|
||
|
||
filteredEndrosmentData = List.from(getEndrosmentType);
|
||
// print('originalData - $filteredEndrosmentData');
|
||
});
|
||
} else {
|
||
getEndrosmentType = [];
|
||
filteredEndrosmentData = [];
|
||
}
|
||
} catch (e) {
|
||
print('Exception occurred: $e');
|
||
} finally {
|
||
setState(() {
|
||
isLoading = false;
|
||
});
|
||
}
|
||
}
|
||
|
||
Future<void> getInsurerDetails() async {
|
||
setState(() => isLoadingA = true);
|
||
try {
|
||
final response = await apiService.fetchMasterDropDown('Insurers');
|
||
|
||
print("***999999999999999999*********** $response");
|
||
|
||
if (response['status'] == 200) {
|
||
print('getInsurerDetails - ${response['data']}');
|
||
|
||
setState(() {
|
||
getInsurerDetailsData =
|
||
List<Map<String, dynamic>>.from(response['data']);
|
||
|
||
filteredInsurerData =
|
||
List<Map<String, dynamic>>.from(getInsurerDetailsData);
|
||
|
||
print("Filtered Insurer Data: $filteredInsurerData");
|
||
});
|
||
} else {
|
||
setState(() {
|
||
getInsurerDetailsData = [];
|
||
filteredInsurerData = [];
|
||
});
|
||
}
|
||
} catch (e) {
|
||
print('Error: $e');
|
||
} finally {
|
||
setState(() => isLoadingA = false);
|
||
}
|
||
}
|
||
|
||
Future<void> loadEndorsementNumbers(String id) async {
|
||
// final response = await apiService.fetchEndorsementNumbers(id);
|
||
//
|
||
// if (response['status'] == 'success') {
|
||
// setState(() {
|
||
// endorsementNumbers =
|
||
// List<Map<String, dynamic>>.from(response['data']);
|
||
// });
|
||
// }
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MainLayout(
|
||
title: "Endorsement",
|
||
body: SelectionArea(
|
||
child: Container(
|
||
// color: Colors.yellow.shade50,
|
||
width: MediaQuery.of(context).size.width,
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.start,
|
||
children: [
|
||
// Container(
|
||
// height: 30,
|
||
// width: MediaQuery.of(context).size.width,
|
||
// child: GestureDetector(
|
||
// onTap: () {
|
||
// context.go(AppRoutes.dashboard);
|
||
// },
|
||
// child: Row(
|
||
// crossAxisAlignment: CrossAxisAlignment.center,
|
||
// mainAxisAlignment: MainAxisAlignment.start,
|
||
// children: [
|
||
// Tooltip(
|
||
// message: 'Back',
|
||
// child: IconButton(
|
||
// icon: const Icon(
|
||
// Icons.arrow_left_sharp,
|
||
// size: 25,
|
||
// color: Color(0xFF425B5B),
|
||
// ),
|
||
// onPressed: () {
|
||
// context.go(AppRoutes.dashboard);
|
||
// },
|
||
// splashRadius: 18,
|
||
// hoverColor: Colors.black12,
|
||
// padding: const EdgeInsets.all(4),
|
||
// constraints: const BoxConstraints(),
|
||
// ),
|
||
// ),
|
||
// const SizedBox(width: 5), // spacing between icon and text
|
||
// Text(
|
||
// "Endorsement",
|
||
// style: GoogleFonts.poppins(
|
||
// fontSize: 14,
|
||
// fontWeight: FontWeight.w400,
|
||
// ),
|
||
// ),
|
||
// ],
|
||
// ),
|
||
// ),
|
||
// ),
|
||
//
|
||
// SizedBox(height: 5),
|
||
Expanded(
|
||
child: Container(
|
||
// color: Colors.green,
|
||
// color: Colors.green.shade50,
|
||
width: MediaQuery.of(context).size.width,
|
||
|
||
padding: EdgeInsets.all(8.0),
|
||
child: Column(
|
||
children: [
|
||
Container(
|
||
// height: 40,
|
||
// color: Colors.pink,
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.start,
|
||
children: [
|
||
Text(
|
||
"Endorsement",
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w400,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
SizedBox(height: 10),
|
||
Container(
|
||
width: double.infinity,
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: buildFilterBar(context),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
SizedBox(height: 10),
|
||
if (!ResponsiveLayout.isMobile(context))
|
||
Container(
|
||
decoration: BoxDecoration(
|
||
color: Color(0xFFF1F5F9),
|
||
// color: Color(0xFFEDF6F5),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 8,
|
||
horizontal: 16,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
flex: 1,
|
||
child: Text('ID ', style: _headerStyle),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text('Vechile TYpe ', style: _headerStyle),
|
||
),
|
||
Expanded(
|
||
flex: 3,
|
||
child: Text('Fuel ', style: _headerStyle),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text('Insurer', style: _headerStyle),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text('RTO', style: _headerStyle),
|
||
),
|
||
Expanded(
|
||
flex: 3,
|
||
child: Text('Broker', style: _headerStyle),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text('Segment', 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: 2,
|
||
child: Text(
|
||
'Remarks',
|
||
style: _headerStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(
|
||
'Partner Comp',
|
||
style: _headerStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(
|
||
'Partner TD',
|
||
style: _headerStyle,
|
||
),
|
||
),
|
||
// if(roleId == 'Accounts')
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(
|
||
'Partner OD',
|
||
style: _headerStyle,
|
||
),
|
||
),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text('Action', style: _headerStyle),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
Expanded(
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
// MAIN TABLE
|
||
Expanded(
|
||
flex: 3,
|
||
child: Container(
|
||
// color: Colors.white,
|
||
child: _buildDataTable(context),
|
||
),
|
||
),
|
||
|
||
// DRAWER (only if open)
|
||
// if (isDrawerOpen)
|
||
// Expanded(
|
||
// flex: 1,
|
||
// child: _buildDrawer(),
|
||
// ),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
Container(
|
||
// height: 20,
|
||
width: MediaQuery.of(context).size.width,
|
||
// color: Colors.green.shade50,
|
||
child: PaginationControls(
|
||
currentPage: currentPage,
|
||
itemsPerPage: itemsPerPage,
|
||
// totalItems: dataVal.length,
|
||
totalItems: filteredData.length,
|
||
// activeColor: layoutColor, // your theme color
|
||
onPageChanged: (page) {
|
||
setState(() {
|
||
currentPage = page;
|
||
});
|
||
},
|
||
onItemsPerPageChanged: (items) {
|
||
setState(() {
|
||
itemsPerPage = items;
|
||
currentPage = 1;
|
||
});
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
), ),
|
||
);
|
||
}
|
||
|
||
Widget buildFilterBar(BuildContext context) {
|
||
return SizedBox(
|
||
width: double.infinity,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
|
||
/// 🔹 LEFT SIDE (SCROLLABLE FILTERS)
|
||
Expanded(
|
||
child: SingleChildScrollView(
|
||
scrollDirection: Axis.horizontal,
|
||
child: Row(
|
||
children: [
|
||
buildRTOSearch(context),
|
||
const SizedBox(width: 12),
|
||
|
||
buildVechileTypeSearch(context),
|
||
const SizedBox(width: 12),
|
||
|
||
buildInsurerSearch(context),
|
||
const SizedBox(width: 12),
|
||
|
||
buildPalntypeSearch(context),
|
||
const SizedBox(width: 12),
|
||
|
||
buildSegmentSearch(context),
|
||
const SizedBox(width: 12),
|
||
|
||
_iconButton(Icons.search, () {
|
||
refresh();
|
||
}),
|
||
const SizedBox(width: 8),
|
||
|
||
_iconButton(Icons.refresh, () {
|
||
selectedRTOVal = null;
|
||
selectedInsurerVal = null;
|
||
selectedVechileTypeVal = null;
|
||
selectedPlanTypeVal = null;
|
||
selectedSegmentVal = null;
|
||
rightSearchController.clear();
|
||
refresh();
|
||
}),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
|
||
/// 🔹 RIGHT SIDE (FIXED)
|
||
const SizedBox(width: 20),
|
||
|
||
_rightSearchField(),
|
||
const SizedBox(width: 12),
|
||
_exportButton(),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDataTable(BuildContext context) {
|
||
if (filteredData.isEmpty) {
|
||
return const SizedBox(
|
||
height: 50,
|
||
child: Center(child: Text('No available data')),
|
||
);
|
||
}
|
||
|
||
final sortedData = [..._paginatedData];
|
||
return ListView.builder(
|
||
itemCount: ResponsiveLayout.isMobile(context)
|
||
? sortedData
|
||
.length // only cards for mobile
|
||
: sortedData.length + 1, // +1 for header in desktop
|
||
itemBuilder: (context, index) {
|
||
if (!ResponsiveLayout.isMobile(context) && index == 0) {
|
||
return _buildHeader();
|
||
}
|
||
|
||
final startIndex = (currentPage - 1) * itemsPerPage;
|
||
final item =
|
||
sortedData[index - (ResponsiveLayout.isMobile(context) ? 0 : 1)];
|
||
final sno = startIndex + index;
|
||
|
||
return !ResponsiveLayout.isMobile(context)
|
||
? _buildDataRow(item, sno)
|
||
: _buildDataCard(item, sno);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildHeader() {
|
||
return SizedBox.shrink();
|
||
}
|
||
|
||
Widget _buildDataRow(Map<String, dynamic> item, sno) {
|
||
final String id = item['id']?.toString() ?? '';
|
||
|
||
|
||
|
||
return MouseRegion(
|
||
cursor: SystemMouseCursors.click,
|
||
child: GestureDetector(
|
||
onTap: () async {
|
||
setState(() {
|
||
selectedRow = item;
|
||
// isDrawerOpen = true;
|
||
});
|
||
|
||
await loadEndorsementNumbers(item['id']);
|
||
},
|
||
child: AnimatedContainer(
|
||
duration: const Duration(milliseconds: 150),
|
||
padding: const EdgeInsets.symmetric(
|
||
vertical: 10, horizontal: 16),
|
||
decoration: BoxDecoration(
|
||
// color: bgColor,
|
||
border: const Border(
|
||
bottom: BorderSide(
|
||
color: Color(0xFFEAEAEA),
|
||
width: 1,
|
||
),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
|
||
Expanded(flex: 1, child: Text('$sno', style: _dataBold)),
|
||
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(
|
||
item['vehicle_type'] != null
|
||
? item['vehicle_type'].toString()
|
||
: '-',
|
||
style: _dataBold,
|
||
),
|
||
),
|
||
|
||
Expanded(
|
||
flex: 3,
|
||
child: Text(
|
||
item['fuel'] != null
|
||
? item['feul'].toString()
|
||
: '-',
|
||
style: _dataBold,
|
||
),
|
||
),
|
||
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['insurer'] ?? '-', style: _dataBold)),
|
||
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['rto'] ?? '-', style: _dataBold)),
|
||
|
||
Expanded(
|
||
flex: 3,
|
||
child:
|
||
Text(item['broker_name'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['swgment'] ?? '-', style: _dataBold)),
|
||
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['comp'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['tp'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['od'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['remarks'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['partner_comp'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['partner_tp'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: Text(item['partner_od'] ?? '-', style: _dataBold)),
|
||
Expanded(
|
||
flex: 2,
|
||
child: item['is_active'] == "1"
|
||
? Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
// --- EDIT ---
|
||
_actionIconButton(
|
||
context: context,
|
||
tooltip: 'Edit',
|
||
iconColor: const Color(0xFF319718),
|
||
onTap: () async {
|
||
_openEditModal(item);
|
||
if (result == true) refresh();
|
||
},
|
||
customIcon: Image.asset(
|
||
"assets/miscellaneous/Edit.png",
|
||
height: 15,
|
||
width: 15,
|
||
),
|
||
),
|
||
],
|
||
)
|
||
: const Text('-'), _buildListWidget
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
|
||
|
||
Widget _buildDataCard(Map<String, dynamic> item, int sno) {
|
||
return Container(
|
||
margin: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF6FEFD),
|
||
borderRadius: BorderRadius.circular(8.0),
|
||
border: Border.all(color: const Color(0xffD9EBE8)),
|
||
),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
Text(item['vehicle_type'] ?? '-', style: _headerStyle),
|
||
PopupMenuButton<int>(
|
||
color: Colors.white,
|
||
padding: EdgeInsets.zero,
|
||
offset: Offset(0, 30),
|
||
icon: Icon(Icons.more_vert, color: Color(0xFF475569), size: 14),
|
||
itemBuilder: (context) => [
|
||
CustomPopupMenuEntry(
|
||
child: Container(
|
||
padding: EdgeInsets.symmetric(horizontal: 8, vertical: 8),
|
||
child: Column(
|
||
mainAxisSize: MainAxisSize.min,
|
||
mainAxisAlignment: MainAxisAlignment.center,
|
||
children: _buildPopupMenuActions(context, item),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
const Divider(color: Color(0xffD9EBE8), thickness: 0.8),
|
||
|
||
// Policy From + Status
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
|
||
Text("Fuel", style: _cardheaderStyle),
|
||
Text(
|
||
item['fuel'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
SizedBox(width: 5),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
//remarks
|
||
Text("remarks", style: _cardheaderStyle),
|
||
Text(
|
||
item['remarks'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
maxLines: 3,
|
||
softWrap: true,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 15),
|
||
|
||
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
|
||
mainAxisAlignment: MainAxisAlignment.start,
|
||
children: [
|
||
//company
|
||
Text("Insurer", style: _cardheaderStyle),
|
||
Text(
|
||
item['insurer'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
maxLines: 3,
|
||
softWrap: true,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 15),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text("RTO", style: _cardheaderStyle),
|
||
Text(
|
||
item['rto'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 15),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisAlignment: MainAxisAlignment.start,
|
||
children: [
|
||
|
||
Text("broker", style: _cardheaderStyle),
|
||
Text(
|
||
item['broker_name'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
maxLines: 2,
|
||
softWrap: true,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 15),
|
||
|
||
// Created On + Remarks
|
||
Row(
|
||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||
children: [
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
|
||
Text("comp", style: _cardheaderStyle),
|
||
Text(
|
||
item['partner_comp'] '&' item['comp'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
SizedBox(width: 5),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
|
||
Text("TP", style: _cardheaderStyle),
|
||
Text(
|
||
item['partner_tp'] '&' item['tp'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(height: 15),
|
||
Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
Text("OD", style: _cardheaderStyle),
|
||
Text(
|
||
item['partner_od'] '&' item['OD'] ?? '-',
|
||
style: _cardBodyStyle,
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildDrawer() {
|
||
return Container(
|
||
margin: const EdgeInsets.only(left: 12),
|
||
decoration: BoxDecoration(
|
||
color: Colors.white,
|
||
border: Border.all(color: const Color(0xFFEAEAEA)),
|
||
borderRadius: BorderRadius.circular(8),
|
||
),
|
||
child: Column(
|
||
children: [
|
||
|
||
// HEADER
|
||
Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(color: Color(0xFFEAEAEA)),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
"${selectedRow?['']} - Grid",
|
||
style: _headerStyle,
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.close),
|
||
onPressed: () {
|
||
setState(() {
|
||
// isDrawerOpen = false;
|
||
selectedRow = null;
|
||
});
|
||
},
|
||
),
|
||
],
|
||
),
|
||
),
|
||
|
||
// TABLE
|
||
Expanded(
|
||
child: endorsementNumbers.isEmpty
|
||
? const Center(child: Text("No "))
|
||
: ListView.builder(
|
||
itemCount: endorsementNumbers.length,
|
||
itemBuilder: (context, index) {
|
||
final item = endorsementNumbers[index];
|
||
return Container(
|
||
padding: const EdgeInsets.all(12),
|
||
decoration: const BoxDecoration(
|
||
border: Border(
|
||
bottom: BorderSide(
|
||
color: Color(0xFFEAEAEA),
|
||
),
|
||
),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
Expanded(
|
||
child: Text(
|
||
item['vechile_type'] ?? '-',
|
||
style: _dataBold,
|
||
),
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.edit, size: 16),
|
||
onPressed: () {},
|
||
),
|
||
IconButton(
|
||
icon: const Icon(Icons.download, size: 16),
|
||
onPressed: () {},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
|
||
// PAGINATION
|
||
PaginationControls(
|
||
currentPage: 1,
|
||
itemsPerPage: 10,
|
||
totalItems: endorsementNumbers.length,
|
||
onPageChanged: (page) {},
|
||
onItemsPerPageChanged: (items) {},
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget buildRTOSearch(BuildContext context) {
|
||
final List<Map<String, dynamic>> endorsementOptions =
|
||
List<Map<String, dynamic>>.from(filteredEndrosmentData);
|
||
|
||
Map<String, dynamic>? selectedItem;
|
||
|
||
if (selectedEndorsementTypeVal != null &&
|
||
selectedEndorsementTypeVal.toString().isNotEmpty &&
|
||
endorsementOptions.isNotEmpty) {
|
||
try {
|
||
selectedItem = endorsementOptions.firstWhere(
|
||
(e) =>
|
||
e['id'].toString() ==
|
||
selectedEndorsementTypeVal.toString(),
|
||
);
|
||
} catch (_) {
|
||
selectedItem = null;
|
||
}
|
||
}
|
||
|
||
return SizedBox(
|
||
height: 33,
|
||
child: Container(
|
||
width: ResponsiveLayout.isMobile(context)
|
||
? null
|
||
: MediaQuery.of(context).size.width * 0.10,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(10),
|
||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||
),
|
||
child: DropdownSearch<Map<String, dynamic>>(
|
||
selectedItem: selectedItem,
|
||
|
||
items: (filter, infiniteScrollProps) {
|
||
if (filter.isEmpty) return endorsementOptions;
|
||
|
||
return endorsementOptions
|
||
.where((item) => item['endorsement_type']
|
||
.toString()
|
||
.toLowerCase()
|
||
.contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
|
||
itemAsString: (item) =>
|
||
item['endorsement_type']?.toString() ?? '',
|
||
|
||
compareFn: (item, selected) {
|
||
if (selected == null) return false;
|
||
return item['id'].toString() ==
|
||
selected['id'].toString();
|
||
},
|
||
|
||
dropdownBuilder: (context, selectedItem) {
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(
|
||
selectedItem != null
|
||
? selectedItem['endorsement_type']
|
||
.toString()
|
||
: '',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 11,
|
||
color: Colors.black,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
);
|
||
},
|
||
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration:
|
||
AppInputDecorations.dropdownDecoration(
|
||
label: "Insurer",
|
||
).copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
contentPadding:
|
||
const EdgeInsets.symmetric(
|
||
horizontal: 5,
|
||
vertical: 1,
|
||
),
|
||
),
|
||
),
|
||
|
||
popupProps: PopupProps.menu(
|
||
showSearchBox: true,
|
||
fit: FlexFit.loose,
|
||
constraints: BoxConstraints(
|
||
maxHeight: 300,
|
||
minWidth: ResponsiveLayout.isMobile(context)
|
||
? 200
|
||
: MediaQuery.of(context).size.width * 0.15,
|
||
),
|
||
menuProps:
|
||
const MenuProps(backgroundColor: Colors.white),
|
||
),
|
||
|
||
onChanged: (val) {
|
||
if (val == null) return;
|
||
|
||
selectedEndorsementTypeVal = val['id'];
|
||
// refresh();
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget buildSelectInsurer(BuildContext context) {
|
||
// Ensure this list has data BEFORE building dropdown
|
||
final List<Map<String, dynamic>> insurerOptions =
|
||
List<Map<String, dynamic>>.from(filteredInsurerData);
|
||
|
||
// Selected item logic
|
||
Map<String, dynamic>? selectedItem;
|
||
|
||
if (selectedInsurerVal != null &&
|
||
selectedInsurerVal.toString().isNotEmpty &&
|
||
insurerOptions.isNotEmpty) {
|
||
try {
|
||
selectedItem = insurerOptions.firstWhere(
|
||
(e) => e['id'].toString() == selectedInsurerVal.toString(),
|
||
);
|
||
} catch (e) {
|
||
selectedItem = null;
|
||
}
|
||
}
|
||
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
height: 33,
|
||
child: Container(
|
||
width: ResponsiveLayout.isMobile(context)
|
||
? null
|
||
: MediaQuery.of(context).size.width * 0.10,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(10 ),
|
||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||
),
|
||
child: DropdownSearch<Map<String, dynamic>>(
|
||
selectedItem: selectedItem,
|
||
|
||
// FILTER LOGIC: This filters the list as the user types
|
||
items: (filter, infiniteScrollProps) {
|
||
if (filter.isEmpty) {
|
||
return insurerOptions;
|
||
}
|
||
return insurerOptions
|
||
.where((item) => item['short_name']
|
||
.toString()
|
||
.toLowerCase()
|
||
.contains(filter.toLowerCase()))
|
||
.toList();
|
||
},
|
||
|
||
itemAsString: (item) => item['short_name']?.toString() ?? '',
|
||
|
||
compareFn: (item, selected) {
|
||
if (selected == null) return false;
|
||
return item['id'].toString() == selected['id'].toString();
|
||
},
|
||
|
||
// FIELD UI (The collapsed state)
|
||
dropdownBuilder: (context, selectedItem) {
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(
|
||
selectedItem != null
|
||
? selectedItem['short_name'].toString()
|
||
: '',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 11,
|
||
color: Colors.black,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
);
|
||
},
|
||
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(
|
||
label: "Select Insurer",
|
||
).copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 5,
|
||
vertical: 1,
|
||
),
|
||
),
|
||
),
|
||
|
||
// POPUP UI (The expanded state with search)
|
||
popupProps: PopupProps.menu(
|
||
showSearchBox: true, // Enabled searching
|
||
fit: FlexFit.loose,
|
||
constraints: BoxConstraints(
|
||
maxHeight: 300,
|
||
minWidth: ResponsiveLayout.isMobile(context)
|
||
? 200
|
||
: MediaQuery.of(context).size.width * 0.10,
|
||
),
|
||
menuProps: const MenuProps(backgroundColor: Colors.white),
|
||
|
||
// Customizing the search field appearance
|
||
searchFieldProps: TextFieldProps(
|
||
style: GoogleFonts.poppins(fontSize: 13),
|
||
decoration: InputDecoration(
|
||
hintText: "Search insurer...",
|
||
hintStyle: GoogleFonts.poppins(fontSize: 12),
|
||
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
),
|
||
),
|
||
|
||
itemBuilder: (context, item, isDisabled, isSelected) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(
|
||
horizontal: 12,
|
||
vertical: 10,
|
||
),
|
||
decoration: BoxDecoration(
|
||
color: isSelected ? Colors.blue.withOpacity(0.1) : Colors.transparent,
|
||
),
|
||
child: Text(
|
||
item['short_name'].toString(),
|
||
style: GoogleFonts.inter(
|
||
fontSize: 13,
|
||
color: Colors.black,
|
||
),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
|
||
// ON CHANGE
|
||
onChanged: (val) {
|
||
if (val == null) return;
|
||
|
||
selectedInsurerVal = val['id'];
|
||
// refresh();
|
||
},
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
|
||
|
||
Widget buildplan(BuildContext context) {
|
||
final List<Map<String, dynamic>> verificationOptions = [
|
||
{'id': 2, 'status': 'All'},
|
||
{'id': 0, 'status': 'To Verify'},
|
||
{'id': 1, 'status': 'Verified'},
|
||
];
|
||
|
||
Map<String, dynamic>? selectedItem;
|
||
|
||
if (selectedVerificationVal != null) {
|
||
try {
|
||
selectedItem = verificationOptions.firstWhere(
|
||
(e) => e['id'] == selectedVerificationVal,
|
||
);
|
||
} catch (_) {
|
||
selectedItem = null;
|
||
}
|
||
}
|
||
|
||
return SizedBox(
|
||
height: 33,
|
||
child: Container(
|
||
width: ResponsiveLayout.isMobile(context)
|
||
? null
|
||
: MediaQuery.of(context).size.width * 0.10,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(10),
|
||
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||
),
|
||
child: DropdownSearch<Map<String, dynamic>>(
|
||
selectedItem: selectedItem,
|
||
items: (filter, infiniteScrollProps) => verificationOptions,
|
||
|
||
itemAsString: (val) => val['status'].toString(),
|
||
|
||
compareFn: (item, selected) {
|
||
if (selected == null) return false;
|
||
return item['id'] == selected['id'];
|
||
},
|
||
|
||
dropdownBuilder: (context, selectedItem) {
|
||
return Align(
|
||
alignment: Alignment.centerLeft,
|
||
child: Text(
|
||
selectedItem != null
|
||
? selectedItem['status'].toString()
|
||
: '',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 11,
|
||
color: Colors.black,
|
||
),
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
),
|
||
);
|
||
},
|
||
|
||
decoratorProps: DropDownDecoratorProps(
|
||
decoration: AppInputDecorations.dropdownDecoration(
|
||
label: "Select Verification",
|
||
).copyWith(
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
isDense: true,
|
||
contentPadding: const EdgeInsets.symmetric(
|
||
horizontal: 5,
|
||
vertical: 1,
|
||
),
|
||
),
|
||
),
|
||
|
||
popupProps: PopupProps.menu(
|
||
showSearchBox: false,
|
||
fit: FlexFit.loose,
|
||
constraints: BoxConstraints(
|
||
maxHeight: 300,
|
||
minWidth: ResponsiveLayout.isMobile(context)
|
||
? 200
|
||
: MediaQuery.of(context).size.width * 0.10,
|
||
),
|
||
menuProps: const MenuProps(backgroundColor: Colors.white),
|
||
),
|
||
|
||
onChanged: (val) {
|
||
if (val == null) return;
|
||
|
||
selectedVerificationVal = val['id'];
|
||
// refresh();
|
||
},
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _iconButton(
|
||
IconData icon, VoidCallback? onTap) {
|
||
return InkWell(
|
||
onTap: onTap,
|
||
child: Container(
|
||
width: 36,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(
|
||
color: const Color(0xFFE5E7EB)),
|
||
color: Colors.white,
|
||
),
|
||
child: Icon(icon,
|
||
size: 18, color: Colors.grey[700]),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _rightSearchField() {
|
||
return Container(
|
||
width: 220,
|
||
height: 36,
|
||
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(8),
|
||
border: Border.all(color: const Color(0xFFE5E7EB)),
|
||
color: Colors.white,
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.search, size: 18, color: Colors.grey),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: TextField(
|
||
controller: rightSearchController,
|
||
decoration: InputDecoration(
|
||
hintText: "Search...",
|
||
border: InputBorder.none,
|
||
isDense: true,
|
||
hintStyle: GoogleFonts.poppins(fontSize: 11),
|
||
),
|
||
style: GoogleFonts.poppins(fontSize: 11),
|
||
onChanged: (val) {
|
||
filterData(val);
|
||
},
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _exportButton() {
|
||
return InkWell(
|
||
onTap: () async {
|
||
await fetchGRidExcelReport();
|
||
},
|
||
child: Container(
|
||
width: 40,
|
||
height: 36,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(8),
|
||
color: const Color(0xFF2E7D6E),
|
||
),
|
||
child:Tooltip(
|
||
message: 'Export',
|
||
child: Image.asset(
|
||
"assets/miscellaneous/export.png",
|
||
height: 13,
|
||
width: 13,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _actionIconButton({
|
||
required BuildContext context,
|
||
IconData? icon,
|
||
Widget? customIcon,
|
||
Widget Function(BuildContext buttonContext)? builderIcon,
|
||
required String tooltip,
|
||
required Color iconColor,
|
||
required VoidCallback? onTap,
|
||
Color? hoverColor, // ✅ Add this parameter
|
||
}) {
|
||
if (builderIcon != null) {
|
||
return Builder(builder: (buttonContext) => builderIcon(buttonContext));
|
||
}
|
||
|
||
return Material(
|
||
color: Colors.transparent,
|
||
child: Tooltip(
|
||
message: tooltip,
|
||
waitDuration: const Duration(milliseconds: 300),
|
||
showDuration: const Duration(seconds: 2),
|
||
child: InkWell(
|
||
onTap: onTap,
|
||
borderRadius: BorderRadius.circular(20),
|
||
hoverColor: hoverColor ?? Colors.grey.shade200, // ✅ default grey
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(5),
|
||
child: customIcon ?? Icon(icon, size: 15, color: iconColor),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
void _openViewModal(Map<String, dynamic> row) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => _PayoutRowViewDialog(row: row),
|
||
);
|
||
}
|
||
|
||
void _openEditModal(Map<String, dynamic> row) {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => _PayoutRowEditDialog(
|
||
initialRow: row,
|
||
canEdit: true,
|
||
onSave: (updated) {
|
||
setState(() {
|
||
final id = updated['id'];
|
||
final idx = _rows.indexWhere(
|
||
(e) => e['id']?.toString() == id?.toString());
|
||
if (idx >= 0) {
|
||
_rows[idx] = updated;
|
||
} else {
|
||
_rows.insert(0, updated);
|
||
}
|
||
});
|
||
ToastHelper.showSuccessToast(context, 'Updated successfully');
|
||
},
|
||
),
|
||
);
|
||
}
|
||
|
||
void _openAddModal() {
|
||
showDialog(
|
||
context: context,
|
||
builder: (context) => const UploadGridModal(),
|
||
).then((result) async {
|
||
// `UploadGridModal` will pop with `true` after successful upload.
|
||
if (result == true && mounted) {
|
||
await _fetchGrid();
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> _exportExcel() async {
|
||
setState(() => _loading = true);
|
||
try {
|
||
await _apiService.downloadgridViewInExcel(
|
||
startDate: null,
|
||
endDate: null,
|
||
endorsementType: _selectedEndorsementType,
|
||
insurer: _selectedInsurer,
|
||
vehicleNo: _selectedVehicle,
|
||
search: _searchValue.trim().isEmpty ? null : _searchValue.trim(),
|
||
);
|
||
} catch (e) {
|
||
ToastHelper.showErrorToast(context, e.toString());
|
||
} finally {
|
||
if (mounted) setState(() => _loading = false);
|
||
}
|
||
}
|
||
|
||
final queryRole = Uri.base.queryParameters['role'];
|
||
final canEdit =
|
||
(queryRole?.toLowerCase() == 'manager') || _isManagerOrAccount;
|
||
|
||
|
||
|
||
|
||
|
||
String formatDateForList(String? dateStr) {
|
||
if (dateStr == null || dateStr.isEmpty || dateStr == '-') return '-';
|
||
|
||
try {
|
||
// Catch invalid dates like "0000-00-00" or "30-11--0001"
|
||
if (dateStr.contains('0000') || dateStr.startsWith('30-11--')) return '-';
|
||
|
||
DateTime parsed = DateTime.parse(dateStr);
|
||
|
||
// Validate year is reasonable
|
||
if (parsed.year < 2000 || parsed.year > 2100) return '-';
|
||
|
||
return DateFormat('dd-MM-yyyy').format(parsed);
|
||
} catch (e) {
|
||
return '-';
|
||
}
|
||
}
|
||
|
||
|
||
static final _dataBold = GoogleFonts.inter(
|
||
fontSize: 11,
|
||
|
||
fontWeight: FontWeight.w400,
|
||
color: Color(0xFF000000),
|
||
);
|
||
|
||
static final _dataSub = GoogleFonts.inter(
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.w300,
|
||
color: Color(0xFF585757),
|
||
);
|
||
|
||
static final _headerStyle = GoogleFonts.poppins(
|
||
fontSize: 11.2,
|
||
fontWeight: FontWeight.w500,
|
||
color: Color(0xFF1E293B),
|
||
);
|
||
static final _cardheaderStyle = GoogleFonts.inter(
|
||
color: Colors.black,
|
||
fontWeight: FontWeight.w600,
|
||
fontSize: 12,
|
||
);
|
||
|
||
static final _cardBodyStyle = GoogleFonts.inter(
|
||
color: const Color(0xFF545454),
|
||
fontWeight: FontWeight.w400,
|
||
fontSize: 12,
|
||
);
|
||
}
|
||
|
||
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// ADD PAYOUT GRID DIALOG – matches Image 1 (Incentive Files modal style)
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
class _gridViewAddDialog extends ConsumerStatefulWidget {
|
||
final Future<void> Function() onAdded;
|
||
|
||
const _gridViewAddDialog({required this.onAdded});
|
||
|
||
@override
|
||
ConsumerState<_gridViewAddDialog> createState() =>
|
||
_gridViewAddDialogState();
|
||
}
|
||
|
||
class _gridViewAddDialogState
|
||
extends ConsumerState<_gridViewAddDialog> {
|
||
final ApiService _apiService = ApiService();
|
||
|
||
bool _submitting = false;
|
||
DateTime? _validFrom;
|
||
PlatformFile? _selectedFile;
|
||
|
||
final TextEditingController _filesSearchController =
|
||
TextEditingController();
|
||
String _filesSearchValue = '';
|
||
|
||
void _refresh() {
|
||
if (_submitting) return;
|
||
setState(() {
|
||
_validFrom = null;
|
||
_selectedFile = null;
|
||
_filesSearchValue = '';
|
||
_filesSearchController.clear();
|
||
});
|
||
}
|
||
|
||
String _formatDateForApi(DateTime date) =>
|
||
date.toIso8601String().split('T').first;
|
||
|
||
@override
|
||
void dispose() {
|
||
_filesSearchController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
Future<void> _pickDate() async {
|
||
final picked = await showDatePicker(
|
||
context: context,
|
||
firstDate: DateTime(2000),
|
||
lastDate: DateTime(2100),
|
||
initialDate: _validFrom ?? DateTime.now(),
|
||
);
|
||
if (picked == null) return;
|
||
setState(() => _validFrom = picked);
|
||
}
|
||
|
||
Future<void> _pickXLFile() async {
|
||
final picked = await FilePicker.platform.pickFiles(
|
||
allowMultiple: false,
|
||
type: FileType.custom,
|
||
allowedExtensions: const ['xls', 'xlsx', 'csv'],
|
||
withData: true,
|
||
);
|
||
if (picked == null || picked.files.isEmpty) return;
|
||
setState(() => _selectedFile = picked.files.first);
|
||
}
|
||
|
||
Future<void> _submit() async {
|
||
if (_validFrom == null) {
|
||
ToastHelper.showErrorToast(context, 'Select valid from date');
|
||
return;
|
||
}
|
||
if (_selectedFile == null) {
|
||
ToastHelper.showErrorToast(context, 'Upload an XL file');
|
||
return;
|
||
}
|
||
|
||
setState(() => _submitting = true);
|
||
try {
|
||
// Requirement: hardcode agent_id to 1 for payout grid upload.
|
||
final agentId = 1;
|
||
final currentUserId = ref.read(userIdProvider);
|
||
|
||
final payload = <String, dynamic>{
|
||
'agent_id': agentId,
|
||
// Backend expects the uploader/creator user id.
|
||
// (Other uploads like incentive use the logged-in user id.)
|
||
'created_by': currentUserId ?? agentId,
|
||
// Backend expects file_type as "grid" only.
|
||
'file_type': 'grid',
|
||
// Backend expects date under `incentive_month`.
|
||
'incentive_month': _formatDateForApi(_validFrom!),
|
||
};
|
||
|
||
final res = await _apiService.uploadgridViewFile(
|
||
file: _selectedFile!,
|
||
data: payload,
|
||
);
|
||
|
||
if ((res['status'] ?? '').toString().toLowerCase() == 'success') {
|
||
Navigator.pop(context);
|
||
await widget.onAdded();
|
||
} else {
|
||
ToastHelper.showErrorToast(
|
||
context, res['data']?.toString() ?? 'Upload failed');
|
||
}
|
||
} catch (e) {
|
||
ToastHelper.showErrorToast(context, e.toString());
|
||
} finally {
|
||
if (mounted) setState(() => _submitting = false);
|
||
}
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
Widget _buildSelectedFilesGrid() {
|
||
final files = _getSelectedFiles();
|
||
final q = _filesSearchValue.trim().toLowerCase();
|
||
|
||
final filtered = q.isEmpty
|
||
? files
|
||
: files.where((f) {
|
||
final name = (f['file_name'] ?? '').toString().toLowerCase();
|
||
final month = (f['month'] ?? '').toString().toLowerCase();
|
||
return name.contains(q) || month.contains(q);
|
||
}).toList();
|
||
|
||
if (filtered.isEmpty) {
|
||
return const Center(child: Text('No files found'));
|
||
}
|
||
|
||
return LayoutBuilder(
|
||
builder: (context, constraints) {
|
||
final fileAreaWidth = constraints.maxWidth;
|
||
final crossAxisCount = fileAreaWidth < 520 ? 1 : 2;
|
||
const spacing = 10.0;
|
||
const targetTileHeight = 70.0;
|
||
|
||
final tileWidth = (fileAreaWidth -
|
||
spacing * (crossAxisCount - 1) -
|
||
16) /
|
||
crossAxisCount;
|
||
final aspectRatio = (tileWidth / targetTileHeight).clamp(3.2, 12.0);
|
||
|
||
return GridView.builder(
|
||
padding: const EdgeInsets.all(8),
|
||
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
|
||
crossAxisCount: crossAxisCount,
|
||
crossAxisSpacing: spacing,
|
||
mainAxisSpacing: spacing,
|
||
childAspectRatio: aspectRatio,
|
||
),
|
||
itemCount: filtered.length,
|
||
itemBuilder: (context, index) {
|
||
final f = filtered[index];
|
||
return gridViewFileRow(
|
||
fileName: f['file_name']?.toString() ?? 'Unknown',
|
||
date: f['month']?.toString() ?? '-',
|
||
onUpload: () async {
|
||
try {
|
||
await _apiService.downloadLatestgridView();
|
||
} catch (_) {}
|
||
},
|
||
onDelete: _refresh,
|
||
);
|
||
},
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
InputDecoration _inputDecoration(String hint) {
|
||
return InputDecoration(
|
||
hintText: hint,
|
||
hintStyle: GoogleFonts.inter(
|
||
fontSize: 12, color: const Color(0xFFCBD5E1)),
|
||
contentPadding:
|
||
const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||
),
|
||
enabledBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide: const BorderSide(color: Color(0xFFE2E8F0)),
|
||
),
|
||
focusedBorder: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6),
|
||
borderSide:
|
||
const BorderSide(color: Color(0xFF2E7D6E), width: 1.5),
|
||
),
|
||
filled: true,
|
||
fillColor: Colors.white,
|
||
);
|
||
}
|
||
}
|
||
|
||
final _labelStyle = GoogleFonts.poppins(
|
||
color: Colors.black,
|
||
fontWeight: FontWeight.w400,
|
||
fontSize: 12,
|
||
);
|
||
|
||
class gridViewFileRow extends StatelessWidget {
|
||
final String fileName;
|
||
final String date;
|
||
final VoidCallback? onUpload;
|
||
final VoidCallback? onDelete;
|
||
|
||
const gridViewFileRow({
|
||
super.key,
|
||
required this.fileName,
|
||
required this.date,
|
||
this.onUpload,
|
||
this.onDelete,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
border: Border.all(color: const Color(0xFFE3E3E3)),
|
||
borderRadius: BorderRadius.circular(6),
|
||
),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.center,
|
||
children: [
|
||
Container(
|
||
padding: const EdgeInsets.all(6.0),
|
||
decoration: BoxDecoration(
|
||
color: const Color(0xFFF4F6F8),
|
||
borderRadius: BorderRadius.circular(6.0),
|
||
border: Border.all(color: const Color(0xFFE3E3E3)),
|
||
),
|
||
child: const Icon(
|
||
Icons.file_present,
|
||
color: Color(0xFF838587),
|
||
size: 18,
|
||
),
|
||
),
|
||
const SizedBox(width: 10),
|
||
Expanded(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Text(
|
||
fileName,
|
||
style: GoogleFonts.poppins(
|
||
fontWeight: FontWeight.w400,
|
||
fontSize: 12,
|
||
),
|
||
overflow: TextOverflow.ellipsis,
|
||
maxLines: 1,
|
||
),
|
||
const SizedBox(height: 2),
|
||
Text(
|
||
date,
|
||
style: GoogleFonts.poppins(
|
||
color: const Color(0xFF6E6E6E),
|
||
fontSize: 10,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
const SizedBox(width: 8),
|
||
Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: onUpload,
|
||
borderRadius: BorderRadius.circular(20),
|
||
child: const Padding(
|
||
padding: EdgeInsets.all(6),
|
||
child: Icon(
|
||
Icons.file_download_outlined,
|
||
color: Color(0xFF6E6E6E),
|
||
size: 20,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
Material(
|
||
color: Colors.transparent,
|
||
child: InkWell(
|
||
onTap: onDelete,
|
||
borderRadius: BorderRadius.circular(20),
|
||
child: const Padding(
|
||
padding: EdgeInsets.all(6),
|
||
child: Icon(
|
||
Icons.delete_outlined,
|
||
color: Color(0xFF6E6E6E),
|
||
size: 20,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// VIEW DIALOG
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
class _PayoutRowViewDialog extends StatelessWidget {
|
||
final Map<String, dynamic> row;
|
||
|
||
const _PayoutRowViewDialog({required this.row});
|
||
|
||
String _val(dynamic v) {
|
||
final s = v?.toString().trim() ?? '';
|
||
return s.isEmpty ? '-' : s;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
const fields = [
|
||
'id', 'vehicle_type', 'fuel', 'insurer', 'rto', 'broker_name',
|
||
'segment', 'comp', 'tp', 'od', 'remarks',
|
||
'partner_comp', 'partner_tp', 'partner_od',
|
||
];
|
||
|
||
final labels = {
|
||
'id': 'ID', 'vehicle_type': 'Vehicle Type', 'fuel': 'Fuel',
|
||
'insurer': 'Insurer', 'rto': 'RTO', 'broker_name': 'Broker',
|
||
'segment': 'Segment', 'comp': 'Comp', 'tp': 'TP', 'od': 'OD',
|
||
'remarks': 'Remarks', 'partner_comp': 'Partner Comp',
|
||
'partner_tp': 'Partner TP', 'partner_od': 'Partner OD',
|
||
};
|
||
|
||
return AlertDialog(
|
||
backgroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
title: Text('Payout Grid Details',
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
color: const Color(0xFF50A398))),
|
||
content: ConstrainedBox(
|
||
constraints: const BoxConstraints(maxWidth: 500),
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: fields.map((k) {
|
||
return Padding(
|
||
padding: const EdgeInsets.symmetric(vertical: 5),
|
||
child: Row(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: [
|
||
SizedBox(
|
||
width: 110,
|
||
child: Text('${labels[k] ?? k}:',
|
||
style: GoogleFonts.inter(
|
||
fontSize: 12,
|
||
fontWeight: FontWeight.w500,
|
||
color: const Color(0xFF475569))),
|
||
),
|
||
Expanded(
|
||
child: Text(_val(row[k]),
|
||
style: GoogleFonts.inter(
|
||
fontSize: 12, color: Colors.black87)),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text('Close',
|
||
style: GoogleFonts.inter(
|
||
fontSize: 12, color: const Color(0xFF2E7D6E))),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
// EDIT DIALOG
|
||
// ══════════════════════════════════════════════════════════════════════════════
|
||
class _PayoutRowEditDialog extends StatefulWidget {
|
||
final Map<String, dynamic> initialRow;
|
||
final bool isNew;
|
||
final bool canEdit;
|
||
final ValueChanged<Map<String, dynamic>> onSave;
|
||
|
||
const _PayoutRowEditDialog({
|
||
super.key,
|
||
required this.initialRow,
|
||
this.isNew = false,
|
||
required this.canEdit,
|
||
required this.onSave,
|
||
});
|
||
|
||
@override
|
||
State<_PayoutRowEditDialog> createState() => _PayoutRowEditDialogState();
|
||
}
|
||
|
||
class _PayoutRowEditDialogState extends State<_PayoutRowEditDialog> {
|
||
late final Map<String, TextEditingController> _ctrls;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
final r = widget.initialRow;
|
||
_ctrls = {
|
||
'id': TextEditingController(
|
||
text: r['id']?.toString().trim().isNotEmpty == true
|
||
? r['id'].toString()
|
||
: DateTime.now().millisecondsSinceEpoch.toString()),
|
||
'vehicle_type': TextEditingController(
|
||
text: r['vehicle_type']?.toString() ?? ''),
|
||
'fuel': TextEditingController(text: r['fuel']?.toString() ?? ''),
|
||
'insurer': TextEditingController(
|
||
text: r['insurer']?.toString() ?? ''),
|
||
'rto': TextEditingController(text: r['rto']?.toString() ?? ''),
|
||
'broker_name': TextEditingController(
|
||
text: r['broker_name']?.toString() ?? ''),
|
||
'segment': TextEditingController(
|
||
text: r['segment']?.toString() ?? ''),
|
||
'comp': TextEditingController(text: r['comp']?.toString() ?? ''),
|
||
'tp': TextEditingController(text: r['tp']?.toString() ?? ''),
|
||
'od': TextEditingController(text: r['od']?.toString() ?? ''),
|
||
'remarks': TextEditingController(
|
||
text: r['remarks']?.toString() ?? ''),
|
||
'partner_comp': TextEditingController(
|
||
text: r['partner_comp']?.toString() ?? ''),
|
||
'partner_tp': TextEditingController(
|
||
text: r['partner_tp']?.toString() ?? ''),
|
||
'partner_od': TextEditingController(
|
||
text: r['partner_od']?.toString() ?? ''),
|
||
};
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
for (final c in _ctrls.values) {
|
||
c.dispose();
|
||
}
|
||
super.dispose();
|
||
}
|
||
|
||
dynamic _val(String key) {
|
||
final t = _ctrls[key]!.text.trim();
|
||
return t.isEmpty ? null : t;
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final title =
|
||
widget.isNew ? 'Add Payout Grid' : 'Edit Payout Grid';
|
||
|
||
final fieldDefs = [
|
||
('ID', 'id', false, 1),
|
||
('Vehicle Type', 'vehicle_type', true, 1),
|
||
('Fuel', 'fuel', true, 1),
|
||
('Insurer', 'insurer', true, 1),
|
||
('RTO', 'rto', true, 1),
|
||
('Broker', 'broker_name', true, 1),
|
||
('Segment', 'segment', true, 1),
|
||
('Comp', 'comp', true, 1),
|
||
('TP', 'tp', true, 1),
|
||
('OD', 'od', true, 1),
|
||
('Remarks', 'remarks', true, 3),
|
||
('Partner Comp', 'partner_comp', true, 1),
|
||
('Partner TP', 'partner_tp', true, 1),
|
||
('Partner OD', 'partner_od', true, 1),
|
||
];
|
||
|
||
return AlertDialog(
|
||
backgroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(10)),
|
||
title: Text(title,
|
||
style: GoogleFonts.poppins(
|
||
fontSize: 14,
|
||
fontWeight: FontWeight.w500,
|
||
color: const Color(0xFF50A398))),
|
||
content: ConstrainedBox(
|
||
constraints: const BoxConstraints(maxWidth: 500, maxHeight: 500),
|
||
child: SingleChildScrollView(
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||
children: fieldDefs.map((def) {
|
||
final (label, key, enabled, maxLines) = def;
|
||
return Padding(
|
||
padding: const EdgeInsets.only(bottom: 10),
|
||
child: TextField(
|
||
controller: _ctrls[key],
|
||
enabled: enabled,
|
||
maxLines: maxLines,
|
||
style: GoogleFonts.inter(fontSize: 12),
|
||
decoration: InputDecoration(
|
||
labelText: label,
|
||
labelStyle: GoogleFonts.inter(fontSize: 12),
|
||
border: OutlineInputBorder(
|
||
borderRadius: BorderRadius.circular(6)),
|
||
isDense: true,
|
||
),
|
||
),
|
||
);
|
||
}).toList(),
|
||
),
|
||
),
|
||
),
|
||
actions: [
|
||
TextButton(
|
||
onPressed: () => Navigator.pop(context),
|
||
child: Text('Cancel',
|
||
style: GoogleFonts.inter(
|
||
fontSize: 12, color: const Color(0xFF64748B))),
|
||
),
|
||
ElevatedButton(
|
||
style: ElevatedButton.styleFrom(
|
||
backgroundColor: const Color(0xFF2E7D6E),
|
||
foregroundColor: Colors.white,
|
||
shape: RoundedRectangleBorder(
|
||
borderRadius: BorderRadius.circular(6)),
|
||
),
|
||
onPressed: widget.canEdit
|
||
? () {
|
||
final updated = {
|
||
for (final k in _ctrls.keys) k: _val(k),
|
||
};
|
||
widget.onSave(updated);
|
||
Navigator.pop(context);
|
||
}
|
||
: null,
|
||
child:
|
||
Text('Save', style: GoogleFonts.inter(fontSize: 12)),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
*/ |