diff --git a/lib/config/environment.dart b/lib/config/environment.dart index 08629ea..66c58e0 100755 --- a/lib/config/environment.dart +++ b/lib/config/environment.dart @@ -50,8 +50,6 @@ class Environment { if (isProd) base = "Nhance HR"; // You can differentiate names if needed switch (routeName) { - case 'hrDashboard': - return "$base - Insights"; case 'claimsOverviewDashboard': return "$base - Dashboard"; case 'policies': diff --git a/lib/customAppBar/enrollmentAppBar.dart b/lib/customAppBar/enrollmentAppBar.dart index 5f38747..0d73645 100755 --- a/lib/customAppBar/enrollmentAppBar.dart +++ b/lib/customAppBar/enrollmentAppBar.dart @@ -52,7 +52,7 @@ class _CustomAppBarState extends State { prefs.setBool('showBackToHR', false); await prefs.remove('hrSelectArgumentsData'); prefs.remove('token'); - Navigator.pushNamed(context, 'hrDashboard'); + Navigator.pushNamed(context, 'policies'); } else { await prefs.clear(); Navigator.pushNamed(context, 'phone'); diff --git a/lib/customAppBar/top_app_bar.dart b/lib/customAppBar/top_app_bar.dart index 0ae9ec0..1bf6527 100644 --- a/lib/customAppBar/top_app_bar.dart +++ b/lib/customAppBar/top_app_bar.dart @@ -1,3 +1,5 @@ +import 'dart:convert'; + import 'package:flutter/material.dart'; import '../service/token_storage_service.dart'; @@ -29,6 +31,18 @@ class _NhanceTopBarState extends State { setState(() {}); } + Future _getBranchSwitchRoute() async { + final postRaw = await tokenStorage.readValue('empAllowed_modules'); + final postModules = postRaw != null && postRaw.isNotEmpty + ? List.from(jsonDecode(postRaw)) + : []; + + if (postModules.isNotEmpty && postModules.contains(5)) { + return 'claimsOverviewDashboard'; + } + return 'policies'; + } + Future _onBranchSelected(Map branch) async { final tokenStorage = TokenStorageService(); @@ -36,9 +50,11 @@ class _NhanceTopBarState extends State { if (!mounted) return; - // 🔄 Refresh CURRENT PAGE only - final route = ModalRoute.of(context)?.settings.name ?? 'hrDashboard'; + _loadBranches(); + final route = await _getBranchSwitchRoute(); + + if (!mounted) return; Navigator.pushReplacementNamed(context, route); } diff --git a/lib/email_verify.dart b/lib/email_verify.dart index 1999208..ca0b010 100755 --- a/lib/email_verify.dart +++ b/lib/email_verify.dart @@ -366,7 +366,7 @@ class _MyEmailVerifyState extends State { if (_postToken != null && _postToken.isNotEmpty) { ToastHelper.showSuccessToast(context, 'Successfully Logged In'); // if (emp_status == 'enrolled' || emp_status == 'active') { - Navigator.pushReplacementNamed(context, 'hrDashboard'); + Navigator.pushReplacementNamed(context, 'policies'); // } else { // Navigator.pushReplacementNamed(context, 'empDetails'); // } @@ -410,7 +410,7 @@ class _MyEmailVerifyState extends State { // if (emp_status == 'enrolled' || emp_status == 'active') { // Navigator.pushReplacementNamed(context, 'home'); // } else { - Navigator.pushReplacementNamed(context, 'hrDashboard'); + Navigator.pushReplacementNamed(context, 'policies'); // } } } diff --git a/lib/main.dart b/lib/main.dart index 12c2aef..4bfe4da 100755 --- a/lib/main.dart +++ b/lib/main.dart @@ -16,7 +16,6 @@ import 'package:nhancepolicy/presentation/policies.dart'; import 'package:nhancepolicy/service/session/session_service.dart'; import 'package:nhancepolicy/service/token_storage_service.dart'; import 'package:nhancepolicy/home.dart'; -import 'package:nhancepolicy/presentation/hrDashboard.dart'; import 'package:nhancepolicy/presentation/claims_overview/claims_overview_dashboard.dart'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:nhancepolicy/hrLogin.dart'; @@ -267,7 +266,7 @@ final Map appRoutes = { 'empDetails': (context) => empDetails(), 'addOnsDetails': (context) => addOnsDetails(), 'empReviewDetails': (context) => empReviewDetails(), - 'hrDashboard': (context) => hrDashboard(), + 'hrDashboard': (context) => policies(), 'claimsOverviewDashboard': (context) => const ClaimsOverviewDashboard(), 'hrPolicyDetails': (context) => hrPolicyDetails( ClientId: '', diff --git a/lib/presentation/claims_overview/claims_overview_dashboard.dart b/lib/presentation/claims_overview/claims_overview_dashboard.dart index c60949e..b6a12a0 100644 --- a/lib/presentation/claims_overview/claims_overview_dashboard.dart +++ b/lib/presentation/claims_overview/claims_overview_dashboard.dart @@ -58,7 +58,7 @@ class _ClaimsOverviewDashboardState extends State (Icons.people_outline, 'Demographics'), (Icons.location_city_outlined, 'Hospitals & Geography'), (Icons.medical_services_outlined, 'Specialty Analysis'), - (Icons.group_add_outlined, 'Insurred'), + (Icons.group_add_outlined, 'Insured'), ]; @override diff --git a/lib/presentation/claims_overview/claims_overview_tabs.dart b/lib/presentation/claims_overview/claims_overview_tabs.dart index 88cde37..f2a394c 100644 --- a/lib/presentation/claims_overview/claims_overview_tabs.dart +++ b/lib/presentation/claims_overview/claims_overview_tabs.dart @@ -1026,7 +1026,7 @@ class _EnrollmentTabState extends State<_EnrollmentTab> ClaimsOverviewTheme.pink, ]; - final donutHeight = widget.forPdfExport ? 190.0 : 300.0; + final donutHeight = widget.forPdfExport ? 220.0 : 300.0; final trendHeight = widget.forPdfExport ? 228.0 : 320.0; final sectionGap = widget.forPdfExport ? 8.0 : 16.0; @@ -1235,19 +1235,16 @@ class _EnrollmentTabState extends State<_EnrollmentTab> animationKey: _chartKey, ); - if (forPdfExport) { - return chart; - } - return Column( children: [ Expanded(child: chart), - const SizedBox(height: 8), + SizedBox(height: forPdfExport ? 4 : 8), _EnrollmentDonutLegend( labels: series.labels, values: series.values, colors: colors, totalLabel: totalLabel, + compact: forPdfExport, ), ], ); @@ -1259,20 +1256,23 @@ class _EnrollmentDonutLegend extends StatelessWidget { final List values; final List colors; final String totalLabel; + final bool compact; const _EnrollmentDonutLegend({ required this.labels, required this.values, required this.colors, required this.totalLabel, + this.compact = false, }); @override Widget build(BuildContext context) { final total = values.fold(0, (a, b) => a + b); + final fontSize = compact ? 9.0 : 11.0; return Wrap( - spacing: 12, - runSpacing: 8, + spacing: compact ? 8 : 12, + runSpacing: compact ? 4 : 8, alignment: WrapAlignment.center, crossAxisAlignment: WrapCrossAlignment.center, children: [ @@ -1282,18 +1282,18 @@ class _EnrollmentDonutLegend extends StatelessWidget { mainAxisSize: MainAxisSize.min, children: [ Container( - width: 10, - height: 10, + width: compact ? 8 : 10, + height: compact ? 8 : 10, decoration: BoxDecoration( color: colors[i % colors.length], shape: BoxShape.circle, ), ), - const SizedBox(width: 6), + SizedBox(width: compact ? 4 : 6), Text( '${labels[i]} ${pct.toStringAsFixed(1)}%', style: GoogleFonts.poppins( - fontSize: 11, + fontSize: fontSize, color: ClaimsOverviewTheme.textSecondary, ), ), @@ -1303,7 +1303,7 @@ class _EnrollmentDonutLegend extends StatelessWidget { Text( 'Total: $totalLabel', style: GoogleFonts.poppins( - fontSize: 11, + fontSize: fontSize, fontWeight: FontWeight.w600, color: ClaimsOverviewTheme.textPrimary, ), diff --git a/lib/presentation/email_template/email_html_preview.dart b/lib/presentation/email_template/email_html_preview.dart new file mode 100644 index 0000000..291d83a --- /dev/null +++ b/lib/presentation/email_template/email_html_preview.dart @@ -0,0 +1,347 @@ +import 'dart:ui_web' as ui; + +import 'package:flutter/foundation.dart' show kIsWeb; +import 'package:flutter/material.dart'; +import 'package:google_fonts/google_fonts.dart'; +import 'package:nhancepolicy/presentation/email_template/email_template_models.dart'; +import 'package:universal_html/html.dart' as html; + +const _caretSelector = '[data-nhance-caret="1"]'; + +class EmailHtmlPreviewController { + _EmailHtmlPreviewState? _state; + + void _bind(_EmailHtmlPreviewState state) => _state = state; + + void _unbind(_EmailHtmlPreviewState state) { + if (_state == state) _state = null; + } + + bool get isReady => _state?.isReady ?? false; + + bool get hasCaretMarker => _state?.hasCaretMarker ?? false; + + String get currentHtml => _state?.currentHtml ?? ''; + + void removeCaretMarker() => _state?.removeCaretMarker(); + + bool insertSnippetAtCursor(String snippet) => + _state?.insertSnippetAtCursor(snippet) ?? false; +} + +class EmailHtmlPreview extends StatefulWidget { + final String html; + final double? height; + final bool editable; + final EmailHtmlPreviewController? controller; + + const EmailHtmlPreview({ + super.key, + required this.html, + this.height, + this.editable = false, + this.controller, + }); + + @override + State createState() => _EmailHtmlPreviewState(); +} + +class _EmailHtmlPreviewState extends State { + late final String _viewType; + html.IFrameElement? _iframe; + late String _activeHtml; + bool _iframeReady = false; + bool _selectionListenersAttached = false; + + bool get isReady => _iframeReady; + + bool get hasCaretMarker { + final doc = _iframeDocument; + if (doc == null) return false; + return doc.querySelector(_caretSelector) != null; + } + + html.Document? get _iframeDocument { + final iframe = _iframe; + if (iframe == null) return null; + try { + return (iframe as dynamic).contentDocument as html.Document?; + } catch (_) { + return null; + } + } + + html.Element? get _iframeBody { + final doc = _iframeDocument; + if (doc == null) return null; + return (doc as dynamic).body as html.Element?; + } + + dynamic get _iframeWindow { + final iframe = _iframe; + if (iframe == null) return null; + return (iframe as dynamic).contentWindow; + } + + String get currentHtml { + removeCaretMarker(); + try { + final root = _iframeDocument?.documentElement; + if (root != null) { + return '\n${root.outerHtml}'; + } + } catch (_) {} + return _stripCaretMarkers(_activeHtml); + } + + @override + void initState() { + super.initState(); + _activeHtml = _stripCaretMarkers(widget.html); + widget.controller?._bind(this); + _viewType = + 'email-preview-${identityHashCode(this)}-${DateTime.now().microsecondsSinceEpoch}'; + if (kIsWeb) { + _registerView(); + } + } + + @override + void dispose() { + widget.controller?._unbind(this); + super.dispose(); + } + + String _srcForIframe(String htmlContent) { + final clean = _stripCaretMarkers(htmlContent); + if (!widget.editable) return clean; + return _injectEditableBody(clean); + } + + String _injectEditableBody(String htmlContent) { + final bodyTag = RegExp(r']*)>', caseSensitive: false); + if (!bodyTag.hasMatch(htmlContent)) return htmlContent; + + return htmlContent.replaceFirstMapped(bodyTag, (match) { + final attrs = match.group(1) ?? ''; + if (RegExp('contenteditable', caseSensitive: false).hasMatch(attrs)) { + return match.group(0)!; + } + return ''; + }); + } + + String _stripCaretMarkers(String htmlContent) { + return htmlContent.replaceAll( + RegExp( + r']*data-nhance-caret="1"[^>]*>.*?', + caseSensitive: false, + dotAll: true, + ), + '', + ); + } + + void _registerView() { + ui.platformViewRegistry.registerViewFactory(_viewType, (int viewId) { + final iframe = html.IFrameElement() + ..style.border = 'none' + ..style.width = '100%' + ..style.height = '100%' + ..style.display = 'block' + ..style.pointerEvents = 'auto' + ..srcdoc = _srcForIframe(_activeHtml); + + if (!widget.editable) { + iframe.setAttribute('sandbox', 'allow-same-origin'); + } + + iframe.onLoad.listen((_) { + _iframeReady = true; + _selectionListenersAttached = false; + _setupEditableIframe(); + }); + + _iframe = iframe; + return iframe; + }); + } + + void _setupEditableIframe() { + if (!widget.editable || _selectionListenersAttached) return; + final body = _iframeBody; + if (body == null) return; + + body.setAttribute('contenteditable', 'true'); + body.style.setProperty('outline', 'none'); + body.style.setProperty('cursor', 'text'); + + // Place an invisible DOM marker at the click position so inserts survive + // focus moving to the sidebar. + body.onMouseUp.listen((_) => _placeCaretMarker()); + body.onKeyUp.listen((_) => _placeCaretMarker()); + body.onClick.listen((_) => _placeCaretMarker()); + + _selectionListenersAttached = true; + } + + html.Element _createCaretMarker(html.Document doc) { + final marker = doc.createElement('span'); + marker.setAttribute('data-nhance-caret', '1'); + marker.text = '\u200B'; + marker.style.setProperty('font-size', '0'); + marker.style.setProperty('line-height', '0'); + marker.style.setProperty('display', 'inline'); + return marker; + } + + void _placeCaretMarker() { + if (!widget.editable || !_iframeReady) return; + final doc = _iframeDocument; + final body = _iframeBody; + if (doc == null || body == null) return; + + removeCaretMarker(); + + final range = _readSelectionRange(body); + if (range == null) return; + + try { + final marker = _createCaretMarker(doc); + range.insertNode(marker); + (range as dynamic).setStartAfter(marker); + (range as dynamic).collapse(true); + + final win = _iframeWindow; + final sel = win?.getSelection() as html.Selection?; + sel?.removeAllRanges(); + sel?.addRange(range); + } catch (_) {} + } + + dynamic _readSelectionRange(html.Element body) { + try { + final win = _iframeWindow; + final sel = win?.getSelection() as html.Selection?; + if (sel == null || (sel.rangeCount ?? 0) == 0) return null; + + final range = sel.getRangeAt(0); + if (!_nodeInside(body, range.startContainer)) return null; + + return (range as dynamic).cloneRange(); + } catch (_) { + return null; + } + } + + void removeCaretMarker() { + final doc = _iframeDocument; + if (doc == null) return; + final markers = doc.querySelectorAll(_caretSelector).toList(); + for (final marker in markers) { + marker.remove(); + } + } + + bool insertSnippetAtCursor(String snippet) { + if (!widget.editable || !_iframeReady || snippet.trim().isEmpty) { + return false; + } + + final doc = _iframeDocument; + if (doc == null) return false; + + final marker = doc.querySelector(_caretSelector); + if (marker == null || marker.parent == null) return false; + + try { + final parent = marker.parent!; + final tempRange = doc.createRange(); + final fragment = + (tempRange as dynamic).createContextualFragment(snippet); + if (fragment == null) return false; + + parent.insertBefore(fragment, marker); + marker.remove(); + _activeHtml = currentHtml; + return true; + } catch (_) { + return false; + } + } + + bool _nodeInside(html.Node ancestor, html.Node node) { + var current = node; + while (true) { + if (current == ancestor) return true; + final parent = current.parentNode; + if (parent == null) return false; + current = parent; + } + } + + @override + Widget build(BuildContext context) { + final preview = kIsWeb + ? ClipRect( + clipBehavior: Clip.hardEdge, + child: SizedBox.expand( + child: HtmlElementView(viewType: _viewType), + ), + ) + : Center( + child: Padding( + padding: const EdgeInsets.all(16), + child: Text( + widget.editable + ? 'Editable email preview is available on web.' + : 'Email preview is available on web.', + style: GoogleFonts.poppins(fontSize: 13, color: Colors.black54), + textAlign: TextAlign.center, + ), + ), + ); + + final decorated = Container( + decoration: BoxDecoration( + color: Colors.white, + borderRadius: BorderRadius.circular(4), + border: Border.all(color: const Color(0xFFE5E7EB)), + ), + clipBehavior: Clip.hardEdge, + child: preview, + ); + + if (widget.height != null) { + return SizedBox(height: widget.height, child: decorated); + } + return decorated; + } +} + +String appendHtmlSnippet(String html, String snippet) { + if (html.trim().isEmpty) { + return EmailTemplateData.wrapPlainTextAsHtml(snippet); + } + + final lower = html.toLowerCase(); + + const footerMarkers = [ + '