This commit is contained in:
Surendiran 2026-08-06 15:03:54 +05:30
parent 7ce3e95b17
commit 863c08b918
6 changed files with 719 additions and 317 deletions

View File

@ -55,6 +55,54 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
return null;
}
Map<String, dynamic>? _decodeBranchToken(String token) {
try {
final parts = token.split('.');
if (parts.length != 3) return null;
final payload = base64Url.normalize(parts[1]);
final decoded = utf8.decode(base64Url.decode(payload));
final parsed = json.decode(decoded);
if (parsed is Map<String, dynamic>) return parsed;
if (parsed is Map) return Map<String, dynamic>.from(parsed);
return null;
} catch (_) {
return null;
}
}
/// Read post/pre client + branch ids from selected branch JWT payload.
Map<String, String>? _clientDetailsParamsFromBranch(
Map<String, dynamic> branch,
) {
final token = branch['token']?.toString() ??
tokenStorage.getCurrentToken() ??
'';
if (token.isEmpty) return null;
final decoded = _decodeBranchToken(token);
if (decoded == null) return null;
final postClientId = decoded['post_client_id']?.toString().trim() ?? '';
final postBranchId = decoded['post_branch_id']?.toString().trim() ?? '';
final preClientId = decoded['pre_client_id']?.toString().trim() ?? '';
final preBranchId = decoded['pre_branch_id']?.toString().trim() ?? '';
if (postClientId.isEmpty &&
postBranchId.isEmpty &&
preClientId.isEmpty &&
preBranchId.isEmpty) {
return null;
}
return {
'post_client_id': postClientId,
'post_branch_id': postBranchId,
'pre_client_id': preClientId,
'pre_branch_id': preBranchId,
};
}
Future<void> _loadBranchLogos() async {
final uniqueBranches = <String, Map<String, dynamic>>{};
for (final branch in branches) {
@ -89,23 +137,11 @@ class _NhanceTopBarState extends State<NhanceTopBar> {
'';
if (token.isEmpty) return null;
final clientId = branch['client_id']?.toString() ?? '';
final branchId = branch['client_branch_id']?.toString() ?? '';
final preBranchId = branch['pre_branch_id']?.toString() ?? branchId;
if (clientId.isEmpty || branchId.isEmpty) return null;
final params = _clientDetailsParamsFromBranch(branch);
if (params == null) return null;
final isPre = branch['enrollment_type']?.toString() == 'pre';
final postClientId = isPre ? '' : clientId;
final postBranchId = isPre ? '' : branchId;
final preClientId = isPre ? clientId : clientId;
final preBranch = isPre ? branchId : preBranchId;
final url = Uri.parse(
'${Environment.apiUrl}getClientDetails'
'?post_client_id=$postClientId'
'&post_branch_id=$postBranchId'
'&pre_client_id=$preClientId'
'&pre_branch_id=$preBranch',
final url = Uri.parse('${Environment.apiUrl}getClientDetails').replace(
queryParameters: params,
);
final response = await http.get(

View File

@ -141,15 +141,23 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
Future<void> _logHrActivity(String activity) async {
try {
final postId = await _tokenService.readValue('empHrId');
final preId = await _tokenService.readValue('enrollmentEmpPrimaryId');
final token = await _tokenService.getCurrentToken();
if (token == null || token.isEmpty) return;
var postId = await _tokenService.readValue('empHrId');
var preId = await _tokenService.readValue('enrollmentEmpPrimaryId');
if (postId == null ||
postId.isEmpty ||
preId == null ||
preId.isEmpty ||
token == null ||
token.isEmpty) {
preId.isEmpty) {
final decoded = _tokenService.getDecodedToken();
postId ??= decoded?['post_hr_id']?.toString();
preId ??= decoded?['pre_hr_id']?.toString();
}
if (postId == null ||
postId.isEmpty ||
preId == null ||
preId.isEmpty) {
return;
}
await _apiService.getPostLogHrActivity(postId, preId, token, activity);

View File

@ -20,6 +20,7 @@ class ReminderEmailTemplateDialog extends StatefulWidget {
final String defaultHtmlBody;
final ReminderEmailTemplateMode mode;
final Future<void> Function(String subject, String htmlBody)? onSend;
final bool embedded;
const ReminderEmailTemplateDialog({
super.key,
@ -32,6 +33,7 @@ class ReminderEmailTemplateDialog extends StatefulWidget {
required this.defaultHtmlBody,
this.mode = ReminderEmailTemplateMode.config,
this.onSend,
this.embedded = false,
});
@override
@ -460,10 +462,147 @@ class _ReminderEmailTemplateDialogState extends State<ReminderEmailTemplateDialo
);
}
@override
Widget build(BuildContext context) {
Widget _buildTemplateContent() {
final primaryLabel =
widget.mode == ReminderEmailTemplateMode.send ? 'Send' : 'Submit';
if (_isLoading) {
return const Center(child: CircularProgressIndicator(color: _teal));
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: EdgeInsets.fromLTRB(20, widget.embedded ? 12 : 16, 12, 8),
child: Row(
children: [
Text(
'Template Name',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _templateNameController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: const Color(0xFFF5F5F5),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none,
),
),
),
),
if (!widget.embedded)
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, size: 20),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Subject',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _subjectController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: const Color(0xFFF5F5F5),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none,
),
),
),
),
const SizedBox(width: 8),
_placeholdersMenu(),
const SizedBox(width: 8),
_webPointerGuard(
child: _tealButton(
label: 'Preview Mail',
onPressed: () => _showPreviewMail(),
width: 120,
),
),
],
),
),
Expanded(child: _buildEditorArea()),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 16),
child: Row(
children: [
_webPointerGuard(
child: TextButton.icon(
onPressed: () => _toggleHtmlEditMode(),
icon: Icon(
Icons.code,
size: 16,
color: _isHtmlEditMode ? _teal : Colors.black54,
),
label: Text(
_isHtmlEditMode ? 'Visual Editor' : 'Dev HTML',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _isHtmlEditMode ? _teal : Colors.black54,
),
),
),
),
const Spacer(),
_webPointerGuard(
child: _tealButton(
label: primaryLabel,
onPressed: _submit,
loading: _isSubmitting,
width: 120,
),
),
],
),
),
],
);
}
@override
Widget build(BuildContext context) {
if (widget.embedded) {
return _buildTemplateContent();
}
final size = MediaQuery.sizeOf(context);
return Dialog(
@ -473,132 +612,7 @@ class _ReminderEmailTemplateDialogState extends State<ReminderEmailTemplateDialo
child: SizedBox(
width: size.width * 0.95,
height: size.height * 0.92,
child: _isLoading
? const Center(child: CircularProgressIndicator(color: _teal))
: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 12, 8),
child: Row(
children: [
Text(
'Template Name',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _templateNameController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: const Color(0xFFF5F5F5),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none,
),
),
),
),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, size: 20),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Text(
'Subject',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(width: 12),
Expanded(
child: TextField(
controller: _subjectController,
style: GoogleFonts.poppins(fontSize: 14),
decoration: InputDecoration(
isDense: true,
filled: true,
fillColor: const Color(0xFFF5F5F5),
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 10,
),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(4),
borderSide: BorderSide.none,
),
),
),
),
const SizedBox(width: 8),
_placeholdersMenu(),
const SizedBox(width: 8),
_webPointerGuard(
child: _tealButton(
label: 'Preview Mail',
onPressed: () => _showPreviewMail(),
width: 120,
),
),
],
),
),
Expanded(child: _buildEditorArea()),
const Divider(height: 1),
Padding(
padding: const EdgeInsets.fromLTRB(20, 12, 20, 16),
child: Row(
children: [
_webPointerGuard(
child: TextButton.icon(
onPressed: () => _toggleHtmlEditMode(),
icon: Icon(
Icons.code,
size: 16,
color: _isHtmlEditMode ? _teal : Colors.black54,
),
label: Text(
_isHtmlEditMode ? 'Visual Editor' : 'Dev HTML',
style: GoogleFonts.poppins(
fontSize: 12,
fontWeight: FontWeight.w600,
color: _isHtmlEditMode ? _teal : Colors.black54,
),
),
),
),
const Spacer(),
_webPointerGuard(
child: _tealButton(
label: primaryLabel,
onPressed: _submit,
loading: _isSubmitting,
width: 120,
),
),
],
),
),
],
),
child: _buildTemplateContent(),
),
);
}

