diff --git a/lib/customAppBar/side_bar.dart b/lib/customAppBar/side_bar.dart index 18da3ea..504f4b4 100644 --- a/lib/customAppBar/side_bar.dart +++ b/lib/customAppBar/side_bar.dart @@ -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 { _isHoveringClaimsMenu = false; } + Future _openHelpDialog() async { + final isPostUser = postModules.isNotEmpty; + + await showDialog( + 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 { 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 { } } +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> _level1Contacts = []; + List> _level2Contacts = []; + Map _hospitalLinks = {}; + String? _errorMessage; + + @override + void initState() { + super.initState(); + if (widget.isPostUser) { + _loadClientRM(); + } + } + + Future _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.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> _parseContacts(dynamic raw) { + if (raw is! List) return []; + return raw + .whereType() + .map((item) => Map.from(item)) + .toList(); + } + + Map _parseHospitalLinks(dynamic raw) { + if (raw is! Map) return {}; + final parsed = {}; + 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 _openHospitalUrl(String url) async { + final uri = Uri.tryParse(url); + if (uri == null) return; + await launchUrl(uri, webOnlyWindowName: '_blank'); + } + + Widget _buildContactCard(Map 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> 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 diff --git a/lib/presentation/hrPolicyDetails.dart b/lib/presentation/hrPolicyDetails.dart index a19b786..052265d 100755 --- a/lib/presentation/hrPolicyDetails.dart +++ b/lib/presentation/hrPolicyDetails.dart @@ -102,6 +102,7 @@ class _HrPolicyDetailsState extends State bool isLoading = false; bool _isLoading = false; bool _isSendingReminder = false; + bool _isLoadingPolicyTerms = false; // dynamic clintID; late TabController _tabController; // List dataPolicy = []; @@ -652,6 +653,105 @@ class _HrPolicyDetailsState extends State return _capitalize(status); } + Future _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>.from(response['data'] ?? []); + Map? 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 = {}; + + 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( + 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 _openReminderConfigDialog() async { + final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId; + final token = localToken ?? widget.Token; + + await showDialog( + 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 _confirmAndSendReminder() async { + Future _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 = >[ @@ -2883,3 +3053,573 @@ class _CDHeaderDelegate extends SliverPersistentHeaderDelegate { bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) => true; } + +class _PolicyTermsDialog extends StatelessWidget { + final Map 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 = { + '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 _selectedWorkingDays = {}; + List> _workingDayOptions = []; + late final TextEditingController _reminderDaysController; + + @override + void initState() { + super.initState(); + _reminderDaysController = TextEditingController(); + _loadConfig(); + } + + @override + void dispose() { + _reminderDaysController.dispose(); + super.dispose(); + } + + Future _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> _parseWorkingDayOptions(dynamic raw) { + if (raw is! List) return _defaultWorkingDayOptions(); + return raw + .whereType() + .map((item) => Map.from(item)) + .toList(); + } + + List> _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.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 = { + 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 _resolveHrId() async { + final tokenService = TokenStorageService(); + final hrId = await tokenService.readValue('enrollmentHrId') ?? + await tokenService.readValue('empHrId'); + return int.tryParse(hrId?.toString() ?? ''); + } + + Future _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 = { + '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( + 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)), + ), + ], + ); + } +} diff --git a/lib/presentation/policies.dart b/lib/presentation/policies.dart index dc3eeaa..b04db5a 100644 --- a/lib/presentation/policies.dart +++ b/lib/presentation/policies.dart @@ -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 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 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 diff --git a/lib/service/api_service.dart b/lib/service/api_service.dart index 2cc4c26..717ae58 100755 --- a/lib/service/api_service.dart +++ b/lib/service/api_service.dart @@ -1088,6 +1088,62 @@ class ApiService { } } + Future> 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> saveReminderMailConfigApi( + String token, + Map 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> 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> 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> getEmployeeAndDependenceToApiPre( String clintID, String getPolicyNo, String empRefId, String token) async { logDebug(_hrtoken);