refactor: update CoveragePage to use StatefulWidget and implement scroll listener
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user