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 createState() => _gridViewScreenState(); } class _gridViewScreenState extends ConsumerState { 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> _grid = []; List> _filteredGrid = []; List _insurers = []; List _rtos = []; List _vehicleTypes = []; List _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:` when the API returns `vehicle_type_id`. final Map _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 _bootstrapGrid() async { await Future.wait([ _loadRetentionRateFromToken(), _loadPartnerRetentionByVehicle(), ]); if (!mounted) return; await _fetch(); } Future _loadRetentionRateFromToken() async { try { final token = await AuthService.getToken(); if (token == null || token.isEmpty) return; final decodedToken = Jwt.parseJwt(token); final data = decodedToken['data']; if (data is! Map) return; final retentionRate = data['retention_rate']?.toString(); final loggedId = data['id']?.toString(); if (mounted) { setState(() { _loggedId = (loggedId == null || loggedId.trim().isEmpty) ? null : loggedId.trim(); if (retentionRate != null && retentionRate.trim().isNotEmpty) { _retentionRate = retentionRate.trim(); } else { _retentionRate = '-'; } _grid = _applyRetentionToGridRows(_grid); _filteredGrid = _applyRetentionToGridRows(_filteredGrid); }); } } catch (_) { // Keep fallback "-" when token parsing fails. } } String _normalizeVehicleTypeKey(String raw) { return raw.toLowerCase().trim().replaceAll(RegExp(r'\s+'), ' '); } /// Loads per–vehicle-type retention for the logged-in agent, or for Accounts with `?agent_id=` on grid URL. Future _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.from(res['data'] as Map); final raw = data['retention_by_vehicle'] ?? data['retentionByVehicle']; final next = {}; if (raw is List) { for (final item in raw) { if (item is! Map) continue; final m = Map.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 _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>.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>( (e) => (e as Map).cast()) .toList() : >[]; _grid = _applyRetentionToGridRows(rawGrid); _filteredGrid = List>.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 _extractPayload(Map res) { final data = res['data']; if (data is Map) return data; if (data is Map) return data.cast(); return res; } List _toStringList(dynamic raw) { if (raw is! List) return []; 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 = [ 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> 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> 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 row) { final vt = row['vehicle_type']?.toString().trim() ?? ''; if (vt.isNotEmpty) { final byName = _retentionByVehicleKey[_normalizeVehicleTypeKey(vt)]; if (byName != null) return byName; } final vid = int.tryParse(row['vehicle_type_id']?.toString() ?? ''); if (vid != null) { final byId = _retentionByVehicleKey['id:$vid']; if (byId != null) return byId; } return null; } /// Accounts preview: subtract loaded per–vehicle retention; ≤0 → '-'; no match → show base. String _partnerValueForRowPreview(Map 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> _applyRetentionToGridRows( List> rows, ) { if (!_showPartnerRetentionValues) { return rows .map( (row) => Map.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.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.from(row); next['partner_comp'] = _partnerValueForRowPreview(next, next['comp']); next['partner_tp'] = _partnerValueForRowPreview(next, next['tp']); next['partner_od'] = _partnerValueForRowPreview(next, next['od']); return next; }).toList(); } /// Edit column: Manager & Accounts only (not Agent). bool get _showActionColumn { final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); return role == 'manager' || role == 'accounts'; } bool get _showAddButton { final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); return role == 'agent'; } /// Partner (agent) login, or Accounts opening grid with `?agent_id=` to preview that partner’s payouts. bool get _accountsAgentPreview { final role = (ref.watch(userRoleProvider) ?? '').toString().trim().toLowerCase(); if (role != 'accounts') return false; final aid = Uri.base.queryParameters['agent_id']?.trim(); return aid != null && aid.isNotEmpty; } bool get _showPartnerRetentionValues => _showAddButton || _accountsAgentPreview; bool get _hasAnyFilterSelection { return (_selectedInsurer != null && _selectedInsurer!.trim().isNotEmpty) || (_selectedRto != null && _selectedRto!.trim().isNotEmpty) || (_selectedVehicleType != null && _selectedVehicleType!.trim().isNotEmpty) || (_selectedSegment != null && _selectedSegment!.trim().isNotEmpty); } String _toEditText(dynamic value) { if (value == null) return ''; final s = value.toString().trim(); if (s.isEmpty || s.toLowerCase() == 'null') return ''; return s; } void _setEditControllersFromRow(Map 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( 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( 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( 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( 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 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 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 createState() => _gridViewScreenState(); } class _gridViewScreenState extends ConsumerState { final ApiService _apiService = ApiService(); bool _isLoading = false; // API response List> _grid = []; List> _filteredGrid = []; List _insurers = []; List> _planTypes = []; List _rtos = []; List _vehicleTypes = []; List _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 _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>.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>( (e) => (e as Map).cast()) .toList() : >[]; _filteredGrid = List>.from(_grid); _insurers = insurers is List ? insurers.map((e) => e.toString()).toList() : []; _planTypes = planTypes is List ? planTypes .map>( (e) => (e as Map).cast()) .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 = [ 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> 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> 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 = >[ {'value': null, 'label': 'All'}, ..._planTypes, ]; Widget dropdownInsurer() { return SizedBox( width: isMobile ? double.infinity : 200, height: 40, child: DropdownSearch( 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>( 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( 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( 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( 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 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 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 createState() => _gridViewScreenState(); } class _gridViewScreenState extends ConsumerState { final ApiService _apiService = ApiService(); bool _isLoading = false; // API response List> _grid = []; List> _filteredGrid = []; List _insurers = []; List> _planTypes = []; List _rtos = []; List _vehicleTypes = []; List _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 _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>.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>((e) => (e as Map).cast()).toList() : >[]; _filteredGrid = List>.from(_grid); _insurers = insurers is List ? insurers.map((e) => e.toString()).toList() : []; _planTypes = planTypes is List ? planTypes .map>((e) => (e as Map).cast()) .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> 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> 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> 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 = >[ {'value': null, 'label': 'All'}, ..._planTypes, ]; Widget dropdownInsurer() { return SizedBox( width: isMobile ? double.infinity : (isCompactFilter ? 180 : 200), height: fieldHeight, child: DropdownSearch( 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>( 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( 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( 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( 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 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 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 createState() => gridViewScreenState(); } class gridViewScreenState extends ConsumerState { int currentPage = 1; int itemsPerPage = 10; late ApiService apiService; // List> dataVal = []; List> getStaffData = []; List> originalData = []; List> filteredData = []; bool isLoading = false; dynamic userId; dynamic roleId; dynamic managerId; // bool isDrawerOpen = false; Map? selectedRow; List> endorsementNumbers = []; List> getEndrosmentType = []; List> 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 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>.from(data); } else if (data is Map) { getStaffData = [ Map.from(data) ]; } else { getStaffData = []; } originalData = getStaffData; filteredData = List.from(originalData); }); } else { getStaffData = []; originalData = []; } } catch (e) { print('Exception occurred: $e'); } finally { setState(() { isLoading = false; }); } } List 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 _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> getInsurerDetailsData = []; List> 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 _buildPopupMenuActions(BuildContext context, Map 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 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 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>.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 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>.from(response['data']); filteredInsurerData = List>.from(getInsurerDetailsData); print("Filtered Insurer Data: $filteredInsurerData"); }); } else { setState(() { getInsurerDetailsData = []; filteredInsurerData = []; }); } } catch (e) { print('Error: $e'); } finally { setState(() => isLoadingA = false); } } Future loadEndorsementNumbers(String id) async { // final response = await apiService.fetchEndorsementNumbers(id); // // if (response['status'] == 'success') { // setState(() { // endorsementNumbers = // List>.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 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 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( 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> endorsementOptions = List>.from(filteredEndrosmentData); Map? 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>( 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> insurerOptions = List>.from(filteredInsurerData); // Selected item logic Map? 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>( 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> verificationOptions = [ {'id': 2, 'status': 'All'}, {'id': 0, 'status': 'To Verify'}, {'id': 1, 'status': 'Verified'}, ]; Map? 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>( 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 row) { showDialog( context: context, builder: (context) => _PayoutRowViewDialog(row: row), ); } void _openEditModal(Map 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 _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 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 _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 _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 _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 = { '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 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 initialRow; final bool isNew; final bool canEdit; final ValueChanged> 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 _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)), ), ], ); } } */