feat: Refactor code structure and optimize performance across multiple modules

This commit is contained in:
Achintha Isuru
2025-11-17 23:29:28 -05:00
parent 831570f2e0
commit a64cbd9edf
1508 changed files with 105319 additions and 0 deletions

View File

@@ -0,0 +1,39 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/common/date_time_extension.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_event.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
class CompleteDialogBloc
extends Bloc<CompleteDialogEvent, CompleteDialogState> {
CompleteDialogBloc() : super(const CompleteDialogState()) {
on<InitializeCompleteDialog>((event, emit) {
emit(state.copyWith(
minLimit: event.minLimit,
startTime: event.minLimit?.toHourMinuteString() ?? DateTime.now().toHourMinuteString(),
endTime: event.minLimit?.add(Duration(minutes: event.breakDurationInMinutes)).toHourMinuteString() ?? DateTime.now().add(Duration(minutes: event.breakDurationInMinutes)).toHourMinuteString(),
breakDuration: Duration(minutes: event.breakDurationInMinutes),
));
});
on<SelectBreakStatus>((event, emit) {
emit(state.copyWith(status: event.status));
});
on<SelectReason>((event, emit) {
emit(state.copyWith(selectedReason: event.reason));
});
on<ChangeStartTime>((event, emit) {
emit(state.copyWith(startTime: event.startTime.toHourMinuteString(),endTime: event.startTime.add(state.breakDuration).toHourMinuteString()));
});
on<ChangeEndTime>((event, emit) {
emit(state.copyWith(endTime: event.endTime.toHourMinuteString()));
});
on<ChangeAdditionalReason>((event, emit) {
emit(state.copyWith(additionalReason: event.additionalReason));
});
}
}

View File

@@ -0,0 +1,63 @@
import 'package:equatable/equatable.dart';
import 'package:krow/features/shifts/domain/blocs/complete_dialog/shift_complete_dialog_state.dart';
abstract class CompleteDialogEvent extends Equatable {
@override
List<Object?> get props => [];
}
class InitializeCompleteDialog extends CompleteDialogEvent {
final DateTime? minLimit;
final int breakDurationInMinutes;
InitializeCompleteDialog(
{this.minLimit, required this.breakDurationInMinutes});
@override
List<Object?> get props => [minLimit];
}
class SelectBreakStatus extends CompleteDialogEvent {
final BreakStatus status;
SelectBreakStatus(this.status);
@override
List<Object?> get props => [status];
}
class SelectReason extends CompleteDialogEvent {
final String reason;
SelectReason(this.reason);
@override
List<Object?> get props => [reason];
}
class ChangeStartTime extends CompleteDialogEvent {
final DateTime startTime;
ChangeStartTime(this.startTime);
@override
List<Object?> get props => [startTime];
}
class ChangeEndTime extends CompleteDialogEvent {
final DateTime endTime;
ChangeEndTime(this.endTime);
@override
List<Object?> get props => [endTime];
}
class ChangeAdditionalReason extends CompleteDialogEvent {
final String additionalReason;
ChangeAdditionalReason(this.additionalReason);
@override
List<Object?> get props => [additionalReason];
}

View File

@@ -0,0 +1,73 @@
import 'package:equatable/equatable.dart';
import 'package:intl/intl.dart';
enum BreakStatus { neutral, positive, negative }
class CompleteDialogState extends Equatable {
final BreakStatus status;
final String? selectedReason;
final String startTime;
final String endTime;
final String additionalReason;
final DateTime? minLimit;
final Duration breakDuration;
const CompleteDialogState({
this.status = BreakStatus.neutral,
this.selectedReason,
this.startTime = '00:00',
this.endTime = '00:00',
this.additionalReason = '',
this.minLimit,
this.breakDuration = const Duration(minutes: 0),
});
String? get breakTimeInputError {
if (startTime == endTime) {
return 'Break start time and end time cannot be the same';
}
if (DateFormat('H:mm')
.parse(startTime)
.isAfter(DateFormat('H:mm').parse(endTime))) {
return 'Break start time cannot be after break end time';
}
if (DateFormat('H:mm').parse(endTime).isAfter(DateTime.now())) {
return 'Break end time cannot be in the future';
}
if (minLimit != null) {
final start = DateFormat('H:mm').parse(startTime);
final min =
DateFormat('H:mm').parse(DateFormat('H:mm').format(minLimit!));
if (start.isBefore(min)) {
return 'Break start time cannot be before the shift start time';
}
}
return null;
}
CompleteDialogState copyWith({
BreakStatus? status,
String? selectedReason,
String? startTime,
String? endTime,
String? additionalReason,
DateTime? minLimit,
Duration? breakDuration,
}) {
return CompleteDialogState(
status: status ?? this.status,
selectedReason: selectedReason ?? this.selectedReason,
startTime: startTime ?? this.startTime,
endTime: endTime ?? this.endTime,
additionalReason: additionalReason ?? this.additionalReason,
minLimit: minLimit ?? this.minLimit,
breakDuration: breakDuration ?? this.breakDuration,
);
}
@override
List<Object?> get props =>
[status, selectedReason, startTime, endTime, additionalReason];
}

