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,
);
}
}

View File

@@ -0,0 +1,44 @@
import 'dart:async';
import 'package:flutter_background_service/flutter_background_service.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/sevices/background_service/background_task.dart';
import 'package:krow/core/sevices/geofencing_serivce.dart';
import 'package:krow/features/shifts/domain/shifts_repository.dart';
class ContinuousClockoutCheckerTask implements BackgroundTask {
@override
Future<void> oneTime(ServiceInstance? service) async {
// var shift = (await getIt<ShiftsRepository>()
// .getShifts(statusFilter: ShiftStatusFilterType.ongoing)
// .onError((error, stackTrace) {
// return [];
// }))
// .firstOrNull;
//
// if (shift == null) {
// if (service is AndroidServiceInstance) {
// service.setAsBackgroundService();
// }
// return;
// } else {
// GeofencingService geofencingService = getIt<GeofencingService>();
// try {
// var permission = await geofencingService.requestGeolocationPermission();
// if (permission == GeolocationStatus.enabled) {
// var inArea = await geofencingService.isInRangeCheck(
// pointLatitude: shift.locationLat,
// pointLongitude: shift.locationLon,
// range: 500,
// );
// if (!inArea) {
// await getIt<ShiftsRepository>().forceClockOut(shift.id);
// }
// }
// } catch (e) {}
// }
}
@override
Future<void> stop() async {}
}

View File

@@ -0,0 +1,37 @@
import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:injectable/injectable.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/sevices/geofencing_serivce.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/domain/shifts_repository.dart';
StreamSubscription? geofencingClockOutStream;
@singleton
class ForceClockoutService {
final GeofencingService geofencingService;
ForceClockoutService(this.geofencingService);
startTrackOngoingLocation(ShiftEntity shift, VoidCallback? onClockOut) {
// if (geofencingClockOutStream != null) {
// geofencingClockOutStream?.cancel();
// }
//
// geofencingClockOutStream = geofencingService
// .isInRangeStream(
// pointLatitude: shift.locationLat, pointLongitude: shift.locationLon)
// .listen(
// (isInRange) async {
// if (!isInRange) {
// await getIt<ShiftsRepository>().forceClockOut(shift.id);
// onClockOut?.call();
// geofencingClockOutStream?.cancel();
// }
// },
// );
}
}

View File

@@ -0,0 +1,52 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:krow/core/application/di/injectable.dart';
import 'package:krow/core/presentation/gen/assets.gen.dart';
import 'package:krow/core/presentation/styles/kw_text_styles.dart';
import 'package:krow/core/presentation/widgets/ui_kit/dialogs/kw_dialog.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/domain/shifts_repository.dart';
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/shift_complete_dialog.dart';
class ShiftCompleterService {
Future<void> startCompleteProcess(BuildContext context, ShiftEntity shift,
{required Null Function() onComplete, bool canSkip = true}) async {
var result = await ShiftCompleteDialog.showCustomDialog(
context,
canSkip,
shift.eventName,
shift.clockIn ?? DateTime.now(),
shift.planingBreakTime ?? 30);
if (result != null) {
if(!kDebugMode)
await getIt<ShiftsRepository>()
.completeShift(shift, result['details'], !canSkip);
if (result['result'] == true) {
await KwDialog.show(
context: context,
icon: Assets.images.icons.medalStar,
state: KwDialogState.positive,
title: 'Congratulations, Shift Completed!',
message:
'Your break has been logged and added to your timeline. Keep up the good work!',
primaryButtonLabel: 'Back to Shift');
} else {
await KwDialog.show(
context: context,
icon: Assets.images.icons.alertTriangle,
state: KwDialogState.negative,
title: 'Your Selection is under review',
message:
'Labor Code § 512 requires California employers to give unpaid lunch breaks to non-exempt employees. Lunch breaks must be uninterrupted. Employers cannot require employees to do any work while on their lunch breaks. They also cannot discourage employees from taking one. However, the employer and employee can agree to waive the meal break if the workers shift is less than 6 hours.',
child: const Text(
'Once resolved you will be notify.\nNo further Action',
textAlign: TextAlign.center,
style: AppTextStyles.bodyMediumMed,
),
primaryButtonLabel: 'Continue');
}
}
}
}

