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 /// Locales: 2
/// Strings: 1038 (519 per locale) /// 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 // coverage:ignore-file
// ignore_for_file: type=lint, unused_import // 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/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart'; import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import '../blocs/coverage_bloc.dart'; import '../blocs/coverage_bloc.dart';
import '../blocs/coverage_event.dart'; import '../blocs/coverage_event.dart';
import '../blocs/coverage_state.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_quick_stats.dart';
import '../widgets/coverage_shift_list.dart'; import '../widgets/coverage_shift_list.dart';
import '../widgets/late_workers_alert.dart'; import '../widgets/late_workers_alert.dart';
@@ -14,10 +15,41 @@ import '../widgets/late_workers_alert.dart';
/// Page for displaying daily coverage information. /// Page for displaying daily coverage information.
/// ///
/// Shows shifts, worker statuses, and coverage statistics for a selected date. /// Shows shifts, worker statuses, and coverage statistics for a selected date.
class CoveragePage extends StatelessWidget { class CoveragePage extends StatefulWidget {
/// Creates a [CoveragePage]. /// Creates a [CoveragePage].
const CoveragePage({super.key}); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider<CoverageBloc>( return BlocProvider<CoverageBloc>(
@@ -26,26 +58,159 @@ class CoveragePage extends StatelessWidget {
child: Scaffold( child: Scaffold(
body: BlocBuilder<CoverageBloc, CoverageState>( body: BlocBuilder<CoverageBloc, CoverageState>(
builder: (BuildContext context, CoverageState state) { builder: (BuildContext context, CoverageState state) {
return Column( final DateTime selectedDate = state.selectedDate ?? DateTime.now();
children: <Widget>[
CoverageHeader( return CustomScrollView(
selectedDate: state.selectedDate ?? DateTime.now(), controller: _scrollController,
coveragePercent: state.stats?.coveragePercent ?? 0, slivers: <Widget>[
totalConfirmed: state.stats?.totalConfirmed ?? 0, SliverAppBar(
totalNeeded: state.stats?.totalNeeded ?? 0, pinned: true,
onDateSelected: (DateTime date) { expandedHeight: 300.0,
BlocProvider.of<CoverageBloc>(context).add( backgroundColor: UiColors.primary,
CoverageLoadRequested(date: date), leading: IconButton(
); onPressed: () => Modular.to.pop(),
}, icon: Container(
onRefresh: () { padding: const EdgeInsets.all(UiConstants.space2),
BlocProvider.of<CoverageBloc>(context).add( decoration: BoxDecoration(
const CoverageRefreshRequested(), 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( SliverList(
child: _buildBody(context: context, state: state), 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), padding: const EdgeInsets.all(UiConstants.space5),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@@ -120,7 +285,7 @@ class CoveragePage extends StatelessWidget {
), ),
const SizedBox(height: UiConstants.space3), const SizedBox(height: UiConstants.space3),
CoverageShiftList(shifts: state.shifts), CoverageShiftList(shifts: state.shifts),
const SizedBox(height: 100), SizedBox(height: MediaQuery.of(context).size.height * 0.8),
], ],
), ),
); );