View File

@@ -0,0 +1,309 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/clients/api/api_exception.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/sevices/geofencing_serivce.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/services/force_clockout_service.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/domain/shifts_repository.dart';
part 'shift_details_event.dart';
part 'shift_details_state.dart';
class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
ShiftDetailsBloc()
: super(ShiftDetailsState(shiftViewModel: ShiftEntity.empty)) {
on<ShiftDetailsInitialEvent>(_onInitial);
on<StaffGeofencingUpdate>(_onStaffGeofencingUpdate);
on<ShiftUpdateTimerEvent>(_onUpdateTimer);
on<ShiftConfirmEvent>(_onConfirm);
on<ShiftClockInEvent>(_onClockIn);
on<ShiftCompleteEvent>(_onComplete);
on<ShiftDeclineEvent>(_onDecline);
on<ShiftCancelEvent>(_onCancel);
on<ShiftRefreshEvent>(_onRefresh);
on<ShiftCheckGeocodingEvent>(_onCheckGeocoding);
on<ShiftErrorWasShownEvent>(_onErrorWasShown);
}
final GeofencingService _geofencingService = getIt<GeofencingService>();
Timer? _timer;
Timer? _refreshTimer;
StreamSubscription<bool>? _geofencingStream;
Future<void> _onInitial(
ShiftDetailsInitialEvent event,
Emitter<ShiftDetailsState> emit,
) async {
emit(state.copyWith(shiftViewModel: event.shift));
if (event.shift.status == EventShiftRoleStaffStatus.ongoing) {
_startOngoingTimer();
}
_runRefreshTimer();
add(const ShiftCheckGeocodingEvent());
}
void _onStaffGeofencingUpdate(
StaffGeofencingUpdate event,
Emitter<ShiftDetailsState> emit,
) {
emit(state.copyWith(isToFar: !event.isInRange));
}
void _onUpdateTimer(
ShiftUpdateTimerEvent event,
Emitter<ShiftDetailsState> emit,
) {
emit(state.copyWith(shiftViewModel: state.shiftViewModel.copyWith()));
}
void _onConfirm(
ShiftConfirmEvent event,
Emitter<ShiftDetailsState> emit,
) async {
try {
emit(state.copyWith(isLoading: true));
await getIt<ShiftsRepository>().confirmShift(state.shiftViewModel);
emit(
state.copyWith(
shiftViewModel: state.shiftViewModel.copyWith(
status: EventShiftRoleStaffStatus.confirmed,
),
),
);
} catch (e) {
if (e is DisplayableException) {
emit(state.copyWith(errorMessage: e.message));
}else{
emit(state.copyWith(errorMessage: e.toString()));
}
}
emit(state.copyWith(isLoading: false));
}
void _onClockIn(
ShiftClockInEvent event, Emitter<ShiftDetailsState> emit) async {
emit(state.copyWith(isLoading: true));
try {
await getIt<ShiftsRepository>().clockInShift(state.shiftViewModel);
_geofencingStream?.cancel();
emit(
state.copyWith(
isLoading: false,
shiftViewModel: state.shiftViewModel.copyWith(
status: EventShiftRoleStaffStatus.ongoing,
clockIn: DateTime.now(),
),
),
);
} catch (e) {
if (e is DisplayableException) {
emit(state.copyWith(errorMessage: e.message));
}else{
emit(state.copyWith(errorMessage: e.toString()));
}
emit(state.copyWith(isLoading: false));
}
_startOngoingTimer();
getIt<ForceClockoutService>().startTrackOngoingLocation(
state.shiftViewModel,
() {
onForceUpdateUI();
},
);
}
void _onComplete(
ShiftCompleteEvent event,
Emitter<ShiftDetailsState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
emit(
state.copyWith(
shiftViewModel: state.shiftViewModel.copyWith(
status: EventShiftRoleStaffStatus.completed,
),
),
);
} catch (e) {
if (e is DisplayableException) {
emit(state.copyWith(errorMessage: e.message));
}else{
emit(state.copyWith(errorMessage: e.toString()));
}
}
emit(state.copyWith(isLoading: false));
}
void _onDecline(
ShiftDeclineEvent event,
Emitter<ShiftDetailsState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
await getIt<ShiftsRepository>().declineShift(
state.shiftViewModel, event.reason, event.additionalReason);
emit(state.copyWith(
needPop: true,
shiftViewModel: state.shiftViewModel.copyWith(
status: EventShiftRoleStaffStatus.canceledByStaff,
)));
} catch (e) {
if (e is DisplayableException) {
emit(state.copyWith(errorMessage: e.message));
}else{
emit(state.copyWith(errorMessage: e.toString()));
}
}
emit(state.copyWith(isLoading: false));
}
void _onErrorWasShown(
ShiftErrorWasShownEvent event,
Emitter<ShiftDetailsState> emit,
) {
emit(state.copyWith(errorMessage: ''));
}
void _onCancel(
ShiftCancelEvent event,
Emitter<ShiftDetailsState> emit,
) async {
emit(state.copyWith(isLoading: true));
try {
await getIt<ShiftsRepository>().cancelShift(
state.shiftViewModel, event.reason, event.additionalReason);
emit(state.copyWith(
shiftViewModel: state.shiftViewModel.copyWith(
status: EventShiftRoleStaffStatus.canceledByStaff,
)));
} catch (e) {
print('!!!!!! ${e}');
if (e is DisplayableException) {
emit(state.copyWith(errorMessage: e.message));
}else{
emit(state.copyWith(errorMessage: e.toString()));
}
}
emit(state.copyWith(isLoading: false));
}
void _onRefresh(
ShiftRefreshEvent event,
Emitter<ShiftDetailsState> emit,
) {
emit(state.copyWith(
shiftViewModel: event.shift,
proximityState: GeofencingProximityState.none));
}
void _onCheckGeocoding(
ShiftCheckGeocodingEvent event,
Emitter<ShiftDetailsState> emit,
) async {
emit(state.copyWith(
proximityState: GeofencingProximityState.none,
));
await _checkByGeocoding(emit);
}
@override
Future<void> close() {
_timer?.cancel();
_refreshTimer?.cancel();
_geofencingStream?.cancel();
return super.close();
}
void _runRefreshTimer() {
_refreshTimer = Timer.periodic(
const Duration(seconds: 10),
(timer) async {
final shift = await getIt<ShiftsRepository>()
.getShiftById(state.shiftViewModel.id);
if (shift == null) {
return;
}
add((ShiftRefreshEvent(shift)));
},
);
}
Future<void> _checkByGeocoding(Emitter<ShiftDetailsState> emit) async {
if (![
EventShiftRoleStaffStatus.assigned,
EventShiftRoleStaffStatus.confirmed,
EventShiftRoleStaffStatus.ongoing
].contains(state.shiftViewModel.status)) {
return;
}
final geolocationCheck =
await _geofencingService.requestGeolocationPermission();
emit(
state.copyWith(
proximityState: switch (geolocationCheck) {
GeolocationStatus.disabled =>
GeofencingProximityState.locationDisabled,
GeolocationStatus.denied => GeofencingProximityState.permissionDenied,
GeolocationStatus.prohibited => GeofencingProximityState.goToSettings,
GeolocationStatus.onlyInUse => GeofencingProximityState.onlyInUse,
GeolocationStatus.enabled => null,
},
),
);
if (geolocationCheck == GeolocationStatus.enabled) {
_geofencingStream = _geofencingService
.isInRangeStream(
pointLatitude: state.shiftViewModel.locationLat,
pointLongitude: state.shiftViewModel.locationLon,
)
.listen(
(isInRange) {
add(StaffGeofencingUpdate(isInRange: isInRange));
},
);
}
}
void _startOngoingTimer() {
_timer?.cancel();
_timer = Timer.periodic(
const Duration(seconds: 10),
(timer) {
if (state.shiftViewModel.status != EventShiftRoleStaffStatus.ongoing) {
timer.cancel();
} else {
add(const ShiftUpdateTimerEvent());
}
},
);
}
Future<void> onForceUpdateUI() async {
_timer?.cancel();
if (!isClosed) {
final shift =
await getIt<ShiftsRepository>().getShiftById(state.shiftViewModel.id);
if (shift == null) {
return;
}
add((ShiftRefreshEvent(shift)));
}
}
}

