refactor: update CoveragePage to use StatefulWidget and implement scroll listener

This commit is contained in:
Achintha Isuru
2026-01-30 00:24:12 -05:00
parent aede5e0ab2
commit bfe00a700a
2 changed files with 189 additions and 24 deletions

View File

@@ -6,7 +6,7 @@
/// Locales: 2
/// Strings: 1038 (519 per locale)
///
/// Built on 2026-01-29 at 15:50 UTC
/// Built on 2026-01-30 at 05:13 UTC
// coverage:ignore-file
// ignore_for_file: type=lint, unused_import

View File

@@ -2,11 +2,12 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import '../blocs/coverage_bloc.dart';
import '../blocs/coverage_event.dart';
import '../blocs/coverage_state.dart';
import '../widgets/coverage_header.dart';
import '../widgets/coverage_calendar_selector.dart';
import '../widgets/coverage_quick_stats.dart';
import '../widgets/coverage_shift_list.dart';
import '../widgets/late_workers_alert.dart';
@@ -14,10 +15,41 @@ import '../widgets/late_workers_alert.dart';
/// Page for displaying daily coverage information.
///
/// Shows shifts, worker statuses, and coverage statistics for a selected date.
class CoveragePage extends StatelessWidget {
class CoveragePage extends StatefulWidget {
/// Creates a [CoveragePage].
const CoveragePage({super.key});
@override
State<CoveragePage> createState() => _CoveragePageState();
}
class _CoveragePageState extends State<CoveragePage> {
late ScrollController _scrollController;
bool _isScrolled = false;
@override
void initState() {
super.initState();
_scrollController = ScrollController();
_scrollController.addListener(_onScroll);
}
@override
void dispose() {
_scrollController.dispose();
super.dispose();
}
void _onScroll() {
if (_scrollController.hasClients) {
if (_scrollController.offset > 180 && !_isScrolled) {
setState(() => _isScrolled = true);
} else if (_scrollController.offset <= 180 && _isScrolled) {
setState(() => _isScrolled = false);
}
}
}
@override
Widget build(BuildContext context) {
return BlocProvider<CoverageBloc>(
@@ -26,26 +58,159 @@ class CoveragePage extends StatelessWidget {
child: Scaffold(
body: BlocBuilder<CoverageBloc, CoverageState>(
builder: (BuildContext context, CoverageState state) {
return Column(
children: <Widget>[
CoverageHeader(
selectedDate: state.selectedDate ?? DateTime.now(),
coveragePercent: state.stats?.coveragePercent ?? 0,
totalConfirmed: state.stats?.totalConfirmed ?? 0,
totalNeeded: state.stats?.totalNeeded ?? 0,
onDateSelected: (DateTime date) {
BlocProvider.of<CoverageBloc>(context).add(
CoverageLoadRequested(date: date),
);
},
onRefresh: () {
BlocProvider.of<CoverageBloc>(context).add(
const CoverageRefreshRequested(),
);
},
final DateTime selectedDate = state.selectedDate ?? DateTime.now();
return CustomScrollView(
controller: _scrollController,
slivers: <Widget>[
SliverAppBar(
pinned: true,
expandedHeight: 300.0,
backgroundColor: UiColors.primary,
leading: IconButton(
onPressed: () => Modular.to.pop(),
icon: Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(
color: UiColors.primaryForeground.withOpacity(0.2),
shape: BoxShape.circle,
),
child: const Icon(
UiIcons.arrowLeft,
color: UiColors.primaryForeground,
size: UiConstants.space4,
),
),
),
title: AnimatedSwitcher(
duration: const Duration(milliseconds: 200),
child: Text(
_isScrolled
? DateFormat('MMMM d').format(selectedDate)
: 'Daily Coverage',
key: ValueKey<bool>(_isScrolled),
style: UiTypography.title2m.copyWith(
color: UiColors.primaryForeground,
),
),
),
actions: <Widget>[
IconButton(
onPressed: () {
BlocProvider.of<CoverageBloc>(context).add(
const CoverageRefreshRequested(),
);
},
icon: Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: BoxDecoration(
color: UiColors.primaryForeground.withOpacity(0.2),
borderRadius: UiConstants.radiusMd,
),
child: const Icon(
UiIcons.rotateCcw,
color: UiColors.primaryForeground,
size: UiConstants.space4,
),
),
),
const SizedBox(width: UiConstants.space4),
],
flexibleSpace: Container(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.primary,
UiColors.primary,
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: FlexibleSpaceBar(
background: Padding(
padding: const EdgeInsets.fromLTRB(
UiConstants.space5,
100, // Top padding to clear AppBar
UiConstants.space5,
UiConstants.space4,
),
child: Column(
mainAxisAlignment: MainAxisAlignment.end,
children: <Widget>[
CoverageCalendarSelector(
selectedDate: selectedDate,
onDateSelected: (DateTime date) {
BlocProvider.of<CoverageBloc>(context).add(
CoverageLoadRequested(date: date),
);
},
),
const SizedBox(height: UiConstants.space4),
// Coverage Stats Container
Container(
padding: const EdgeInsets.all(UiConstants.space4),
decoration: BoxDecoration(
color:
UiColors.primaryForeground.withOpacity(0.1),
borderRadius: UiConstants.radiusLg,
),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
Column(
crossAxisAlignment:
CrossAxisAlignment.start,
children: <Widget>[
Text(
'Coverage Status',
style: UiTypography.body2r.copyWith(
color: UiColors.primaryForeground
.withOpacity(0.7),
),
),
Text(
'${state.stats?.coveragePercent ?? 0}%',
style: UiTypography.display1b.copyWith(
color: UiColors.primaryForeground,
),
),
],
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text(
'Workers',
style: UiTypography.body2r.copyWith(
color: UiColors.primaryForeground
.withOpacity(0.7),
),
),
Text(
'${state.stats?.totalConfirmed ?? 0}/${state.stats?.totalNeeded ?? 0}',
style: UiTypography.title2m.copyWith(
color: UiColors.primaryForeground,
),
),
],
),
],
),
),
],
),
),
),
),
),
Expanded(
child: _buildBody(context: context, state: state),
SliverList(
delegate: SliverChildListDelegate(
<Widget>[
_buildBody(context: context, state: state),
],
),
),
],
);
@@ -99,7 +264,7 @@ class CoveragePage extends StatelessWidget {
);
}
return SingleChildScrollView(
return Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
@@ -120,7 +285,7 @@ class CoveragePage extends StatelessWidget {
),
const SizedBox(height: UiConstants.space3),
CoverageShiftList(shifts: state.shifts),
const SizedBox(height: 100),
SizedBox(height: MediaQuery.of(context).size.height * 0.8),
],
),
);