View File

@ -1,3 +1,4 @@
import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'package:nhancepolicy/customAppBar/customAppBar.dart';
@ -216,6 +217,10 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
localToken = widget.Token.isNotEmpty
? widget.Token
: await tokenService.readValue('hr_Token');
// Browser refresh often drops constructor Token use session token.
if (localToken == null || localToken!.toString().trim().isEmpty) {
localToken = tokenService.getCurrentToken();
}
localTokenType = widget.TokenType.isNotEmpty
? widget.TokenType
@ -252,9 +257,65 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
? widget.is_ecard_bulk_download_for_employee
: int.tryParse(savedBulk ?? '0') ?? 0;
// Persist so browser refresh can rebuild this page without empty API params.
await _persistPolicyContext();
if (!_hasRequiredPolicyContext()) {
logDebug(
'hrPolicyDetails missing client/policy/branch after restore — '
'skipping API and returning to policies',
);
if (!mounted) return;
setState(() {
_isLoadingCards = false;
isLoading = false;
});
WidgetsBinding.instance.addPostFrameCallback((_) {
if (!mounted) return;
Navigator.pushReplacementNamed(context, 'policies');
});
return;
}
getCDPoliciesDetails();
}
bool _hasRequiredPolicyContext() {
final clientId = (localClientId ?? '').toString().trim();
final policyId = (localClientPolicyId ?? '').toString().trim();
final branchId = (localClientBranchId ?? '').toString().trim();
final token = (localToken ?? '').toString().trim();
return clientId.isNotEmpty &&
policyId.isNotEmpty &&
branchId.isNotEmpty &&
token.isNotEmpty;
}
Future<void> _persistPolicyContext() async {
Future<void> writeIfPresent(String key, dynamic value) async {
final text = value?.toString().trim() ?? '';
if (text.isEmpty) return;
await tokenService.writeValue(key, text);
}
await writeIfPresent('hr_ClientId', localClientId);
await writeIfPresent('hr_policyTypeId', localPolicyTypeId);
await writeIfPresent('hr_ClientPoliyId', localClientPolicyId);
await writeIfPresent('hr_clientBranchId', localClientBranchId);
await writeIfPresent('hr_Token', localToken);
await writeIfPresent('hr_TokenType', localTokenType);
await writeIfPresent('hr_cardType', localCardType);
await writeIfPresent('hr_cardPolicyNo', localCardPolicyNo);
await writeIfPresent('hr_cardInsurer_name', localCardInsurerName);
await writeIfPresent('hr_cardPolicy_name', localCardPolicyName);
await writeIfPresent('hr_cardPolicy_ExpDate', localCardPolicyExpDate);
await writeIfPresent('hr_total_premium', localTotalPremium);
await tokenService.writeValue(
'hr_is_ecard_bulk_download_for_employee',
localIsEcardBulkDownload.toString(),
);
}
Future<void> clearPolicyStorage() async {
await tokenService.removeValue('hr_ClientId');
await tokenService.removeValue('hr_policyTypeId');
@ -377,10 +438,28 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
dynamic cardData,
String? searchKey,
}) async {
final clientId = localClientId ?? widget.ClientId;
final policyId = localClientPolicyId ?? widget.ClientPoliyId;
final branchId = localClientBranchId ?? widget.clientBranchId;
final token = localToken ?? widget.Token;
final clientId = (localClientId ?? widget.ClientId).toString().trim();
final policyId = (localClientPolicyId ?? widget.ClientPoliyId).toString().trim();
final branchId =
(localClientBranchId ?? widget.clientBranchId).toString().trim();
var token = (localToken ?? widget.Token).toString().trim();
if (token.isEmpty) {
token = tokenService.getCurrentToken()?.trim() ?? '';
localToken = token;
}
if (clientId.isEmpty || policyId.isEmpty || branchId.isEmpty || token.isEmpty) {
logDebug(
'Skipping getEmployeeAndDependenceByClientId — empty params '
'client=$clientId policy=$policyId branch=$branchId tokenEmpty=${token.isEmpty}',
);
return {
'status': 'error',
'code': 400,
'message': 'Missing policy context',
'data': [],
};
}
if (localTokenType == 'post') {
return apiService.getEmployeeAndDependenceToApi(
@ -397,7 +476,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
clientId,
policyId,
branchId,
token!,
token,
cardData: cardData,
searchKey: searchKey,
);
@ -1531,7 +1610,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
}
}
Future<void> _openReminderConfigDialog() async {
Future<void> _openReminderDialog() async {
final clientId = localClientId ?? widget.ClientId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final clientBranchId = localClientBranchId ?? widget.clientBranchId;
@ -1541,7 +1620,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return _ReminderMailConfigDialog(
return _ReminderHubDialog(
clientId: clientId,
clientPolicyId: clientPolicyId,
clientBranchId: clientBranchId,
@ -1549,6 +1628,10 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
apiService: apiService,
defaultSubject: _defaultReminderSubject(),
defaultHtmlBody: _defaultReminderHtmlBody(),
onSend: (subject, htmlBody) => _sendEnrollmentReminder(
emailSubject: subject,
emailBody: htmlBody,
),
);
},
);
@ -1563,34 +1646,6 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
return kDefaultEnrollmentReminderHtmlBody;
}
Future<void> _openReminderTemplateDialog() async {
final clientId = localClientId ?? widget.ClientId;
final clientPolicyId = localClientPolicyId ?? widget.ClientPoliyId;
final clientBranchId = localClientBranchId ?? widget.clientBranchId;
final token = localToken ?? widget.Token;
await showDialog<void>(
context: context,
barrierDismissible: false,
builder: (dialogContext) {
return ReminderEmailTemplateDialog(
clientId: clientId,
clientPolicyId: clientPolicyId,
clientBranchId: clientBranchId,
token: token,
apiService: apiService,
defaultSubject: _defaultReminderSubject(),
defaultHtmlBody: _defaultReminderHtmlBody(),
mode: ReminderEmailTemplateMode.send,
onSend: (subject, htmlBody) => _sendEnrollmentReminder(
emailSubject: subject,
emailBody: htmlBody,
),
);
},
);
}
Future<void> _sendEnrollmentReminder({
required String emailSubject,
required String emailBody,
@ -1920,15 +1975,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
if (localTokenType == 'pre') ...[
(
label: 'Reminder',
icon: Icons.tune_rounded,
icon: Icons.notifications_active_outlined,
color: const Color(0xFF009195),
onTap: _openReminderConfigDialog,
),
(
label: 'Mail Template',
icon: Icons.mail_outline_rounded,
color: const Color(0xFFE26728),
onTap: _isSendingReminder ? null : _openReminderTemplateDialog,
onTap: _isSendingReminder ? null : _openReminderDialog,
),
],
if (localTokenType == 'post') ...[
@ -2364,18 +2413,12 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
),
if (localTokenType == 'pre') ...[
_buildHeaderActionButton(
label: 'Reminder Config',
label: 'Reminder',
color: const Color(0xFF009195),
width: isCompact ? 130 : 150,
onPressed: _openReminderConfigDialog,
),
_buildHeaderActionButton(
label: 'Reminder Mail Template',
color: const Color(0xFFE26728),
width: isCompact ? 170 : 210,
width: isCompact ? 120 : 130,
onPressed: _isSendingReminder
? null
: _openReminderTemplateDialog,
: _openReminderDialog,
isLoading: _isSendingReminder,
),
],
@ -3193,7 +3236,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
}
Widget _buildCDDataTable(BuildContext context) {
final showUHID = localPolicyTypeId != '6' && localPolicyTypeId != '7';
final showUHID = localTokenType == 'post' &&
localPolicyTypeId != '6' &&
localPolicyTypeId != '7';
final showLoggedIn = localTokenType == 'pre';
final showAction =
localTokenType != 'pre' && (hasAnyEcardLink || hasModule);
@ -3205,7 +3250,7 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
pinned: true,
delegate: _CDHeaderDelegate(
showUHID: showUHID,
uhidHeaderLabel: localTokenType == 'post' ? 'TPA ID' : 'UHID',
uhidHeaderLabel: 'TPA ID',
showLoggedIn: showLoggedIn,
showAction: showAction,
),
@ -3256,14 +3301,14 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
),
),
/// UHID / TPA ID
if (localPolicyTypeId != '6' && localPolicyTypeId != '7')
/// TPA ID (post only)
if (localTokenType == 'post' &&
localPolicyTypeId != '6' &&
localPolicyTypeId != '7')
Expanded(
flex: 2,
child: Text(
localTokenType == 'post'
? (item['tpa_id'] ?? '-')
: (item['uhid'] ?? '-'),
item['tpa_id'] ?? '-',
style: _dataBold,
),
),
@ -4500,6 +4545,150 @@ class _PolicyTermsDialog extends StatelessWidget {
}
}
class _ReminderHubDialog extends StatefulWidget {
final String clientId;
final String clientPolicyId;
final String clientBranchId;
final String token;
final ApiService apiService;
final String defaultSubject;
final String defaultHtmlBody;
final Future<void> Function(String subject, String htmlBody) onSend;
const _ReminderHubDialog({
required this.clientId,
required this.clientPolicyId,
required this.clientBranchId,
required this.token,
required this.apiService,
required this.defaultSubject,
required this.defaultHtmlBody,
required this.onSend,
});
@override
State<_ReminderHubDialog> createState() => _ReminderHubDialogState();
}
class _ReminderHubDialogState extends State<_ReminderHubDialog>
with SingleTickerProviderStateMixin {
static const _teal = Color(0xFF009195);
late final TabController _tabController;
@override
void initState() {
super.initState();
_tabController = TabController(length: 2, vsync: this);
_tabController.addListener(_onTabChanged);
}
void _onTabChanged() {
if (_tabController.indexIsChanging) return;
setState(() {});
}
@override
void dispose() {
_tabController.removeListener(_onTabChanged);
_tabController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final size = MediaQuery.sizeOf(context);
final isConfigTab = _tabController.index == 0;
final dialogWidth = isConfigTab
? math.min(520.0, size.width - 32)
: size.width * 0.95;
final dialogHeight = isConfigTab
? math.min(460.0, size.height * 0.72)
: size.height * 0.92;
return Dialog(
insetPadding: const EdgeInsets.all(16),
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: SizedBox(
width: dialogWidth,
height: dialogHeight,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 8, 0),
child: Row(
children: [
Text(
'Enrollment Reminder',
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w600,
),
),
const Spacer(),
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(Icons.close, size: 20),
),
],
),
),
TabBar(
controller: _tabController,
labelColor: _teal,
unselectedLabelColor: Colors.black54,
indicatorColor: _teal,
labelStyle: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
unselectedLabelStyle: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w500,
),
tabs: const [
Tab(text: 'Setup'),
Tab(text: 'Mail Template'),
],
),
const Divider(height: 1),
Expanded(
child: TabBarView(
controller: _tabController,
children: [
_ReminderMailConfigDialog(
clientId: widget.clientId,
clientPolicyId: widget.clientPolicyId,
clientBranchId: widget.clientBranchId,
token: widget.token,
apiService: widget.apiService,
defaultSubject: widget.defaultSubject,
defaultHtmlBody: widget.defaultHtmlBody,
embedded: true,
),
ReminderEmailTemplateDialog(
clientId: widget.clientId,
clientPolicyId: widget.clientPolicyId,
clientBranchId: widget.clientBranchId,
token: widget.token,
apiService: widget.apiService,
defaultSubject: widget.defaultSubject,
defaultHtmlBody: widget.defaultHtmlBody,
mode: ReminderEmailTemplateMode.send,
onSend: widget.onSend,
embedded: true,
),
],
),
),
],
),
),
);
}
}
class _ReminderMailConfigDialog extends StatefulWidget {
final String clientId;
final String clientPolicyId;
@ -4508,6 +4697,7 @@ class _ReminderMailConfigDialog extends StatefulWidget {
final ApiService apiService;
final String defaultSubject;
final String defaultHtmlBody;
final bool embedded;
const _ReminderMailConfigDialog({
required this.clientId,
@ -4517,6 +4707,7 @@ class _ReminderMailConfigDialog extends StatefulWidget {
required this.apiService,
required this.defaultSubject,
required this.defaultHtmlBody,
this.embedded = false,
});
@override
@ -4854,9 +5045,11 @@ class _ReminderMailConfigDialogState extends State<_ReminderMailConfigDialog> {
TextField(
controller: _reminderDaysController,
onChanged: (value) => _reminderDaysText = value,
style: GoogleFonts.poppins(fontSize: 14),
style: GoogleFonts.poppins(fontSize: 13),
decoration: InputDecoration(
isDense: true,
hintText: helperText,
hintStyle: GoogleFonts.poppins(fontSize: 12, color: Colors.black38),
filled: true,
fillColor: const Color(0xFFF5F5F5),
border: OutlineInputBorder(
@ -4897,9 +5090,188 @@ class _ReminderMailConfigDialogState extends State<_ReminderMailConfigDialog> {
);
}
Widget _buildConfigContent() {
final isBusy = _isSaving;
final formMaxWidth = widget.embedded ? 420.0 : 560.0;
if (_isLoadingConfig) {
return const Center(child: CircularProgressIndicator());
}
final form = ConstrainedBox(
constraints: BoxConstraints(maxWidth: formMaxWidth),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
'Configure the schedule for automated enrollment reminder emails.',
style: GoogleFonts.poppins(
fontSize: 13,
color: Colors.black54,
),
),
const SizedBox(height: 16),
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 4),
decoration: BoxDecoration(
color: const Color(0xFFF8FAFA),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: const Color(0xFFE8EEF0)),
),
child: Row(
children: [
Expanded(
child: Text(
'Enable scheduled reminders',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
),
Switch(
value: _isEnabled,
activeColor: const Color(0xFF009195),
onChanged: isBusy
? null
: (value) => setState(() => _isEnabled = value),
),
],
),
),
const SizedBox(height: 16),
Text(
'Frequency',
style: GoogleFonts.poppins(
fontSize: 13,
fontWeight: FontWeight.w600,
),
),
const SizedBox(height: 6),
SizedBox(
width: double.infinity,
child: DropdownButtonFormField<String>(
value: _frequency,
isExpanded: true,
isDense: 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: 8,
),
),
items: _frequencies.entries
.map(
(entry) => DropdownMenuItem(
value: entry.key,
child: Text(
entry.value,
style: GoogleFonts.poppins(fontSize: 13),
),
),
)
.toList(),
onChanged: isBusy
? null
: (value) {
if (value == null) return;
setState(() {
_frequency = value;
_reminderDaysText = '';
_reminderDaysController.clear();
_selectedWorkingDays.clear();
});
},
),
),
const SizedBox(height: 14),
_buildDaysField(),
],
),
);
return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 8),
child: Align(
alignment: Alignment.topCenter,
child: form,
),
);
}
Widget _buildConfigActions() {
final isBusy = _isSaving;
final formMaxWidth = widget.embedded ? 420.0 : 560.0;
return Padding(
padding: const EdgeInsets.fromLTRB(20, 8, 20, 16),
child: Align(
alignment: Alignment.topCenter,
child: ConstrainedBox(
constraints: BoxConstraints(maxWidth: formMaxWidth),
child: Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: isBusy ? null : () => Navigator.pop(context),
child: Text(
'Cancel',
style: GoogleFonts.poppins(color: Colors.black54),
),
),
const SizedBox(width: 8),
ElevatedButton(
onPressed: isBusy || _isLoadingConfig ? null : _saveConfig,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
minimumSize: const Size(88, 36),
padding: const EdgeInsets.symmetric(horizontal: 16),
),
child: _isSaving
? const SizedBox(
width: 18,
height: 18,
child: CircularProgressIndicator(
strokeWidth: 2,
color: Colors.white,
),
)
: Text(
'Save',
style: GoogleFonts.poppins(color: Colors.white),
),
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final isBusy = _isSaving;
if (widget.embedded) {
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Align(
alignment: Alignment.topCenter,
child: _buildConfigContent(),
),
),
const Divider(height: 1),
_buildConfigActions(),
],
);
}
return AlertDialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
@ -4909,119 +5281,15 @@ class _ReminderMailConfigDialogState extends State<_ReminderMailConfigDialog> {
),
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(),
const SizedBox(height: 20),
// OutlinedButton.icon(
// onPressed: isBusy ? null : _openTemplateEditor,
// icon: const Icon(Icons.edit_outlined, size: 18),
// label: Text(
// 'Edit Email Template',
// style: GoogleFonts.poppins(
// fontSize: 13,
// fontWeight: FontWeight.w600,
// ),
// ),
// style: OutlinedButton.styleFrom(
// foregroundColor: const Color(0xFF009195),
// side: const BorderSide(color: Color(0xFF009195)),
// padding: const EdgeInsets.symmetric(
// horizontal: 16,
// vertical: 12,
// ),
// ),
// ),
],
),
),
child: _buildConfigContent(),
),
actions: [
TextButton(
onPressed: isBusy ? null : () => Navigator.pop(context),
onPressed: _isSaving ? null : () => Navigator.pop(context),
child: Text('Cancel', style: GoogleFonts.poppins(color: Colors.black54)),
),
ElevatedButton(
onPressed: isBusy || _isLoadingConfig ? null : _saveConfig,
onPressed: _isSaving || _isLoadingConfig ? null : _saveConfig,
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFF009195),
),

