2077 lines
62 KiB
Dart
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),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|