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,18 +47,33 @@ 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>(
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<AvailabilityBloc, AvailabilityState>(
builder: (context, state) { builder: (context, state) {
if (state is AvailabilityLoading) { if (state is AvailabilityLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} else if (state is AvailabilityLoaded) { } else if (state is AvailabilityLoaded) {
return SingleChildScrollView( return Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 100), padding: const EdgeInsets.only(bottom: 100),
child: Column( child: Column(
children: [ children: [
//_buildHeader(),
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: 20), padding: const EdgeInsets.symmetric(horizontal: 20),
child: Column( child: Column(
@@ -79,6 +94,15 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
), ),
], ],
), ),
),
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}'));
@@ -87,52 +111,6 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
}, },
), ),
), ),
);
}
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,
),
),
],
),
],
),
],
),
],
), ),
); );
} }