feat: enhance availability management with success message handling and loading state

This commit is contained in:
Achintha Isuru
2026-01-30 16:19:22 -05:00
parent aa39b0fd06
commit f1ccc97fae
3 changed files with 114 additions and 84 deletions

View File

@@ -1,5 +1,4 @@
import 'package:flutter_bloc/flutter_bloc.dart'; 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/apply_quick_set_usecase.dart';
import '../../domain/usecases/get_weekly_availability_usecase.dart'; import '../../domain/usecases/get_weekly_availability_usecase.dart';
import '../../domain/usecases/update_day_availability_usecase.dart'; import '../../domain/usecases/update_day_availability_usecase.dart';
@@ -45,7 +44,11 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
void _onSelectDate(SelectDate event, Emitter<AvailabilityState> emit) { void _onSelectDate(SelectDate event, Emitter<AvailabilityState> emit) {
if (state is AvailabilityLoaded) { 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<AvailabilityEvent, AvailabilityState> {
) async { ) async {
if (state is AvailabilityLoaded) { if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded; final currentState = state as AvailabilityLoaded;
// Clear message
emit(currentState.copyWith(clearSuccessMessage: true));
final newWeekStart = currentState.currentWeekStart final newWeekStart = currentState.currentWeekStart
.add(Duration(days: event.direction * 7)); .add(Duration(days: event.direction * 7));
@@ -77,12 +84,23 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
return d.date == event.day.date ? newDay : d; return d.date == event.day.date ? newDay : d;
}).toList(); }).toList();
emit(currentState.copyWith(days: updatedDays)); // Optimistic update
emit(currentState.copyWith(
days: updatedDays,
clearSuccessMessage: true,
));
try { try {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay)); await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
// Success feedback
if (state is AvailabilityLoaded) {
emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated'));
}
} catch (e) { } 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<AvailabilityEvent, AvailabilityState> {
return d.date == event.day.date ? newDay : d; return d.date == event.day.date ? newDay : d;
}).toList(); }).toList();
emit(currentState.copyWith(days: updatedDays)); // Optimistic update
emit(currentState.copyWith(
days: updatedDays,
clearSuccessMessage: true,
));
try { try {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay)); await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
// Success feedback
if (state is AvailabilityLoaded) {
emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated'));
}
} catch (e) { } 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<AvailabilityEvent, AvailabilityState> {
if (state is AvailabilityLoaded) { if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded; final currentState = state as AvailabilityLoaded;
emit(currentState.copyWith(
isActionInProgress: true,
clearSuccessMessage: true,
));
try { try {
final newDays = await applyQuickSet( final newDays = await applyQuickSet(
ApplyQuickSetParams(currentState.currentWeekStart, event.type)); ApplyQuickSetParams(currentState.currentWeekStart, event.type));
emit(currentState.copyWith(days: newDays));
emit(currentState.copyWith(
days: newDays,
isActionInProgress: false,
successMessage: 'Availability updated',
));
} catch (e) { } 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.
));
} }
} }
} }

View File

@@ -15,11 +15,15 @@ class AvailabilityLoaded extends AvailabilityState {
final List<DayAvailability> days; final List<DayAvailability> days;
final DateTime currentWeekStart; final DateTime currentWeekStart;
final DateTime selectedDate; final DateTime selectedDate;
final bool isActionInProgress;
final String? successMessage;
const AvailabilityLoaded({ const AvailabilityLoaded({
required this.days, required this.days,
required this.currentWeekStart, required this.currentWeekStart,
required this.selectedDate, required this.selectedDate,
this.isActionInProgress = false,
this.successMessage,
}); });
/// Helper to get the currently selected day's availability object /// Helper to get the currently selected day's availability object
@@ -34,11 +38,16 @@ class AvailabilityLoaded extends AvailabilityState {
List<DayAvailability>? days, List<DayAvailability>? days,
DateTime? currentWeekStart, DateTime? currentWeekStart,
DateTime? selectedDate, DateTime? selectedDate,
bool? isActionInProgress,
String? successMessage, // Nullable override
bool clearSuccessMessage = false,
}) { }) {
return AvailabilityLoaded( return AvailabilityLoaded(
days: days ?? this.days, days: days ?? this.days,
currentWeekStart: currentWeekStart ?? this.currentWeekStart, currentWeekStart: currentWeekStart ?? this.currentWeekStart,
selectedDate: selectedDate ?? this.selectedDate, selectedDate: selectedDate ?? this.selectedDate,
isActionInProgress: isActionInProgress ?? this.isActionInProgress,
successMessage: clearSuccessMessage ? null : (successMessage ?? this.successMessage),
); );
} }
@@ -47,7 +56,7 @@ class AvailabilityLoaded extends AvailabilityState {
} }
@override @override
List<Object?> get props => [days, currentWeekStart, selectedDate]; List<Object?> get props => [days, currentWeekStart, selectedDate, isActionInProgress, successMessage];
} }
class AvailabilityError extends AvailabilityState { class AvailabilityError extends AvailabilityState {

View File

@@ -47,92 +47,70 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
backgroundColor: AppColors.krowBackground, backgroundColor: AppColors.krowBackground,
appBar: UiAppBar( appBar: UiAppBar(
title: 'My Availability', title: 'My Availability',
centerTitle: false,
showBackButton: true, showBackButton: true,
), ),
body: BlocBuilder<AvailabilityBloc, AvailabilityState>( body: BlocListener<AvailabilityBloc, AvailabilityState>(
builder: (context, state) { listener: (context, state) {
if (state is AvailabilityLoading) { if (state is AvailabilityLoaded && state.successMessage != null) {
return const Center(child: CircularProgressIndicator()); ScaffoldMessenger.of(context).hideCurrentSnackBar();
} else if (state is AvailabilityLoaded) { ScaffoldMessenger.of(context).showSnackBar(
return SingleChildScrollView( SnackBar(
padding: const EdgeInsets.only(bottom: 100), content: Text(state.successMessage!),
child: Column( backgroundColor: Colors.green,
behavior: SnackBarBehavior.floating,
),
);
}
},
child: BlocBuilder<AvailabilityBloc, AvailabilityState>(
builder: (context, state) {
if (state is AvailabilityLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is AvailabilityLoaded) {
return Stack(
children: [ children: [
//_buildHeader(), SingleChildScrollView(
Padding( padding: const EdgeInsets.only(bottom: 100),
padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [ children: [
_buildQuickSet(context), Padding(
const SizedBox(height: 24), padding: const EdgeInsets.symmetric(horizontal: 20),
_buildWeekNavigation(context, state), child: Column(
const SizedBox(height: 24), crossAxisAlignment: CrossAxisAlignment.stretch,
_buildSelectedDayAvailability( children: [
context, _buildQuickSet(context),
state.selectedDayAvailability, 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) {
} else if (state is AvailabilityError) { return Center(child: Text('Error: ${state.message}'));
return Center(child: Text('Error: ${state.message}')); }
} return const SizedBox.shrink();
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,
),
),
],
),
],
),
],
), ),
], ),
), ),
); );
} }