nhance_partner/lib/presentation/screens/Enquiry/policy_claims_endros/claims.dart
2026-06-09 09:11:30 +05:30

2077 lines
62 KiB
Dart

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:intl/intl.dart';
import 'package:nhance_partner/presentation/screens/Enquiry/policy_claims_endros/add_claim_stepper_dialog.dart';
import 'package:nhance_partner/presentation/providers/userRoleProvider.dart';
import '../../../../core/services/api_service.dart';
import '../../../../data/utils/Pagination.dart';
import '../../../../data/utils/toastNotification.dart';
import '../../../layouts/main_layout.dart';
import '../../../layouts/responsive_layout.dart';
import '../../../providers/manager_provider.dart';
import '../../../themes/indicators/date_range_picker_field.dart';
import '../../../themes/indicators/export_btn.dart';
import '../../../themes/indicators/search_field_theme.dart';
class claimList extends ConsumerStatefulWidget {
const claimList({super.key});
@override
ConsumerState<claimList> createState() => claimListState();
}
class claimListState extends ConsumerState<claimList> {
static const Color _teal = Color(0xFF14A492);
static const Color _surfaceGrey = Color(0xFFF1F5F9);
static const Color _border = Color(0xFFE2E8F0);
static const Color _muted = Color(0xFF64748B);
static const Color _filterFocusTeal = Color(0xFF50A398);
static const Color _actionDownloadBlue = Color(0xFF1565C0);
int currentPage = 1;
int itemsPerPage = 10;
late ApiService apiService;
List<Map<String, dynamic>> getStaffData = [];
List<Map<String, dynamic>> originalData = [];
List<Map<String, dynamic>> filteredData = [];
bool isLoading = false;
final TextEditingController _quickSearchController = TextEditingController();
final TextEditingController _startDateController = TextEditingController();
final TextEditingController _endDateController = TextEditingController();
final ScrollController _tableVScrollController = ScrollController();
dynamic userId;
dynamic roleId;
dynamic managerId;
// bool _pendingTab = true;
List<Map<String, dynamic>> _masterClaimTypeMaps = [];
List<Map<String, dynamic>> _masterInsurerMaps = [];
List<Map<String, dynamic>> _masterClaimStatusMaps = [];
String? _filterClaimType;
String? _filterInsurer;
/// Selected claim status master id (stored in filter choice `v`).
String? _filterStatus;
@override
void initState() {
super.initState();
apiService = ApiService();
Future.microtask(() {
managerId = ref.read(managerIdProvider);
roleId = ref.read(userRoleProvider);
userId = ref.read(userIdProvider);
if (userId != null) {
loadClaimsList();
}
_loadFilterMasters();
});
}
Future<void> _loadFilterMasters() async {
try {
final claimR = await apiService.fetchMasterDropDown('Claim');
if ((claimR['status'] == 'success' || claimR['status'] == 200) &&
claimR['data'] is List) {
if (mounted) {
setState(() {
_masterClaimTypeMaps =
List<Map<String, dynamic>>.from(claimR['data']);
});
}
}
final insR = await apiService.fetchMasterDropDown('Insurers');
if ((insR['status'] == 'success' || insR['status'] == 200) &&
insR['data'] is List) {
if (mounted) {
setState(() {
_masterInsurerMaps =
List<Map<String, dynamic>>.from(insR['data']);
});
}
}
final statusR = await apiService.fetchClaimStatusList();
if ((statusR['status'] == 'success' || statusR['status'] == 200) &&
statusR['data'] is List &&
(statusR['data'] as List).isNotEmpty) {
if (mounted) {
setState(() {
_masterClaimStatusMaps =
List<Map<String, dynamic>>.from(statusR['data']);
});
}
}
} catch (_) {}
}
@override
void dispose() {
_quickSearchController.dispose();
_startDateController.dispose();
_endDateController.dispose();
_tableVScrollController.dispose();
super.dispose();
}
String? _apiDateFromPicker(String text) {
final t = text.trim();
if (t.isEmpty) return null;
try {
final d = DateRangePickerField.storageFormat.parse(t);
return DateFormat('yyyy-MM-dd').format(d);
} catch (_) {
return null;
}
}
int? _claimTypeIdForFilter() {
if (_filterClaimType == null || _filterClaimType!.trim().isEmpty) {
return null;
}
return int.tryParse(_filterClaimType!.trim());
}
int? _insurerIdForFilter() {
if (_filterInsurer == null || _filterInsurer!.trim().isEmpty) return null;
return int.tryParse(_filterInsurer!.trim());
}
int? _claimStatusIdForFilter() {
if (_filterStatus == null || _filterStatus!.trim().isEmpty) return null;
return int.tryParse(_filterStatus!.trim());
}
Future<void> loadClaimsList() async {
if (!mounted) return;
setState(() => isLoading = true);
try {
final role = roleId?.toString().toLowerCase() ?? '';
final mid = int.tryParse(managerId?.toString() ?? '');
final uid = int.tryParse(userId?.toString() ?? '');
final response = await apiService.fetchClaims(
managerId: (role == 'manager' || role == 'accounts') ? mid : null,
agentId: role == 'agent' ? uid : null,
handlerId: role == 'handler' ? uid : null,
staffId: (role != 'manager' &&
role != 'accounts' &&
role != 'agent' &&
role != 'handler')
? uid
: null,
claimTypeId: _claimTypeIdForFilter(),
claimStatusId: _claimStatusIdForFilter(),
insurerId: _insurerIdForFilter(),
fromDate: _apiDateFromPicker(_startDateController.text),
toDate: _apiDateFromPicker(_endDateController.text),
);
if (!mounted) return;
if (response['status'] == 'success' ||
response['status'] == 200 ||
response['status'] == '200') {
final data = response['data'];
setState(() {
if (data is List) {
getStaffData = data
.whereType<Map>()
.map((e) => Map<String, dynamic>.from(e))
.toList();
} else if (data is Map) {
getStaffData = [Map<String, dynamic>.from(data)];
} else {
getStaffData = [];
}
originalData = List<Map<String, dynamic>>.from(getStaffData);
_rebuildFiltered();
});
} else {
setState(() {
getStaffData = [];
originalData = [];
filteredData = [];
});
}
} catch (e) {
debugPrint('Claims list error: $e');
if (mounted) {
setState(() {
getStaffData = [];
originalData = [];
filteredData = [];
});
}
} finally {
if (mounted) setState(() => isLoading = false);
}
}
static String _pickStr(Map<String, dynamic> row, List<String> keys) {
for (final k in keys) {
final v = row[k];
if (v != null && v.toString().trim().isNotEmpty) {
return v.toString();
}
}
return '-';
}
// bool _isSettled(Map<String, dynamic> item) {
// final raw = _pickStr(item, ['claim_status_id']);
// final id = int.tryParse(raw);
// return id == 65;
// }
// bool _matchesTab(Map<String, dynamic> item) {
// return _pendingTab ? !_isSettled(item) : _isSettled(item);
// }
DateTime? _parseCreated(Map<String, dynamic> item) {
final raw = _pickStr(item, [
'created_at',
'created_date',
'created_on',
'inserted_at',
]);
if (raw == '-') return null;
try {
return DateTime.parse(raw);
} catch (_) {
return null;
}
}
void _addSearchText(List<String> out, String? value) {
if (value == null) return;
final s = value.trim();
if (s.isEmpty || s == '-') return;
out.add(s);
}
void _addSearchDate(List<String> out, Map<String, dynamic> item, List<String> keys) {
final raw = _pickStr(item, keys);
if (raw == '-') return;
_addSearchText(out, raw);
_addSearchText(out, _formatDate(raw));
}
List<String> _searchableTexts(Map<String, dynamic> item) {
final texts = <String>[];
for (final value in item.values) {
_addSearchText(texts, value?.toString());
}
_addSearchText(texts, _pickStr(item, ['policy_no', 'policy_number']));
_addSearchText(texts, _pickStr(item, ['claim_number']));
_addSearchText(texts, _pickStr(item, ['broker_name', 'broker']));
_addSearchText(
texts,
_pickStr(item, ['agent_name', 'partner_name', 'partner']),
);
_addSearchDate(texts, item, [
'date_of_incident',
'accident_date',
'accident_datetime',
'date_of_accident',
]);
_addSearchDate(texts, item, [
'created_at',
'created_date',
'created_on',
'inserted_at',
]);
return texts;
}
bool _matchesQuickSearch(Map<String, dynamic> item, String query) {
if (query.isEmpty) return true;
final q = query.toLowerCase();
return _searchableTexts(item).any((t) => t.toLowerCase().contains(q));
}
void _rebuildFiltered() {
final q = _quickSearchController.text.trim();
filteredData = originalData.where((item) {
return _matchesQuickSearch(item, q);
}).toList();
}
void _applyFilterBar() {
setState(() => currentPage = 1);
loadClaimsList();
}
void _onQuickSearch(String _) {
setState(() {
_rebuildFiltered();
currentPage = 1;
});
}
// void _setTab(bool pending) {
// if (_pendingTab == pending) return;
// setState(() {
// _pendingTab = pending;
// _rebuildFiltered();
// currentPage = 1;
// });
// }
// int get _pendingCount =>
// originalData.where((e) => !_isSettled(e)).length;
// int get _settledCount => originalData.where(_isSettled).length;
bool get _canShowDeleteAction {
final role = roleId?.toString().trim().toLowerCase() ?? '';
return role == 'manager' || role == 'accounts' || role == 'account';
}
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 Claim',
style: GoogleFonts.poppins(fontSize: 15, fontWeight: FontWeight.w600),
),
],
),
content: Text(
'Are you sure you want to delete this claim?\nThis action cannot be undone.',
style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade700),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(ctx),
child: Text(
'Cancel',
style: GoogleFonts.poppins(fontSize: 13, color: Colors.grey.shade600),
),
),
ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.red.shade600,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: () async {
Navigator.pop(ctx);
await _deleteClaim(id);
},
child: Text(
'Delete',
style: GoogleFonts.poppins(fontSize: 13, color: Colors.white),
),
),
],
),
);
}
Future<void> _deleteClaim(String id) async {
if (id.isEmpty || id == '-') {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Invalid claim id.',
style: GoogleFonts.poppins(fontSize: 13),
),
backgroundColor: Colors.red.shade600,
behavior: SnackBarBehavior.floating,
),
);
return;
}
final updatedBy = userId?.toString().trim();
if (updatedBy == null || updatedBy.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'User id not available.',
style: GoogleFonts.poppins(fontSize: 13),
),
backgroundColor: Colors.red.shade600,
behavior: SnackBarBehavior.floating,
),
);
return;
}
try {
setState(() => isLoading = true);
final response = await apiService.deleteClaim(
id: id,
updatedBy: updatedBy,
);
if (response['status'] == 'success' || response['code'] == 200) {
refresh();
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(
'Claim 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']?.toString() ?? 'Failed to delete claim.',
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 {
if (mounted) setState(() => isLoading = false);
}
}
List<String> _uniqueFrom(String Function(Map<String, dynamic>) pick) {
final s = <String>{};
for (final r in originalData) {
final v = pick(r);
if (v != '-') s.add(v);
}
return s.toList()..sort();
}
List<Map<String, dynamic>> get _paginatedSorted {
final sorted = [...filteredData]
..sort((a, b) {
final ida = int.tryParse(_pickStr(a, ['id'])) ?? 0;
final idb = int.tryParse(_pickStr(b, ['id'])) ?? 0;
return idb.compareTo(ida);
});
if (sorted.isEmpty) return [];
final maxPage = (sorted.length / itemsPerPage).ceil();
final safePage = currentPage.clamp(1, maxPage);
final start = (safePage - 1) * itemsPerPage;
final end = (start + itemsPerPage).clamp(0, sorted.length);
return sorted.sublist(start, end);
}
DateTime? _parseClaimDateTime(String raw) {
final t = raw.trim();
if (t.isEmpty || t == '-') return null;
final patterns = <DateFormat>[
DateFormat('dd-MM-yyyy hh:mm a'),
DateFormat('dd-MM-yyyy HH:mm'),
DateFormat('dd-MM-yyyy'),
DateFormat('yyyy-MM-dd HH:mm:ss'),
DateFormat('yyyy-MM-dd'),
];
for (final p in patterns) {
try {
return p.parse(t);
} catch (_) {}
}
try {
return DateTime.parse(t);
} catch (_) {
return null;
}
}
String _formatDate(String rawDate) {
final dt = _parseClaimDateTime(rawDate);
if (dt != null) return DateFormat('dd-MM-yyyy').format(dt);
return rawDate;
}
String _formatDateTime(String raw) {
final dt = _parseClaimDateTime(raw);
if (dt != null) {
return DateFormat('dd-MM-yyyy, hh:mm a').format(dt);
}
return raw;
}
void refresh() {
loadClaimsList();
}
void _clearFilters() {
setState(() {
_startDateController.clear();
_endDateController.clear();
_filterClaimType = null;
_filterInsurer = null;
_filterStatus = null;
_quickSearchController.clear();
currentPage = 1;
});
loadClaimsList();
}
InputDecoration _filterDropdownDecoration(String hintText) {
return InputDecoration(
isDense: true,
hintText: hintText,
hintStyle: GoogleFonts.inter(fontSize: 11, color: const Color(0xFF94A3B8)),
filled: true,
fillColor: Colors.white,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _filterFocusTeal, width: 1.5),
),
);
}
Map<String, dynamic> _filterAllItem(String label) => {
'v': '',
'label': label,
'sub': '',
};
Map<String, dynamic> _findSelectedChoice(
List<Map<String, dynamic>> choices,
String? value,
) {
final v = value ?? '';
for (final c in choices) {
if ((c['v']?.toString() ?? '') == v) return c;
}
return choices.isNotEmpty ? choices.first : _filterAllItem('');
}
List<Map<String, dynamic>> _claimTypeFilterChoices(List<String> fromData) {
final byId = <String, Map<String, dynamic>>{};
for (final m in _masterClaimTypeMaps) {
final id = (m['id'] ?? '').toString().trim();
final label = (m['claim_type'] ?? '').toString().trim();
if (id.isEmpty || label.isEmpty) continue;
byId[id] = {'v': id, 'label': label, 'sub': ''};
}
for (final label in fromData) {
if (label.isEmpty) continue;
if (byId.values.any((c) => (c['label'] as String) == label)) continue;
for (final row in originalData) {
if (_pickStr(row, ['claim_type_value']) == label) {
final id = (row['claim_type_id'] ?? '').toString().trim();
if (id.isNotEmpty) byId[id] = {'v': id, 'label': label, 'sub': ''};
break;
}
}
}
final list = byId.values.toList()
..sort((a, b) => (a['label'] as String).compareTo(b['label'] as String));
return [_filterAllItem('Claim type'), ...list];
}
List<Map<String, dynamic>> _statusFilterChoices(List<String> fromData) {
final byId = <String, Map<String, dynamic>>{};
void addStatus(String? id, String label) {
final idStr = id?.toString().trim() ?? '';
final lab = label.trim();
if (idStr.isEmpty || lab.isEmpty || lab == '-') return;
byId[idStr] = {'v': idStr, 'label': lab, 'sub': ''};
}
for (final m in _masterClaimStatusMaps) {
addStatus(
m['id']?.toString(),
(m['claim_status'] ??
m['status'] ??
m['name'] ??
m['claim_status_value'] ??
m['status_name'] ??
'')
.toString(),
);
}
for (final row in originalData) {
addStatus(
row['claim_status_id']?.toString(),
_pickStr(row, ['claim_status_value']),
);
}
for (final label in fromData) {
if (label.isEmpty) continue;
if (byId.values.any((c) => (c['label'] as String) == label)) continue;
for (final row in originalData) {
if (_pickStr(row, ['claim_status_value']) == label) {
addStatus(row['claim_status_id']?.toString(), label);
break;
}
}
}
final list = byId.values.toList()
..sort((a, b) => (a['label'] as String).compareTo(b['label'] as String));
return [_filterAllItem('Status'), ...list];
}
List<Map<String, dynamic>> _insurerFilterChoices(List<String> fromData) {
final byId = <String, Map<String, dynamic>>{};
for (final m in _masterInsurerMaps) {
final id = (m['id'] ?? '').toString().trim();
final label = (m['insurer_short_name'] ??
m['insurer_name'] ??
m['name'] ??
'')
.toString()
.trim();
if (id.isEmpty || label.isEmpty) continue;
byId[id] = {'v': id, 'label': label, 'sub': ''};
}
for (final label in fromData) {
if (label.isEmpty) continue;
if (byId.values.any((c) => (c['label'] as String) == label)) continue;
for (final row in originalData) {
if (_pickStr(row, ['insurer_short_name']) == label) {
final id = (row['insurer_id'] ?? '').toString().trim();
if (id.isNotEmpty) byId[id] = {'v': id, 'label': label, 'sub': ''};
break;
}
}
}
final list = byId.values.toList()
..sort((a, b) => (a['label'] as String).compareTo(b['label'] as String));
return [_filterAllItem('Select Insurer'), ...list];
}
InputDecoration _popupSearchDecoration(String hint) {
return InputDecoration(
filled: true,
fillColor: Colors.white,
hintText: hint,
hintStyle: GoogleFonts.inter(fontSize: 12, color: const Color(0xFF94A3B8)),
isDense: true,
contentPadding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: _filterFocusTeal, width: 1.5),
),
);
}
Widget _buildFilterDropdownSearch({
required double width,
String? placeholder,
required String? selectedValue,
required List<Map<String, dynamic>> choices,
required void Function(String?) onChanged,
bool twoLineItems = false,
}) {
final hint = placeholder ??
(choices.isNotEmpty
? (choices.first['label'] ?? '').toString()
: '');
final selected = _findSelectedChoice(choices, selectedValue);
return SizedBox(
height: 38,
width: width,
child: DropdownSearch<Map<String, dynamic>>(
selectedItem: selected,
items: (filter, infiniteScrollProps) => choices,
itemAsString: (m) {
final label = (m['label'] ?? '').toString();
final sub = (m['sub'] ?? '').toString();
if (twoLineItems && sub.isNotEmpty) return '$label $sub';
return label;
},
compareFn: (a, b) =>
(a['v']?.toString() ?? '') == (b['v']?.toString() ?? ''),
decoratorProps: DropDownDecoratorProps(
decoration: _filterDropdownDecoration(hint),
),
dropdownBuilder: (context, selectedItem) {
final m = selectedItem ?? _filterAllItem(hint);
final isAll = (m['v']?.toString() ?? '').isEmpty;
final label = (m['label'] ?? hint).toString();
return Align(
alignment: Alignment.centerLeft,
child: Text(
label,
style: GoogleFonts.inter(
fontSize: 11,
color: isAll
? const Color(0xFF94A3B8)
: const Color(0xFF334155),
),
overflow: TextOverflow.ellipsis,
maxLines: 1,
),
);
},
popupProps: PopupProps.menu(
showSearchBox: true,
fit: FlexFit.loose,
constraints: const BoxConstraints(maxHeight: 280),
menuProps: MenuProps(
backgroundColor: Colors.white,
elevation: 8,
borderRadius: BorderRadius.circular(8),
),
searchFieldProps: TextFieldProps(
decoration: _popupSearchDecoration('Search'),
),
itemBuilder: (context, item, isDisabled, isSelected) {
final isAll = (item['v']?.toString() ?? '').isEmpty;
if (isAll) return const SizedBox.shrink();
final label = (item['label'] ?? '').toString();
final sub = (item['sub'] ?? '').toString();
final showTwoLine =
twoLineItems && !isAll && sub.isNotEmpty;
return Container(
color: isSelected ? const Color(0xFFF1F5F9) : null,
child: Padding(
padding:
const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
child: showTwoLine
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
label.toUpperCase(),
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w700,
color: const Color(0xFF0F172A),
),
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (sub.isNotEmpty) ...[
const SizedBox(height: 2),
Text(
sub,
style: GoogleFonts.inter(
fontSize: 10,
color: const Color(0xFF94A3B8),
),
),
],
],
)
: Text(
label,
style: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: const Color(0xFF0F172A),
),
),
),
);
},
),
onChanged: (val) {
final v = val?['v']?.toString() ?? '';
onChanged(v.isEmpty ? null : v);
},
),
);
}
Map<String, dynamic> get _exportConfig => {
'displayHeaders': const [
'S No',
'Created Date',
'Claim Type',
'Vehicle No',
'Insured Name',
'Policy Number',
'Claim Number',
'Accident Date',
'Insurer',
'Broker',
'Partner',
'Remarks',
'Status',
'Pending Days',
],
'keys': const [
'sno',
'created_at',
'claim_type_value',
'reg_no',
'insured_name',
'policy_no',
'claim_number',
'accident_date',
'insurer_short_name',
'broker',
'partner',
'remark',
'claim_status_value',
'pending_days',
],
'valueFormatters': {
'created_at': (dynamic v, Map<String, dynamic> row) {
final raw = _pickStr(row, [
'created_at',
'created_date',
'created_on',
]);
if (raw == '-') return '-';
return _formatDate(raw);
},
'accident_date': (dynamic v, Map<String, dynamic> row) {
final raw = _pickStr(row, [
'date_of_incident',
'accident_date',
'accident_datetime',
'date_of_accident',
]);
if (raw == '-') return '-';
return _formatDate(raw);
},
'insured_name': (dynamic v, Map<String, dynamic> row) => _pickStr(row, [
'insured_name',
'customer_name',
'proposer_name',
]),
'claim_number': (dynamic v, Map<String, dynamic> row) =>
_pickStr(row, ['claim_number']),
'broker': (dynamic v, Map<String, dynamic> row) =>
_pickStr(row, ['broker_name', 'broker']),
'partner': (dynamic v, Map<String, dynamic> row) =>
_pickStr(row, ['agent_name', 'partner_name', 'partner']),
'remark': (dynamic v, Map<String, dynamic> row) =>
_pickStr(row, ['remark']),
'pending_days': (dynamic v, Map<String, dynamic> row) =>
_pendingDaysLabel(row),
'settled_amount': (dynamic v, Map<String, dynamic> row) => _pickStr(row, [
'settled_amount',
'claim_amount',
'settlement_amount',
]),
},
};
Future<void> _onExportClaims() async {
final cfg = _exportConfig;
try {
await ExcelExporter.exportToExcel(
sheetName: 'Claims',
data: filteredData,
displayHeaders: cfg['displayHeaders'] as List<String>,
keys: cfg['keys'] as List<String>,
fileName: 'claim_list',
valueFormatters:
cfg['valueFormatters'] as Map<String, ExportValueFormatter>?,
);
if (mounted) {
ToastHelper.showSuccessToast(context, 'Export finished');
}
} catch (e) {
if (mounted) {
ToastHelper.showErrorToast(context, 'Export failed: $e');
}
}
}
@override
Widget build(BuildContext context) {
return MainLayout(
title: 'Claims',
body: SelectionArea(
child: Padding(
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_buildHeaderTabs(context),
const SizedBox(height: 14),
_buildFilterBar(context),
const SizedBox(height: 12),
Expanded(
child: isLoading
? const Center(child: CircularProgressIndicator())
: ResponsiveLayout.isMobile(context)
? _buildMobileList()
: _buildDesktopTable(),
),
const SizedBox(height: 8),
_buildFooter(context),
],
),
),
),
);
}
Widget _buildHeaderTabs(BuildContext context) {
return Text(
'Claims',
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w400,
),
);
}
// Widget _tabSwitcher() {
// return Container(
// padding: const EdgeInsets.all(4),
// decoration: BoxDecoration(
// color: const Color(0xFFF1F5F9),
// borderRadius: BorderRadius.circular(10),
// ),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// _tabChip(
// label: 'Pending Claims',
// count: _pendingCount,
// selected: _pendingTab,
// onTap: () => _setTab(true),
// ),
// const SizedBox(width: 6),
// _tabChip(
// label: 'Settled Claims',
// count: _settledCount,
// selected: !_pendingTab,
// onTap: () => _setTab(false),
// ),
// ],
// ),
// );
// }
// Widget _tabChip({
// required String label,
// required int count,
// required bool selected,
// required VoidCallback onTap,
// }) {
// return Material(
// color: Colors.transparent,
// child: InkWell(
// onTap: onTap,
// borderRadius: BorderRadius.circular(8),
// child: AnimatedContainer(
// duration: const Duration(milliseconds: 180),
// padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
// decoration: BoxDecoration(
// color: selected ? Colors.white : Colors.transparent,
// borderRadius: BorderRadius.circular(8),
// boxShadow: selected
// ? [
// BoxShadow(
// color: Colors.black.withValues(alpha: 0.06),
// blurRadius: 8,
// offset: const Offset(0, 2),
// ),
// ]
// : null,
// ),
// child: Row(
// mainAxisSize: MainAxisSize.min,
// children: [
// Text(
// label,
// style: GoogleFonts.inter(
// fontSize: 12,
// fontWeight: FontWeight.w600,
// color: selected ? _teal : _muted,
// ),
// ),
// const SizedBox(width: 8),
// Container(
// width: 26,
// height: 26,
// alignment: Alignment.center,
// decoration: BoxDecoration(
// color: selected
// ? _teal.withValues(alpha: 0.12)
// : const Color(0xFFE2E8F0),
// shape: BoxShape.circle,
// ),
// child: Text(
// '$count',
// style: GoogleFonts.inter(
// fontSize: 11,
// fontWeight: FontWeight.w700,
// color: selected ? _teal : _muted,
// ),
// ),
// ),
// ],
// ),
// ),
// ),
// );
// }
Widget _buildFilterBar(BuildContext context) {
final isMobile = ResponsiveLayout.isMobile(context);
final inner = isMobile
? Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_filterControlsRow(context),
const SizedBox(height: 10),
_buildSearchAndActions(context),
],
)
: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: _filterControlsRow(context)),
_buildSearchAndActions(context),
],
);
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 10),
decoration: BoxDecoration(
border: Border.all(color: _border),
borderRadius: BorderRadius.circular(10),
color: Colors.white,
),
child: inner,
);
}
Widget _filterControlsRow(BuildContext context) {
final isMobile = ResponsiveLayout.isMobile(context);
final claimTypeChoices = _claimTypeFilterChoices(
_uniqueFrom((r) => _pickStr(r, ['claim_type_value'])),
);
final insurerChoices = _insurerFilterChoices(
_uniqueFrom((r) => _pickStr(r, ['insurer_short_name'])),
);
final statusChoices = _statusFilterChoices(
_uniqueFrom((r) => _pickStr(r, ['claim_status_value'])),
);
final dateW = isMobile ? 168.0 : 175.0;
final dropW = isMobile ? 128.0 : 140.0;
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
DateRangePickerField(
startController: _startDateController,
endController: _endDateController,
hintText: 'Select date range',
showLabelAboveField: false,
txtwidth: dateW,
txtheight: 38,
lastDate: DateTime.now(),
useFormField: false,
),
const SizedBox(width: 8),
_buildFilterDropdownSearch(
width: dropW,
selectedValue: _filterClaimType,
choices: claimTypeChoices,
twoLineItems: true,
onChanged: (v) => setState(() => _filterClaimType = v),
),
const SizedBox(width: 8),
_buildFilterDropdownSearch(
width: dropW + 8,
selectedValue: _filterInsurer,
choices: insurerChoices,
twoLineItems: true,
onChanged: (v) => setState(() => _filterInsurer = v),
),
const SizedBox(width: 8),
_buildFilterDropdownSearch(
width: dropW,
selectedValue: _filterStatus,
choices: statusChoices,
onChanged: (v) => setState(() => _filterStatus = v),
),
const SizedBox(width: 8),
_squareFilterButton(
icon: Icons.search,
filled: true,
onTap: _applyFilterBar,
),
const SizedBox(width: 8),
_squareFilterButton(
icon: Icons.refresh,
filled: false,
onTap: _clearFilters,
),
],
),
);
}
Widget _squareFilterButton({
required VoidCallback onTap,
required bool filled,
IconData? icon,
String? assetPath,
String? tooltip,
}) {
assert(icon != null || assetPath != null);
final child = assetPath != null
? Image.asset(
assetPath,
height: 16,
width: 16,
color: filled ? Colors.white : _muted,
errorBuilder: (_, __, ___) => Icon(
Icons.open_in_new_rounded,
size: 20,
color: filled ? Colors.white : _muted,
),
)
: Icon(
icon,
size: 20,
color: filled ? Colors.white : _muted,
);
final button = Material(
color: filled ? _teal : Colors.white,
borderRadius: BorderRadius.circular(8),
child: InkWell(
onTap: onTap,
borderRadius: BorderRadius.circular(8),
child: Container(
width: 38,
height: 38,
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8),
border: filled ? null : Border.all(color: _border),
),
child: child,
),
),
);
if (tooltip != null) {
return Tooltip(message: tooltip, child: button);
}
return button;
}
Widget _buildSearchAndActions(BuildContext context) {
final isMobile = ResponsiveLayout.isMobile(context);
final searchField = ThemedSearchField(
hintText: 'Search...',
backgroundColor: Colors.white,
txtHeight: 38,
onChanged: _onQuickSearch,
controller: _quickSearchController,
txtwidth: isMobile
? MediaQuery.sizeOf(context).width * 0.5
: 200,
);
final actions = Row(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(width: 10),
_squareFilterButton(
icon: Icons.add,
filled: true,
tooltip: 'Add claim',
onTap: () {
showDialog<void>(
context: context,
builder: (ctx) => AddClaimStepperDialog(
managerId: managerId,
userId: userId,
role: roleId,
onSuccess: refresh,
),
);
},
),
const SizedBox(width: 8),
_squareFilterButton(
filled: true,
assetPath: 'assets/miscellaneous/export.png',
tooltip: 'Export',
onTap: _onExportClaims,
),
],
);
if (isMobile) {
return Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(child: searchField),
actions,
],
);
}
return Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
searchField,
actions,
],
);
}
Widget _buildFooter(BuildContext context) {
final total = filteredData.length;
final showing = _paginatedSorted.length;
final label = total == 1 ? 'claim' : 'claims';
return Row(
children: [
Text(
'Showing $showing of $total $label',
style: GoogleFonts.inter(fontSize: 12, color: _muted),
),
const Spacer(),
Text(
total == 0 ? 'Page 1' : 'Page $currentPage',
style: GoogleFonts.inter(fontSize: 12, color: _muted),
),
const SizedBox(width: 16),
PaginationControls(
currentPage: currentPage,
itemsPerPage: itemsPerPage,
totalItems: filteredData.length,
onPageChanged: (page) {
setState(() => currentPage = page);
},
onItemsPerPageChanged: (items) {
setState(() {
itemsPerPage = items;
currentPage = 1;
});
},
),
],
);
}
int _calculatePendingDays(String? createdAt) {
if (createdAt == null || createdAt.isEmpty) return 0;
try {
final created = DateFormat('dd-MM-yyyy').parse(createdAt.split(' ')[0]);
final today = DateTime.now();
return today.difference(created).inDays;
} catch (e) {
return 0;
}
}
bool _isClosedClaimStatus(Map<String, dynamic> item) {
final status =
_pickStr(item, ['claim_status_value', 'status']).toLowerCase();
return status.contains('closed') || status.contains('settled');
}
String _pendingDaysLabel(Map<String, dynamic> item) {
final days = _calculatePendingDays(item['created_at']?.toString());
return _isClosedClaimStatus(item) ? '-' : '$days';
}
Widget _pendingDaysCell(Map<String, dynamic> item) {
return Expanded(
flex: 1,
child: Builder(
builder: (context) {
final days = _calculatePendingDays(item['created_at']?.toString());
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 12),
child: Center(
child: Text(
_isClosedClaimStatus(item) ? '-' : '$days',
textAlign: TextAlign.center,
style: _dataBold.copyWith(
color: _isClosedClaimStatus(item)
? _muted
: (days > 7
? Colors.red
: days > 3
? Colors.orange
: Colors.green),
),
),
),
);
},
),
);
}
Widget _buildDesktopTable() {
if (filteredData.isEmpty) {
return Center(
child: Text(
'No claims match your filters.',
style: GoogleFonts.inter(color: _muted),
),
);
}
final rows = _paginatedSorted;
final startSerial = (currentPage - 1) * itemsPerPage;
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
_tableHeaderRow(),
Expanded(
child: Scrollbar(
controller: _tableVScrollController,
thumbVisibility: true,
interactive: true,
child: ListView(
controller: _tableVScrollController,
primary: false,
padding: EdgeInsets.zero,
physics: const ClampingScrollPhysics(),
children: [
for (var index = 0; index < rows.length; index++) ...[
if (index > 0) const Divider(height: 1, color: _border),
_pendingDataRow(rows[index], startSerial + index + 1),
],
],
),
),
),
],
);
}
Widget _headerCell(
String text, {
int flex = 1,
TextAlign align = TextAlign.start,
}) {
return Expanded(
flex: flex,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
child: Align(
alignment: align == TextAlign.center
? Alignment.center
: Alignment.centerLeft,
child: Text(
text.toUpperCase(),
softWrap: true,
maxLines: 2,
overflow: TextOverflow.ellipsis,
textAlign: align,
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
color: const Color(0xFF475569),
),
),
),
),
);
}
Widget _headerCellFixed(
String text, {
required double width,
TextAlign align = TextAlign.start,
}) {
return SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
child: Align(
alignment: align == TextAlign.center
? Alignment.center
: Alignment.centerLeft,
child: Text(
text.toUpperCase(),
softWrap: false,
maxLines: 1,
overflow: TextOverflow.visible,
textAlign: align,
style: GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.3,
color: const Color(0xFF475569),
),
),
),
),
);
}
Widget _headerCellSno() {
return Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: 6, right: 2, top: 12, bottom: 12),
child: Align(
alignment: Alignment.centerLeft,
child: Text(
'S NO',
softWrap: true,
maxLines: 2,
style: GoogleFonts.inter(
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 0.2,
color: const Color(0xFF475569),
),
),
),
),
);
}
Widget _tableHeaderRow() {
return Container(
decoration: BoxDecoration(
color: _surfaceGrey,
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
_headerCellSno(),
_headerCell('Created Date', flex: 2),
_headerCell('Claim Type', flex: 2),
_headerCell('Vehicle No', flex: 2),
_headerCell('Insured Name', flex: 2),
_headerCell('Policy Number', flex: 2),
_headerCell('Claim Number', flex: 2),
_headerCell('Accident Date', flex: 2),
_headerCell('Insurer', flex: 2),
_headerCell('Broker', flex: 2),
_headerCell('Partner', flex: 2),
_headerCell('Remarks', flex: 2),
_headerCell('Status', flex: 2),
_headerCell('Pending Days', flex: 1, align: TextAlign.center),
_headerCell('Action', flex: 2, align: TextAlign.center),
],
),
);
}
Widget _cell(Widget child, {int flex = 1}) {
return Expanded(
flex: flex,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
child: Align(
alignment: Alignment.centerLeft,
child: child,
),
),
);
}
Widget _cellFixed(Widget child, {required double width}) {
return SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
child: Align(
alignment: Alignment.centerLeft,
child: child,
),
),
);
}
Widget _cellSno(Widget child) {
return Expanded(
flex: 1,
child: Padding(
padding: const EdgeInsets.only(left: 6, right: 2, top: 12, bottom: 12),
child: Align(
alignment: Alignment.centerLeft,
child: child,
),
),
);
}
Widget _cellText(
String text, {
int flex = 1,
int maxLines = 2,
FontWeight? fontWeight,
bool ellipsis = true,
}) {
return _cell(
Text(
text,
style: _dataStyle.copyWith(
fontWeight: fontWeight ?? FontWeight.w400,
),
maxLines: maxLines,
softWrap: maxLines > 1,
overflow: ellipsis ? TextOverflow.ellipsis : TextOverflow.visible,
),
flex: flex,
);
}
Widget _cellTextFixed(
String text, {
required double width,
int maxLines = 1,
FontWeight? fontWeight,
TextAlign align = TextAlign.start,
}) {
return SizedBox(
width: width,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 12),
child: Align(
alignment: align == TextAlign.center
? Alignment.center
: Alignment.centerLeft,
child: Text(
text,
textAlign: align,
style: _dataStyle.copyWith(
fontWeight: fontWeight ?? FontWeight.w400,
),
maxLines: maxLines,
softWrap: maxLines > 1,
overflow: TextOverflow.visible,
),
),
),
);
}
Widget _cellRemarks(String text, {int flex = 3}) {
final display = text.trim().isEmpty ? '-' : text.trim();
final textWidget = Text(
display,
style: _dataStyle,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
);
if (display == '-') {
return _cell(textWidget, flex: flex);
}
return _cell(
Tooltip(
richMessage: WidgetSpan(
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 420),
child: Text(
display,
style: GoogleFonts.inter(
fontSize: 13,
height: 1.45,
fontWeight: FontWeight.w400,
color: Colors.white,
),
),
),
),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
margin: const EdgeInsets.only(bottom: 6),
preferBelow: false,
verticalOffset: 10,
waitDuration: const Duration(milliseconds: 250),
showDuration: const Duration(seconds: 10),
decoration: BoxDecoration(
color: const Color(0xFF334155),
borderRadius: BorderRadius.circular(8),
boxShadow: const [
BoxShadow(
color: Color(0x40000000),
blurRadius: 10,
offset: Offset(0, 3),
),
],
),
child: textWidget,
),
flex: flex,
);
}
Widget _pendingDataRow(Map<String, dynamic> item, int sno) {
final createdRaw = _pickStr(item, [
'created_at',
'created_date',
'created_on',
]);
final accidentRaw = _pickStr(item, [
'date_of_incident',
'accident_date',
'accident_datetime',
'date_of_accident',
]);
final insured = _pickStr(item, [
'insured_name',
'customer_name',
'proposer_name',
]);
final remarks = _pickStr(item, ['remark']);
return Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
_cellSno(Text('$sno', style: _dataStyle)),
_cellText(
createdRaw != '-' ? _formatDate(createdRaw) : '-',
flex: 2,
maxLines: 2,
),
_cell(
_claimTypeChip(_pickStr(item, ['claim_type_value'])),
flex: 2,
),
_cellText(_pickStr(item, ['reg_no']), flex: 2, maxLines: 2),
_cellText(insured, flex: 2, maxLines: 2, fontWeight: FontWeight.w600),
_cellText(
_pickStr(item, ['policy_no', 'policy_number']),
flex: 2,
maxLines: 2,
),
_cellText(
_pickStr(item, ['claim_number']),
flex: 2,
maxLines: 2,
),
_cellText(
accidentRaw != '-' ? _formatDate(accidentRaw) : '-',
flex: 2,
maxLines: 2,
),
_cellText(_pickStr(item, ['insurer_short_name']), flex: 2, maxLines: 2),
_cellText(_pickStr(item, ['broker_name', 'broker']), flex: 2, maxLines: 2),
_cellText(
_pickStr(item, ['agent_name', 'partner_name', 'partner']),
flex: 2,
maxLines: 2,
),
_cellRemarks(remarks, flex: 2),
_cell(
_statusChip(_pickStr(item, ['claim_status_value'])),
flex: 2,
),
_pendingDaysCell(item),
_cell(Center(child: _actionIcons(item)), flex: 2),
],
);
}
// Widget _settledDataRow(Map<String, dynamic> item, int sno) {
// final accidentRaw = _pickStr(item, [
// 'date_of_incident',
// 'accident_date',
// 'accident_datetime',
// 'date_of_accident',
// ]);
// final insured = _pickStr(item, [
// 'insured_name',
// 'customer_name',
// 'proposer_name',
// ]);
// final remarks = _pickStr(item, ['remarks', 'claim_description']);
// final settled = _pickStr(item, [
// 'settled_amount',
// 'claim_amount',
// 'settlement_amount',
// ]);
// return Row(
// crossAxisAlignment: CrossAxisAlignment.center,
// children: [
// _cellSno(Text('$sno', style: _dataStyle)),
// _cellText(_pickStr(item, ['reg_no']), flex: 2, maxLines: 2),
// _cellText(insured, flex: 2, maxLines: 2, fontWeight: FontWeight.w600),
// _cellText(_pickStr(item, ['policy_no']), flex: 2, maxLines: 2),
// _cellText(
// accidentRaw != '-' ? _formatDate(accidentRaw) : '-',
// flex: 2,
// maxLines: 2,
// ),
// _cellText(_pickStr(item, ['insurer_short_name']), flex: 2, maxLines: 2),
// _cellText(_pickStr(item, ['broker_name', 'broker']), flex: 2, maxLines: 2),
// _cellText(
// _pickStr(item, ['agent_name', 'partner_name', 'partner']),
// flex: 2,
// maxLines: 2,
// ),
// _cellRemarks(remarks, flex: 2),
// _cell(
// _statusChip(_pickStr(item, ['claim_status_value'])),
// flex: 2,
// ),
// _pendingDaysCell(item),
// _cellText(settled == '-' ? '-' : settled, flex: 2, maxLines: 2),
// _cell(_actionIcons(item), flex: 1),
// ],
// );
// }
Widget _actionIcons(Map<String, dynamic> item) {
return Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
tooltip: 'Edit',
icon: const Icon(Icons.edit_outlined, color: _teal, size: 18),
onPressed: () {
showDialog<void>(
context: context,
builder: (ctx) => AddClaimStepperDialog(
managerId: managerId,
userId: userId,
role: roleId,
claimToEdit: item,
onSuccess: refresh,
),
);
},
),
if (_canShowDeleteAction) const SizedBox(width: 6),
// IconButton(
// padding: EdgeInsets.zero,
// constraints: const BoxConstraints(minWidth: 30, minHeight: 30),
// tooltip: 'Upload',
// icon: const Icon(Icons.cloud_upload_outlined, color: _teal, size: 18),
// onPressed: () {},
// ),
// IconButton(
// padding: EdgeInsets.zero,
// constraints: const BoxConstraints(minWidth: 30, minHeight: 30),
// tooltip: 'Download',
// icon: const Icon(
// Icons.cloud_download_outlined,
// color: _actionDownloadBlue,
// size: 18,
// ),
// onPressed: () {},
// ),
if (_canShowDeleteAction)
IconButton(
padding: EdgeInsets.zero,
constraints: const BoxConstraints(minWidth: 28, minHeight: 28),
tooltip: 'Delete',
icon: const Icon(
Icons.delete_outline,
color: Color(0xFFC62828),
size: 18,
),
onPressed: () {
final claimId = _pickStr(item, ['id']);
_confirmDelete(context, claimId);
},
),
],
);
}
Widget _claimTypeChip(String text) {
final t = text.toLowerCase();
Color bg;
Color fg;
if (t.contains('theft')) {
bg = const Color(0xFFFFEBEE);
fg = const Color(0xFFC62828);
} else if (t.contains('tp') && !t.contains('own')) {
bg = const Color(0xFFE3F2FD);
fg = const Color(0xFF1565C0);
} else if (t.contains('od') || t.contains('own damage')) {
bg = const Color(0xFFF3E5F5);
fg = const Color(0xFF6A1B9A);
} else {
bg = const Color(0xFFF3E5F5);
fg = const Color(0xFF6A1B9A);
}
return Align(
alignment: Alignment.centerLeft,
child: _pillWrapWords(text, bg, fg),
);
}
String _statusDisplayLabel(String text) {
final t = text.trim();
if (t.isEmpty || t == '-') return t.isEmpty ? '-' : t;
final compact = t.replaceAll(RegExp(r'\s+'), '').toUpperCase();
if (compact == 'INPROGRESS') return 'IN PROGRESS';
return t;
}
Widget _statusChip(String text) {
final label = _statusDisplayLabel(text);
final t = label.toLowerCase();
Color bg;
Color fg;
if (t.contains('reject')) {
bg = const Color(0xFFFFEBEE);
fg = const Color(0xFFC62828);
} else if (t.contains('open')) {
bg = const Color(0xFFFFF3E0);
fg = const Color(0xFFEF6C00);
} else if (t.contains('progress')) {
bg = const Color(0xFFF3E5F5);
fg = const Color(0xFF6A1B9A);
} else if (t.contains('settled')) {
bg = const Color(0xFFE8F5E9);
fg = const Color(0xFF2E7D32);
} else {
bg = const Color(0xFFF1F5F9);
fg = const Color(0xFF475569);
}
return Align(
alignment: Alignment.centerLeft,
child: _pill(label, bg, fg),
);
}
Widget _pill(String text, Color bg, Color fg, {bool showFull = false}) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(20),
),
child: Text(
text,
maxLines: 1,
softWrap: false,
overflow: TextOverflow.visible,
style: GoogleFonts.inter(
fontSize: 10.5,
fontWeight: FontWeight.w600,
color: fg,
),
),
);
}
/// Pill label that wraps only between words, never inside a word.
Widget _pillWrapWords(String text, Color bg, Color fg) {
final style = GoogleFonts.inter(
fontSize: 10.5,
fontWeight: FontWeight.w600,
color: fg,
);
final words = text.trim().split(RegExp(r'\s+')).where((w) => w.isNotEmpty);
if (words.isEmpty) {
return _pill('-', bg, fg);
}
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(
color: bg,
borderRadius: BorderRadius.circular(20),
),
child: Wrap(
spacing: 4,
runSpacing: 2,
crossAxisAlignment: WrapCrossAlignment.center,
children: words
.map((w) => Text(w, style: style))
.toList(),
),
);
}
TextStyle get _dataStyle => GoogleFonts.inter(
fontSize: 11.5,
fontWeight: FontWeight.w400,
color: const Color(0xFF0F172A),
);
TextStyle get _dataBold => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w700,
color: const Color(0xFF0F172A),
);
Widget _buildMobileList() {
if (filteredData.isEmpty) {
return Center(
child: Text(
'No claims match your filters.',
style: GoogleFonts.inter(color: _muted),
),
);
}
final rows = _paginatedSorted;
final startSerial = (currentPage - 1) * itemsPerPage;
return ListView.separated(
itemCount: rows.length,
separatorBuilder: (_, _) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final item = rows[index];
final sno = startSerial + index + 1;
return _mobileCard(item, sno);
},
);
}
Widget _mobileCard(Map<String, dynamic> item, int sno) {
return Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(10),
border: Border.all(color: _border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Text('#$sno', style: _dataStyle.copyWith(color: _muted)),
const Spacer(),
_statusChip(_pickStr(item, ['claim_status_value'])),
],
),
const SizedBox(height: 8),
Text(
_pickStr(item, ['reg_no']),
style: _dataStyle.copyWith(
fontWeight: FontWeight.w700,
fontSize: 14,
),
),
const SizedBox(height: 4),
Text(
_pickStr(item, [
'insured_name',
'customer_name',
'proposer_name',
]),
style: _dataStyle.copyWith(fontWeight: FontWeight.w500),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
_claimTypeChip(_pickStr(item, ['claim_type_value'])),
],
),
const Divider(height: 20),
_mobilePair(
'Policy',
_pickStr(item, ['policy_no', 'policy_number']),
),
_mobilePair(
'Claim No',
_pickStr(item, ['claim_number']),
),
_mobilePair('Insurer', _pickStr(item, ['insurer_short_name'])),
_mobilePair('Pending days', _pendingDaysLabel(item)),
// if (!_pendingTab) ...[
// _mobilePair(
// 'Settled (₹)',
// _pickStr(item, [
// 'settled_amount',
// 'claim_amount',
// 'settlement_amount',
// ]),
// ),
// ],
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: _actionIcons(item),
),
],
),
);
}
Widget _mobilePair(String k, String v) {
return Padding(
padding: const EdgeInsets.only(bottom: 6),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 88,
child: Text(
k,
style: GoogleFonts.inter(
fontSize: 11,
color: _muted,
fontWeight: FontWeight.w500,
),
),
),
Expanded(
child: Text(v, style: _dataStyle),
),
],
),
);
}
}