new requirement
This commit is contained in:
parent
fbcfe20b9b
commit
3abde687ca
@ -2,10 +2,12 @@ import 'dart:convert';
|
|||||||
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_svg/svg.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/api_service.dart';
|
||||||
import 'package:nhancepolicy/service/svg_service.dart';
|
import 'package:nhancepolicy/service/svg_service.dart';
|
||||||
import 'package:nhancepolicy/service/token_storage_service.dart';
|
import 'package:nhancepolicy/service/token_storage_service.dart';
|
||||||
import 'package:nhancepolicy/logger.dart';
|
import 'package:nhancepolicy/logger.dart';
|
||||||
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
|
|
||||||
class NhanceSideBar extends StatefulWidget {
|
class NhanceSideBar extends StatefulWidget {
|
||||||
const NhanceSideBar({super.key});
|
const NhanceSideBar({super.key});
|
||||||
@ -211,6 +213,19 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
_isHoveringClaimsMenu = false;
|
_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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Container(
|
return Container(
|
||||||
@ -355,6 +370,17 @@ class _NhanceSideBarState extends State<NhanceSideBar> {
|
|||||||
|
|
||||||
const Spacer(),
|
const Spacer(),
|
||||||
|
|
||||||
|
if (enrollmentModules.isNotEmpty || postModules.isNotEmpty)
|
||||||
|
_SideItem(
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.help_outline,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 30,
|
||||||
|
),
|
||||||
|
label: 'Help',
|
||||||
|
onTap: _openHelpDialog,
|
||||||
|
),
|
||||||
|
|
||||||
_SideItem(
|
_SideItem(
|
||||||
icon: SvgPicture.string(
|
icon: SvgPicture.string(
|
||||||
SvgService.getSvg('logout'),
|
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 {
|
class _SideItem extends StatelessWidget {
|
||||||
// final IconData icon;
|
// final IconData icon;
|
||||||
final Widget icon; // 👈 changed
|
final Widget icon; // 👈 changed
|
||||||
|
|||||||
@ -102,6 +102,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
bool isLoading = false;
|
bool isLoading = false;
|
||||||
bool _isLoading = false;
|
bool _isLoading = false;
|
||||||
bool _isSendingReminder = false;
|
bool _isSendingReminder = false;
|
||||||
|
bool _isLoadingPolicyTerms = false;
|
||||||
// dynamic clintID;
|
// dynamic clintID;
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
// List<dynamic> dataPolicy = [];
|
// List<dynamic> dataPolicy = [];
|
||||||
@ -652,6 +653,105 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
|||||||
return _capitalize(status);
|
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() {
|
String _defaultReminderSubject() {
|
||||||
final policyName = localCardPolicyName ?? widget.cardPolicy_name;
|
final policyName = localCardPolicyName ?? widget.cardPolicy_name;
|
||||||
return 'Reminder: Complete Your Enrollment - $policyName';
|
return 'Reminder: Complete Your Enrollment - $policyName';
|
||||||
@ -706,7 +806,7 @@ HR Team''';
|
|||||||
return (subject: subject, body: body);
|
return (subject: subject, body: body);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _confirmAndSendReminder() async {
|
Future<void> _openReminderTemplateDialog() async {
|
||||||
final template = await _loadReminderEmailTemplate();
|
final template = await _loadReminderEmailTemplate();
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
|
|
||||||
@ -866,8 +966,8 @@ HR Team''';
|
|||||||
|
|
||||||
final ok = response['status'] == 'success' || response['status'] == true;
|
final ok = response['status'] == 'success' || response['status'] == true;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
final message = response['message']?.toString() ??
|
final message =
|
||||||
'Reminder sent successfully';
|
response['message']?.toString() ?? 'Reminder sent successfully';
|
||||||
ToastHelper.showSuccessToast(context, message);
|
ToastHelper.showSuccessToast(context, message);
|
||||||
await _logReminderActivity();
|
await _logReminderActivity();
|
||||||
} else {
|
} else {
|
||||||
@ -1112,13 +1212,40 @@ HR Team''';
|
|||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
if (localTokenType == 'pre') ...[
|
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(
|
SizedBox(
|
||||||
width: 142,
|
width: 142,
|
||||||
height: 37,
|
height: 37,
|
||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: _isSendingReminder
|
onPressed: _isSendingReminder
|
||||||
? null
|
? null
|
||||||
: _confirmAndSendReminder,
|
: _openReminderTemplateDialog,
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
backgroundColor: const Color(0xFFE26728),
|
backgroundColor: const Color(0xFFE26728),
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
@ -1153,6 +1280,49 @@ HR Team''';
|
|||||||
const SizedBox(width: 12),
|
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(
|
SizedBox(
|
||||||
width: 116,
|
width: 116,
|
||||||
height: 37,
|
height: 37,
|
||||||
@ -1777,7 +1947,7 @@ HR Team''';
|
|||||||
Widget _buildStatusSummary() {
|
Widget _buildStatusSummary() {
|
||||||
if (originalData.isEmpty) {
|
if (originalData.isEmpty) {
|
||||||
return const SizedBox();
|
return const SizedBox();
|
||||||
}
|
}
|
||||||
|
|
||||||
final statusCounts = getPreStatusCounts();
|
final statusCounts = getPreStatusCounts();
|
||||||
const filters = <Map<String, String>>[
|
const filters = <Map<String, String>>[
|
||||||
@ -2883,3 +3053,573 @@ class _CDHeaderDelegate extends SliverPersistentHeaderDelegate {
|
|||||||
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
bool shouldRebuild(covariant SliverPersistentHeaderDelegate oldDelegate) =>
|
||||||
true;
|
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)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -579,6 +579,7 @@ class _PolicyGrid extends StatelessWidget {
|
|||||||
final Function(String clientPolicyId)? onBulkDownload;
|
final Function(String clientPolicyId)? onBulkDownload;
|
||||||
|
|
||||||
static const double _enrollmentCardHeight = 156;
|
static const double _enrollmentCardHeight = 156;
|
||||||
|
static const double _activePolicyCardHeight = 170;
|
||||||
|
|
||||||
const _PolicyGrid({
|
const _PolicyGrid({
|
||||||
super.key,
|
super.key,
|
||||||
@ -699,7 +700,7 @@ class _PolicyGrid extends StatelessWidget {
|
|||||||
if (isEnrollment) {
|
if (isEnrollment) {
|
||||||
cardHeight = _enrollmentCardHeight;
|
cardHeight = _enrollmentCardHeight;
|
||||||
} else {
|
} else {
|
||||||
cardHeight = 170;
|
cardHeight = _activePolicyCardHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
final double totalHeight = rowCount * cardHeight + ((rowCount - 1) * 16);
|
final double totalHeight = rowCount * cardHeight + ((rowCount - 1) * 16);
|
||||||
@ -714,7 +715,8 @@ class _PolicyGrid extends StatelessWidget {
|
|||||||
childAspectRatio: config.childAspectRatio,
|
childAspectRatio: config.childAspectRatio,
|
||||||
crossAxisSpacing: 16,
|
crossAxisSpacing: 16,
|
||||||
mainAxisSpacing: 16,
|
mainAxisSpacing: 16,
|
||||||
mainAxisExtent: isEnrollment ? _enrollmentCardHeight : 150,
|
mainAxisExtent:
|
||||||
|
isEnrollment ? _enrollmentCardHeight : _activePolicyCardHeight,
|
||||||
),
|
),
|
||||||
itemCount: list.length,
|
itemCount: list.length,
|
||||||
itemBuilder: (context, index) {
|
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 {
|
class _ActivePolicyCardNew extends StatelessWidget {
|
||||||
final Map<String, dynamic> data;
|
final Map<String, dynamic> data;
|
||||||
final VoidCallback? onTap;
|
final VoidCallback? onTap;
|
||||||
@ -1100,6 +1115,8 @@ class _ActivePolicyCardNew extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final tpaLabel = _formatTpaDisplayName(data);
|
||||||
|
|
||||||
return MouseRegion(
|
return MouseRegion(
|
||||||
cursor: SystemMouseCursors.click, // 👈 pointer on hover
|
cursor: SystemMouseCursors.click, // 👈 pointer on hover
|
||||||
child: InkWell(
|
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),
|
const SizedBox(height: 4),
|
||||||
|
|
||||||
/// DATE RANGE
|
/// DATE RANGE
|
||||||
|
|||||||
@ -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(
|
Future<Map<String, dynamic>> getEmployeeAndDependenceToApiPre(
|
||||||
String clintID, String getPolicyNo, String empRefId, String token) async {
|
String clintID, String getPolicyNo, String empRefId, String token) async {
|
||||||
logDebug(_hrtoken);
|
logDebug(_hrtoken);
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user