View File

@@ -0,0 +1,62 @@
part of 'shift_details_bloc.dart';
@immutable
sealed class ShiftDetailsEvent {
const ShiftDetailsEvent();
}
class ShiftDetailsInitialEvent extends ShiftDetailsEvent {
final ShiftEntity shift;
const ShiftDetailsInitialEvent({required this.shift});
}
class StaffGeofencingUpdate extends ShiftDetailsEvent {
const StaffGeofencingUpdate({required this.isInRange});
final bool isInRange;
}
class ShiftUpdateTimerEvent extends ShiftDetailsEvent {
const ShiftUpdateTimerEvent();
}
class ShiftCompleteEvent extends ShiftDetailsEvent {
const ShiftCompleteEvent();
}
class ShiftClockInEvent extends ShiftDetailsEvent {
const ShiftClockInEvent();
}
class ShiftConfirmEvent extends ShiftDetailsEvent {
const ShiftConfirmEvent();
}
class ShiftDeclineEvent extends ShiftDetailsEvent {
final String? reason;
final String? additionalReason;
const ShiftDeclineEvent(this.reason, this.additionalReason);
}
class ShiftCancelEvent extends ShiftDetailsEvent {
final String? reason;
final String? additionalReason;
const ShiftCancelEvent(this.reason, this.additionalReason);
}
class ShiftRefreshEvent extends ShiftDetailsEvent {
final ShiftEntity shift;
const ShiftRefreshEvent(this.shift);
}
class ShiftCheckGeocodingEvent extends ShiftDetailsEvent {
const ShiftCheckGeocodingEvent();
}
class ShiftErrorWasShownEvent extends ShiftDetailsEvent {
const ShiftErrorWasShownEvent();
}

