refactor: migrate shifts BLoC state management to a single state class with a status enum.

This commit is contained in:
Achintha Isuru
2026-02-22 10:24:01 -05:00
parent a9ead783e4
commit b593647800
6 changed files with 325 additions and 325 deletions

View File

@@ -32,7 +32,7 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
required this.getCancelledShifts, required this.getCancelledShifts,
required this.getHistoryShifts, required this.getHistoryShifts,
required this.getProfileCompletion, required this.getProfileCompletion,
}) : super(ShiftsInitial()) { }) : super(const ShiftsState()) {
on<LoadShiftsEvent>(_onLoadShifts); on<LoadShiftsEvent>(_onLoadShifts);
on<LoadHistoryShiftsEvent>(_onLoadHistoryShifts); on<LoadHistoryShiftsEvent>(_onLoadHistoryShifts);
on<LoadAvailableShiftsEvent>(_onLoadAvailableShifts); on<LoadAvailableShiftsEvent>(_onLoadAvailableShifts);
@@ -46,8 +46,8 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
LoadShiftsEvent event, LoadShiftsEvent event,
Emitter<ShiftsState> emit, Emitter<ShiftsState> emit,
) async { ) async {
if (state is! ShiftsLoaded) { if (state.status != ShiftsStatus.loaded) {
emit(ShiftsLoading()); emit(state.copyWith(status: ShiftsStatus.loading));
} }
await handleError( await handleError(
@@ -58,22 +58,26 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
GetMyShiftsArguments(start: days.first, end: days.last), GetMyShiftsArguments(start: days.first, end: days.last),
); );
emit(ShiftsLoaded( emit(
myShifts: myShiftsResult, state.copyWith(
pendingShifts: const [], status: ShiftsStatus.loaded,
cancelledShifts: const [], myShifts: myShiftsResult,
availableShifts: const [], pendingShifts: const [],
historyShifts: const [], cancelledShifts: const [],
availableLoading: false, availableShifts: const [],
availableLoaded: false, historyShifts: const [],
historyLoading: false, availableLoading: false,
historyLoaded: false, availableLoaded: false,
myShiftsLoaded: true, historyLoading: false,
searchQuery: '', historyLoaded: false,
jobType: 'all', myShiftsLoaded: true,
)); searchQuery: '',
jobType: 'all',
),
);
}, },
onError: (String errorKey) => ShiftsError(errorKey), onError: (String errorKey) =>
state.copyWith(status: ShiftsStatus.error, errorMessage: errorKey),
); );
} }
@@ -81,27 +85,29 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
LoadHistoryShiftsEvent event, LoadHistoryShiftsEvent event,
Emitter<ShiftsState> emit, Emitter<ShiftsState> emit,
) async { ) async {
final currentState = state; if (state.status != ShiftsStatus.loaded) return;
if (currentState is! ShiftsLoaded) return; if (state.historyLoading || state.historyLoaded) return;
if (currentState.historyLoading || currentState.historyLoaded) return;
emit(currentState.copyWith(historyLoading: true)); emit(state.copyWith(historyLoading: true));
await handleError( await handleError(
emit: emit.call, emit: emit.call,
action: () async { action: () async {
final historyResult = await getHistoryShifts(); final historyResult = await getHistoryShifts();
emit(currentState.copyWith( emit(
myShiftsLoaded: true, state.copyWith(
historyShifts: historyResult, myShiftsLoaded: true,
historyLoading: false, historyShifts: historyResult,
historyLoaded: true, historyLoading: false,
)); historyLoaded: true,
),
);
}, },
onError: (String errorKey) { onError: (String errorKey) {
if (state is ShiftsLoaded) { return state.copyWith(
return (state as ShiftsLoaded).copyWith(historyLoading: false); historyLoading: false,
} status: ShiftsStatus.error,
return ShiftsError(errorKey); errorMessage: errorKey,
);
}, },
); );
} }
@@ -110,33 +116,32 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
LoadAvailableShiftsEvent event, LoadAvailableShiftsEvent event,
Emitter<ShiftsState> emit, Emitter<ShiftsState> emit,
) async { ) async {
final currentState = state; if (state.status != ShiftsStatus.loaded) return;
if (currentState is! ShiftsLoaded) return; if (!event.force && (state.availableLoading || state.availableLoaded)) {
if (!event.force &&
(currentState.availableLoading || currentState.availableLoaded)) {
return; return;
} }
emit(currentState.copyWith( emit(state.copyWith(availableLoading: true, availableLoaded: false));
availableLoading: true,
availableLoaded: false,
));
await handleError( await handleError(
emit: emit.call, emit: emit.call,
action: () async { action: () async {
final availableResult = final availableResult = await getAvailableShifts(
await getAvailableShifts(const GetAvailableShiftsArguments()); const GetAvailableShiftsArguments(),
emit(currentState.copyWith( );
availableShifts: _filterPastShifts(availableResult), emit(
availableLoading: false, state.copyWith(
availableLoaded: true, availableShifts: _filterPastShifts(availableResult),
)); availableLoading: false,
availableLoaded: true,
),
);
}, },
onError: (String errorKey) { onError: (String errorKey) {
if (state is ShiftsLoaded) { return state.copyWith(
return (state as ShiftsLoaded).copyWith(availableLoading: false); availableLoading: false,
} status: ShiftsStatus.error,
return ShiftsError(errorKey); errorMessage: errorKey,
);
}, },
); );
} }
@@ -145,62 +150,51 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
LoadFindFirstEvent event, LoadFindFirstEvent event,
Emitter<ShiftsState> emit, Emitter<ShiftsState> emit,
) async { ) async {
if (state is! ShiftsLoaded) { if (state.status != ShiftsStatus.loaded) {
emit(const ShiftsLoaded( emit(
myShifts: [], state.copyWith(
pendingShifts: [], status: ShiftsStatus.loading,
cancelledShifts: [], myShifts: const [],
availableShifts: [], pendingShifts: const [],
historyShifts: [], cancelledShifts: const [],
availableLoading: false, availableShifts: const [],
availableLoaded: false, historyShifts: const [],
historyLoading: false, availableLoading: false,
historyLoaded: false, availableLoaded: false,
myShiftsLoaded: false, historyLoading: false,
searchQuery: '', historyLoaded: false,
jobType: 'all', myShiftsLoaded: false,
)); searchQuery: '',
jobType: 'all',
),
);
} }
final currentState = state is ShiftsLoaded ? state as ShiftsLoaded : null; if (state.availableLoaded) return;
if (currentState != null && currentState.availableLoaded) return;
if (currentState != null) { emit(state.copyWith(availableLoading: true));
emit(currentState.copyWith(availableLoading: true));
}
await handleError( await handleError(
emit: emit.call, emit: emit.call,
action: () async { action: () async {
final availableResult = final availableResult = await getAvailableShifts(
await getAvailableShifts(const GetAvailableShiftsArguments()); const GetAvailableShiftsArguments(),
final loadedState = state is ShiftsLoaded );
? state as ShiftsLoaded emit(
: const ShiftsLoaded( state.copyWith(
myShifts: [], status: ShiftsStatus.loaded,
pendingShifts: [], availableShifts: _filterPastShifts(availableResult),
cancelledShifts: [], availableLoading: false,
availableShifts: [], availableLoaded: true,
historyShifts: [], ),
availableLoading: true, );
availableLoaded: false,
historyLoading: false,
historyLoaded: false,
myShiftsLoaded: false,
searchQuery: '',
jobType: 'all',
);
emit(loadedState.copyWith(
availableShifts: _filterPastShifts(availableResult),
availableLoading: false,
availableLoaded: true,
));
}, },
onError: (String errorKey) { onError: (String errorKey) {
if (state is ShiftsLoaded) { return state.copyWith(
return (state as ShiftsLoaded).copyWith(availableLoading: false); availableLoading: false,
} status: ShiftsStatus.error,
return ShiftsError(errorKey); errorMessage: errorKey,
);
}, },
); );
} }
@@ -216,31 +210,16 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
GetMyShiftsArguments(start: event.start, end: event.end), GetMyShiftsArguments(start: event.start, end: event.end),
); );
if (state is ShiftsLoaded) { emit(
final currentState = state as ShiftsLoaded; state.copyWith(
emit(currentState.copyWith( status: ShiftsStatus.loaded,
myShifts: myShiftsResult, myShifts: myShiftsResult,
myShiftsLoaded: true, myShiftsLoaded: true,
)); ),
return; );
}
emit(ShiftsLoaded(
myShifts: myShiftsResult,
pendingShifts: const [],
cancelledShifts: const [],
availableShifts: const [],
historyShifts: const [],
availableLoading: false,
availableLoaded: false,
historyLoading: false,
historyLoaded: false,
myShiftsLoaded: true,
searchQuery: '',
jobType: 'all',
));
}, },
onError: (String errorKey) => ShiftsError(errorKey), onError: (String errorKey) =>
state.copyWith(status: ShiftsStatus.error, errorMessage: errorKey),
); );
} }
@@ -248,9 +227,8 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
FilterAvailableShiftsEvent event, FilterAvailableShiftsEvent event,
Emitter<ShiftsState> emit, Emitter<ShiftsState> emit,
) async { ) async {
final currentState = state; if (state.status == ShiftsStatus.loaded) {
if (currentState is ShiftsLoaded) { if (!state.availableLoaded && !state.availableLoading) {
if (!currentState.availableLoaded && !currentState.availableLoading) {
add(LoadAvailableShiftsEvent()); add(LoadAvailableShiftsEvent());
return; return;
} }
@@ -258,21 +236,26 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
await handleError( await handleError(
emit: emit.call, emit: emit.call,
action: () async { action: () async {
final result = await getAvailableShifts(GetAvailableShiftsArguments( final result = await getAvailableShifts(
query: event.query ?? currentState.searchQuery, GetAvailableShiftsArguments(
type: event.jobType ?? currentState.jobType, query: event.query ?? state.searchQuery,
)); type: event.jobType ?? state.jobType,
),
);
emit(currentState.copyWith( emit(
availableShifts: _filterPastShifts(result), state.copyWith(
searchQuery: event.query ?? currentState.searchQuery, availableShifts: _filterPastShifts(result),
jobType: event.jobType ?? currentState.jobType, searchQuery: event.query ?? state.searchQuery,
)); jobType: event.jobType ?? state.jobType,
),
);
}, },
onError: (String errorKey) { onError: (String errorKey) {
// Stay on current state for filtering errors, maybe show a snackbar? return state.copyWith(
// For now just logging is enough via handleError mixin. status: ShiftsStatus.error,
return currentState; errorMessage: errorKey,
);
}, },
); );
} }
@@ -282,17 +265,14 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
CheckProfileCompletionEvent event, CheckProfileCompletionEvent event,
Emitter<ShiftsState> emit, Emitter<ShiftsState> emit,
) async { ) async {
final currentState = state;
if (currentState is! ShiftsLoaded) return;
await handleError( await handleError(
emit: emit.call, emit: emit.call,
action: () async { action: () async {
final bool isComplete = await getProfileCompletion(); final bool isComplete = await getProfileCompletion();
emit(currentState.copyWith(profileComplete: isComplete)); emit(state.copyWith(profileComplete: isComplete));
}, },
onError: (String errorKey) { onError: (String errorKey) {
return currentState.copyWith(profileComplete: false); return state.copyWith(profileComplete: false);
}, },
); );
} }

