feat: Refactor code structure and optimize performance across multiple modules
This commit is contained in:
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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];
|
||||
}
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 {}
|
||||
}
|
||||
@@ -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();
|
||||
// }
|
||||
// },
|
||||
// );
|
||||
}
|
||||
}
|
||||
@@ -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 worker’s 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');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user