View File

@@ -0,0 +1,213 @@
import 'package:flutter/foundation.dart';
import 'package:krow/features/shifts/data/models/cancellation_reason.dart';
import 'package:krow/features/shifts/data/models/event.dart';
import 'package:krow/features/shifts/data/models/event_tag.dart';
import 'package:krow/features/shifts/data/models/staff_shift.dart';
@immutable
class ShiftEntity {
final String id;
final String imageUrl;
final String skillName;
final String businessName;
final DateTime assignedDate;
final DateTime startDate;
final DateTime endDate;
final DateTime? clockIn;
final DateTime? clockOut;
final String locationName;
final double locationLat;
final double locationLon;
final EventShiftRoleStaffStatus status;
final String? rate;
final List<EventTag>? tags;
final StaffRating? rating;
final int? planingBreakTime;
final int? totalBreakTime;
final int? paymentStatus;
final int? canceledReason;
final List<Addon>? additionalData;
final List<ShiftManager> managers;
final String? additionalInfo;
final String? cursor;
final CancellationReason? cancellationReason;
final String eventId;
final String eventName;
const ShiftEntity({
required this.id,
required this.skillName,
required this.businessName,
required this.locationName,
required this.locationLat,
required this.locationLon,
required this.status,
required this.rate,
required this.imageUrl,
required this.assignedDate,
required this.startDate,
required this.endDate,
required this.clockIn,
required this.clockOut,
required this.tags,
required this.planingBreakTime,
required this.totalBreakTime,
required this.paymentStatus,
required this.canceledReason,
required this.additionalData,
required this.managers,
required this.rating,
required this.additionalInfo,
required this.eventId,
required this.eventName,
this.cancellationReason,
this.cursor,
});
ShiftEntity copyWith({
String? id,
String? imageUrl,
String? skillName,
String? businessName,
DateTime? assignedDate,
DateTime? startDate,
DateTime? endDate,
DateTime? clockIn,
DateTime? clockOut,
String? locationName,
double? locationLat,
double? locationLon,
EventShiftRoleStaffStatus? status,
String? rate,
List<EventTag>? tags,
StaffRating? rating,
int? planingBreakTime,
int? totalBreakTime,
int? paymentStatus,
int? canceledReason,
List<Addon>? additionalData,
List<ShiftManager>? managers,
String? additionalInfo,
String? eventId,
String? eventName,
String? cursor,
}) =>
ShiftEntity(
id: id ?? this.id,
imageUrl: imageUrl ?? this.imageUrl,
skillName: skillName ?? this.skillName,
businessName: businessName ?? this.businessName,
locationName: locationName ?? this.locationName,
locationLat: locationLat ?? this.locationLat,
locationLon: locationLon ?? this.locationLon,
status: status ?? this.status,
rate: rate ?? this.rate,
assignedDate: assignedDate ?? this.assignedDate,
startDate: startDate ?? this.startDate,
endDate: endDate ?? this.endDate,
clockIn: clockIn ?? this.clockIn,
clockOut: clockOut ?? this.clockOut,
tags: tags ?? this.tags,
planingBreakTime: planingBreakTime ?? this.planingBreakTime,
totalBreakTime: totalBreakTime ?? this.totalBreakTime,
paymentStatus: paymentStatus ?? this.paymentStatus,
canceledReason: canceledReason ?? this.canceledReason,
additionalData: additionalData ?? this.additionalData,
managers: managers ?? this.managers,
rating: rating ?? this.rating,
additionalInfo: additionalInfo ?? this.additionalInfo,
cancellationReason: cancellationReason,
eventId: eventId ?? this.eventId,
eventName: eventName ?? this.eventName,
cursor: cursor ?? this.cursor,
);
static ShiftEntity empty = ShiftEntity(
id: '',
skillName: '',
businessName: '',
locationName: '',
locationLat: .0,
locationLon: .0,
status: EventShiftRoleStaffStatus.assigned,
rate: '',
imageUrl: '',
assignedDate: DateTime.now(),
clockIn: null,
clockOut: null,
startDate: DateTime.now(),
endDate: DateTime.now(),
tags: [],
planingBreakTime: 0,
totalBreakTime: 0,
paymentStatus: 0,
canceledReason: 0,
rating: StaffRating(id: '0', rating: 0),
additionalData: [],
managers: [],
eventId: '',
eventName: '',
additionalInfo: null,
);
static ShiftEntity fromStaffShift(
StaffShift shift, {
required String? cursor,
}) {
return ShiftEntity(
id: shift.id,
eventId: shift.position?.shift?.event?.id ?? '',
eventName: shift.position?.shift?.event?.name ?? '',
cursor: cursor,
skillName: shift.position?.businessSkill?.skill?.name ?? '',
businessName: shift.position?.shift?.event?.business?.name ?? '',
locationName: shift.position?.shift?.fullAddress?.formattedAddress ?? '',
locationLat: shift.position?.shift?.fullAddress?.latitude ?? 0,
locationLon: shift.position?.shift?.fullAddress?.longitude ?? 0,
status: shift.status,
rate: shift.position?.rate?.toStringAsFixed(2) ?? '0',
imageUrl: shift.position?.shift?.event?.business?.avatar ?? '',
additionalInfo: shift.position?.shift?.event?.additionalInfo,
assignedDate: shift.statusUpdatedAt ?? DateTime.now(),
clockIn: shift.clockIn,
clockOut: shift.clockOut,
startDate: shift.startAt ?? DateTime.now(),
endDate: shift.endAt ?? DateTime.now(),
tags: shift.position?.shift?.event?.tags,
planingBreakTime: shift.position?.breakMinutes ?? 0,
totalBreakTime: shift.breakOut
?.difference(shift.breakIn ?? DateTime.now())
.inMinutes ??
0,
paymentStatus: 0,
canceledReason: 0,
rating: shift.rating,
cancellationReason: shift.cancelReason?.firstOrNull?.reason,
additionalData: shift.position?.shift?.event?.addons ?? [],
managers: [
for (final contact in shift.position?.shift?.contacts ?? [])
ShiftManager(
id: contact.id,
name: '${contact.firstName} ${contact.lastName}',
imageUrl: contact.avatar ?? '',
phoneNumber: contact.authInfo.phone,
)
],
);
}
}
@immutable
class ShiftManager {
final String id;
final String name;
final String imageUrl;
final String phoneNumber;
const ShiftManager({
required this.id,
required this.name,
required this.imageUrl,
required this.phoneNumber,
});
}

View File

@@ -0,0 +1,33 @@
import 'package:krow/features/shifts/data/models/staff_shift.dart';
import 'package:krow/features/shifts/domain/shift_entity.dart';
import 'package:krow/features/shifts/presentation/dialogs/complete_dialog/shift_complete_dialog.dart';
enum ShiftStatusFilterType { assigned, confirmed, ongoing, completed, canceled }
abstract class ShiftsRepository {
Stream<EventShiftRoleStaffStatus> get statusStream;
Future<List<ShiftEntity>> getShifts(
{String? lastItemId, required ShiftStatusFilterType statusFilter});
Future<void> confirmShift(ShiftEntity shiftViewModel);
Future<void> clockInShift(ShiftEntity shiftViewModel);
Future<void> completeShift(
ShiftEntity shiftViewModel, ClockOutDetails clockOutDetails,bool isPast);
Future<void> forceClockOut(String id);
void dispose();
declineShift(
ShiftEntity shiftViewModel, String? reason, String? additionalReason);
cancelShift(
ShiftEntity shiftViewModel, String? reason, String? additionalReason);
Future<ShiftEntity?> getShiftById(String id);
Future<List<ShiftEntity>> getMissBreakFinishedShift();
}