View File

@@ -1,18 +1,9 @@
part of 'shifts_bloc.dart'; part of 'shifts_bloc.dart';
@immutable enum ShiftsStatus { initial, loading, loaded, error }
sealed class ShiftsState extends Equatable {
const ShiftsState();
@override
List<Object> get props => [];
}
class ShiftsInitial extends ShiftsState {} class ShiftsState extends Equatable {
final ShiftsStatus status;
class ShiftsLoading extends ShiftsState {}
class ShiftsLoaded extends ShiftsState {
final List<Shift> myShifts; final List<Shift> myShifts;
final List<Shift> pendingShifts; final List<Shift> pendingShifts;
final List<Shift> cancelledShifts; final List<Shift> cancelledShifts;
@@ -26,24 +17,28 @@ class ShiftsLoaded extends ShiftsState {
final String searchQuery; final String searchQuery;
final String jobType; final String jobType;
final bool? profileComplete; final bool? profileComplete;
final String? errorMessage;
const ShiftsLoaded({ const ShiftsState({
required this.myShifts, this.status = ShiftsStatus.initial,
required this.pendingShifts, this.myShifts = const [],
required this.cancelledShifts, this.pendingShifts = const [],
required this.availableShifts, this.cancelledShifts = const [],
required this.historyShifts, this.availableShifts = const [],
required this.availableLoading, this.historyShifts = const [],
required this.availableLoaded, this.availableLoading = false,
required this.historyLoading, this.availableLoaded = false,
required this.historyLoaded, this.historyLoading = false,
required this.myShiftsLoaded, this.historyLoaded = false,
required this.searchQuery, this.myShiftsLoaded = false,
required this.jobType, this.searchQuery = '',
this.jobType = 'all',
this.profileComplete, this.profileComplete,
this.errorMessage,
}); });
ShiftsLoaded copyWith({ ShiftsState copyWith({
ShiftsStatus? status,
List<Shift>? myShifts, List<Shift>? myShifts,
List<Shift>? pendingShifts, List<Shift>? pendingShifts,
List<Shift>? cancelledShifts, List<Shift>? cancelledShifts,
@@ -57,8 +52,10 @@ class ShiftsLoaded extends ShiftsState {
String? searchQuery, String? searchQuery,
String? jobType, String? jobType,
bool? profileComplete, bool? profileComplete,
String? errorMessage,
}) { }) {
return ShiftsLoaded( return ShiftsState(
status: status ?? this.status,
myShifts: myShifts ?? this.myShifts, myShifts: myShifts ?? this.myShifts,
pendingShifts: pendingShifts ?? this.pendingShifts, pendingShifts: pendingShifts ?? this.pendingShifts,
cancelledShifts: cancelledShifts ?? this.cancelledShifts, cancelledShifts: cancelledShifts ?? this.cancelledShifts,
@@ -72,32 +69,26 @@ class ShiftsLoaded extends ShiftsState {
searchQuery: searchQuery ?? this.searchQuery, searchQuery: searchQuery ?? this.searchQuery,
jobType: jobType ?? this.jobType, jobType: jobType ?? this.jobType,
profileComplete: profileComplete ?? this.profileComplete, profileComplete: profileComplete ?? this.profileComplete,
errorMessage: errorMessage ?? this.errorMessage,
); );
} }
@override @override
List<Object> get props => [ List<Object?> get props => [
myShifts, status,
pendingShifts, myShifts,
cancelledShifts, pendingShifts,
availableShifts, cancelledShifts,
historyShifts, availableShifts,
availableLoading, historyShifts,
availableLoaded, availableLoading,
historyLoading, availableLoaded,
historyLoaded, historyLoading,
myShiftsLoaded, historyLoaded,
searchQuery, myShiftsLoaded,
jobType, searchQuery,
profileComplete ?? '', jobType,
]; profileComplete,
} errorMessage,
];
class ShiftsError extends ShiftsState {
final String message;
const ShiftsError(this.message);
@override
List<Object> get props => [message];
} }

View File

@@ -5,12 +5,13 @@ import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart'; import 'package:core_localization/core_localization.dart';
import 'package:krow_domain/krow_domain.dart'; import 'package:krow_domain/krow_domain.dart';
import '../blocs/shifts/shifts_bloc.dart'; import '../blocs/shifts/shifts_bloc.dart';
import '../utils/shift_tab_type.dart';
import '../widgets/tabs/my_shifts_tab.dart'; import '../widgets/tabs/my_shifts_tab.dart';
import '../widgets/tabs/find_shifts_tab.dart'; import '../widgets/tabs/find_shifts_tab.dart';
import '../widgets/tabs/history_shifts_tab.dart'; import '../widgets/tabs/history_shifts_tab.dart';
class ShiftsPage extends StatefulWidget { class ShiftsPage extends StatefulWidget {
final String? initialTab; final ShiftTabType? initialTab;
final DateTime? selectedDate; final DateTime? selectedDate;
final bool refreshAvailable; final bool refreshAvailable;
const ShiftsPage({ const ShiftsPage({
@@ -25,7 +26,7 @@ class ShiftsPage extends StatefulWidget {
} }
class _ShiftsPageState extends State<ShiftsPage> { class _ShiftsPageState extends State<ShiftsPage> {
late String _activeTab; late ShiftTabType _activeTab;
DateTime? _selectedDate; DateTime? _selectedDate;
bool _prioritizeFind = false; bool _prioritizeFind = false;
bool _refreshAvailable = false; bool _refreshAvailable = false;
@@ -35,9 +36,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_activeTab = widget.initialTab ?? 'myshifts'; _activeTab = widget.initialTab ?? ShiftTabType.find;
_selectedDate = widget.selectedDate; _selectedDate = widget.selectedDate;
_prioritizeFind = widget.initialTab == 'find'; _prioritizeFind = _activeTab == ShiftTabType.find;
_refreshAvailable = widget.refreshAvailable; _refreshAvailable = widget.refreshAvailable;
_pendingAvailableRefresh = widget.refreshAvailable; _pendingAvailableRefresh = widget.refreshAvailable;
if (_prioritizeFind) { if (_prioritizeFind) {
@@ -45,16 +46,15 @@ class _ShiftsPageState extends State<ShiftsPage> {
} else { } else {
_bloc.add(LoadShiftsEvent()); _bloc.add(LoadShiftsEvent());
} }
if (_activeTab == 'history') { if (_activeTab == ShiftTabType.history) {
_bloc.add(LoadHistoryShiftsEvent()); _bloc.add(LoadHistoryShiftsEvent());
} }
if (_activeTab == 'find') { if (_activeTab == ShiftTabType.find) {
if (!_prioritizeFind) { if (!_prioritizeFind) {
_bloc.add( _bloc.add(LoadAvailableShiftsEvent(force: _refreshAvailable));
LoadAvailableShiftsEvent(force: _refreshAvailable),
);
} }
} }
// Check profile completion // Check profile completion
_bloc.add(const CheckProfileCompletionEvent()); _bloc.add(const CheckProfileCompletionEvent());
} }
@@ -65,7 +65,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
if (widget.initialTab != null && widget.initialTab != _activeTab) { if (widget.initialTab != null && widget.initialTab != _activeTab) {
setState(() { setState(() {
_activeTab = widget.initialTab!; _activeTab = widget.initialTab!;
_prioritizeFind = widget.initialTab == 'find'; _prioritizeFind = _activeTab == ShiftTabType.find;
}); });
} }
if (widget.selectedDate != null && widget.selectedDate != _selectedDate) { if (widget.selectedDate != null && widget.selectedDate != _selectedDate) {
@@ -86,50 +86,31 @@ class _ShiftsPageState extends State<ShiftsPage> {
value: _bloc, value: _bloc,
child: BlocConsumer<ShiftsBloc, ShiftsState>( child: BlocConsumer<ShiftsBloc, ShiftsState>(
listener: (context, state) { listener: (context, state) {
if (state is ShiftsError) { if (state.status == ShiftsStatus.error &&
state.errorMessage != null) {
UiSnackbar.show( UiSnackbar.show(
context, context,
message: translateErrorKey(state.message), message: translateErrorKey(state.errorMessage!),
type: UiSnackbarType.error, type: UiSnackbarType.error,
); );
} }
}, },
builder: (context, state) { builder: (context, state) {
if (_pendingAvailableRefresh && state is ShiftsLoaded) { if (_pendingAvailableRefresh && state.status == ShiftsStatus.loaded) {
_pendingAvailableRefresh = false; _pendingAvailableRefresh = false;
_bloc.add(const LoadAvailableShiftsEvent(force: true)); _bloc.add(const LoadAvailableShiftsEvent(force: true));
} }
final bool baseLoaded = state is ShiftsLoaded; final bool baseLoaded = state.status == ShiftsStatus.loaded;
final List<Shift> myShifts = (state is ShiftsLoaded) final List<Shift> myShifts = state.myShifts;
? state.myShifts final List<Shift> availableJobs = state.availableShifts;
: []; final bool availableLoading = state.availableLoading;
final List<Shift> availableJobs = (state is ShiftsLoaded) final bool availableLoaded = state.availableLoaded;
? state.availableShifts final List<Shift> pendingAssignments = state.pendingShifts;
: []; final List<Shift> cancelledShifts = state.cancelledShifts;
final bool availableLoading = (state is ShiftsLoaded) final List<Shift> historyShifts = state.historyShifts;
? state.availableLoading final bool historyLoading = state.historyLoading;
: false; final bool historyLoaded = state.historyLoaded;
final bool availableLoaded = (state is ShiftsLoaded) final bool myShiftsLoaded = state.myShiftsLoaded;
? state.availableLoaded
: false;
final List<Shift> pendingAssignments = (state is ShiftsLoaded)
? state.pendingShifts
: [];
final List<Shift> cancelledShifts = (state is ShiftsLoaded)
? state.cancelledShifts
: [];
final List<Shift> historyShifts = (state is ShiftsLoaded)
? state.historyShifts
: [];
final bool historyLoading = (state is ShiftsLoaded)
? state.historyLoading
: false;
final bool historyLoaded = (state is ShiftsLoaded)
? state.historyLoaded
: false;
final bool myShiftsLoaded = (state is ShiftsLoaded)
? state.myShiftsLoaded
: false;
final bool blockTabsForFind = _prioritizeFind && !availableLoaded; final bool blockTabsForFind = _prioritizeFind && !availableLoaded;
// Note: "filteredJobs" logic moved to FindShiftsTab // Note: "filteredJobs" logic moved to FindShiftsTab
@@ -160,44 +141,47 @@ class _ShiftsPageState extends State<ShiftsPage> {
// Tabs // Tabs
Row( Row(
children: [ children: [
if (state is ShiftsLoaded && state.profileComplete != false) if (state.profileComplete != false)
Expanded( Expanded(
child: _buildTab( child: _buildTab(
"myshifts", ShiftTabType.myShifts,
t.staff_shifts.tabs.my_shifts, t.staff_shifts.tabs.my_shifts,
UiIcons.calendar, UiIcons.calendar,
myShifts.length, myShifts.length,
showCount: myShiftsLoaded, showCount: myShiftsLoaded,
enabled: !blockTabsForFind && (state.profileComplete ?? false), enabled:
!blockTabsForFind &&
(state.profileComplete ?? false),
), ),
) )
else else
const SizedBox.shrink(), const SizedBox.shrink(),
if (state is ShiftsLoaded && state.profileComplete != false) if (state.profileComplete != false)
const SizedBox(width: UiConstants.space2) const SizedBox(width: UiConstants.space2)
else else
const SizedBox.shrink(), const SizedBox.shrink(),
_buildTab( _buildTab(
"find", ShiftTabType.find,
t.staff_shifts.tabs.find_work, t.staff_shifts.tabs.find_work,
UiIcons.search, UiIcons.search,
availableJobs.length, availableJobs.length,
showCount: availableLoaded, showCount: availableLoaded,
enabled: baseLoaded, enabled: baseLoaded,
), ),
if (state is ShiftsLoaded && state.profileComplete != false) if (state.profileComplete != false)
const SizedBox(width: UiConstants.space2) const SizedBox(width: UiConstants.space2)
else else
const SizedBox.shrink(), const SizedBox.shrink(),
if (state is ShiftsLoaded && state.profileComplete != false) if (state.profileComplete != false)
Expanded( Expanded(
child: _buildTab( child: _buildTab(
"history", ShiftTabType.history,
t.staff_shifts.tabs.history, t.staff_shifts.tabs.history,
UiIcons.clock, UiIcons.clock,
historyShifts.length, historyShifts.length,
showCount: historyLoaded, showCount: historyLoaded,
enabled: !blockTabsForFind && enabled:
!blockTabsForFind &&
baseLoaded && baseLoaded &&
(state.profileComplete ?? false), (state.profileComplete ?? false),
), ),
@@ -212,9 +196,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
// Body Content // Body Content
Expanded( Expanded(
child: state is ShiftsLoading child: state.status == ShiftsStatus.loading
? const Center(child: CircularProgressIndicator()) ? const Center(child: CircularProgressIndicator())
: state is ShiftsError : state.status == ShiftsStatus.error
? Center( ? Center(
child: Padding( child: Padding(
padding: const EdgeInsets.all(UiConstants.space5), padding: const EdgeInsets.all(UiConstants.space5),
@@ -222,7 +206,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
Text( Text(
translateErrorKey(state.message), translateErrorKey(state.errorMessage ?? ''),
style: UiTypography.body2r.textSecondary, style: UiTypography.body2r.textSecondary,
textAlign: TextAlign.center, textAlign: TextAlign.center,
), ),
@@ -258,47 +242,45 @@ class _ShiftsPageState extends State<ShiftsPage> {
bool historyLoading, bool historyLoading,
) { ) {
switch (_activeTab) { switch (_activeTab) {
case 'myshifts': case ShiftTabType.myShifts:
return MyShiftsTab( return MyShiftsTab(
myShifts: myShifts, myShifts: myShifts,
pendingAssignments: pendingAssignments, pendingAssignments: pendingAssignments,
cancelledShifts: cancelledShifts, cancelledShifts: cancelledShifts,
initialDate: _selectedDate, initialDate: _selectedDate,
); );
case 'find': case ShiftTabType.find:
if (availableLoading) { if (availableLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return FindShiftsTab(availableJobs: availableJobs); return FindShiftsTab(availableJobs: availableJobs);
case 'history': case ShiftTabType.history:
if (historyLoading) { if (historyLoading) {
return const Center(child: CircularProgressIndicator()); return const Center(child: CircularProgressIndicator());
} }
return HistoryShiftsTab(historyShifts: historyShifts); return HistoryShiftsTab(historyShifts: historyShifts);
default:
return const SizedBox.shrink();
} }
} }
Widget _buildTab( Widget _buildTab(
String id, ShiftTabType type,
String label, String label,
IconData icon, IconData icon,
int count, { int count, {
bool showCount = true, bool showCount = true,
bool enabled = true, bool enabled = true,
}) { }) {
final isActive = _activeTab == id; final isActive = _activeTab == type;
return Expanded( return Expanded(
child: GestureDetector( child: GestureDetector(
onTap: !enabled onTap: !enabled
? null ? null
: () { : () {
setState(() => _activeTab = id); setState(() => _activeTab = type);
if (id == 'history') { if (type == ShiftTabType.history) {
_bloc.add(LoadHistoryShiftsEvent()); _bloc.add(LoadHistoryShiftsEvent());
} }
if (id == 'find') { if (type == ShiftTabType.find) {
_bloc.add(LoadAvailableShiftsEvent()); _bloc.add(LoadAvailableShiftsEvent());
} }
}, },

View File

@@ -0,0 +1,30 @@
enum ShiftTabType {
myShifts,
find,
history;
static ShiftTabType fromString(String? value) {
if (value == null) return ShiftTabType.find;
switch (value.toLowerCase()) {
case 'myshifts':
return ShiftTabType.myShifts;
case 'find':
return ShiftTabType.find;
case 'history':
return ShiftTabType.history;
default:
return ShiftTabType.find;
}
}
String get id {
switch (this) {
case ShiftTabType.myShifts:
return 'myshifts';
case ShiftTabType.find:
return 'find';
case ShiftTabType.history:
return 'history';
}
}
}

View File

@@ -27,7 +27,6 @@ class MyShiftCard extends StatefulWidget {
} }
class _MyShiftCardState extends State<MyShiftCard> { class _MyShiftCardState extends State<MyShiftCard> {
String _formatTime(String time) { String _formatTime(String time) {
if (time.isEmpty) return ''; if (time.isEmpty) return '';
try { try {
@@ -77,22 +76,23 @@ class _MyShiftCardState extends State<MyShiftCard> {
String _getShiftType() { String _getShiftType() {
// Handling potential localization key availability // Handling potential localization key availability
try { try {
final String orderType = (widget.shift.orderType ?? '').toUpperCase(); final String orderType = (widget.shift.orderType ?? '').toUpperCase();
if (orderType == 'PERMANENT') { if (orderType == 'PERMANENT') {
return t.staff_shifts.filter.long_term; return t.staff_shifts.filter.long_term;
} }
if (orderType == 'RECURRING') { if (orderType == 'RECURRING') {
return t.staff_shifts.filter.multi_day; return t.staff_shifts.filter.multi_day;
} }
if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) { if (widget.shift.durationDays != null &&
return t.staff_shifts.filter.long_term; widget.shift.durationDays! > 30) {
} return t.staff_shifts.filter.long_term;
if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) { }
return t.staff_shifts.filter.multi_day; if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) {
} return t.staff_shifts.filter.multi_day;
return t.staff_shifts.filter.one_day; }
return t.staff_shifts.filter.one_day;
} catch (_) { } catch (_) {
return "One Day"; return "One Day";
} }
} }
@@ -110,34 +110,34 @@ class _MyShiftCardState extends State<MyShiftCard> {
// Fallback localization if keys missing // Fallback localization if keys missing
try { try {
if (status == 'confirmed') { if (status == 'confirmed') {
statusText = t.staff_shifts.status.confirmed; statusText = t.staff_shifts.status.confirmed;
statusColor = UiColors.textLink; statusColor = UiColors.textLink;
statusBg = UiColors.primary; statusBg = UiColors.primary;
} else if (status == 'checked_in') { } else if (status == 'checked_in') {
statusText = 'Checked in'; statusText = 'Checked in';
statusColor = UiColors.textSuccess; statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess; statusBg = UiColors.iconSuccess;
} else if (status == 'pending' || status == 'open') { } else if (status == 'pending' || status == 'open') {
statusText = t.staff_shifts.status.act_now; statusText = t.staff_shifts.status.act_now;
statusColor = UiColors.destructive; statusColor = UiColors.destructive;
statusBg = UiColors.destructive; statusBg = UiColors.destructive;
} else if (status == 'swap') { } else if (status == 'swap') {
statusText = t.staff_shifts.status.swap_requested; statusText = t.staff_shifts.status.swap_requested;
statusColor = UiColors.textWarning; statusColor = UiColors.textWarning;
statusBg = UiColors.textWarning; statusBg = UiColors.textWarning;
statusIcon = UiIcons.swap; statusIcon = UiIcons.swap;
} else if (status == 'completed') { } else if (status == 'completed') {
statusText = t.staff_shifts.status.completed; statusText = t.staff_shifts.status.completed;
statusColor = UiColors.textSuccess; statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess; statusBg = UiColors.iconSuccess;
} else if (status == 'no_show') { } else if (status == 'no_show') {
statusText = t.staff_shifts.status.no_show; statusText = t.staff_shifts.status.no_show;
statusColor = UiColors.destructive; statusColor = UiColors.destructive;
statusBg = UiColors.destructive; statusBg = UiColors.destructive;
} }
} catch (_) { } catch (_) {
statusText = status?.toUpperCase() ?? ""; statusText = status?.toUpperCase() ?? "";
} }
final schedules = widget.shift.schedules ?? <ShiftSchedule>[]; final schedules = widget.shift.schedules ?? <ShiftSchedule>[];
@@ -145,8 +145,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
final List<ShiftSchedule> visibleSchedules = schedules.length <= 5 final List<ShiftSchedule> visibleSchedules = schedules.length <= 5
? schedules ? schedules
: schedules.take(3).toList(); : schedules.take(3).toList();
final int remainingSchedules = final int remainingSchedules = schedules.length <= 5
schedules.length <= 5 ? 0 : schedules.length - 3; ? 0
: schedules.length - 3;
final String scheduleRange = hasSchedules final String scheduleRange = hasSchedules
? () { ? () {
final first = schedules.first.date; final first = schedules.first.date;
@@ -192,7 +193,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
children: [ children: [
if (statusIcon != null) if (statusIcon != null)
Padding( Padding(
padding: const EdgeInsets.only(right: UiConstants.space2), padding: const EdgeInsets.only(
right: UiConstants.space2,
),
child: Icon( child: Icon(
statusIcon, statusIcon,
size: UiConstants.iconXs, size: UiConstants.iconXs,
@@ -203,7 +206,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
Container( Container(
width: 8, width: 8,
height: 8, height: 8,
margin: const EdgeInsets.only(right: UiConstants.space2), margin: const EdgeInsets.only(
right: UiConstants.space2,
),
decoration: BoxDecoration( decoration: BoxDecoration(
color: statusBg, color: statusBg,
shape: BoxShape.circle, shape: BoxShape.circle,
@@ -257,14 +262,18 @@ class _MyShiftCardState extends State<MyShiftCard> {
begin: Alignment.topLeft, begin: Alignment.topLeft,
end: Alignment.bottomRight, end: Alignment.bottomRight,
), ),
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
border: Border.all( border: Border.all(
color: UiColors.primary.withValues(alpha: 0.09), color: UiColors.primary.withValues(alpha: 0.09),
), ),
), ),
child: widget.shift.logoUrl != null child: widget.shift.logoUrl != null
? ClipRRect( ? ClipRRect(
borderRadius: BorderRadius.circular(UiConstants.radiusBase), borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
child: Image.network( child: Image.network(
widget.shift.logoUrl!, widget.shift.logoUrl!,
fit: BoxFit.contain, fit: BoxFit.contain,
@@ -290,8 +299,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
children: [ children: [
Expanded( Expanded(
child: Column( child: Column(
crossAxisAlignment: crossAxisAlignment: CrossAxisAlignment.start,
CrossAxisAlignment.start,
children: [ children: [
Text( Text(
widget.shift.title, widget.shift.title,
@@ -347,11 +355,13 @@ class _MyShiftCardState extends State<MyShiftCard> {
), ),
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Padding( Padding(
padding: const EdgeInsets.only(bottom: 2), padding: const EdgeInsets.only(bottom: 2),
child: Text( child: Text(
scheduleRange, scheduleRange,
style: UiTypography.footnote2r.copyWith(color: UiColors.primary), style: UiTypography.footnote2r.copyWith(
color: UiColors.primary,
), ),
),
), ),
...visibleSchedules.map( ...visibleSchedules.map(
(schedule) => Padding( (schedule) => Padding(
@@ -368,7 +378,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
Text( Text(
'+$remainingSchedules more schedules', '+$remainingSchedules more schedules',
style: UiTypography.footnote2r.copyWith( style: UiTypography.footnote2r.copyWith(
color: UiColors.primary.withOpacity(0.7), color: UiColors.primary.withValues(
alpha: 0.7,
),
), ),
), ),
], ],
@@ -410,10 +422,11 @@ class _MyShiftCardState extends State<MyShiftCard> {
Text( Text(
'... +${widget.shift.durationDays! - 1} more days', '... +${widget.shift.durationDays! - 1} more days',
style: UiTypography.footnote2r.copyWith( style: UiTypography.footnote2r.copyWith(
color: color: UiColors.primary.withValues(
UiColors.primary.withOpacity(0.7), alpha: 0.7,
),
), ),
) ),
], ],
), ),
] else ...[ ] else ...[

View File

@@ -13,6 +13,7 @@ import 'domain/usecases/apply_for_shift_usecase.dart';
import 'domain/usecases/get_shift_details_usecase.dart'; import 'domain/usecases/get_shift_details_usecase.dart';
import 'presentation/blocs/shifts/shifts_bloc.dart'; import 'presentation/blocs/shifts/shifts_bloc.dart';
import 'presentation/blocs/shift_details/shift_details_bloc.dart'; import 'presentation/blocs/shift_details/shift_details_bloc.dart';
import 'presentation/utils/shift_tab_type.dart';
import 'presentation/pages/shifts_page.dart'; import 'presentation/pages/shifts_page.dart';
class StaffShiftsModule extends Module { class StaffShiftsModule extends Module {
@@ -45,14 +46,16 @@ class StaffShiftsModule extends Module {
i.add(GetShiftDetailsUseCase.new); i.add(GetShiftDetailsUseCase.new);
// Bloc // Bloc
i.add(() => ShiftsBloc( i.add(
getMyShifts: i.get(), () => ShiftsBloc(
getAvailableShifts: i.get(), getMyShifts: i.get(),
getPendingAssignments: i.get(), getAvailableShifts: i.get(),
getCancelledShifts: i.get(), getPendingAssignments: i.get(),
getHistoryShifts: i.get(), getCancelledShifts: i.get(),
getProfileCompletion: i.get(), getHistoryShifts: i.get(),
)); getProfileCompletion: i.get(),
),
);
i.add(ShiftDetailsBloc.new); i.add(ShiftDetailsBloc.new);
} }
@@ -63,8 +66,9 @@ class StaffShiftsModule extends Module {
child: (_) { child: (_) {
final args = r.args.data as Map?; final args = r.args.data as Map?;
final queryParams = r.args.queryParams; final queryParams = r.args.queryParams;
final initialTabStr = queryParams['tab'] ?? args?['initialTab'];
return ShiftsPage( return ShiftsPage(
initialTab: queryParams['tab'] ?? args?['initialTab'], initialTab: ShiftTabType.fromString(initialTabStr),
selectedDate: args?['selectedDate'], selectedDate: args?['selectedDate'],
refreshAvailable: args?['refreshAvailable'] == true, refreshAvailable: args?['refreshAvailable'] == true,
); );