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: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<AvailabilityEvent, AvailabilityState> {
void _onSelectDate(SelectDate event, Emitter<AvailabilityState> 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<AvailabilityEvent, AvailabilityState> {
) 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<AvailabilityEvent, AvailabilityState> {
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<AvailabilityEvent, AvailabilityState> {
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<AvailabilityEvent, AvailabilityState> {
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.
));
}
}
}

View File

@@ -15,11 +15,15 @@ class AvailabilityLoaded extends AvailabilityState {
final List<DayAvailability> 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<DayAvailability>? 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<Object?> get props => [days, currentWeekStart, selectedDate];
List<Object?> get props => [days, currentWeekStart, selectedDate, isActionInProgress, successMessage];
}
class AvailabilityError extends AvailabilityState {

View File

@@ -47,18 +47,33 @@ class _AvailabilityPageState extends State<AvailabilityPage> {
backgroundColor: AppColors.krowBackground,
appBar: UiAppBar(
title: 'My Availability',
centerTitle: false,
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) {
if (state is AvailabilityLoading) {
return const Center(child: CircularProgressIndicator());
} else if (state is AvailabilityLoaded) {
return SingleChildScrollView(
return Stack(
children: [
SingleChildScrollView(
padding: const EdgeInsets.only(bottom: 100),
child: Column(
children: [
//_buildHeader(),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
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) {
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,
),
),
],
),
],
),
],
),
],
),
);
}