From bfe00a700aaac8bc1e4c4972ec7a292d614e58f4 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Fri, 30 Jan 2026 00:24:12 -0500 Subject: [PATCH] refactor: update CoveragePage to use StatefulWidget and implement scroll listener --- .../lib/src/l10n/strings.g.dart | 2 +- .../src/presentation/pages/coverage_page.dart | 211 ++++++++++++++++-- 2 files changed, 189 insertions(+), 24 deletions(-) diff --git a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart b/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart index af448a4c..df057650 100644 --- a/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart +++ b/apps/mobile/packages/core_localization/lib/src/l10n/strings.g.dart @@ -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 diff --git a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart index 6559945d..9042d07c 100644 --- a/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart +++ b/apps/mobile/packages/features/client/client_coverage/lib/src/presentation/pages/coverage_page.dart @@ -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 createState() => _CoveragePageState(); +} + +class _CoveragePageState extends State { + 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( @@ -26,26 +58,159 @@ class CoveragePage extends StatelessWidget { child: Scaffold( body: BlocBuilder( builder: (BuildContext context, CoverageState state) { - return Column( - children: [ - 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(context).add( - CoverageLoadRequested(date: date), - ); - }, - onRefresh: () { - BlocProvider.of(context).add( - const CoverageRefreshRequested(), - ); - }, + final DateTime selectedDate = state.selectedDate ?? DateTime.now(); + + return CustomScrollView( + controller: _scrollController, + slivers: [ + 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(_isScrolled), + style: UiTypography.title2m.copyWith( + color: UiColors.primaryForeground, + ), + ), + ), + actions: [ + IconButton( + onPressed: () { + BlocProvider.of(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: [ + 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: [ + CoverageCalendarSelector( + selectedDate: selectedDate, + onDateSelected: (DateTime date) { + BlocProvider.of(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: [ + Column( + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + 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: [ + 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( + [ + _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), ], ), );