View File

@ -585,6 +585,46 @@ class _PolicyGrid extends StatelessWidget {
return allocg == 'non-eb';
}
/// Persist policy context so hrPolicyDetails survives browser refresh.
Future<void> _persistHrPolicyContext(
TokenStorageService tokenService, {
required String clientId,
required String policyTypeId,
required String clientPolicyId,
required String clientBranchId,
required String token,
required String tokenType,
required String cardType,
required String cardPolicyNo,
required String cardInsurerName,
required String cardPolicyName,
required String cardPolicyExpDate,
required String totalPremium,
required dynamic isEcardBulkDownload,
}) async {
Future<void> write(String key, dynamic value) async {
final text = value?.toString() ?? '';
await tokenService.writeValue(key, text);
}
await write('hr_ClientId', clientId);
await write('hr_policyTypeId', policyTypeId);
await write('hr_ClientPoliyId', clientPolicyId);
await write('hr_clientBranchId', clientBranchId);
await write('hr_Token', token);
await write('hr_TokenType', tokenType);
await write('hr_cardType', cardType);
await write('hr_cardPolicyNo', cardPolicyNo);
await write('hr_cardInsurer_name', cardInsurerName);
await write('hr_cardPolicy_name', cardPolicyName);
await write('hr_cardPolicy_ExpDate', cardPolicyExpDate);
await write('hr_total_premium', totalPremium);
await write(
'hr_is_ecard_bulk_download_for_employee',
isEcardBulkDownload ?? 0,
);
}
ResponsiveGridConfig _getGridConfig(
BuildContext context,
bool isEnrollment,
@ -728,6 +768,23 @@ class _PolicyGrid extends StatelessWidget {
return;
}
await _persistHrPolicyContext(
tokenService,
clientId: clientId,
policyTypeId: data['policy_type_id'].toString(),
clientPolicyId: data['client_policy_id'].toString(),
clientBranchId: branchId,
token: token,
tokenType: 'pre',
cardType: data['type'].toString(),
cardPolicyNo: data['policy_no'].toString(),
cardInsurerName: data['insurer_short_name'].toString(),
cardPolicyName: data['policy_name'].toString(),
cardPolicyExpDate: data['policy_expiry_date'].toString(),
totalPremium: '',
isEcardBulkDownload: 0,
);
Navigator.push(
context,
MaterialPageRoute(
@ -801,6 +858,24 @@ class _PolicyGrid extends StatelessWidget {
return;
}
await _persistHrPolicyContext(
tokenService,
clientId: clientId,
policyTypeId: data['policy_type_id'].toString(),
clientPolicyId: data['client_policy_id'].toString(),
clientBranchId: branchId,
token: token,
tokenType: 'post',
cardType: data['type'].toString(),
cardPolicyNo: data['policy_no'].toString(),
cardInsurerName: data['insurer_short_name'].toString(),
cardPolicyName: data['policy_name'].toString(),
cardPolicyExpDate: data['policy_expiry_date'].toString(),
totalPremium: data['total_premium'].toString(),
isEcardBulkDownload:
data['is_ecard_bulk_download_for_employee'],
);
Navigator.push(
context,
MaterialPageRoute(

View File

@ -595,6 +595,7 @@ class ApiService {
final headers = {
'Authorization': 'Bearer ${token ?? ''}',
'Content-Type': 'application/json',
'APP-SIGNATURE':
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
};