reminder and config and ecard
This commit is contained in:
parent
3abde687ca
commit
2120e997f0
@ -91,10 +91,10 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'Last Updated',
|
||||
'MIS Creation Date',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 11,
|
||||
fontWeight: FontWeight.w600,
|
||||
fontWeight: FontWeight.w400,
|
||||
color: ClaimsOverviewTheme.green,
|
||||
),
|
||||
),
|
||||
@ -102,7 +102,8 @@ class _ClaimsLiveIndicatorState extends State<ClaimsLiveIndicator>
|
||||
Text(
|
||||
'· $createdAt',
|
||||
style: GoogleFonts.poppins(
|
||||
fontSize: 10,
|
||||
fontSize: 14,
|
||||
fontWeight: FontWeight.w600,
|
||||
color: ClaimsOverviewTheme.green,
|
||||
),
|
||||
),
|
||||
|
||||
@ -30,6 +30,8 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
||||
late TabController _tabController;
|
||||
late AnimationController _headerFadeController;
|
||||
late Animation<double> _headerFade;
|
||||
late final ScrollController _scrollController;
|
||||
late final List<GlobalKey> _sectionKeys;
|
||||
|
||||
late final ApiService _apiService;
|
||||
final _tokenService = TokenStorageService();
|
||||
@ -63,6 +65,8 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
||||
void initState() {
|
||||
super.initState();
|
||||
_apiService = ApiService(context);
|
||||
_scrollController = ScrollController();
|
||||
_sectionKeys = List.generate(_tabs.length, (_) => GlobalKey());
|
||||
_tabController = TabController(length: _tabs.length, vsync: this);
|
||||
_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 {
|
||||
final isInitialLoad = _replayToken == 0;
|
||||
setState(() {
|
||||
@ -407,6 +425,7 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
||||
void dispose() {
|
||||
_tabController.removeListener(_onTabChanged);
|
||||
_tabController.dispose();
|
||||
_scrollController.dispose();
|
||||
_headerFadeController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
@ -433,23 +452,30 @@ class _ClaimsOverviewDashboardState extends State<ClaimsOverviewDashboard>
|
||||
child: EnrollmentOverviewScope(
|
||||
key: ValueKey('enrollment-$_selectedPolicyId-$_replayToken'),
|
||||
data: _enrollmentViewData,
|
||||
child: TabBarView(
|
||||
controller: _tabController,
|
||||
physics: _isRefreshing
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: null,
|
||||
children: List.generate(
|
||||
_tabs.length,
|
||||
(i) => Padding(
|
||||
padding: const EdgeInsets.only(top: 16),
|
||||
child: ClaimsTabAnimatedShell(
|
||||
isLoading: _isLoading,
|
||||
skeletonBlocks: i == 0 ? 5 : 4,
|
||||
child: ClaimsOverviewTab(
|
||||
index: i,
|
||||
replayToken: _replayToken,
|
||||
isActive: _currentTab == i,
|
||||
),
|
||||
child: ClaimsTabAnimatedShell(
|
||||
isLoading: _isLoading,
|
||||
skeletonBlocks: 5,
|
||||
child: Scrollbar(
|
||||
controller: _scrollController,
|
||||
thumbVisibility: true,
|
||||
child: SingleChildScrollView(
|
||||
controller: _scrollController,
|
||||
physics: _isRefreshing
|
||||
? const NeverScrollableScrollPhysics()
|
||||
: const ClampingScrollPhysics(),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
for (var i = 0; i < _tabs.length; 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(
|
||||
controller: _tabController,
|
||||
onTap: _scrollToSection,
|
||||
isScrollable: true,
|
||||
tabAlignment: TabAlignment.start,
|
||||
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() {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
|
||||
@ -35,7 +35,7 @@ const _tabNames = [
|
||||
const _tabFallbackHeights = [
|
||||
720.0,
|
||||
680.0,
|
||||
660.0,
|
||||
820.0,
|
||||
760.0,
|
||||
680.0,
|
||||
580.0,
|
||||
@ -255,8 +255,10 @@ Widget _buildTabCapture({
|
||||
}
|
||||
|
||||
Future<void> _waitForPaint() async {
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 32));
|
||||
for (var i = 0; i < 3; i++) {
|
||||
await WidgetsBinding.instance.endOfFrame;
|
||||
await Future<void>.delayed(const Duration(milliseconds: 48));
|
||||
}
|
||||
}
|
||||
|
||||
double _capturePixelRatio({required bool isEnrollmentTab}) {
|
||||
@ -285,6 +287,7 @@ Future<Uint8List?> _captureTabPng({
|
||||
required OverlayState overlay,
|
||||
required Widget child,
|
||||
required double fallbackHeight,
|
||||
double? maxMeasureHeight,
|
||||
bool isEnrollmentTab = false,
|
||||
ClaimsPdfExportProgress? onProgress,
|
||||
String? progressLabel,
|
||||
@ -309,14 +312,19 @@ Future<Uint8List?> _captureTabPng({
|
||||
key: boundaryKey,
|
||||
child: MediaQuery(
|
||||
data: const MediaQueryData(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _captureWidth,
|
||||
maxHeight: isEnrollmentTab
|
||||
? _enrollmentMaxMeasureHeight
|
||||
: _maxMeasureHeight,
|
||||
child: SingleChildScrollView(
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
child: ConstrainedBox(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: _captureWidth,
|
||||
minHeight: fallbackHeight,
|
||||
maxHeight: maxMeasureHeight ??
|
||||
(isEnrollmentTab
|
||||
? _enrollmentMaxMeasureHeight
|
||||
: _maxMeasureHeight),
|
||||
),
|
||||
child: child,
|
||||
),
|
||||
child: ClipRect(child: child),
|
||||
),
|
||||
),
|
||||
),
|
||||
@ -339,6 +347,15 @@ Future<Uint8List?> _captureTabPng({
|
||||
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…)');
|
||||
|
||||
var ratio = _capturePixelRatio(isEnrollmentTab: isEnrollmentTab);
|
||||
@ -455,6 +472,7 @@ Future<void> exportClaimsOverviewChartsPdf({
|
||||
overlay: overlay,
|
||||
child: tabWidget,
|
||||
fallbackHeight: _tabFallbackHeights[i],
|
||||
maxMeasureHeight: i == 2 ? 1000.0 : null,
|
||||
isEnrollmentTab: i == 6,
|
||||
onProgress: onProgress,
|
||||
progressLabel: captureLabel,
|
||||
|
||||
@ -12,8 +12,15 @@ import 'claims_overview_widgets.dart';
|
||||
Widget _tabBodyWrapper({
|
||||
required bool forPdfExport,
|
||||
required Widget child,
|
||||
bool scrollable = true,
|
||||
}) {
|
||||
if (forPdfExport) return child;
|
||||
if (!scrollable) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 0),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
return SingleChildScrollView(
|
||||
padding: const EdgeInsets.fromLTRB(20, 0, 20, 24),
|
||||
child: child,
|
||||
@ -38,6 +45,7 @@ class ClaimsOverviewTab extends StatelessWidget {
|
||||
final int replayToken;
|
||||
final bool isActive;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const ClaimsOverviewTab({
|
||||
super.key,
|
||||
@ -45,6 +53,7 @@ class ClaimsOverviewTab extends StatelessWidget {
|
||||
required this.replayToken,
|
||||
this.isActive = false,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -53,33 +62,40 @@ class ClaimsOverviewTab extends StatelessWidget {
|
||||
0 => _OverviewTab(
|
||||
replayToken: replayToken,
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
),
|
||||
1 => _PolicyExperienceTab(
|
||||
replayToken: replayToken,
|
||||
isActive: isActive || forPdfExport,
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
),
|
||||
2 => _ClaimsAnalysisTab(
|
||||
replayToken: replayToken,
|
||||
isActive: isActive || forPdfExport,
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
),
|
||||
3 => _DemographicsTab(
|
||||
replayToken: replayToken,
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
),
|
||||
4 => _HospitalsTab(
|
||||
replayToken: replayToken,
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
),
|
||||
5 => _SpecialtyTab(
|
||||
replayToken: replayToken,
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
),
|
||||
6 => _EnrollmentTab(
|
||||
replayToken: replayToken,
|
||||
isActive: isActive || forPdfExport,
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
),
|
||||
_ => const SizedBox(),
|
||||
};
|
||||
@ -89,10 +105,12 @@ class ClaimsOverviewTab extends StatelessWidget {
|
||||
class _OverviewTab extends StatelessWidget {
|
||||
final int replayToken;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const _OverviewTab({
|
||||
required this.replayToken,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -100,6 +118,7 @@ class _OverviewTab extends StatelessWidget {
|
||||
final d = ClaimsOverviewScope.of(context);
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
@ -134,6 +153,7 @@ class _OverviewTab extends StatelessWidget {
|
||||
left: ClaimsSectionCard(
|
||||
title: 'Claims Experience',
|
||||
icon: Icons.analytics_outlined,
|
||||
minHeight: ClaimsSectionCard.pairedSectionMinHeight,
|
||||
child: ClaimsExperienceBody(
|
||||
incurredClaims: d.incurredClaims,
|
||||
incurredRatio: d.incurredRatio,
|
||||
@ -145,6 +165,7 @@ class _OverviewTab extends StatelessWidget {
|
||||
right: ClaimsSectionCard(
|
||||
title: 'Inception',
|
||||
icon: Icons.flag_outlined,
|
||||
minHeight: ClaimsSectionCard.pairedSectionMinHeight,
|
||||
child: ClaimsInceptionBody(
|
||||
inceptionEmployee: d.inceptionEmployee,
|
||||
inceptionLives: d.inceptionLives,
|
||||
@ -160,8 +181,12 @@ class _OverviewTab extends StatelessWidget {
|
||||
required int baseIndex,
|
||||
required Widget left,
|
||||
required Widget right,
|
||||
bool equalizeHeight = false,
|
||||
}) {
|
||||
return ClaimsTabPanelRow(
|
||||
final row = ClaimsTabPanelRow(
|
||||
crossAxisAlignment: equalizeHeight
|
||||
? CrossAxisAlignment.stretch
|
||||
: CrossAxisAlignment.start,
|
||||
panels: [
|
||||
ClaimsTabPanel(
|
||||
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 bool isActive;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const _PolicyExperienceTab({
|
||||
required this.replayToken,
|
||||
required this.isActive,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -240,41 +269,33 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
|
||||
final countSeries = d.claimsCountByStatus;
|
||||
|
||||
final kpiMetrics = [
|
||||
('Total Claims', d.sidebarTotalClaims, Icons.tag, ClaimsOverviewTheme.blue),
|
||||
('Total Claims', d.sidebarTotalClaims, null, Icons.tag, ClaimsOverviewTheme.blue),
|
||||
(
|
||||
'Total Incurred Amount',
|
||||
d.sidebarIncurredAmount,
|
||||
null,
|
||||
Icons.currency_rupee,
|
||||
ClaimsOverviewTheme.pink,
|
||||
),
|
||||
(
|
||||
'Total Reimbursement',
|
||||
d.sidebarReimbursement,
|
||||
d.sidebarReimbursementPct,
|
||||
Icons.account_balance_wallet_outlined,
|
||||
ClaimsOverviewTheme.teal,
|
||||
),
|
||||
(
|
||||
'Reimbursement %',
|
||||
d.sidebarReimbursementPct,
|
||||
Icons.percent,
|
||||
ClaimsOverviewTheme.orange,
|
||||
),
|
||||
(
|
||||
'Cashless Claim Amount',
|
||||
d.sidebarCashless,
|
||||
d.sidebarCashlessPct,
|
||||
Icons.shopping_bag_outlined,
|
||||
ClaimsOverviewTheme.purple,
|
||||
),
|
||||
(
|
||||
'Cashless %',
|
||||
d.sidebarCashlessPct,
|
||||
Icons.pie_chart_outline,
|
||||
ClaimsOverviewTheme.teal,
|
||||
),
|
||||
];
|
||||
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: widget.forPdfExport,
|
||||
scrollable: widget.scrollable,
|
||||
child: ClaimsTabPanelRow(
|
||||
panels: [
|
||||
ClaimsTabPanel(
|
||||
@ -339,8 +360,9 @@ class _PolicyExperienceTabState extends State<_PolicyExperienceTab>
|
||||
child: ClaimsMetricCard(
|
||||
label: kpiMetrics[i].$1,
|
||||
value: kpiMetrics[i].$2,
|
||||
icon: kpiMetrics[i].$3,
|
||||
accentColor: kpiMetrics[i].$4,
|
||||
secondaryValue: kpiMetrics[i].$3,
|
||||
icon: kpiMetrics[i].$4,
|
||||
accentColor: kpiMetrics[i].$5,
|
||||
replayToken: widget.replayToken,
|
||||
),
|
||||
),
|
||||
@ -358,11 +380,13 @@ class _ClaimsAnalysisTab extends StatefulWidget {
|
||||
final int replayToken;
|
||||
final bool isActive;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const _ClaimsAnalysisTab({
|
||||
required this.replayToken,
|
||||
required this.isActive,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -371,7 +395,7 @@ class _ClaimsAnalysisTab extends StatefulWidget {
|
||||
|
||||
class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
||||
with AutomaticKeepAliveClientMixin {
|
||||
static const _pairedChartHeight = 280.0;
|
||||
double get _pairedChartHeight => widget.forPdfExport ? 210.0 : 280.0;
|
||||
|
||||
int _chartAnimKey = 0;
|
||||
|
||||
@ -416,7 +440,9 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
||||
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: widget.forPdfExport,
|
||||
scrollable: widget.scrollable,
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
_chartRow(
|
||||
0,
|
||||
@ -515,7 +541,6 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
||||
),
|
||||
],
|
||||
);
|
||||
if (widget.forPdfExport) return row;
|
||||
return IntrinsicHeight(child: row);
|
||||
}
|
||||
}
|
||||
@ -523,10 +548,12 @@ class _ClaimsAnalysisTabState extends State<_ClaimsAnalysisTab>
|
||||
class _DemographicsTab extends StatelessWidget {
|
||||
final int replayToken;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const _DemographicsTab({
|
||||
required this.replayToken,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -548,6 +575,7 @@ class _DemographicsTab extends StatelessWidget {
|
||||
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
child: Column(
|
||||
children: [
|
||||
if (relationships.isNotEmpty) ...[
|
||||
@ -633,10 +661,12 @@ class _DemographicsTab extends StatelessWidget {
|
||||
class _HospitalsTab extends StatelessWidget {
|
||||
final int replayToken;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const _HospitalsTab({
|
||||
required this.replayToken,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -672,6 +702,7 @@ class _HospitalsTab extends StatelessWidget {
|
||||
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
child: Column(
|
||||
children: [
|
||||
IntrinsicHeight(
|
||||
@ -771,10 +802,12 @@ class _HospitalsTab extends StatelessWidget {
|
||||
class _SpecialtyTab extends StatelessWidget {
|
||||
final int replayToken;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const _SpecialtyTab({
|
||||
required this.replayToken,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -800,6 +833,7 @@ class _SpecialtyTab extends StatelessWidget {
|
||||
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: forPdfExport,
|
||||
scrollable: scrollable,
|
||||
child: Column(
|
||||
children: [
|
||||
ClaimsTabPanelRow(
|
||||
@ -933,11 +967,13 @@ class _EnrollmentTab extends StatefulWidget {
|
||||
final int replayToken;
|
||||
final bool isActive;
|
||||
final bool forPdfExport;
|
||||
final bool scrollable;
|
||||
|
||||
const _EnrollmentTab({
|
||||
required this.replayToken,
|
||||
required this.isActive,
|
||||
this.forPdfExport = false,
|
||||
this.scrollable = true,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -1161,6 +1197,7 @@ class _EnrollmentTabState extends State<_EnrollmentTab>
|
||||
|
||||
return _tabBodyWrapper(
|
||||
forPdfExport: widget.forPdfExport,
|
||||
scrollable: widget.scrollable,
|
||||
child: widget.forPdfExport ? ClipRect(child: body) : body,
|
||||
);
|
||||
}
|
||||
|
||||
@ -216,42 +216,40 @@ class ClaimsExperienceBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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(
|
||||
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(
|
||||
children: [
|
||||
incurredTile,
|
||||
@ -263,6 +261,7 @@ class ClaimsExperienceBody extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return _OverviewThreeColumnRow(
|
||||
equalizeHeight: true,
|
||||
children: [incurredTile, projectedTile, incidenceTile],
|
||||
);
|
||||
},
|
||||
@ -282,29 +281,32 @@ class ClaimsInceptionBody extends StatelessWidget {
|
||||
|
||||
@override
|
||||
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(
|
||||
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(
|
||||
children: [
|
||||
employeeTile,
|
||||
@ -314,6 +316,7 @@ class ClaimsInceptionBody extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
return _OverviewTwoColumnRow(
|
||||
equalizeHeight: true,
|
||||
children: [employeeTile, livesTile],
|
||||
);
|
||||
},
|
||||
@ -348,12 +351,14 @@ class _OverviewInnerTile extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color accentColor;
|
||||
final Widget child;
|
||||
final bool matchRowHeight;
|
||||
|
||||
const _OverviewInnerTile({
|
||||
required this.label,
|
||||
required this.icon,
|
||||
required this.accentColor,
|
||||
required this.child,
|
||||
this.matchRowHeight = false,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -362,12 +367,14 @@ class _OverviewInnerTile extends StatelessWidget {
|
||||
accentColor: accentColor,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: matchRowHeight ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(14, 10, 14, 8),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Row(
|
||||
@ -401,6 +408,7 @@ class _OverviewInnerTile extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (matchRowHeight) const Spacer(),
|
||||
Container(
|
||||
height: 3,
|
||||
decoration: BoxDecoration(
|
||||
@ -419,11 +427,11 @@ class _OverviewInnerTile extends StatelessWidget {
|
||||
|
||||
class _ClaimsExperienceDualValues extends StatelessWidget {
|
||||
final String primaryValue;
|
||||
final String secondaryValue;
|
||||
final String? secondaryValue;
|
||||
|
||||
const _ClaimsExperienceDualValues({
|
||||
required this.primaryValue,
|
||||
required this.secondaryValue,
|
||||
this.secondaryValue,
|
||||
});
|
||||
|
||||
@override
|
||||
@ -435,25 +443,26 @@ class _ClaimsExperienceDualValues extends StatelessWidget {
|
||||
height: 1.2,
|
||||
);
|
||||
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Expanded(
|
||||
child: FittedBox(
|
||||
fit: BoxFit.scaleDown,
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Text(
|
||||
primaryValue,
|
||||
maxLines: 1,
|
||||
style: valueStyle,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
secondaryValue,
|
||||
style: valueStyle.copyWith(fontSize: 16),
|
||||
primaryValue,
|
||||
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.
|
||||
class _OverviewTwoColumnRow extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
final bool equalizeHeight;
|
||||
|
||||
const _OverviewTwoColumnRow({required this.children});
|
||||
const _OverviewTwoColumnRow({
|
||||
required this.children,
|
||||
this.equalizeHeight = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(children.length == 2);
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
final row = Row(
|
||||
crossAxisAlignment: equalizeHeight
|
||||
? CrossAxisAlignment.stretch
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(child: children[0]),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(child: children[1]),
|
||||
],
|
||||
);
|
||||
if (!equalizeHeight) return row;
|
||||
return IntrinsicHeight(child: row);
|
||||
}
|
||||
}
|
||||
|
||||
/// Three equal columns — matches Policy Information top-row alignment.
|
||||
class _OverviewThreeColumnRow extends StatelessWidget {
|
||||
final List<Widget> children;
|
||||
final bool equalizeHeight;
|
||||
|
||||
const _OverviewThreeColumnRow({required this.children});
|
||||
const _OverviewThreeColumnRow({
|
||||
required this.children,
|
||||
this.equalizeHeight = false,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
assert(children.isNotEmpty && children.length <= 3);
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
final row = Row(
|
||||
crossAxisAlignment: equalizeHeight
|
||||
? CrossAxisAlignment.stretch
|
||||
: CrossAxisAlignment.start,
|
||||
children: [
|
||||
for (var i = 0; i < 3; i++) ...[
|
||||
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 value;
|
||||
final String? secondaryValue;
|
||||
final IconData icon;
|
||||
final Color accentColor;
|
||||
final int replayToken;
|
||||
@ -603,6 +629,7 @@ class ClaimsMetricCard extends StatelessWidget {
|
||||
super.key,
|
||||
required this.label,
|
||||
required this.value,
|
||||
this.secondaryValue,
|
||||
required this.icon,
|
||||
required this.accentColor,
|
||||
this.replayToken = 0,
|
||||
@ -673,17 +700,42 @@ class ClaimsMetricCard extends StatelessWidget {
|
||||
height: compact ? 40 : 48,
|
||||
child: Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: compact || !animateValue
|
||||
? Text(
|
||||
value,
|
||||
style: valueStyle,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: ClaimsAnimatedValueText(
|
||||
value: value,
|
||||
replayToken: replayToken,
|
||||
style: valueStyle,
|
||||
child: secondaryValue == null
|
||||
? (compact || !animateValue
|
||||
? Text(
|
||||
value,
|
||||
style: valueStyle,
|
||||
maxLines: 3,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
)
|
||||
: ClaimsAnimatedValueText(
|
||||
value: value,
|
||||
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 {
|
||||
/// Min height for side-by-side overview pairs (e.g. Claims Experience / Inception).
|
||||
static const double pairedSectionMinHeight = 170;
|
||||
|
||||
final String title;
|
||||
final IconData icon;
|
||||
final Widget child;
|
||||
final bool fillHeight;
|
||||
final double? height;
|
||||
final double? minHeight;
|
||||
|
||||
const ClaimsSectionCard({
|
||||
super.key,
|
||||
@ -744,6 +800,7 @@ class ClaimsSectionCard extends StatelessWidget {
|
||||
required this.child,
|
||||
this.fillHeight = false,
|
||||
this.height,
|
||||
this.minHeight,
|
||||
});
|
||||
|
||||
@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;
|
||||
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
height: height,
|
||||
constraints: height == null && minHeight != null
|
||||
? BoxConstraints(minHeight: minHeight!)
|
||||
: null,
|
||||
decoration: BoxDecoration(
|
||||
color: ClaimsOverviewTheme.cardBg,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
@ -794,7 +855,7 @@ class ClaimsSectionCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
mainAxisSize:
|
||||
useExpandedBody ? MainAxisSize.max : MainAxisSize.min,
|
||||
stretchVertically ? MainAxisSize.max : MainAxisSize.min,
|
||||
children: [
|
||||
header,
|
||||
bodySlot,
|
||||
|
||||
@ -17,6 +17,7 @@ import 'package:universal_html/html.dart' as html;
|
||||
import 'dart:typed_data';
|
||||
import 'dart:io';
|
||||
import 'package:flutter/foundation.dart' show kIsWeb;
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:intl/intl.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 {
|
||||
final Uri uri = Uri.parse(url); // Parse the URL properly
|
||||
logDebug('_launchURL $uri');
|
||||
@ -530,6 +582,9 @@ class _HrPolicyDetailsState extends State<hrPolicyDetails>
|
||||
}
|
||||
|
||||
filteredData = data.toList();
|
||||
hasAnyEcardLink = filteredData.any(
|
||||
(item) => item['ecard_download_link'] != null,
|
||||
);
|
||||
}
|
||||
|
||||
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) {
|
||||
if (value == null || value.isEmpty) return '';
|
||||
return value[0].toUpperCase() + value.substring(1).toLowerCase();
|
||||
@ -1446,31 +1668,7 @@ HR Team''';
|
||||
const SizedBox(width: 12),
|
||||
|
||||
/// ⬇️ Export Button
|
||||
SizedBox(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildExportButton(),
|
||||
],
|
||||
),
|
||||
]),
|
||||
@ -2143,7 +2341,7 @@ HR Team''';
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
/// --- eCard Button (Fixed Space) ---
|
||||
/// --- eCard Menu (Fixed Space) ---
|
||||
SizedBox(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@ -2152,30 +2350,68 @@ HR Team''';
|
||||
maintainSize: true,
|
||||
maintainAnimation: true,
|
||||
maintainState: true,
|
||||
child: Tooltip(
|
||||
message: 'Download e-Card',
|
||||
child: MouseRegion(
|
||||
cursor: SystemMouseCursors.click,
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
child: PopupMenuButton<String>(
|
||||
tooltip: 'Ecard',
|
||||
offset: const Offset(0, 40),
|
||||
onSelected: (value) {
|
||||
switch (value) {
|
||||
case 'download':
|
||||
getEcardDownload(
|
||||
item['emp_code'],
|
||||
item['employee_id'],
|
||||
item['client_policy_id'],
|
||||
item['policy_no'],
|
||||
);
|
||||
},
|
||||
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,
|
||||
break;
|
||||
case 'email':
|
||||
sendEcardViaEmail(
|
||||
item['id'],
|
||||
item['client_policy_id'],
|
||||
);
|
||||
break;
|
||||
case 'copy':
|
||||
copyEcardLink(
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@ -874,6 +874,73 @@ class ApiService {
|
||||
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(
|
||||
String token, request) async {
|
||||
final url = Uri.parse('${Environment.apiUrlPost}api/v1/non-eb-claim/policies');
|
||||
|
||||
20
web/flutter_bootstrap.js
Normal file
20
web/flutter_bootstrap.js
Normal 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();
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -80,47 +80,11 @@
|
||||
}
|
||||
|
||||
</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>
|
||||
<body style="overflow:hidden">
|
||||
<div id="loading_indicator" class="container overlay">
|
||||
<img class="indicator" src="assets/nhance-loader.gif">
|
||||
</div>
|
||||
<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>
|
||||
<script src="flutter_bootstrap.js" async></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user