reminder and config and ecard

This commit is contained in:
Surendiran 2026-06-26 12:07:23 +05:30
parent 3abde687ca
commit 2120e997f0
9 changed files with 701 additions and 220 deletions

View File

@ -91,10 +91,10 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Text( Text(
'Last Updated', 'MIS Creation Date',
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 11, fontSize: 11,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w400,
color: ClaimsOverviewTheme.green, color: ClaimsOverviewTheme.green,
), ),
), ),
@ -102,7 +102,8 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
Text( Text(
'· $createdAt', '· $createdAt',
style: GoogleFonts.poppins( style: GoogleFonts.poppins(
fontSize: 10, fontSize: 14,
fontWeight: FontWeight.w600,
color: ClaimsOverviewTheme.green, color: ClaimsOverviewTheme.green,
), ),
), ),

View File

@ -30,6 +30,8 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
late TabController _tabController; late TabController _tabController;
late AnimationController _headerFadeController; late AnimationController _headerFadeController;
late Animation<double> _headerFade; late Animation<double> _headerFade;
late final ScrollController _scrollController;
late final List<GlobalKey> _sectionKeys;
late final ApiService _apiService; late final ApiService _apiService;
final _tokenService = TokenStorageService(); final _tokenService = TokenStorageService();
@ -63,6 +65,8 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
void initState() { void initState() {
super.initState(); super.initState();
_apiService = ApiService(context); _apiService = ApiService(context);
_scrollController = ScrollController();
_sectionKeys = List.generate(_tabs.length, (_) => GlobalKey());
_tabController = TabController(length: _tabs.length, vsync: this); _tabController = TabController(length: _tabs.length, vsync: this);
_tabController.addListener(_onTabChanged); _tabController.addListener(_onTabChanged);
@ -87,6 +91,20 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
} }
} }
void _scrollToSection(int index) {
setState(() => _currentTab = index);
WidgetsBinding.instance.addPostFrameCallback((_) {
final sectionContext = _sectionKeys[index].currentContext;
if (sectionContext == null) return;
Scrollable.ensureVisible(
sectionContext,
duration: const Duration(milliseconds: 450),
curve: Curves.easeInOut,
alignment: 0.02,
);
});
}
Future<void> _loadDashboard({bool reloadPolicies = false}) async { Future<void> _loadDashboard({bool reloadPolicies = false}) async {
final isInitialLoad = _replayToken == 0; final isInitialLoad = _replayToken == 0;
setState(() { setState(() {
@ -407,6 +425,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
void dispose() { void dispose() {
_tabController.removeListener(_onTabChanged); _tabController.removeListener(_onTabChanged);
_tabController.dispose(); _tabController.dispose();
_scrollController.dispose();
_headerFadeController.dispose(); _headerFadeController.dispose();
super.dispose(); super.dispose();
} }
@ -433,23 +452,30 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
child: EnrollmentOverviewScope( child: EnrollmentOverviewScope(
key: ValueKey('enrollment-$_selectedPolicyId-$_replayToken'), key: ValueKey('enrollment-$_selectedPolicyId-$_replayToken'),
data: _enrollmentViewData, data: _enrollmentViewData,
child: TabBarView( child: ClaimsTabAnimatedShell(
controller: _tabController, isLoading: _isLoading,
physics: _isRefreshing skeletonBlocks: 5,
? const NeverScrollableScrollPhysics() child: Scrollbar(
: null, controller: _scrollController,
children: List.generate( thumbVisibility: true,
_tabs.length, child: SingleChildScrollView(
(i) => Padding( controller: _scrollController,
padding: const EdgeInsets.only(top: 16), physics: _isRefreshing
child: ClaimsTabAnimatedShell( ? const NeverScrollableScrollPhysics()
isLoading: _isLoading, : const ClampingScrollPhysics(),
skeletonBlocks: i == 0 ? 5 : 4, child: Column(
child: ClaimsOverviewTab( crossAxisAlignment: CrossAxisAlignment.stretch,
index: i, children: [
replayToken: _replayToken, for (var i = 0; i < _tabs.length; i++) ...[
isActive: _currentTab == i, _buildSection(
), index: i,
replayToken: _replayToken,
),
if (i < _tabs.length - 1)
const SizedBox(height: 8),
],
const SizedBox(height: 24),
],
), ),
), ),
), ),
@ -697,6 +723,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
), ),
TabBar( TabBar(
controller: _tabController, controller: _tabController,
onTap: _scrollToSection,
isScrollable: true, isScrollable: true,
tabAlignment: TabAlignment.start, tabAlignment: TabAlignment.start,
labelColor: ClaimsOverviewTheme.primary, labelColor: ClaimsOverviewTheme.primary,
@ -745,6 +772,56 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
); );
} }
Widget _buildSection({
required int index,
required int replayToken,
}) {
final tab = _tabs[index];
return Container(
key: _sectionKeys[index],
margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: ClaimsOverviewTheme.cardBg,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: ClaimsOverviewTheme.border),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Padding(
padding: const EdgeInsets.fromLTRB(20, 16, 20, 0),
child: Row(
children: [
Icon(
tab.$1,
size: 22,
color: ClaimsOverviewTheme.primary,
),
const SizedBox(width: 10),
Text(
tab.$2,
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
),
),
],
),
),
const SizedBox(height: 12),
ClaimsOverviewTab(
index: index,
replayToken: replayToken,
isActive: true,
scrollable: false,
),
const SizedBox(height: 16),
],
),
);
}
Widget _buildFooter() { Widget _buildFooter() {
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 12), padding: const EdgeInsets.symmetric(vertical: 12),

View File

@ -35,7 +35,7 @@ const _tabNames = [
const _tabFallbackHeights = [ const _tabFallbackHeights = [
720.0, 720.0,
680.0, 680.0,
660.0, 820.0,
760.0, 760.0,
680.0, 680.0,
580.0, 580.0,
@ -255,8 +255,10 @@ Widget _buildTabCapture({
} }
Future<void> _waitForPaint() async { Future<void> _waitForPaint() async {
await WidgetsBinding.instance.endOfFrame; for (var i = 0; i < 3; i++) {
await Future<void>.delayed(const Duration(milliseconds: 32)); await WidgetsBinding.instance.endOfFrame;
await Future<void>.delayed(const Duration(milliseconds: 48));
}
} }
double _capturePixelRatio({required bool isEnrollmentTab}) { double _capturePixelRatio({required bool isEnrollmentTab}) {
@ -285,6 +287,7 @@ Future<Uint8List?> _captureTabPng({
required OverlayState overlay, required OverlayState overlay,
required Widget child, required Widget child,
required double fallbackHeight, required double fallbackHeight,
double? maxMeasureHeight,
bool isEnrollmentTab = false, bool isEnrollmentTab = false,
ClaimsPdfExportProgress? onProgress, ClaimsPdfExportProgress? onProgress,
String? progressLabel, String? progressLabel,
@ -309,14 +312,19 @@ Future<Uint8List?> _captureTabPng({
key: boundaryKey, key: boundaryKey,
child: MediaQuery( child: MediaQuery(
data: const MediaQueryData(), data: const MediaQueryData(),
child: ConstrainedBox( child: SingleChildScrollView(
constraints: BoxConstraints( physics: const NeverScrollableScrollPhysics(),
maxWidth: _captureWidth, child: ConstrainedBox(
maxHeight: isEnrollmentTab constraints: BoxConstraints(
? _enrollmentMaxMeasureHeight maxWidth: _captureWidth,
: _maxMeasureHeight, minHeight: fallbackHeight,
maxHeight: maxMeasureHeight ??
(isEnrollmentTab
? _enrollmentMaxMeasureHeight
: _maxMeasureHeight),
),
child: child,
), ),
child: ClipRect(child: child),
), ),
), ),
), ),
@ -339,6 +347,15 @@ Future<Uint8List?> _captureTabPng({
return null; return null;
} }
final measuredHeight = renderObject.size.height;
if (measuredHeight < fallbackHeight * 0.85) {
debugPrint(
'PDF export: $progressLabel measured ${measuredHeight.toStringAsFixed(0)}px '
'(expected >= ${fallbackHeight.toStringAsFixed(0)}px)',
);
await _waitForPaint();
}
onProgress?.call(0, 0, '$progressLabel (rasterize…)'); onProgress?.call(0, 0, '$progressLabel (rasterize…)');
var ratio = _capturePixelRatio(isEnrollmentTab: isEnrollmentTab); var ratio = _capturePixelRatio(isEnrollmentTab: isEnrollmentTab);
@ -455,6 +472,7 @@ Future<void> exportClaimsOverviewChartsPdf({
overlay: overlay, overlay: overlay,
child: tabWidget, child: tabWidget,
fallbackHeight: _tabFallbackHeights[i], fallbackHeight: _tabFallbackHeights[i],
maxMeasureHeight: i == 2 ? 1000.0 : null,
isEnrollmentTab: i == 6, isEnrollmentTab: i == 6,
onProgress: onProgress, onProgress: onProgress,
progressLabel: captureLabel, progressLabel: captureLabel,

View File

@ -12,8 +12,15 @@ import 'claims_overview_widgets.dart';
Widget _tabBodyWrapper({ Widget _tabBodyWrapper({
required bool forPdfExport, required bool forPdfExport,
required Widget child, required Widget child,
bool scrollable = true,
}) { }) {
if (forPdfExport) return child; if (forPdfExport) return child;
if (!scrollable) {
return Padding(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
child: child,
);
}
return SingleChildScrollView( return SingleChildScrollView(
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24), padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
child: child, child: child,
@ -38,6 +45,7 @@ class ClaimsOverviewTab extends StatelessWidget {
final int replayToken; final int replayToken;
final bool isActive; final bool isActive;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const ClaimsOverviewTab({ const ClaimsOverviewTab({
super.key, super.key,
@ -45,6 +53,7 @@ class ClaimsOverviewTab extends StatelessWidget {
required this.replayToken, required this.replayToken,
this.isActive = false, this.isActive = false,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -53,33 +62,40 @@ class ClaimsOverviewTab extends StatelessWidget {
0 => _OverviewTab( 0 => _OverviewTab(
replayToken: replayToken, replayToken: replayToken,
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
), ),
1 => _PolicyExperienceTab( 1 => _PolicyExperienceTab(
replayToken: replayToken, replayToken: replayToken,
isActive: isActive || forPdfExport, isActive: isActive || forPdfExport,
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
), ),
2 => _ClaimsAnalysisTab( 2 => _ClaimsAnalysisTab(
replayToken: replayToken, replayToken: replayToken,
isActive: isActive || forPdfExport, isActive: isActive || forPdfExport,
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
), ),
3 => _DemographicsTab( 3 => _DemographicsTab(
replayToken: replayToken, replayToken: replayToken,
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
), ),
4 => _HospitalsTab( 4 => _HospitalsTab(
replayToken: replayToken, replayToken: replayToken,
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
), ),
5 => _SpecialtyTab( 5 => _SpecialtyTab(
replayToken: replayToken, replayToken: replayToken,
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
), ),
6 => _EnrollmentTab( 6 => _EnrollmentTab(
replayToken: replayToken, replayToken: replayToken,
isActive: isActive || forPdfExport, isActive: isActive || forPdfExport,
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
), ),
_ => const SizedBox(), _ => const SizedBox(),
}; };
@ -89,10 +105,12 @@ class ClaimsOverviewTab extends StatelessWidget {
class _OverviewTab extends StatelessWidget { class _OverviewTab extends StatelessWidget {
final int replayToken; final int replayToken;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const _OverviewTab({ const _OverviewTab({
required this.replayToken, required this.replayToken,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -100,6 +118,7 @@ class _OverviewTab extends StatelessWidget {
final d = ClaimsOverviewScope.of(context); final d = ClaimsOverviewScope.of(context);
return _tabBodyWrapper( return _tabBodyWrapper(
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
@ -134,6 +153,7 @@ class _OverviewTab extends StatelessWidget {
left: ClaimsSectionCard( left: ClaimsSectionCard(
title: 'Claims Experience', title: 'Claims Experience',
icon: Icons.analytics_outlined, icon: Icons.analytics_outlined,
minHeight: ClaimsSectionCard.pairedSectionMinHeight,
child: ClaimsExperienceBody( child: ClaimsExperienceBody(
incurredClaims: d.incurredClaims, incurredClaims: d.incurredClaims,
incurredRatio: d.incurredRatio, incurredRatio: d.incurredRatio,
@ -145,6 +165,7 @@ class _OverviewTab extends StatelessWidget {
right: ClaimsSectionCard( right: ClaimsSectionCard(
title: 'Inception', title: 'Inception',
icon: Icons.flag_outlined, icon: Icons.flag_outlined,
minHeight: ClaimsSectionCard.pairedSectionMinHeight,
child: ClaimsInceptionBody( child: ClaimsInceptionBody(
inceptionEmployee: d.inceptionEmployee, inceptionEmployee: d.inceptionEmployee,
inceptionLives: d.inceptionLives, inceptionLives: d.inceptionLives,
@ -160,8 +181,12 @@ class _OverviewTab extends StatelessWidget {
required int baseIndex, required int baseIndex,
required Widget left, required Widget left,
required Widget right, required Widget right,
bool equalizeHeight = false,
}) { }) {
return ClaimsTabPanelRow( final row = ClaimsTabPanelRow(
crossAxisAlignment: equalizeHeight
? CrossAxisAlignment.stretch
: CrossAxisAlignment.start,
panels: [ panels: [
ClaimsTabPanel( ClaimsTabPanel(
flex: 1, flex: 1,
@ -181,6 +206,8 @@ class _OverviewTab extends StatelessWidget {
), ),
], ],
); );
if (!equalizeHeight) return row;
return IntrinsicHeight(child: row);
} }
} }
@ -189,11 +216,13 @@ class _PolicyExperienceTab extends StatefulWidget {
final int replayToken; final int replayToken;
final bool isActive; final bool isActive;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const _PolicyExperienceTab({ const _PolicyExperienceTab({
required this.replayToken, required this.replayToken,
required this.isActive, required this.isActive,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -240,41 +269,33 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
final countSeries = d.claimsCountByStatus; final countSeries = d.claimsCountByStatus;
final kpiMetrics = [ final kpiMetrics = [
('Total Claims', d.sidebarTotalClaims, Icons.tag, ClaimsOverviewTheme.blue), ('Total Claims', d.sidebarTotalClaims, null, Icons.tag, ClaimsOverviewTheme.blue),
( (
'Total Incurred Amount', 'Total Incurred Amount',
d.sidebarIncurredAmount, d.sidebarIncurredAmount,
null,
Icons.currency_rupee, Icons.currency_rupee,
ClaimsOverviewTheme.pink, ClaimsOverviewTheme.pink,
), ),
( (
'Total Reimbursement', 'Total Reimbursement',
d.sidebarReimbursement, d.sidebarReimbursement,
d.sidebarReimbursementPct,
Icons.account_balance_wallet_outlined, Icons.account_balance_wallet_outlined,
ClaimsOverviewTheme.teal, ClaimsOverviewTheme.teal,
), ),
(
'Reimbursement %',
d.sidebarReimbursementPct,
Icons.percent,
ClaimsOverviewTheme.orange,
),
( (
'Cashless Claim Amount', 'Cashless Claim Amount',
d.sidebarCashless, d.sidebarCashless,
d.sidebarCashlessPct,
Icons.shopping_bag_outlined, Icons.shopping_bag_outlined,
ClaimsOverviewTheme.purple, ClaimsOverviewTheme.purple,
), ),
(
'Cashless %',
d.sidebarCashlessPct,
Icons.pie_chart_outline,
ClaimsOverviewTheme.teal,
),
]; ];
return _tabBodyWrapper( return _tabBodyWrapper(
forPdfExport: widget.forPdfExport, forPdfExport: widget.forPdfExport,
scrollable: widget.scrollable,
child: ClaimsTabPanelRow( child: ClaimsTabPanelRow(
panels: [ panels: [
ClaimsTabPanel( ClaimsTabPanel(
@ -339,8 +360,9 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
child: ClaimsMetricCard( child: ClaimsMetricCard(
label: kpiMetrics[i].$1, label: kpiMetrics[i].$1,
value: kpiMetrics[i].$2, value: kpiMetrics[i].$2,
icon: kpiMetrics[i].$3, secondaryValue: kpiMetrics[i].$3,
accentColor: kpiMetrics[i].$4, icon: kpiMetrics[i].$4,
accentColor: kpiMetrics[i].$5,
replayToken: widget.replayToken, replayToken: widget.replayToken,
), ),
), ),
@ -358,11 +380,13 @@ class _ClaimsAnalysisTab extends StatefulWidget {
final int replayToken; final int replayToken;
final bool isActive; final bool isActive;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const _ClaimsAnalysisTab({ const _ClaimsAnalysisTab({
required this.replayToken, required this.replayToken,
required this.isActive, required this.isActive,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -371,7 +395,7 @@ class _ClaimsAnalysisTab extends StatefulWidget {
class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab> class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
with AutomaticKeepAliveClientMixin { with AutomaticKeepAliveClientMixin {
static const _pairedChartHeight = 280.0; double get _pairedChartHeight => widget.forPdfExport ? 210.0 : 280.0;
int _chartAnimKey = 0; int _chartAnimKey = 0;
@ -416,7 +440,9 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
return _tabBodyWrapper( return _tabBodyWrapper(
forPdfExport: widget.forPdfExport, forPdfExport: widget.forPdfExport,
scrollable: widget.scrollable,
child: Column( child: Column(
mainAxisSize: MainAxisSize.min,
children: [ children: [
_chartRow( _chartRow(
0, 0,
@ -515,7 +541,6 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
), ),
], ],
); );
if (widget.forPdfExport) return row;
return IntrinsicHeight(child: row); return IntrinsicHeight(child: row);
} }
} }
@ -523,10 +548,12 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
class _DemographicsTab extends StatelessWidget { class _DemographicsTab extends StatelessWidget {
final int replayToken; final int replayToken;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const _DemographicsTab({ const _DemographicsTab({
required this.replayToken, required this.replayToken,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -548,6 +575,7 @@ class _DemographicsTab extends StatelessWidget {
return _tabBodyWrapper( return _tabBodyWrapper(
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
child: Column( child: Column(
children: [ children: [
if (relationships.isNotEmpty) ...[ if (relationships.isNotEmpty) ...[
@ -633,10 +661,12 @@ class _DemographicsTab extends StatelessWidget {
class _HospitalsTab extends StatelessWidget { class _HospitalsTab extends StatelessWidget {
final int replayToken; final int replayToken;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const _HospitalsTab({ const _HospitalsTab({
required this.replayToken, required this.replayToken,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -672,6 +702,7 @@ class _HospitalsTab extends StatelessWidget {
return _tabBodyWrapper( return _tabBodyWrapper(
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
child: Column( child: Column(
children: [ children: [
IntrinsicHeight( IntrinsicHeight(
@ -771,10 +802,12 @@ class _HospitalsTab extends StatelessWidget {
class _SpecialtyTab extends StatelessWidget { class _SpecialtyTab extends StatelessWidget {
final int replayToken; final int replayToken;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const _SpecialtyTab({ const _SpecialtyTab({
required this.replayToken, required this.replayToken,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -800,6 +833,7 @@ class _SpecialtyTab extends StatelessWidget {
return _tabBodyWrapper( return _tabBodyWrapper(
forPdfExport: forPdfExport, forPdfExport: forPdfExport,
scrollable: scrollable,
child: Column( child: Column(
children: [ children: [
ClaimsTabPanelRow( ClaimsTabPanelRow(
@ -933,11 +967,13 @@ class _EnrollmentTab extends StatefulWidget {
final int replayToken; final int replayToken;
final bool isActive; final bool isActive;
final bool forPdfExport; final bool forPdfExport;
final bool scrollable;
const _EnrollmentTab({ const _EnrollmentTab({
required this.replayToken, required this.replayToken,
required this.isActive, required this.isActive,
this.forPdfExport = false, this.forPdfExport = false,
this.scrollable = true,
}); });
@override @override
@ -1161,6 +1197,7 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
return _tabBodyWrapper( return _tabBodyWrapper(
forPdfExport: widget.forPdfExport, forPdfExport: widget.forPdfExport,
scrollable: widget.scrollable,
child: widget.forPdfExport ? ClipRect(child: body) : body, child: widget.forPdfExport ? ClipRect(child: body) : body,
); );
} }

View File

@ -216,42 +216,40 @@ class ClaimsExperienceBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final incurredTile = _OverviewInnerTile(
label: 'Incurred Claims',
icon: Icons.gps_fixed,
accentColor: ClaimsOverviewTheme.red,
child: _ClaimsExperienceDualValues(
primaryValue: incurredClaims,
secondaryValue: incurredRatio,
),
);
final projectedTile = _OverviewInnerTile(
label: 'Projected Claims',
icon: Icons.trending_up,
accentColor: ClaimsOverviewTheme.orange,
child: _ClaimsExperienceDualValues(
primaryValue: projectedClaims,
secondaryValue: projectedRatio,
),
);
final incidenceTile = _OverviewInnerTile(
label: 'Claims Incidence Rate',
icon: Icons.timeline,
accentColor: ClaimsOverviewTheme.cyan,
child: Text(
claimsIncidenceRate,
style: GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.25,
),
),
);
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
if (constraints.maxWidth < 520) { final wide = constraints.maxWidth >= 520;
final incurredTile = _OverviewInnerTile(
label: 'Incurred Claims',
icon: Icons.gps_fixed,
accentColor: ClaimsOverviewTheme.red,
matchRowHeight: wide,
child: _ClaimsExperienceDualValues(
primaryValue: incurredClaims,
secondaryValue: incurredRatio,
),
);
final projectedTile = _OverviewInnerTile(
label: 'Projected Claims',
icon: Icons.trending_up,
accentColor: ClaimsOverviewTheme.orange,
matchRowHeight: wide,
child: _ClaimsExperienceDualValues(
primaryValue: projectedClaims,
secondaryValue: projectedRatio,
),
);
final incidenceTile = _OverviewInnerTile(
label: 'Claims Incidence Rate',
icon: Icons.timeline,
accentColor: ClaimsOverviewTheme.cyan,
matchRowHeight: wide,
child: _ClaimsExperienceDualValues(
primaryValue: claimsIncidenceRate,
),
);
if (!wide) {
return Column( return Column(
children: [ children: [
incurredTile, incurredTile,
@ -263,6 +261,7 @@ class ClaimsExperienceBody extends StatelessWidget {
); );
} }
return _OverviewThreeColumnRow( return _OverviewThreeColumnRow(
equalizeHeight: true,
children: [incurredTile, projectedTile, incidenceTile], children: [incurredTile, projectedTile, incidenceTile],
); );
}, },
@ -282,29 +281,32 @@ class ClaimsInceptionBody extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final valueStyle = GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.25,
);
final employeeTile = _OverviewInnerTile(
label: 'Inception Employee',
icon: Icons.person_add_alt,
accentColor: ClaimsOverviewTheme.blue,
child: Text(inceptionEmployee, style: valueStyle),
);
final livesTile = _OverviewInnerTile(
label: 'Inception Lives',
icon: Icons.favorite_border,
accentColor: ClaimsOverviewTheme.pink,
child: Text(inceptionLives, style: valueStyle),
);
return LayoutBuilder( return LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
if (constraints.maxWidth < 400) { final wide = constraints.maxWidth >= 400;
final valueStyle = GoogleFonts.poppins(
fontSize: 18,
fontWeight: FontWeight.w700,
color: ClaimsOverviewTheme.textPrimary,
height: 1.25,
);
final employeeTile = _OverviewInnerTile(
label: 'Inception Employee',
icon: Icons.person_add_alt,
accentColor: ClaimsOverviewTheme.blue,
matchRowHeight: wide,
child: Text(inceptionEmployee, style: valueStyle),
);
final livesTile = _OverviewInnerTile(
label: 'Inception Lives',
icon: Icons.favorite_border,
accentColor: ClaimsOverviewTheme.pink,
matchRowHeight: wide,
child: Text(inceptionLives, style: valueStyle),
);
if (!wide) {
return Column( return Column(
children: [ children: [
employeeTile, employeeTile,
@ -314,6 +316,7 @@ class ClaimsInceptionBody extends StatelessWidget {
); );
} }
return _OverviewTwoColumnRow( return _OverviewTwoColumnRow(
equalizeHeight: true,
children: [employeeTile, livesTile], children: [employeeTile, livesTile],
); );
}, },
@ -348,12 +351,14 @@ class _OverviewInnerTile extends StatelessWidget {
final IconData icon; final IconData icon;
final Color accentColor; final Color accentColor;
final Widget child; final Widget child;
final bool matchRowHeight;
const _OverviewInnerTile({ const _OverviewInnerTile({
required this.label, required this.label,
required this.icon, required this.icon,
required this.accentColor, required this.accentColor,
required this.child, required this.child,
this.matchRowHeight = false,
}); });
@override @override
@ -362,12 +367,14 @@ class _OverviewInnerTile extends StatelessWidget {
accentColor: accentColor, accentColor: accentColor,
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: matchRowHeight ? MainAxisSize.max : MainAxisSize.min,
children: [ children: [
Padding( Padding(
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8), padding: const EdgeInsets.fromLTRB(14, 10, 14, 8),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Row( Row(
@ -401,6 +408,7 @@ class _OverviewInnerTile extends StatelessWidget {
], ],
), ),
), ),
if (matchRowHeight) const Spacer(),
Container( Container(
height: 3, height: 3,
decoration: BoxDecoration( decoration: BoxDecoration(
@ -419,11 +427,11 @@ class _OverviewInnerTile extends StatelessWidget {
class _ClaimsExperienceDualValues extends StatelessWidget { class _ClaimsExperienceDualValues extends StatelessWidget {
final String primaryValue; final String primaryValue;
final String secondaryValue; final String? secondaryValue;
const _ClaimsExperienceDualValues({ const _ClaimsExperienceDualValues({
required this.primaryValue, required this.primaryValue,
required this.secondaryValue, this.secondaryValue,
}); });
@override @override
@ -435,25 +443,26 @@ class _ClaimsExperienceDualValues extends StatelessWidget {
height: 1.2, height: 1.2,
); );
return Row( return Column(
crossAxisAlignment: CrossAxisAlignment.end, crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [ children: [
Expanded(
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.centerLeft,
child: Text(
primaryValue,
maxLines: 1,
style: valueStyle,
),
),
),
const SizedBox(width: 8),
Text( Text(
secondaryValue, primaryValue,
style: valueStyle.copyWith(fontSize: 16), maxLines: 2,
overflow: TextOverflow.ellipsis,
style: valueStyle,
), ),
if (secondaryValue != null && secondaryValue!.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
secondaryValue!,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: valueStyle.copyWith(fontSize: 16),
),
],
], ],
); );
} }
@ -546,34 +555,48 @@ class ClaimsPremiumMembershipBody extends StatelessWidget {
/// Two equal columns used for TPA / Insurer side-by-side rows. /// Two equal columns used for TPA / Insurer side-by-side rows.
class _OverviewTwoColumnRow extends StatelessWidget { class _OverviewTwoColumnRow extends StatelessWidget {
final List<Widget> children; final List<Widget> children;
final bool equalizeHeight;
const _OverviewTwoColumnRow({required this.children}); const _OverviewTwoColumnRow({
required this.children,
this.equalizeHeight = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(children.length == 2); assert(children.length == 2);
return Row( final row = Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: equalizeHeight
? CrossAxisAlignment.stretch
: CrossAxisAlignment.start,
children: [ children: [
Expanded(child: children[0]), Expanded(child: children[0]),
const SizedBox(width: 12), const SizedBox(width: 12),
Expanded(child: children[1]), Expanded(child: children[1]),
], ],
); );
if (!equalizeHeight) return row;
return IntrinsicHeight(child: row);
} }
} }
/// Three equal columns matches Policy Information top-row alignment. /// Three equal columns matches Policy Information top-row alignment.
class _OverviewThreeColumnRow extends StatelessWidget { class _OverviewThreeColumnRow extends StatelessWidget {
final List<Widget> children; final List<Widget> children;
final bool equalizeHeight;
const _OverviewThreeColumnRow({required this.children}); const _OverviewThreeColumnRow({
required this.children,
this.equalizeHeight = false,
});
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
assert(children.isNotEmpty && children.length <= 3); assert(children.isNotEmpty && children.length <= 3);
return Row( final row = Row(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: equalizeHeight
? CrossAxisAlignment.stretch
: CrossAxisAlignment.start,
children: [ children: [
for (var i = 0; i < 3; i++) ...[ for (var i = 0; i < 3; i++) ...[
if (i > 0) const SizedBox(width: 12), if (i > 0) const SizedBox(width: 12),
@ -585,6 +608,8 @@ class _OverviewThreeColumnRow extends StatelessWidget {
], ],
], ],
); );
if (!equalizeHeight) return row;
return IntrinsicHeight(child: row);
} }
} }
@ -594,6 +619,7 @@ class ClaimsMetricCard extends StatelessWidget {
final String label; final String label;
final String value; final String value;
final String? secondaryValue;
final IconData icon; final IconData icon;
final Color accentColor; final Color accentColor;
final int replayToken; final int replayToken;
@ -603,6 +629,7 @@ class ClaimsMetricCard extends StatelessWidget {
super.key, super.key,
required this.label, required this.label,
required this.value, required this.value,
this.secondaryValue,
required this.icon, required this.icon,
required this.accentColor, required this.accentColor,
this.replayToken = 0, this.replayToken = 0,
@ -673,17 +700,42 @@ class ClaimsMetricCard extends StatelessWidget {
height: compact ? 40 : 48, height: compact ? 40 : 48,
child: Align( child: Align(
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: compact || !animateValue child: secondaryValue == null
? Text( ? (compact || !animateValue
value, ? Text(
style: valueStyle, value,
maxLines: 3, style: valueStyle,
overflow: TextOverflow.ellipsis, maxLines: 3,
) overflow: TextOverflow.ellipsis,
: ClaimsAnimatedValueText( )
value: value, : ClaimsAnimatedValueText(
replayToken: replayToken, value: value,
style: valueStyle, replayToken: replayToken,
style: valueStyle,
))
: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: compact || !animateValue
? Text(
value,
style: valueStyle,
maxLines: 2,
overflow: TextOverflow.ellipsis,
)
: ClaimsAnimatedValueText(
value: value,
replayToken: replayToken,
style: valueStyle,
),
),
const SizedBox(width: 8),
Text(
secondaryValue!,
style: valueStyle.copyWith(fontSize: compact ? 13 : 16),
),
],
), ),
), ),
), ),
@ -731,11 +783,15 @@ class ClaimsMetricCard extends StatelessWidget {
} }
class ClaimsSectionCard extends StatelessWidget { class ClaimsSectionCard extends StatelessWidget {
/// Min height for side-by-side overview pairs (e.g. Claims Experience / Inception).
static const double pairedSectionMinHeight = 170;
final String title; final String title;
final IconData icon; final IconData icon;
final Widget child; final Widget child;
final bool fillHeight; final bool fillHeight;
final double? height; final double? height;
final double? minHeight;
const ClaimsSectionCard({ const ClaimsSectionCard({
super.key, super.key,
@ -744,6 +800,7 @@ class ClaimsSectionCard extends StatelessWidget {
required this.child, required this.child,
this.fillHeight = false, this.fillHeight = false,
this.height, this.height,
this.minHeight,
}); });
@override @override
@ -773,12 +830,16 @@ class ClaimsSectionCard extends StatelessWidget {
), ),
); );
final useExpandedBody = fillHeight || height != null; final useExpandedBody = height != null;
final stretchVertically = fillHeight || height != null;
final bodySlot = useExpandedBody ? Expanded(child: body) : body; final bodySlot = useExpandedBody ? Expanded(child: body) : body;
return Container( return Container(
width: double.infinity, width: double.infinity,
height: height, height: height,
constraints: height == null && minHeight != null
? BoxConstraints(minHeight: minHeight!)
: null,
decoration: BoxDecoration( decoration: BoxDecoration(
color: ClaimsOverviewTheme.cardBg, color: ClaimsOverviewTheme.cardBg,
borderRadius: BorderRadius.circular(12), borderRadius: BorderRadius.circular(12),
@ -794,7 +855,7 @@ class ClaimsSectionCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,
mainAxisSize: mainAxisSize:
useExpandedBody ? MainAxisSize.max : MainAxisSize.min, stretchVertically ? MainAxisSize.max : MainAxisSize.min,
children: [ children: [
header, header,
bodySlot, bodySlot,

View File

@ -17,6 +17,7 @@ import 'package:universal_html/html.dart' as html;
import 'dart:typed_data'; import 'dart:typed_data';
import 'dart:io'; import 'dart:io';
import 'package:flutter/foundation.dart' show kIsWeb; import 'package:flutter/foundation.dart' show kIsWeb;
import 'package:flutter/services.dart';
import 'package:intl/intl.dart'; import 'package:intl/intl.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -448,6 +449,57 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
} }
} }
Future<void> copyEcardLink(String? ecardLink) async {
if (ecardLink == null || ecardLink.trim().isEmpty) {
if (mounted) {
ToastHelper.showErrorToast(context, 'E-card link not available');
}
return;
}
if (kIsWeb) {
await html.window.navigator.clipboard?.writeText(ecardLink);
} else {
await Clipboard.setData(ClipboardData(text: ecardLink));
}
if (mounted) {
ToastHelper.showSuccessToast(context, 'Link copied to clipboard');
}
}
Future<void> sendEcardViaEmail(
dynamic empPolicyId,
dynamic clientPolicyId,
) async {
try {
final response = await apiService.sendMailForIndividualEmployeeEcard(
empPolicyId: empPolicyId,
clientPolicyId: clientPolicyId,
token: localToken!,
);
if (!mounted) return;
if (response['status'] == true) {
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ?? 'Mail sent successfully',
);
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ?? 'Failed to send mail.',
);
}
} catch (e) {
logDebug('❌ sendEcardViaEmail error: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to send mail. Please try again.',
);
}
}
}
Future<void> _launchURL(String url) async { Future<void> _launchURL(String url) async {
final Uri uri = Uri.parse(url); // Parse the URL properly final Uri uri = Uri.parse(url); // Parse the URL properly
logDebug('_launchURL $uri'); logDebug('_launchURL $uri');
@ -530,6 +582,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
} }
filteredData = data.toList(); filteredData = data.toList();
hasAnyEcardLink = filteredData.any(
(item) => item['ecard_download_link'] != null,
);
} }
void search(String query) { void search(String query) {
@ -640,6 +695,173 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
} }
} }
dynamic _parseNumericId(dynamic value) {
if (value == null) return value;
return int.tryParse(value.toString()) ?? value;
}
List<String> _getInceptionExportStatus() {
switch (_preStatusFilter) {
case 'draft':
return ['draft'];
case 'enrolled':
return ['enrolled'];
default:
return ['enrolled'];
}
}
({String empCode, String empName}) _getInceptionSearchParams() {
final query = searchController.text.trim();
if (query.isEmpty) {
return (empCode: '', empName: '');
}
final lowerQuery = query.toLowerCase();
final codeMatch = originalData.any(
(row) =>
row['emp_code']?.toString().toLowerCase().contains(lowerQuery) ??
false,
);
final nameMatch = originalData.any(
(row) =>
row['name']?.toString().toLowerCase().contains(lowerQuery) ?? false,
);
if (codeMatch && !nameMatch) {
return (empCode: query, empName: '');
}
if (nameMatch && !codeMatch) {
return (empCode: '', empName: query);
}
return (empCode: query, empName: query);
}
Future<void> downloadInceptionExport() async {
try {
final clientId = await tokenService.readValue('enrollmentClient_id') ??
localClientId ??
widget.ClientId;
final branchId =
await tokenService.readValue('enrollmentEmpClientBranchId') ??
localClientBranchId ??
widget.clientBranchId;
final policyId = localClientPolicyId ?? widget.ClientPoliyId;
final searchParams = _getInceptionSearchParams();
final status = _getInceptionExportStatus();
logDebug(
'downloadInceptionExport client=$clientId branch=$branchId '
'policies=$policyId status=$status '
'empCode=${searchParams.empCode} empName=${searchParams.empName}',
);
final response = await apiService.downloadInception(
client: _parseNumericId(clientId),
branch: _parseNumericId(branchId),
policies: _parseNumericId(policyId),
status: status,
empCode: searchParams.empCode,
empName: searchParams.empName,
token: localToken!,
);
if (!mounted) return;
if (response['status'] == true) {
final downloadUrl = response['data']?['downloadUrl'];
if (downloadUrl != null && downloadUrl.toString().isNotEmpty) {
await _launchURL(downloadUrl.toString());
ToastHelper.showSuccessToast(
context,
response['message']?.toString() ??
'Inception export generated successfully.',
);
} else {
ToastHelper.showErrorToast(context, 'Download URL not available');
}
} else {
ToastHelper.showErrorToast(
context,
response['message']?.toString() ??
'Failed to generate inception export.',
);
}
} catch (e) {
logDebug('downloadInceptionExport error: $e');
if (mounted) {
ToastHelper.showErrorToast(
context,
'Failed to generate inception export. Please try again.',
);
}
}
}
Widget _buildExportButton() {
final buttonStyle = ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
);
final textStyle = GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 1,
);
if (localTokenType == 'pre') {
return SizedBox(
width: 116,
height: 37,
child: PopupMenuButton<String>(
offset: const Offset(0, 37),
onSelected: (value) {
if (value == 'csv') {
exportToCsv(filteredData);
} else if (value == 'inception') {
downloadInceptionExport();
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'csv',
child: Text(
'Export',
style: GoogleFonts.poppins(fontSize: 13),
),
),
PopupMenuItem(
value: 'inception',
child: Text(
'Inception Export',
style: GoogleFonts.poppins(fontSize: 13),
),
),
],
child: IgnorePointer(
child: ElevatedButton(
onPressed: () {},
style: buttonStyle,
child: Text('Export', style: textStyle),
),
),
),
);
}
return SizedBox(
width: 116,
height: 37,
child: ElevatedButton(
onPressed: () => exportToCsv(filteredData),
style: buttonStyle,
child: Text('Export', style: textStyle),
),
);
}
String _capitalize(String? value) { String _capitalize(String? value) {
if (value == null || value.isEmpty) return ''; if (value == null || value.isEmpty) return '';
return value[0].toUpperCase() + value.substring(1).toLowerCase(); return value[0].toUpperCase() + value.substring(1).toLowerCase();
@ -1446,31 +1668,7 @@ HR Team''';
const SizedBox(width: 12), const SizedBox(width: 12),
/// Export Button /// Export Button
SizedBox( _buildExportButton(),
width: 116,
height: 37,
child: ElevatedButton(
onPressed: () {
exportToCsv(filteredData);
},
style: ElevatedButton.styleFrom(
backgroundColor: const Color(0xFFE26728),
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
),
child: Text(
'Export',
style: GoogleFonts.poppins(
fontSize: 16,
fontWeight: FontWeight.w700,
color: Colors.white,
letterSpacing: 1,
),
),
),
),
], ],
), ),
]), ]),
@ -2143,7 +2341,7 @@ HR Team''';
mainAxisAlignment: MainAxisAlignment.center, mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
children: [ children: [
/// --- eCard Button (Fixed Space) --- /// --- eCard Menu (Fixed Space) ---
SizedBox( SizedBox(
width: 40, width: 40,
height: 40, height: 40,
@ -2152,30 +2350,68 @@ HR Team''';
maintainSize: true, maintainSize: true,
maintainAnimation: true, maintainAnimation: true,
maintainState: true, maintainState: true,
child: Tooltip( child: PopupMenuButton<String>(
message: 'Download e-Card', tooltip: 'Ecard',
child: MouseRegion( offset: const Offset(0, 40),
cursor: SystemMouseCursors.click, onSelected: (value) {
child: GestureDetector( switch (value) {
onTap: () { case 'download':
getEcardDownload( getEcardDownload(
item['emp_code'], item['emp_code'],
item['employee_id'], item['employee_id'],
item['client_policy_id'], item['client_policy_id'],
item['policy_no'], item['policy_no'],
); );
}, break;
child: Container( case 'email':
decoration: BoxDecoration( sendEcardViaEmail(
color: const Color(0xFFE6F5F6), item['id'],
borderRadius: BorderRadius.circular(8), item['client_policy_id'],
), );
padding: const EdgeInsets.all(6), break;
child: Image.asset( case 'copy':
'assets/credit_card.png', copyEcardLink(
fit: BoxFit.contain, item['ecard_download_link']?.toString(),
);
break;
}
},
itemBuilder: (context) => [
PopupMenuItem(
value: 'download',
child: Text(
'Download Ecard',
style: GoogleFonts.poppins(fontSize: 13),
),
),
if (isSelf)
PopupMenuItem(
value: 'email',
child: Text(
'Send E-card via email',
style: GoogleFonts.poppins(fontSize: 13),
), ),
), ),
PopupMenuItem(
value: 'copy',
child: Text(
'Copy link',
style: GoogleFonts.poppins(fontSize: 13),
),
),
],
child: MouseRegion(
cursor: SystemMouseCursors.click,
child: Container(
decoration: BoxDecoration(
color: const Color(0xFFE6F5F6),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(6),
child: Image.asset(
'assets/credit_card.png',
fit: BoxFit.contain,
),
), ),
), ),
), ),

View File

@ -874,6 +874,73 @@ class ApiService {
return response; return response;
} }
Future<Map<String, dynamic>> sendMailForIndividualEmployeeEcard({
required dynamic empPolicyId,
required dynamic clientPolicyId,
required String token,
}) async {
final url = Uri.parse(
'${Environment.apiUrlPost}sendMailForIndividualEmployeeEcard');
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'APP-SIGNATURE':
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
};
final body = {
'emp_policy_id': empPolicyId,
'client_policy_id': clientPolicyId,
};
final response = await http.post(
url,
headers: headers,
body: jsonEncode(body),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception(
'Failed to send e-card mail: ${response.statusCode} ${response.body}');
}
}
Future<Map<String, dynamic>> downloadInception({
required dynamic client,
required dynamic branch,
required dynamic policies,
required List<String> status,
required String empCode,
required String empName,
required String token,
}) async {
final url = Uri.parse('${Environment.apiUrl}download_inception');
final headers = {
'Authorization': 'Bearer $token',
'Content-Type': 'application/json',
'APP-SIGNATURE':
'nhance-2025-signature-T8f6gV9k5fK2wf5V5c4vId3b0J8z6cAm2x8Y',
};
final body = {
'client': client,
'branch': branch,
'policies': policies,
'status': status,
'empCode': empCode,
'empName': empName,
};
final response = await http.post(
url,
headers: headers,
body: jsonEncode(body),
);
if (response.statusCode == 200) {
return jsonDecode(response.body);
} else {
throw Exception(
'Failed to download inception export: ${response.statusCode} ${response.body}');
}
}
Future<Map<String, dynamic>> getNonEBClaimPoliciesToApi( Future<Map<String, dynamic>> getNonEBClaimPoliciesToApi(
String token, request) async { String token, request) async {
final url = Uri.parse('${Environment.apiUrlPost}api/v1/non-eb-claim/policies'); final url = Uri.parse('${Environment.apiUrlPost}api/v1/non-eb-claim/policies');

20
web/flutter_bootstrap.js Normal file
View File

@ -0,0 +1,20 @@
{{flutter_js}}
{{flutter_build_config}}
const loadingIndicator = document.getElementById('loading_indicator');
_flutter.loader.load({
config: {
// CanvasKit is required for reliable chart capture (`toImage`) in PDF export.
renderer: 'canvaskit',
},
onEntrypointLoaded: async function (engineInitializer) {
const appRunner = await engineInitializer.initializeEngine({
renderer: 'canvaskit',
});
await appRunner.runApp();
if (loadingIndicator) {
loadingIndicator.remove();
}
},
});

View File

@ -80,47 +80,11 @@
} }
</style> </style>
<script>
// The value below is injected by flutter build, do not touch.
const serviceWorkerVersion = null;
</script>
<!-- This script adds the flutter initialization JS code -->
<script src="flutter.js" defer></script>
</head> </head>
<body style="overflow:hidden"> <body style="overflow:hidden">
<div id="loading_indicator" class="container overlay"> <div id="loading_indicator" class="container overlay">
<img class="indicator" src="assets/nhance-loader.gif"> <img class="indicator" src="assets/nhance-loader.gif">
</div> </div>
<script> <script src="flutter_bootstrap.js" async></script>
window.addEventListener('load', function(ev) {
// Download main.dart.js
_flutter.loader.loadEntrypoint({
serviceWorker: {
serviceWorkerVersion: serviceWorkerVersion,
},
onEntrypointLoaded: function(engineInitializer) {
// CanvasKit renderer is required for reliable chart capture (`toImage`)
// used by dashboard PDF export.
engineInitializer.initializeEngine({
renderer: "canvaskit"
}).then(function(appRunner) {
appRunner.runApp();
});
}
});
});
</script>
<script>
window.onLoad = function(){
setTimeout(function () {
var loadingIndicator = document.getElementById("loading_indicator");
if(loadingIndicator){
loadingIndicator.remove();
}
},5000);
};
</script>
</body> </body>
</html> </html>