new requirement

This commit is contained in:
Surendiran 2026-06-25 15:39:25 +05:30
parent fbcfe20b9b
commit 3abde687ca
4 changed files with 1238 additions and 7 deletions

View File

@ -2,10 +2,12 @@ import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhancepolicy/service/api_service.dart';
import 'package:nhancepolicy/service/svg_service.dart';
import 'package:nhancepolicy/service/token_storage_service.dart';
import 'package:nhancepolicy/logger.dart';
import 'package:url_launcher/url_launcher.dart';
class NhanceSideBar extends StatefulWidget {
const NhanceSideBar({super.key});
@ -211,6 +213,19 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
_isHoveringClaimsMenu = false;
}
Future<void> _openHelpDialog() async {
final isPostUser = postModules.isNotEmpty;
await showDialog<void>(
context: context,
builder: (dialogContext) => _HelpContactDialog(
isPostUser: isPostUser,
apiService: apiService,
tokenService: tokenService,
),
);
}
@override
Widget build(BuildContext context) {
return Container(
@ -355,6 +370,17 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
const Spacer(),
if (enrollmentModules.isNotEmpty || postModules.isNotEmpty)
_SideItem(
icon: const Icon(
Icons.help_outline,
color: Colors.white,
size: 30,
),
label: 'Help',
onTap: _openHelpDialog,
),
_SideItem(
icon: SvgPicture.string(
SvgService.getSvg('logout'),
@ -376,6 +402,380 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
}
}
class _HelpContactDialog extends StatefulWidget {
final bool isPostUser;
final ApiService apiService;
final TokenStorageService tokenService;
const _HelpContactDialog({
required this.isPostUser,
required this.apiService,
required this.tokenService,
});
@override
State<_HelpContactDialog> createState() => _HelpContactDialogState();
}
class _HelpContactDialogState extends State<_HelpContactDialog> {
bool _isLoading = false;
List<Map<String, dynamic>> _level1Contacts = [];
List<Map<String, dynamic>> _level2Contacts = [];
Map<String, String> _hospitalLinks = {};
String? _errorMessage;
@override
void initState() {
super.initState();
if (widget.isPostUser) {
_loadClientRM();
}
}
Future<void> _loadClientRM() async {
setState(() {
_isLoading = true;
_errorMessage = null;
});
try {
final clientId = await widget.tokenService.readValue('empClientId');
final token = widget.tokenService.getCurrentToken();
if (clientId == null ||
clientId.toString().isEmpty ||
token == null ||
token.isEmpty) {
setState(() {
_errorMessage = 'Client details not found';
_isLoading = false;
});
return;
}
final response = await widget.apiService.getClientRMApi(
clientId.toString(),
token,
);
if (!mounted) return;
if (response['status'] == 'success' && response['data'] is Map) {
final data = Map<String, dynamic>.from(response['data'] as Map);
setState(() {
_level1Contacts = _parseContacts(data['level_1']);
_level2Contacts = _parseContacts(data['level_2']);
_hospitalLinks = _parseHospitalLinks(data['tpa_network_hospitals']);
_isLoading = false;
});
} else {
setState(() {
_errorMessage =
response['message']?.toString() ?? 'Failed to load contacts';
_isLoading = false;
});
}
} catch (e) {
logDebug('getClientRM failed: $e');
if (mounted) {
setState(() {
_errorMessage = 'Failed to load contacts';
_isLoading = false;
});
}
}
}
List<Map<String, dynamic>> _parseContacts(dynamic raw) {
if (raw is! List) return [];
return raw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
Map<String, String> _parseHospitalLinks(dynamic raw) {
if (raw is! Map) return {};
final parsed = <String, String>{};
raw.forEach((key, value) {
final name = key.toString().trim();
final url = value?.toString().trim() ?? '';
if (name.isNotEmpty && url.isNotEmpty) {
parsed[name] = url;
}
});
return parsed;
}
Future<void> _openHospitalUrl(String url) async {
final uri = Uri.tryParse(url);
if (uri == null) return;
await launchUrl(uri, webOnlyWindowName: '_blank');
}
Widget _buildContactCard(Map<String, dynamic> contact) {
final name = contact['first_name']?.toString() ?? '-';
final email = contact['email']?.toString() ?? '-';
final mobile = contact['mobile']?.toString() ?? '-';
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 10),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
name,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
const SizedBox(height: 6),
Text(
email,
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black87),
),
const SizedBox(height: 2),
Text(
mobile,
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black87),
),
],
),
);
}
Widget _buildLevelSection({
required String title,
required List<Map<String, dynamic>> contacts,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: GoogleFonts.poppins(
fontSize: 14,
fontWeight: FontWeight.w600,
color: Colors.black87,
),
),
const SizedBox(height: 8),
if (contacts.isEmpty)
Text(
'No contact assigned',
style: GoogleFonts.poppins(fontSize: 13, color: Colors.black54),
)
else
...contacts.map(_buildContactCard),
const SizedBox(height: 12),
],
);
}
Widget _buildContent() {
if (!widget.isPostUser) {
return Center(
child: Text(
'No Level Contact',
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.black54,
),
),
);
}
if (_isLoading) {
return const SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
);
}
if (_errorMessage != null) {
return Center(
child: Text(
_errorMessage!,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(fontSize: 14, color: Colors.red),
),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildLevelSection(
title: 'Level 1 - Account Manager',
contacts: _level1Contacts,
),
_buildLevelSection(
title: 'Level 2 - Head',
contacts: _level2Contacts,
),
],
);
}
Widget _buildHospitalListContent() {
if (!widget.isPostUser) {
return Center(
child: Text(
'No Network Hospital List',
style: GoogleFonts.poppins(
fontSize: 15,
fontWeight: FontWeight.w500,
color: Colors.black54,
),
),
);
}
if (_isLoading) {
return const SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
);
}
if (_errorMessage != null) {
return Center(
child: Text(
_errorMessage!,
textAlign: TextAlign.center,
style: GoogleFonts.poppins(fontSize: 14, color: Colors.red),
),
);
}
if (_hospitalLinks.isEmpty) {
return Center(
child: Text(
'No Network Hospital List',
style: GoogleFonts.poppins(fontSize: 14, color: Colors.black54),
),
);
}
return ListView.separated(
shrinkWrap: true,
itemCount: _hospitalLinks.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final entry = _hospitalLinks.entries.elementAt(index);
return InkWell(
onTap: () => _openHospitalUrl(entry.value),
borderRadius: BorderRadius.circular(8),
child: Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8)),
),
child: Row(
children: [
Expanded(
child: Text(
entry.key,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
),
const SizedBox(width: 8),
const Icon(
Icons.open_in_new,
size: 16,
color: Color(0xFF009195),
),
],
),
),
);
},
);
}
@override
Widget build(BuildContext context) {
final maxHeight = MediaQuery.of(context).size.height * 0.65;
final cappedMaxHeight = maxHeight.clamp(280.0, 520.0);
return DefaultTabController(
length: 2,
child: AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'Help',
style:
GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
const SizedBox(height: 12),
TabBar(
labelColor: const Color(0xFF009195),
unselectedLabelColor: Colors.black54,
indicatorColor: const Color(0xFF009195),
labelStyle: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
tabs: const [
Tab(text: 'Contacts'),
Tab(text: 'Network Hospital List'),
],
),
],
),
content: SizedBox(
width: 560,
child: ConstrainedBox(
constraints: BoxConstraints(maxHeight: cappedMaxHeight),
child: TabBarView(
children: [
Scrollbar(
child: SingleChildScrollView(child: _buildContent()),
),
Scrollbar(
child: SingleChildScrollView(
child: _buildHospitalListContent(),
),
),
],
),
),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
),
],
),
);
}
}
class _SideItem extends StatelessWidget {
// final IconData icon;
final Widget icon; // 👈 changed

View File

@ -102,6 +102,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
bool isLoading = false;
bool _isLoading = false;
bool _isSendingReminder = false;
bool _isLoadingPolicyTerms = false;
// dynamic clintID;
late TabController _tabController;
// List<dynamic> dataPolicy = [];
@ -652,6 +653,105 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
return _capitalize(status);
}
Future<void> _openPolicyTermsDialog() async {
setState(() => _isLoadingPolicyTerms = true);
try {
final clientId = localClientId ?? widget.ClientId;
final branchId = localClientBranchId ?? widget.clientBranchId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final token = localToken ?? widget.Token;
final hrId = await tokenService.readValue('empHrId');
if (hrId == null || hrId.toString().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(context, 'HR ID not found');
}
return;
}
final response = await apiService.getActiveCashDepositDetailsToApi(
clientId,
branchId,
hrId.toString(),
token,
1,
);
if (!mounted) return;
if (response['status'] != 'success') {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to load policy terms',
);
return;
}
final policies = List<Map<String, dynamic>>.from(response['data'] ?? []);
Map<String, dynamic>? matchedPolicy;
for (final policy in policies) {
if (policy['client_policy_id'].toString() == clientPolicyId) {
matchedPolicy = policy;
break;
}
}
if (matchedPolicy == null) {
ToastHelper.showErrorToast(context, 'Policy not found');
return;
}
final rawTerms =
matchedPolicy['policy_terms'] ?? matchedPolicy['Policy_Terms'];
final terms = <String, String>{};
if (rawTerms is Map) {
rawTerms.forEach((key, value) {
if (value != null && value.toString().trim().isNotEmpty) {
terms[key.toString()] = value.toString();
}
});
}
if (terms.isEmpty) {
ToastHelper.showWarningToast(context, 'No policy terms available');
return;
}
await showDialog<void>(
context: context,
builder: (dialogContext) => _PolicyTermsDialog(terms: terms),
);
} catch (e) {
logDebug('Policy terms load failed: $e');
if (mounted) {
ToastHelper.showErrorToast(context, 'Failed to load policy terms');
}
} finally {
if (mounted) {
setState(() => _isLoadingPolicyTerms = false);
}
}
}
Future<void> _openReminderConfigDialog() async {
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final token = localToken ?? widget.Token;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return _ReminderMailConfigDialog(
clientPolicyId: clientPolicyId,
token: token,
apiService: apiService,
);
},
);
}
String _defaultReminderSubject() {
final policyName = localCardPolicyName ?? widget.cardPolicy_name;
return 'Reminder: Complete Your Enrollment - $policyName';
@ -706,7 +806,7 @@ HR Team''';
return (subject: subject, body: body);
}
Future<void> _confirmAndSendReminder() async {
Future<void> _openReminderTemplateDialog() async {
final template = await _loadReminderEmailTemplate();
if (!mounted) return;
@ -866,8 +966,8 @@ HR Team''';
final ok = response['status'] == 'success' || response['status'] == true;
if (ok) {
final message = response['message']?.toString() ??
'Reminder sent successfully';
final message =
response['message']?.toString() ?? 'Reminder sent successfully';
ToastHelper.showSuccessToast(context, message);
await _logReminderActivity();
} else {
@ -1112,13 +1212,40 @@ HR Team''';
const SizedBox(width: 12),
if (localTokenType == 'pre') ...[
SizedBox(
width: 150,
height: 37,
child: ElevatedButton(
onPressed: _openReminderConfigDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
elevation: 0,
padding: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
'Reminder Config',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 142,
height: 37,
child: ElevatedButton(
onPressed: _isSendingReminder
? null
: _confirmAndSendReminder,
: _openReminderTemplateDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
@ -1153,6 +1280,49 @@ HR Team''';
const SizedBox(width: 12),
],
if (localTokenType == 'post') ...[
SizedBox(
width: 130,
height: 37,
child: ElevatedButton(
onPressed: _isLoadingPolicyTerms
? null
: _openPolicyTermsDialog,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
elevation: 0,
padding: EdgeInsets.zero,
disabledBackgroundColor:
const Color(0xFF009195).withValues(alpha: 0.6),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: _isLoadingPolicyTerms
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Policy Terms',
maxLines: 1,
softWrap: false,
overflow: TextOverflow.ellipsis,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w700,
color: Colors.white,
),
),
),
),
const SizedBox(width: 12),
],
SizedBox(
width: 116,
height: 37,
@ -1777,7 +1947,7 @@ HR Team''';
Widget _buildStatusSummary() {
if (originalData.isEmpty) {
return const SizedBox();
}
}
final statusCounts = getPreStatusCounts();
const filters = <Map<String, String>>[
@ -2883,3 +3053,573 @@ class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
true;
}
class _PolicyTermsDialog extends StatelessWidget {
final Map<String, String> terms;
const _PolicyTermsDialog({required this.terms});
@override
Widget build(BuildContext context) {
final entries = terms.entries.toList();
final maxHeight = MediaQuery.of(context).size.height * 0.65;
final cappedMaxHeight = maxHeight.clamp(320.0, 560.0);
final contentHeight =
(entries.length * 88.0).clamp(120.0, cappedMaxHeight);
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(
'Policy Terms',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
content: SizedBox(
width: 640,
height: contentHeight,
child: Scrollbar(
thumbVisibility: entries.length > 4,
child: ListView.separated(
itemCount: entries.length,
separatorBuilder: (_, __) => const SizedBox(height: 10),
itemBuilder: (context, index) {
final entry = entries[index];
return Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color(0xFFF5F5F5),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8E8E8)),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
entry.key,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
color: const Color(0xFF009195),
),
),
const SizedBox(height: 4),
Text(
entry.value,
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w400,
color: Colors.black87,
),
),
],
),
);
},
),
),
),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: Text('Close', style: GoogleFonts.poppins(color: Colors.white)),
),
],
);
}
}
class _ReminderMailConfigDialog extends StatefulWidget {
final String clientPolicyId;
final String token;
final ApiService apiService;
const _ReminderMailConfigDialog({
required this.clientPolicyId,
required this.token,
required this.apiService,
});
@override
State<_ReminderMailConfigDialog> createState() =>
_ReminderMailConfigDialogState();
}
class _ReminderMailConfigDialogState extends State<_ReminderMailConfigDialog> {
static const _frequencies = <String, String>{
'daily': 'Daily',
'weekly': 'Weekly',
'monthly': 'Monthly',
'custom': 'Custom',
'working_days': 'Working Days',
};
bool _isLoadingConfig = true;
bool _isSaving = false;
int? _configId;
String _frequency = 'daily';
bool _isEnabled = true;
String _reminderDaysText = '';
final Set<String> _selectedWorkingDays = {};
List<Map<String, dynamic>> _workingDayOptions = [];
late final TextEditingController _reminderDaysController;
@override
void initState() {
super.initState();
_reminderDaysController = TextEditingController();
_loadConfig();
}
@override
void dispose() {
_reminderDaysController.dispose();
super.dispose();
}
Future<void> _loadConfig() async {
setState(() => _isLoadingConfig = true);
try {
final response = await widget.apiService.getReminderMailConfigApi(
widget.clientPolicyId,
widget.token,
);
if (!mounted) return;
final ok = response['status'] == true || response['status'] == 'success';
if (ok) {
_workingDayOptions = _parseWorkingDayOptions(
response['working_day_options'],
);
_applyConfig(response['data']);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to load reminder config',
);
}
} catch (e) {
logDebug('Reminder config load failed: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to load reminder configuration',
);
}
} finally {
if (mounted) {
setState(() => _isLoadingConfig = false);
}
}
}
List<Map<String, dynamic>> _parseWorkingDayOptions(dynamic raw) {
if (raw is! List) return _defaultWorkingDayOptions();
return raw
.whereType<Map>()
.map((item) => Map<String, dynamic>.from(item))
.toList();
}
List<Map<String, dynamic>> _defaultWorkingDayOptions() {
return [
{'value': 1, 'label': 'Mon', 'key': 'mon'},
{'value': 2, 'label': 'Tue', 'key': 'tue'},
{'value': 3, 'label': 'Wed', 'key': 'wed'},
{'value': 4, 'label': 'Thu', 'key': 'thu'},
{'value': 5, 'label': 'Fri', 'key': 'fri'},
{'value': 6, 'label': 'Sat', 'key': 'sat'},
{'value': 7, 'label': 'Sun', 'key': 'sun'},
];
}
void _applyConfig(dynamic data) {
_configId = null;
_frequency = 'daily';
_isEnabled = true;
_reminderDaysText = '';
_reminderDaysController.clear();
_selectedWorkingDays.clear();
if (data is! Map) return;
final config = Map<String, dynamic>.from(data);
_configId = int.tryParse(config['id']?.toString() ?? '');
_frequency = config['frequency']?.toString() ?? 'daily';
_isEnabled = config['is_enabled']?.toString() != '0';
if (_frequency == 'working_days') {
final labels = config['working_day_labels'];
if (labels is List && labels.isNotEmpty) {
_selectedWorkingDays.addAll(labels.map((e) => e.toString()));
} else {
_applyWorkingDaysFromReminderDays(
config['reminder_days']?.toString() ?? '',
);
}
} else if (_frequency != 'daily') {
_reminderDaysText = config['reminder_days']?.toString() ?? '';
_reminderDaysController.text = _reminderDaysText;
}
}
void _applyWorkingDaysFromReminderDays(String reminderDays) {
if (reminderDays.isEmpty) return;
final options = _workingDayOptions.isNotEmpty
? _workingDayOptions
: _defaultWorkingDayOptions();
final valueToLabel = <String, String>{
for (final option in options)
option['value'].toString(): option['label'].toString(),
};
for (final part in reminderDays.split(',')) {
final token = part.trim();
if (token.isEmpty) continue;
if (RegExp(r'^\d+$').hasMatch(token)) {
final normalized = token == '0' ? '7' : token;
final label = valueToLabel[normalized];
if (label != null) {
_selectedWorkingDays.add(label);
}
} else {
final match = options.firstWhere(
(option) =>
option['label'].toString().toLowerCase() == token.toLowerCase() ||
option['key'].toString().toLowerCase() == token.toLowerCase(),
orElse: () => {},
);
if (match.isNotEmpty) {
_selectedWorkingDays.add(match['label'].toString());
}
}
}
}
String? _buildReminderDays() {
switch (_frequency) {
case 'daily':
return null;
case 'working_days':
if (_selectedWorkingDays.isEmpty) return null;
return _selectedWorkingDays.join(',');
default:
final value = _reminderDaysText.trim();
return value.isEmpty ? null : value;
}
}
String? _validateBeforeSave() {
if (_frequency == 'working_days' && _selectedWorkingDays.isEmpty) {
return 'Select at least one working day';
}
if (_frequency != 'daily' &&
_frequency != 'working_days' &&
_reminderDaysText.trim().isEmpty) {
return 'Reminder days are required for $_frequency frequency';
}
return null;
}
Future<int?> _resolveHrId() async {
final tokenService = TokenStorageService();
final hrId = await tokenService.readValue('enrollmentHrId') ??
await tokenService.readValue('empHrId');
return int.tryParse(hrId?.toString() ?? '');
}
Future<void> _saveConfig() async {
final validationError = _validateBeforeSave();
if (validationError != null) {
ToastHelper.showWarningToast(context, validationError);
return;
}
setState(() => _isSaving = true);
try {
final policyId = int.tryParse(widget.clientPolicyId);
if (policyId == null) {
ToastHelper.showErrorToast(context, 'Invalid client policy id');
return;
}
final payload = <String, dynamic>{
'client_policy_id': policyId,
'frequency': _frequency,
'is_enabled': _isEnabled ? 1 : 0,
};
final reminderDays = _buildReminderDays();
if (reminderDays != null) {
payload['reminder_days'] = reminderDays;
}
final hrId = await _resolveHrId();
if (hrId != null) {
payload['hr_id'] = hrId;
}
if (_configId != null) {
payload['id'] = _configId;
}
final response = await widget.apiService.saveReminderMailConfigApi(
widget.token,
payload,
);
if (!mounted) return;
final ok = response['status'] == true || response['status'] == 'success';
if (ok) {
final data = response['data'];
if (data is Map) {
_applyConfig(data);
}
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ??
'Reminder mail configuration saved successfully',
);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to save configuration',
);
}
} catch (e) {
logDebug('Reminder config save failed: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to save reminder configuration',
);
}
} finally {
if (mounted) {
setState(() => _isSaving = false);
}
}
}
Widget _buildDaysField() {
if (_frequency == 'daily') {
return const SizedBox.shrink();
}
if (_frequency == 'working_days') {
final options = _workingDayOptions.isNotEmpty
? _workingDayOptions
: _defaultWorkingDayOptions();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Working Days',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: options.map((option) {
final label = option['label'].toString();
final selected = _selectedWorkingDays.contains(label);
return FilterChip(
label: Text(label, style: GoogleFonts.poppins(fontSize: 12)),
selected: selected,
onSelected: (value) {
setState(() {
if (value) {
_selectedWorkingDays.add(label);
} else {
_selectedWorkingDays.remove(label);
}
});
},
selectedColor: const Color(0xFFC5F2F4),
checkmarkColor: const Color(0xFF009195),
);
}).toList(),
),
],
);
}
final helperText = switch (_frequency) {
'weekly' => 'Weekday numbers 0-6 (Sun-Sat), comma-separated. Example: 1,3,5',
'monthly' => 'Days of month 1-31, comma-separated. Example: 1,15,28',
'custom' => 'Custom days of month 1-31, comma-separated. Example: 5,10,20',
_ => '',
};
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Reminder Days',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
TextField(
controller: _reminderDaysController,
onChanged: (value) => _reminderDaysText = value,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
hintText: helperText,
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
),
const SizedBox(height: 4),
Text(
helperText,
style: GoogleFonts.poppins(fontSize: 11, color: Colors.black45),
),
],
);
}
@override
Widget build(BuildContext context) {
final isBusy = _isSaving;
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
title: Text(
'Email Reminder Config',
style: GoogleFonts.poppins(fontWeight: FontWeight.w600, fontSize: 18),
),
content: SizedBox(
width: 560,
child: _isLoadingConfig
? const SizedBox(
height: 180,
child: Center(child: CircularProgressIndicator()),
)
: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Configure the schedule for automated enrollment reminder emails.',
style: GoogleFonts.poppins(
fontSize: 13,
color: Colors.black54,
),
),
const SizedBox(height: 16),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: Text(
'Enable scheduled reminders',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
value: _isEnabled,
activeColor: const Color(0xFF009195),
onChanged: isBusy
? null
: (value) => setState(() => _isEnabled = value),
),
const SizedBox(height: 8),
Text(
'Frequency',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
DropdownButtonFormField<String>(
value: _frequency,
isExpanded: true,
decoration: InputDecoration(
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: BorderSide.none,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
),
items: _frequencies.entries
.map(
(entry) => DropdownMenuItem(
value: entry.key,
child: Text(
entry.value,
style: GoogleFonts.poppins(fontSize: 14),
),
),
)
.toList(),
onChanged: isBusy
? null
: (value) {
if (value == null) return;
setState(() {
_frequency = value;
_reminderDaysText = '';
_reminderDaysController.clear();
_selectedWorkingDays.clear();
});
},
),
const SizedBox(height: 14),
_buildDaysField(),
],
),
),
),
actions: [
TextButton(
onPressed: isBusy ? null : () => Navigator.pop(context),
child: Text('Cancel', style: GoogleFonts.poppins(color: Colors.black54)),
),
ElevatedButton(
onPressed: isBusy || _isLoadingConfig ? null : _saveConfig,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),
child: _isSaving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text('Save', style: GoogleFonts.poppins(color: Colors.white)),
),
],
);
}
}

View File

@ -579,6 +579,7 @@ class _PolicyGrid extends StatelessWidget {
final Function(String clientPolicyId)? onBulkDownload;
static const double _enrollmentCardHeight = 156;
static const double _activePolicyCardHeight = 170;
const _PolicyGrid({
super.key,
@ -699,7 +700,7 @@ class _PolicyGrid extends StatelessWidget {
if (isEnrollment) {
cardHeight = _enrollmentCardHeight;
} else {
cardHeight = 170;
cardHeight = _activePolicyCardHeight;
}
final double totalHeight = rowCount * cardHeight + ((rowCount - 1) * 16);
@ -714,7 +715,8 @@ class _PolicyGrid extends StatelessWidget {
childAspectRatio: config.childAspectRatio,
crossAxisSpacing: 16,
mainAxisSpacing: 16,
mainAxisExtent: isEnrollment ? _enrollmentCardHeight : 150,
mainAxisExtent:
isEnrollment ? _enrollmentCardHeight : _activePolicyCardHeight,
),
itemCount: list.length,
itemBuilder: (context, index) {
@ -1085,6 +1087,19 @@ class _EnrollmentPolicyCardNew extends StatelessWidget {
}
}
String? _formatTpaDisplayName(Map<String, dynamic> data) {
final tpaName = data['tpa_name']?.toString().trim() ?? '';
final tpaShortName = data['tpa_short_name']?.toString().trim() ?? '';
final raw = tpaName.isNotEmpty ? tpaName : tpaShortName;
if (raw.isEmpty) return null;
final normalized = raw.toLowerCase().replaceAll(RegExp(r'[\s_-]+'), '');
if (normalized == 'inhouse') {
return 'In-house';
}
return raw;
}
class _ActivePolicyCardNew extends StatelessWidget {
final Map<String, dynamic> data;
final VoidCallback? onTap;
@ -1100,6 +1115,8 @@ class _ActivePolicyCardNew extends StatelessWidget {
@override
Widget build(BuildContext context) {
final tpaLabel = _formatTpaDisplayName(data);
return MouseRegion(
cursor: SystemMouseCursors.click, // 👈 pointer on hover
child: InkWell(
@ -1197,6 +1214,24 @@ class _ActivePolicyCardNew extends StatelessWidget {
),
),
if (tpaLabel != null) ...[
const SizedBox(height: 2),
Tooltip(
message: tpaLabel,
waitDuration: const Duration(milliseconds: 300),
child: Text(
tpaLabel,
maxLines: 1,
overflow: TextOverflow.ellipsis,
softWrap: false,
style: GoogleFonts.poppins(
fontSize: 11,
color: Colors.grey,
),
),
),
],
const SizedBox(height: 4),
/// DATE RANGE

View File

@ -1088,6 +1088,62 @@ class ApiService {
}
}
Future<Map<String, dynamic>> getReminderMailConfigApi(
String clientPolicyId,
String token,
) async {
final url = Uri.parse(
'${Environment.apiUrl}getReminderMailConfig'
'?client_policy_id=$clientPolicyId',
);
final headers = {
'Authorization': 'Bearer $token',
};
return _makeGetRequest(url, headers);
}
Future<Map<String, dynamic>> saveReminderMailConfigApi(
String token,
Map<String, dynamic> payload,
) async {
final url = Uri.parse('${Environment.apiUrl}saveReminderMailConfig');
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
return _makePostRequest(url, jsonEncode(payload), headers);
}
Future<Map<String, dynamic>> sendReminderMailApi(
String clientPolicyId,
String token,
) async {
final url = Uri.parse('${Environment.apiUrl}sendReminderMail');
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
};
final policyId = int.tryParse(clientPolicyId) ?? clientPolicyId;
return _makePostRequest(
url,
jsonEncode({'client_policy_id': policyId}),
headers,
);
}
Future<Map<String, dynamic>> getClientRMApi(
String postClientId,
String token,
) async {
final url = Uri.parse(
'${Environment.apiUrlPost}getClientRM?client_id=$postClientId',
);
final headers = {
'Authorization': 'Bearer $token',
};
return _makeGetRequest(url, headers);
}
Future<Map<String, dynamic>> getEmployeeAndDependenceToApiPre(
String clintID, String getPolicyNo, String empRefId, String token) async {
logDebug(_hrtoken);