View File

@@ -0,0 +1,47 @@
part of 'shift_details_bloc.dart';
@immutable
class ShiftDetailsState {
final ShiftEntity shiftViewModel;
final bool isToFar;
final bool isLoading;
final GeofencingProximityState proximityState;
final String? errorMessage;
final bool needPop;
const ShiftDetailsState({
required this.shiftViewModel,
this.isToFar = true,
this.isLoading = false,
this.proximityState = GeofencingProximityState.none,
this.errorMessage,
this.needPop = false,
});
ShiftDetailsState copyWith({
ShiftEntity? shiftViewModel,
bool? isToFar,
bool? isLoading,
GeofencingProximityState? proximityState,
String? errorMessage,
bool? needPop
}) {
return ShiftDetailsState(
shiftViewModel: shiftViewModel ?? this.shiftViewModel,
isToFar: isToFar ?? this.isToFar,
isLoading: isLoading ?? false,
proximityState: proximityState ?? this.proximityState,
errorMessage: errorMessage,
needPop: needPop ?? this.needPop,
);
}
}
enum GeofencingProximityState {
none,
tooFar,
locationDisabled,
goToSettings,
onlyInUse,
permissionDenied,
}

View File

@@ -0,0 +1,133 @@
import 'package:flutter/foundation.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_event.dart';
import 'package:krow/features/shifts/domain/blocs/shifts_list_bloc/shifts_state.dart';
import 'package:krow/features/shifts/domain/services/force_clockout_service.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/domain/shifts_repository.dart';
class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
var indexToStatus = <int, ShiftStatusFilterType>{
0: ShiftStatusFilterType.assigned,
1: ShiftStatusFilterType.confirmed,
2: ShiftStatusFilterType.ongoing,
3: ShiftStatusFilterType.completed,
4: ShiftStatusFilterType.canceled,
};
ShiftsBloc()
: super(const ShiftsState(tabs: {
0: ShiftTabState(items: [], isLoading: true),
1: ShiftTabState(items: []),
2: ShiftTabState(items: []),
3: ShiftTabState(items: []),
4: ShiftTabState(items: []),
})) {
on<ShiftsInitialEvent>(_onInitial);
on<ShiftsTabChangedEvent>(_onTabChanged);
on<LoadTabShiftEvent>(_onLoadTabItems);
on<LoadMoreShiftEvent>(_onLoadMoreTabItems);
on<ReloadMissingBreakShift>(_onReloadMissingBreakShift);
getIt<ShiftsRepository>().statusStream.listen((event) {
add(LoadTabShiftEvent(status: event.index));
});
}
Future<void> _onInitial(ShiftsInitialEvent event, emit) async {
add(const LoadTabShiftEvent(status: 0));
add(const LoadTabShiftEvent(status: 2));
var missedShifts =
await getIt<ShiftsRepository>().getMissBreakFinishedShift();
if (missedShifts.isNotEmpty) {
emit(state.copyWith(
missedShifts: missedShifts,
));
}
}
Future<void> _onTabChanged(ShiftsTabChangedEvent event, emit) async {
emit(state.copyWith(tabIndex: event.tabIndex));
final currentTabState = state.tabs[event.tabIndex]!;
if (currentTabState.items.isEmpty && !currentTabState.isLoading) {
add(LoadTabShiftEvent(status: event.tabIndex));
}
}
Future<void> _onLoadTabItems(LoadTabShiftEvent event, emit) async {
await _fetchShifts(event.status, null, emit);
}
Future<void> _onLoadMoreTabItems(LoadMoreShiftEvent event, emit) async {
final currentTabState = state.tabs[event.status]!;
if (!currentTabState.hasMoreItems || currentTabState.isLoading) return;
await _fetchShifts(event.status, currentTabState.items, emit);
}
_fetchShifts(int tabIndex, List<ShiftEntity>? previousItems, emit) async {
if (previousItems != null && previousItems.lastOrNull?.cursor == null) {
return;
}
final currentTabState = state.tabs[tabIndex]!;
emit(state.copyWith(
tabs: {
...state.tabs,
tabIndex: currentTabState.copyWith(isLoading: true),
},
));
try {
var items = await getIt<ShiftsRepository>().getShifts(
statusFilter: indexToStatus[tabIndex]!,
lastItemId: previousItems?.lastOrNull?.cursor,
);
// if(items.isNotEmpty){
// items = List.generate(20, (i)=>items[0]);
// }
var allItems = (previousItems ?? [])..addAll(items);
emit(state.copyWith(
tabs: {
...state.tabs,
tabIndex: currentTabState.copyWith(
items: allItems,
hasMoreItems: items.isNotEmpty,
isLoading: false,
),
},
));
if (tabIndex == 2 &&
allItems.isNotEmpty ) {
getIt<ForceClockoutService>()
.startTrackOngoingLocation(allItems.first, () {});
}
} catch (e, s) {
debugPrint(e.toString());
debugPrint(s.toString());
emit(state.copyWith(
tabs: {
...state.tabs,
tabIndex: currentTabState.copyWith(isLoading: false),
},
));
}
}
Future<void> _onReloadMissingBreakShift(
ReloadMissingBreakShift event, emit) async {
emit(state.copyWith(missedShifts: []));
var missedShifts =
await getIt<ShiftsRepository>().getMissBreakFinishedShift();
emit(state.copyWith(missedShifts: missedShifts));
}
@override
Future<void> close() {
getIt<ShiftsRepository>().dispose();
return super.close();
}
}

