From f1ccc97fae7e7970388a4b5839bb0a723b195c31 Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Fri, 30 Jan 2026 16:19:22 -0500 Subject: [PATCH] feat: enhance availability management with success message handling and loading state --- .../presentation/blocs/availability_bloc.dart | 59 ++++++-- .../blocs/availability_state.dart | 11 +- .../presentation/pages/availability_page.dart | 128 ++++++++---------- 3 files changed, 114 insertions(+), 84 deletions(-) diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart index 4073db48..2e1f32a3 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_bloc.dart @@ -1,5 +1,4 @@ import 'package:flutter_bloc/flutter_bloc.dart'; -import 'package:krow_domain/krow_domain.dart'; import '../../domain/usecases/apply_quick_set_usecase.dart'; import '../../domain/usecases/get_weekly_availability_usecase.dart'; import '../../domain/usecases/update_day_availability_usecase.dart'; @@ -45,7 +44,11 @@ class AvailabilityBloc extends Bloc { void _onSelectDate(SelectDate event, Emitter emit) { if (state is AvailabilityLoaded) { - emit((state as AvailabilityLoaded).copyWith(selectedDate: event.date)); + // Clear success message on navigation + emit((state as AvailabilityLoaded).copyWith( + selectedDate: event.date, + clearSuccessMessage: true, + )); } } @@ -55,6 +58,10 @@ class AvailabilityBloc extends Bloc { ) async { if (state is AvailabilityLoaded) { final currentState = state as AvailabilityLoaded; + + // Clear message + emit(currentState.copyWith(clearSuccessMessage: true)); + final newWeekStart = currentState.currentWeekStart .add(Duration(days: event.direction * 7)); @@ -77,12 +84,23 @@ class AvailabilityBloc extends Bloc { return d.date == event.day.date ? newDay : d; }).toList(); - emit(currentState.copyWith(days: updatedDays)); + // Optimistic update + emit(currentState.copyWith( + days: updatedDays, + clearSuccessMessage: true, + )); try { await updateDayAvailability(UpdateDayAvailabilityParams(newDay)); + // Success feedback + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated')); + } } catch (e) { - emit(currentState.copyWith(days: currentState.days)); + // Revert + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(days: currentState.days)); + } } } } @@ -107,12 +125,23 @@ class AvailabilityBloc extends Bloc { return d.date == event.day.date ? newDay : d; }).toList(); - emit(currentState.copyWith(days: updatedDays)); + // Optimistic update + emit(currentState.copyWith( + days: updatedDays, + clearSuccessMessage: true, + )); try { await updateDayAvailability(UpdateDayAvailabilityParams(newDay)); + // Success feedback + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated')); + } } catch (e) { - emit(currentState.copyWith(days: currentState.days)); + // Revert + if (state is AvailabilityLoaded) { + emit((state as AvailabilityLoaded).copyWith(days: currentState.days)); + } } } } @@ -124,12 +153,26 @@ class AvailabilityBloc extends Bloc { if (state is AvailabilityLoaded) { final currentState = state as AvailabilityLoaded; + emit(currentState.copyWith( + isActionInProgress: true, + clearSuccessMessage: true, + )); + try { final newDays = await applyQuickSet( ApplyQuickSetParams(currentState.currentWeekStart, event.type)); - emit(currentState.copyWith(days: newDays)); + + emit(currentState.copyWith( + days: newDays, + isActionInProgress: false, + successMessage: 'Availability updated', + )); } catch (e) { - // Handle error + emit(currentState.copyWith( + isActionInProgress: false, + // Could set error message here if we had a field for it, or emit AvailabilityError + // But emitting AvailabilityError would replace the whole screen. + )); } } } diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart index 5c8b52ba..e48fed83 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/blocs/availability_state.dart @@ -15,11 +15,15 @@ class AvailabilityLoaded extends AvailabilityState { final List days; final DateTime currentWeekStart; final DateTime selectedDate; + final bool isActionInProgress; + final String? successMessage; const AvailabilityLoaded({ required this.days, required this.currentWeekStart, required this.selectedDate, + this.isActionInProgress = false, + this.successMessage, }); /// Helper to get the currently selected day's availability object @@ -34,11 +38,16 @@ class AvailabilityLoaded extends AvailabilityState { List? days, DateTime? currentWeekStart, DateTime? selectedDate, + bool? isActionInProgress, + String? successMessage, // Nullable override + bool clearSuccessMessage = false, }) { return AvailabilityLoaded( days: days ?? this.days, currentWeekStart: currentWeekStart ?? this.currentWeekStart, selectedDate: selectedDate ?? this.selectedDate, + isActionInProgress: isActionInProgress ?? this.isActionInProgress, + successMessage: clearSuccessMessage ? null : (successMessage ?? this.successMessage), ); } @@ -47,7 +56,7 @@ class AvailabilityLoaded extends AvailabilityState { } @override - List get props => [days, currentWeekStart, selectedDate]; + List get props => [days, currentWeekStart, selectedDate, isActionInProgress, successMessage]; } class AvailabilityError extends AvailabilityState { diff --git a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart index cf6a39c1..91fc33ee 100644 --- a/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart +++ b/apps/mobile/packages/features/staff/availability/lib/src/presentation/pages/availability_page.dart @@ -47,92 +47,70 @@ class _AvailabilityPageState extends State { backgroundColor: AppColors.krowBackground, appBar: UiAppBar( title: 'My Availability', + centerTitle: false, showBackButton: true, ), - body: BlocBuilder( - builder: (context, state) { - if (state is AvailabilityLoading) { - return const Center(child: CircularProgressIndicator()); - } else if (state is AvailabilityLoaded) { - return SingleChildScrollView( - padding: const EdgeInsets.only(bottom: 100), - child: Column( + body: BlocListener( + listener: (context, state) { + if (state is AvailabilityLoaded && state.successMessage != null) { + ScaffoldMessenger.of(context).hideCurrentSnackBar(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.successMessage!), + backgroundColor: Colors.green, + behavior: SnackBarBehavior.floating, + ), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + if (state is AvailabilityLoading) { + return const Center(child: CircularProgressIndicator()); + } else if (state is AvailabilityLoaded) { + return Stack( children: [ - //_buildHeader(), - Padding( - padding: const EdgeInsets.symmetric(horizontal: 20), + SingleChildScrollView( + padding: const EdgeInsets.only(bottom: 100), child: Column( - crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - _buildQuickSet(context), - const SizedBox(height: 24), - _buildWeekNavigation(context, state), - const SizedBox(height: 24), - _buildSelectedDayAvailability( - context, - state.selectedDayAvailability, + Padding( + padding: const EdgeInsets.symmetric(horizontal: 20), + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + _buildQuickSet(context), + const SizedBox(height: 24), + _buildWeekNavigation(context, state), + const SizedBox(height: 24), + _buildSelectedDayAvailability( + context, + state.selectedDayAvailability, + ), + const SizedBox(height: 24), + _buildInfoCard(), + ], + ), ), - const SizedBox(height: 24), - _buildInfoCard(), ], ), ), + if (state.isActionInProgress) + Container( + color: Colors.black.withOpacity(0.3), + child: const Center( + child: CircularProgressIndicator(), + ), + ), ], - ), - ); - } else if (state is AvailabilityError) { - return Center(child: Text('Error: ${state.message}')); - } - return const SizedBox.shrink(); - }, - ), - ), - ); - } - - Widget _buildHeader() { - return Container( - padding: const EdgeInsets.fromLTRB(20, 60, 20, 20), - child: Column( - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - IconButton( - icon: const Icon( - LucideIcons.arrowLeft, - color: AppColors.krowCharcoal, - ), - onPressed: () => Modular.to.pop(), - ), - const SizedBox(width: 12), - const Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - 'My Availability', - style: TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - 'Set when you can work', - style: TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - ], + ); + } else if (state is AvailabilityError) { + return Center(child: Text('Error: ${state.message}')); + } + return const SizedBox.shrink(); + }, ), - ], + ), ), ); }