nhance_partner/lib/presentation/screens/Grid/gridView.dart
2026-04-08 15:55:08 +05:30

5394 lines
174 KiB
Dart
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 pervehicle-type retention for the logged-in agent, or for Accounts with `?agent_id=` on grid URL.
Future<void> _loadPartnerRetentionByVehicle() async {
final appRole =
(ref.read(userRoleProvider) ?? '').toString().trim().toLowerCase();
String? agentPk;
if (appRole == 'agent') {
try {
final token = await AuthService.getToken();
if (token == null || token.isEmpty) return;
final data = Jwt.parseJwt(token)['data'];
if (data is Map) {
agentPk = (data['id']?.toString() ?? '').trim();
if (agentPk.isEmpty) agentPk = null;
}
} catch (_) {
return;
}
} else if (appRole == 'accounts') {
agentPk = Uri.base.queryParameters['agent_id']?.trim();
if (agentPk != null && agentPk.isEmpty) agentPk = null;
}
if (agentPk == null || agentPk.isEmpty) {
if (mounted) {
setState(() {
_retentionByVehicleKey.clear();
_grid = _applyRetentionToGridRows(_grid);
_filteredGrid = _applyRetentionToGridRows(_filteredGrid);
});
}
return;
}
try {
final res = await _apiService.findSingleAgentData(agentPk);
if ((res['status'] ?? '').toString().toLowerCase() != 'success' ||
res['data'] == null) {
return;
}
final data = Map<String, dynamic>.from(res['data'] as Map);
final raw = data['retention_by_vehicle'] ?? data['retentionByVehicle'];
final next = <String, num>{};
if (raw is List) {
for (final item in raw) {
if (item is! Map) continue;
final m = Map<String, dynamic>.from(item);
final rate = _toNum(m['retention_rate'] ?? m['retentionRate']);
if (rate == null) continue;
final name = (m['vehicle_type'] ?? '').toString().trim();
if (name.isNotEmpty) {
next[_normalizeVehicleTypeKey(name)] = rate;
}
final vid =
int.tryParse((m['vehicle_type_id'] ?? m['vehicleTypeId'] ?? '')
.toString());
if (vid != null) {
next['id:$vid'] = rate;
}
}
}
if (mounted) {
setState(() {
_retentionByVehicleKey
..clear()
..addAll(next);
_grid = _applyRetentionToGridRows(_grid);
_filteredGrid = _applyRetentionToGridRows(_filteredGrid);
});
}
} catch (_) {
if (mounted) {
setState(() {
_retentionByVehicleKey.clear();
});
}
}
}
String? _apiRoleFromAppRole(String? role) {
if (role == null) return null;
final r = role.trim();
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 pervehicle 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 partners payouts.
bool get _accountsAgentPreview {
final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase();
if (role != 'accounts') return false;
final aid = Uri.base.queryParameters['agent_id']?.trim();
return aid != null && aid.isNotEmpty;
}
bool get _showPartnerRetentionValues =>
_showAddButton || _accountsAgentPreview;
bool get _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)),
),
],
);
}
}
*/