View File

@@ -0,0 +1,30 @@
sealed class ShiftsEvent {
const ShiftsEvent();
}
class ShiftsInitialEvent extends ShiftsEvent {
const ShiftsInitialEvent();
}
class ShiftsTabChangedEvent extends ShiftsEvent {
final int tabIndex;
const ShiftsTabChangedEvent({required this.tabIndex});
}
class LoadTabShiftEvent extends ShiftsEvent {
final int status;
const LoadTabShiftEvent({required this.status});
}
class LoadMoreShiftEvent extends ShiftsEvent {
final int status;
const LoadMoreShiftEvent({required this.status});
}
class ReloadMissingBreakShift extends ShiftsEvent {
const ReloadMissingBreakShift();
}

View File

@@ -0,0 +1,53 @@
import 'package:krow/features/shifts/domain/shift_entity.dart';
class ShiftsState {
final bool isLoading;
final int tabIndex;
final Map<int, ShiftTabState> tabs;
final List<ShiftEntity> missedShifts;
const ShiftsState(
{this.isLoading = false,
this.tabIndex = 0,
required this.tabs,
this.missedShifts = const []});
ShiftsState copyWith({
bool? isLoading,
int? tabIndex,
Map<int, ShiftTabState>? tabs,
List<ShiftEntity>? missedShifts,
}) {
return ShiftsState(
isLoading: isLoading ?? this.isLoading,
tabIndex: tabIndex ?? this.tabIndex,
tabs: tabs ?? this.tabs,
missedShifts: missedShifts ?? this.missedShifts,
);
}
}
class ShiftTabState {
final List<ShiftEntity> items;
final bool isLoading;
final bool hasMoreItems;
const ShiftTabState({
required this.items,
this.isLoading = false,
this.hasMoreItems = true,
});
ShiftTabState copyWith({
List<ShiftEntity>? items,
bool? isLoading,
bool? hasMoreItems,
}) {
return ShiftTabState(
items: items ?? this.items,
isLoading: isLoading ?? this.isLoading,
hasMoreItems: hasMoreItems ?? this.hasMoreItems,
);
}
}