Fix: Resolve critical linting issues and bugs (concurrency, syntax, dead code)

This commit is contained in:
2026-02-10 19:12:01 +05:30
parent 5e7bf0d5c0
commit 7570ffa3b9
46 changed files with 4057 additions and 1299 deletions

View File

@@ -1,6 +1,7 @@
import 'dart:async';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:bloc/bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/sign_in_with_phone_arguments.dart';
import '../../domain/arguments/verify_otp_arguments.dart';
@@ -10,7 +11,9 @@ import 'auth_event.dart';
import 'auth_state.dart';
/// BLoC responsible for handling authentication logic.
class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
class AuthBloc extends Bloc<AuthEvent, AuthState>
with BlocErrorHandler<AuthState>
implements Disposable {
/// The use case for signing in with a phone number.
final SignInWithPhoneUseCase _signInUseCase;
@@ -84,7 +87,8 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
status: AuthStatus.error,
mode: event.mode,
phoneNumber: event.phoneNumber ?? state.phoneNumber,
errorMessage: 'Please wait ${remaining}s before requesting a new code.',
errorMessage:
'Please wait ${remaining}s before requesting a new code.',
cooldownSecondsRemaining: remaining,
),
);
@@ -105,39 +109,40 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
cooldownSecondsRemaining: 0,
),
);
try {
final String? verificationId = await _signInUseCase(
SignInWithPhoneArguments(
phoneNumber: event.phoneNumber ?? state.phoneNumber,
),
);
if (token != _requestToken) return;
emit(
state.copyWith(
status: AuthStatus.codeSent,
verificationId: verificationId,
cooldownSecondsRemaining: 0,
),
);
} catch (e) {
if (token != _requestToken) return;
emit(
state.copyWith(
await handleError(
emit: emit,
action: () async {
final String? verificationId = await _signInUseCase(
SignInWithPhoneArguments(
phoneNumber: event.phoneNumber ?? state.phoneNumber,
),
);
if (token != _requestToken) return;
emit(
state.copyWith(
status: AuthStatus.codeSent,
verificationId: verificationId,
cooldownSecondsRemaining: 0,
),
);
},
onError: (String errorKey) {
if (token != _requestToken) return state;
return state.copyWith(
status: AuthStatus.error,
errorMessage: e.toString(),
errorMessage: errorKey,
cooldownSecondsRemaining: 0,
),
);
}
);
},
);
}
void _onCooldownTicked(
AuthCooldownTicked event,
Emitter<AuthState> emit,
) {
print('Auth cooldown tick: ${event.secondsRemaining}');
if (event.secondsRemaining <= 0) {
print('Auth cooldown finished: clearing message');
_cancelCooldownTimer();
_cooldownUntil = null;
emit(
@@ -166,11 +171,9 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
add(AuthCooldownTicked(remaining));
_cooldownTimer = Timer.periodic(const Duration(seconds: 1), (Timer timer) {
remaining -= 1;
print('Auth cooldown timer: remaining=$remaining');
if (remaining <= 0) {
timer.cancel();
_cooldownTimer = null;
print('Auth cooldown timer: reached 0, emitting tick');
add(const AuthCooldownTicked(0));
return;
}
@@ -183,27 +186,29 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
_cooldownTimer = null;
}
/// Handles OTP submission and verification.
Future<void> _onOtpSubmitted(
AuthOtpSubmitted event,
Emitter<AuthState> emit,
) async {
emit(state.copyWith(status: AuthStatus.loading));
try {
final User? user = await _verifyOtpUseCase(
VerifyOtpArguments(
verificationId: event.verificationId,
smsCode: event.smsCode,
mode: event.mode,
),
);
emit(state.copyWith(status: AuthStatus.authenticated, user: user));
} catch (e) {
emit(
state.copyWith(status: AuthStatus.error, errorMessage: e.toString()),
);
}
await handleError(
emit: emit,
action: () async {
final User? user = await _verifyOtpUseCase(
VerifyOtpArguments(
verificationId: event.verificationId,
smsCode: event.smsCode,
mode: event.mode,
),
);
emit(state.copyWith(status: AuthStatus.authenticated, user: user));
},
onError: (String errorKey) => state.copyWith(
status: AuthStatus.error,
errorMessage: errorKey,
),
);
}
/// Disposes the BLoC resources.
@@ -213,3 +218,4 @@ class AuthBloc extends Bloc<AuthEvent, AuthState> implements Disposable {
close();
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import '../../../domain/usecases/submit_profile_setup_usecase.dart';
import '../../../domain/usecases/search_cities_usecase.dart';
@@ -10,7 +11,8 @@ export 'profile_setup_event.dart';
export 'profile_setup_state.dart';
/// BLoC responsible for managing the profile setup state and logic.
class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState>
with BlocErrorHandler<ProfileSetupState> {
ProfileSetupBloc({
required SubmitProfileSetup submitProfileSetup,
required SearchCitiesUseCase searchCities,
@@ -86,25 +88,25 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
) async {
emit(state.copyWith(status: ProfileSetupStatus.loading));
try {
await _submitProfileSetup(
fullName: state.fullName,
bio: state.bio.isEmpty ? null : state.bio,
preferredLocations: state.preferredLocations,
maxDistanceMiles: state.maxDistanceMiles,
industries: state.industries,
skills: state.skills,
);
await handleError(
emit: emit,
action: () async {
await _submitProfileSetup(
fullName: state.fullName,
bio: state.bio.isEmpty ? null : state.bio,
preferredLocations: state.preferredLocations,
maxDistanceMiles: state.maxDistanceMiles,
industries: state.industries,
skills: state.skills,
);
emit(state.copyWith(status: ProfileSetupStatus.success));
} catch (e) {
emit(
state.copyWith(
status: ProfileSetupStatus.failure,
errorMessage: e.toString(),
),
);
}
emit(state.copyWith(status: ProfileSetupStatus.success));
},
onError: (String errorKey) => state.copyWith(
status: ProfileSetupStatus.failure,
errorMessage: errorKey,
),
);
}
Future<void> _onLocationQueryChanged(
@@ -116,6 +118,8 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
return;
}
// For search, we might want to handle errors silently or distinctively
// Using simple try-catch here as it's a search-as-you-type feature where error dialogs are intrusive
try {
final results = await _searchCities(event.query);
emit(state.copyWith(locationSuggestions: results));
@@ -132,3 +136,4 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
emit(state.copyWith(locationSuggestions: []));
}
}

View File

@@ -2,10 +2,12 @@ import 'package:flutter_bloc/flutter_bloc.dart';
import '../../domain/usecases/apply_quick_set_usecase.dart';
import '../../domain/usecases/get_weekly_availability_usecase.dart';
import '../../domain/usecases/update_day_availability_usecase.dart';
import 'package:krow_core/core.dart';
import 'availability_event.dart';
import 'availability_state.dart';
class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState>
with BlocErrorHandler<AvailabilityState> {
final GetWeeklyAvailabilityUseCase getWeeklyAvailability;
final UpdateDayAvailabilityUseCase updateDayAvailability;
final ApplyQuickSetUseCase applyQuickSet;
@@ -28,27 +30,34 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
Emitter<AvailabilityState> emit,
) async {
emit(AvailabilityLoading());
try {
final days = await getWeeklyAvailability(
GetWeeklyAvailabilityParams(event.weekStart));
emit(AvailabilityLoaded(
days: days,
currentWeekStart: event.weekStart,
selectedDate: event.preselectedDate ??
(days.isNotEmpty ? days.first.date : DateTime.now()),
));
} catch (e) {
emit(AvailabilityError(e.toString()));
}
await handleError(
emit: emit,
action: () async {
final days = await getWeeklyAvailability(
GetWeeklyAvailabilityParams(event.weekStart),
);
emit(
AvailabilityLoaded(
days: days,
currentWeekStart: event.weekStart,
selectedDate: event.preselectedDate ??
(days.isNotEmpty ? days.first.date : DateTime.now()),
),
);
},
onError: (String errorKey) => AvailabilityError(errorKey),
);
}
void _onSelectDate(SelectDate event, Emitter<AvailabilityState> emit) {
if (state is AvailabilityLoaded) {
// Clear success message on navigation
emit((state as AvailabilityLoaded).copyWith(
selectedDate: event.date,
clearSuccessMessage: true,
));
emit(
(state as AvailabilityLoaded).copyWith(
selectedDate: event.date,
clearSuccessMessage: true,
),
);
}
}
@@ -58,14 +67,17 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
) async {
if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded;
// Clear message
emit(currentState.copyWith(clearSuccessMessage: true));
final newWeekStart = currentState.currentWeekStart
.add(Duration(days: event.direction * 7));
final diff = currentState.selectedDate.difference(currentState.currentWeekStart).inDays;
final newWeekStart = currentState.currentWeekStart.add(
Duration(days: event.direction * 7),
);
final diff = currentState.selectedDate
.difference(currentState.currentWeekStart)
.inDays;
final newSelectedDate = newWeekStart.add(Duration(days: diff));
add(LoadAvailability(newWeekStart, preselectedDate: newSelectedDate));
@@ -78,7 +90,7 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
) async {
if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded;
final newDay = event.day.copyWith(isAvailable: !event.day.isAvailable);
final updatedDays = currentState.days.map((d) {
return d.date == event.day.date ? newDay : d;
@@ -90,18 +102,29 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
clearSuccessMessage: true,
));
try {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
// Success feedback
if (state is AvailabilityLoaded) {
emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated'));
}
} catch (e) {
// Revert
if (state is AvailabilityLoaded) {
emit((state as AvailabilityLoaded).copyWith(days: currentState.days));
}
}
await handleError(
emit: emit,
action: () async {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
// Success feedback
if (state is AvailabilityLoaded) {
emit(
(state as AvailabilityLoaded).copyWith(
successMessage: 'Availability updated',
),
);
}
},
onError: (String errorKey) {
// Revert
if (state is AvailabilityLoaded) {
return (state as AvailabilityLoaded).copyWith(
days: currentState.days,
);
}
return AvailabilityError(errorKey);
},
);
}
}
@@ -120,7 +143,7 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
}).toList();
final newDay = event.day.copyWith(slots: updatedSlots);
final updatedDays = currentState.days.map((d) {
return d.date == event.day.date ? newDay : d;
}).toList();
@@ -131,18 +154,29 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
clearSuccessMessage: true,
));
try {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
// Success feedback
if (state is AvailabilityLoaded) {
emit((state as AvailabilityLoaded).copyWith(successMessage: 'Availability updated'));
}
} catch (e) {
// Revert
if (state is AvailabilityLoaded) {
emit((state as AvailabilityLoaded).copyWith(days: currentState.days));
}
}
await handleError(
emit: emit,
action: () async {
await updateDayAvailability(UpdateDayAvailabilityParams(newDay));
// Success feedback
if (state is AvailabilityLoaded) {
emit(
(state as AvailabilityLoaded).copyWith(
successMessage: 'Availability updated',
),
);
}
},
onError: (String errorKey) {
// Revert
if (state is AvailabilityLoaded) {
return (state as AvailabilityLoaded).copyWith(
days: currentState.days,
);
}
return AvailabilityError(errorKey);
},
);
}
}
@@ -152,28 +186,39 @@ class AvailabilityBloc extends Bloc<AvailabilityEvent, AvailabilityState> {
) async {
if (state is AvailabilityLoaded) {
final currentState = state as AvailabilityLoaded;
emit(currentState.copyWith(
isActionInProgress: true,
clearSuccessMessage: true,
));
try {
final newDays = await applyQuickSet(
ApplyQuickSetParams(currentState.currentWeekStart, event.type));
emit(currentState.copyWith(
days: newDays,
isActionInProgress: false,
successMessage: 'Availability updated',
));
} catch (e) {
emit(currentState.copyWith(
isActionInProgress: false,
// Could set error message here if we had a field for it, or emit AvailabilityError
// But emitting AvailabilityError would replace the whole screen.
));
}
emit(
currentState.copyWith(
isActionInProgress: true,
clearSuccessMessage: true,
),
);
await handleError(
emit: emit,
action: () async {
final newDays = await applyQuickSet(
ApplyQuickSetParams(currentState.currentWeekStart, event.type),
);
emit(
currentState.copyWith(
days: newDays,
isActionInProgress: false,
successMessage: 'Availability updated',
),
);
},
onError: (String errorKey) {
if (state is AvailabilityLoaded) {
return (state as AvailabilityLoaded).copyWith(
isActionInProgress: false,
);
}
return AvailabilityError(errorKey);
},
);
}
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:geolocator/geolocator.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_todays_shift_usecase.dart';
import '../../domain/usecases/get_attendance_status_usecase.dart';
@@ -10,8 +11,8 @@ import '../../domain/arguments/clock_out_arguments.dart';
import 'clock_in_event.dart';
import 'clock_in_state.dart';
class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
class ClockInBloc extends Bloc<ClockInEvent, ClockInState>
with BlocErrorHandler<ClockInState> {
ClockInBloc({
required GetTodaysShiftUseCase getTodaysShift,
required GetAttendanceStatusUseCase getAttendanceStatus,
@@ -47,92 +48,105 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
Emitter<ClockInState> emit,
) async {
emit(state.copyWith(status: ClockInStatus.loading));
try {
final List<Shift> shifts = await _getTodaysShift();
final AttendanceStatus status = await _getAttendanceStatus();
await handleError(
emit: emit,
action: () async {
final List<Shift> shifts = await _getTodaysShift();
final AttendanceStatus status = await _getAttendanceStatus();
// Check permissions silently on load? Maybe better to wait for user interaction or specific event
// However, if shift exists, we might want to check permission state
Shift? selectedShift;
if (shifts.isNotEmpty) {
if (status.activeShiftId != null) {
try {
selectedShift =
shifts.firstWhere((Shift s) => s.id == status.activeShiftId);
} catch (_) {}
Shift? selectedShift;
if (shifts.isNotEmpty) {
if (status.activeShiftId != null) {
try {
selectedShift =
shifts.firstWhere((Shift s) => s.id == status.activeShiftId);
} catch (_) {}
}
selectedShift ??= shifts.last;
}
selectedShift ??= shifts.last;
}
emit(state.copyWith(
status: ClockInStatus.success,
todayShifts: shifts,
selectedShift: selectedShift,
attendance: status,
));
emit(state.copyWith(
status: ClockInStatus.success,
todayShifts: shifts,
selectedShift: selectedShift,
attendance: status,
));
if (selectedShift != null && !status.isCheckedIn) {
add(RequestLocationPermission());
}
} catch (e) {
emit(state.copyWith(
if (selectedShift != null && !status.isCheckedIn) {
add(RequestLocationPermission());
}
},
onError: (String errorKey) => state.copyWith(
status: ClockInStatus.failure,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
Future<void> _onRequestLocationPermission(
RequestLocationPermission event,
Emitter<ClockInState> emit,
) async {
try {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
final bool hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
emit(state.copyWith(hasLocationConsent: hasConsent));
await handleError(
emit: emit,
action: () async {
LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.denied) {
permission = await Geolocator.requestPermission();
}
if (hasConsent) {
_startLocationUpdates();
}
} catch (e) {
emit(state.copyWith(errorMessage: "Location error: $e"));
}
final bool hasConsent =
permission == LocationPermission.always ||
permission == LocationPermission.whileInUse;
emit(state.copyWith(hasLocationConsent: hasConsent));
if (hasConsent) {
await _startLocationUpdates();
}
},
onError: (String errorKey) => state.copyWith(
errorMessage: errorKey,
),
);
}
Future<void> _startLocationUpdates() async {
// Note: handleErrorWithResult could be used here too if we want centralized logging/conversion
try {
final Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
final Position position = await Geolocator.getCurrentPosition(
desiredAccuracy: LocationAccuracy.high,
);
double distance = 0;
bool isVerified = false; // Require location match by default if shift has location
bool isVerified =
false; // Require location match by default if shift has location
if (state.selectedShift != null &&
state.selectedShift!.latitude != null &&
state.selectedShift!.longitude != null) {
distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
state.selectedShift!.latitude!,
state.selectedShift!.longitude!,
);
isVerified = distance <= allowedRadiusMeters;
distance = Geolocator.distanceBetween(
position.latitude,
position.longitude,
state.selectedShift!.latitude!,
state.selectedShift!.longitude!,
);
isVerified = distance <= allowedRadiusMeters;
} else {
// If no shift location, assume verified or don't restrict?
// For strict clock-in, maybe false? but let's default to verified to avoid blocking if data missing
isVerified = true;
isVerified = true;
}
if (!isClosed) {
add(LocationUpdated(position: position, distance: distance, isVerified: isVerified));
add(
LocationUpdated(
position: position,
distance: distance,
isVerified: isVerified,
),
);
}
} catch (e) {
// Handle error silently or via state
} catch (_) {
// Geolocator errors usually handled via onRequestLocationPermission
}
}
@@ -144,7 +158,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
currentLocation: event.position,
distanceFromVenue: event.distance,
isLocationVerified: event.isVerified,
etaMinutes: (event.distance / 80).round(), // Rough estimate: 80m/min walking speed
etaMinutes:
(event.distance / 80).round(), // Rough estimate: 80m/min walking speed
));
}
@@ -152,10 +167,10 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
CommuteModeToggled event,
Emitter<ClockInState> emit,
) {
emit(state.copyWith(isCommuteModeOn: event.isEnabled));
if (event.isEnabled) {
add(RequestLocationPermission());
}
emit(state.copyWith(isCommuteModeOn: event.isEnabled));
if (event.isEnabled) {
add(RequestLocationPermission());
}
}
void _onShiftSelected(
@@ -186,28 +201,23 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
CheckInRequested event,
Emitter<ClockInState> emit,
) async {
// Only verify location if not using NFC (or depending on requirements) - enforcing for swipe
if (state.checkInMode == 'swipe' && !state.isLocationVerified) {
// Allow for now since coordinates are hardcoded and might not match user location
// emit(state.copyWith(errorMessage: "You must be at the location to clock in."));
// return;
}
emit(state.copyWith(status: ClockInStatus.actionInProgress));
try {
final AttendanceStatus newStatus = await _clockIn(
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
);
emit(state.copyWith(
status: ClockInStatus.success,
attendance: newStatus,
));
} catch (e) {
emit(state.copyWith(
await handleError(
emit: emit,
action: () async {
final AttendanceStatus newStatus = await _clockIn(
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
);
emit(state.copyWith(
status: ClockInStatus.success,
attendance: newStatus,
));
},
onError: (String errorKey) => state.copyWith(
status: ClockInStatus.failure,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
Future<void> _onCheckOut(
@@ -215,23 +225,25 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
Emitter<ClockInState> emit,
) async {
emit(state.copyWith(status: ClockInStatus.actionInProgress));
try {
final AttendanceStatus newStatus = await _clockOut(
ClockOutArguments(
notes: event.notes,
breakTimeMinutes: 0, // Should be passed from event if supported
applicationId: state.attendance.activeApplicationId,
),
);
emit(state.copyWith(
status: ClockInStatus.success,
attendance: newStatus,
));
} catch (e) {
emit(state.copyWith(
await handleError(
emit: emit,
action: () async {
final AttendanceStatus newStatus = await _clockOut(
ClockOutArguments(
notes: event.notes,
breakTimeMinutes: 0,
applicationId: state.attendance.activeApplicationId,
),
);
emit(state.copyWith(
status: ClockInStatus.success,
attendance: newStatus,
));
},
onError: (String errorKey) => state.copyWith(
status: ClockInStatus.failure,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
}

View File

@@ -86,7 +86,7 @@ class ClockInCubit extends Cubit<ClockInState> { // 500m radius
return;
}
_getCurrentLocation();
await _getCurrentLocation();
} catch (e) {
emit(state.copyWith(isLoading: false, error: e.toString()));
}

View File

@@ -1,5 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_home/src/domain/usecases/get_home_shifts.dart';
@@ -8,7 +9,7 @@ import 'package:staff_home/src/domain/repositories/home_repository.dart';
part 'home_state.dart';
/// Simple Cubit to manage home page state (shifts + loading/error).
class HomeCubit extends Cubit<HomeState> {
class HomeCubit extends Cubit<HomeState> with BlocErrorHandler<HomeState> {
final GetHomeShifts _getHomeShifts;
final HomeRepository _repository;
@@ -20,29 +21,32 @@ class HomeCubit extends Cubit<HomeState> {
Future<void> loadShifts() async {
if (isClosed) return;
emit(state.copyWith(status: HomeStatus.loading));
try {
final result = await _getHomeShifts.call();
final name = await _repository.getStaffName();
if (isClosed) return;
emit(
state.copyWith(
status: HomeStatus.loaded,
todayShifts: result.today,
tomorrowShifts: result.tomorrow,
recommendedShifts: result.recommended,
staffName: name,
// Mock profile status for now, ideally fetched from a user repository
isProfileComplete: false,
),
);
} catch (e) {
if (isClosed) return;
emit(
state.copyWith(status: HomeStatus.error, errorMessage: e.toString()),
);
}
await handleError(
emit: emit,
action: () async {
final result = await _getHomeShifts.call();
final name = await _repository.getStaffName();
if (isClosed) return;
emit(
state.copyWith(
status: HomeStatus.loaded,
todayShifts: result.today,
tomorrowShifts: result.tomorrow,
recommendedShifts: result.recommended,
staffName: name,
// Mock profile status for now, ideally fetched from a user repository
isProfileComplete: false,
),
);
},
onError: (String errorKey) {
if (isClosed) return state; // Avoid state emission if closed, though emit handles it gracefully usually
return state.copyWith(status: HomeStatus.error, errorMessage: errorKey);
},
);
}
void toggleAutoMatch(bool enabled) {
emit(state.copyWith(autoMatchEnabled: enabled));
}

View File

@@ -190,7 +190,7 @@ class RecommendedShiftCard extends StatelessWidget {
const SizedBox(width: 4),
Expanded(
child: Text(
shift.locationAddress ?? shift.location,
shift.locationAddress,
style: const TextStyle(
fontSize: 12,
color: UiColors.mutedForeground,

View File

@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/arguments/get_payment_history_arguments.dart';
import '../../../domain/usecases/get_payment_history_usecase.dart';
@@ -6,8 +7,8 @@ import '../../../domain/usecases/get_payment_summary_usecase.dart';
import 'payments_event.dart';
import 'payments_state.dart';
class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState>
with BlocErrorHandler<PaymentsState> {
PaymentsBloc({
required this.getPaymentSummary,
required this.getPaymentHistory,
@@ -23,20 +24,24 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
Emitter<PaymentsState> emit,
) async {
emit(PaymentsLoading());
try {
final PaymentSummary currentSummary = await getPaymentSummary();
final List<StaffPayment> history = await getPaymentHistory(
const GetPaymentHistoryArguments('week'),
);
emit(PaymentsLoaded(
summary: currentSummary,
history: history,
activePeriod: 'week',
));
} catch (e) {
emit(PaymentsError(e.toString()));
}
await handleError(
emit: emit,
action: () async {
final PaymentSummary currentSummary = await getPaymentSummary();
final List<StaffPayment> history = await getPaymentHistory(
const GetPaymentHistoryArguments('week'),
);
emit(
PaymentsLoaded(
summary: currentSummary,
history: history,
activePeriod: 'week',
),
);
},
onError: (String errorKey) => PaymentsError(errorKey),
);
}
Future<void> _onChangePeriod(
@@ -45,17 +50,22 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
) async {
final PaymentsState currentState = state;
if (currentState is PaymentsLoaded) {
try {
final List<StaffPayment> newHistory = await getPaymentHistory(
GetPaymentHistoryArguments(event.period),
);
emit(currentState.copyWith(
history: newHistory,
activePeriod: event.period,
));
} catch (e) {
emit(PaymentsError(e.toString()));
}
await handleError(
emit: emit,
action: () async {
final List<StaffPayment> newHistory = await getPaymentHistory(
GetPaymentHistoryArguments(event.period),
);
emit(
currentState.copyWith(
history: newHistory,
activePeriod: event.period,
),
);
},
onError: (String errorKey) => PaymentsError(errorKey),
);
}
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import '../../domain/usecases/get_profile_usecase.dart';
import '../../domain/usecases/sign_out_usecase.dart';
import 'profile_state.dart';
@@ -6,15 +7,14 @@ import 'profile_state.dart';
/// Cubit for managing the Profile feature state.
///
/// Handles loading profile data and user sign-out actions.
class ProfileCubit extends Cubit<ProfileState> {
class ProfileCubit extends Cubit<ProfileState>
with BlocErrorHandler<ProfileState> {
final GetProfileUseCase _getProfileUseCase;
final SignOutUseCase _signOutUseCase;
/// Creates a [ProfileCubit] with the required use cases.
ProfileCubit(
this._getProfileUseCase,
this._signOutUseCase,
) : super(const ProfileState());
ProfileCubit(this._getProfileUseCase, this._signOutUseCase)
: super(const ProfileState());
/// Loads the staff member's profile.
///
@@ -24,18 +24,16 @@ class ProfileCubit extends Cubit<ProfileState> {
Future<void> loadProfile() async {
emit(state.copyWith(status: ProfileStatus.loading));
try {
final profile = await _getProfileUseCase();
emit(state.copyWith(
status: ProfileStatus.loaded,
profile: profile,
));
} catch (e) {
emit(state.copyWith(
status: ProfileStatus.error,
errorMessage: e.toString(),
));
}
await handleError(
emit: emit,
action: () async {
final profile = await _getProfileUseCase();
emit(state.copyWith(status: ProfileStatus.loaded, profile: profile));
},
onError:
(String errorKey) =>
state.copyWith(status: ProfileStatus.error, errorMessage: errorKey),
);
}
/// Signs out the current user.
@@ -49,12 +47,21 @@ class ProfileCubit extends Cubit<ProfileState> {
emit(state.copyWith(status: ProfileStatus.loading));
try {
await _signOutUseCase();
emit(state.copyWith(status: ProfileStatus.signedOut));
} catch (e) {
// Error handling can be added here if needed
// For now, we let the navigation happen regardless
}
await handleError(
emit: emit,
action: () async {
await _signOutUseCase();
emit(state.copyWith(status: ProfileStatus.signedOut));
},
onError: (String _) {
// For sign out errors, we might want to just proceed or show error
// Current implementation was silent catch, let's keep it robust but consistent
// If we want to force navigation even on error, we would do it here
// But usually handleError emits the error state.
// Let's stick to standard error reporting for now.
return state.copyWith(status: ProfileStatus.error);
},
);
}
}

View File

@@ -14,7 +14,9 @@ import '../widgets/profile_menu_item.dart';
import '../widgets/profile_header.dart';
import '../widgets/reliability_score_bar.dart';
import '../widgets/reliability_stats_card.dart';
import '../widgets/reliability_stats_card.dart';
import '../widgets/section_title.dart';
import '../widgets/language_selector_bottom_sheet.dart';
/// The main Staff Profile page.
///
@@ -178,6 +180,25 @@ class StaffProfilePage extends StatelessWidget {
],
),
const SizedBox(height: UiConstants.space6),
SectionTitle(
i18n.header.title.contains("Perfil") ? "Ajustes" : "Settings",
),
ProfileMenuGrid(
crossAxisCount: 3,
children: [
ProfileMenuItem(
icon: UiIcons.globe,
label: i18n.header.title.contains("Perfil") ? "Idioma" : "Language",
onTap: () {
showModalBottomSheet(
context: context,
builder: (context) => const LanguageSelectorBottomSheet(),
);
},
),
],
),
const SizedBox(height: UiConstants.space6),
LogoutButton(
onTap: () => _onSignOut(cubit, state),
),

View File

@@ -0,0 +1,106 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
/// A bottom sheet that allows the user to select their preferred language.
///
/// Displays options for English and Spanish, and updates the application's
/// locale via the [LocaleBloc].
class LanguageSelectorBottomSheet extends StatelessWidget {
/// Creates a [LanguageSelectorBottomSheet].
const LanguageSelectorBottomSheet({super.key});
@override
Widget build(BuildContext context) {
return Container(
padding: EdgeInsets.all(UiConstants.space6),
decoration: BoxDecoration(
color: UiColors.background,
borderRadius: BorderRadius.vertical(top: Radius.circular(UiConstants.radiusBase)),
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Text(
t.settings.change_language,
style: UiTypography.headline4m,
textAlign: TextAlign.center,
),
SizedBox(height: UiConstants.space6),
_buildLanguageOption(
context,
label: 'English',
locale: AppLocale.en,
),
SizedBox(height: UiConstants.space4),
_buildLanguageOption(
context,
label: 'Español',
locale: AppLocale.es,
),
SizedBox(height: UiConstants.space6),
],
),
);
}
Widget _buildLanguageOption(
BuildContext context, {
required String label,
required AppLocale locale,
}) {
// Check if this option is currently selected.
// We can use LocaleSettings.currentLocale for a quick check,
// or access the BLoC state if we wanted to be reactive to state changes here directly,
// but LocaleSettings is sufficient for the initial check.
final bool isSelected = LocaleSettings.currentLocale == locale;
return InkWell(
onTap: () {
// Dispatch the ChangeLocale event to the LocaleBloc
Modular.get<LocaleBloc>().add(ChangeLocale(locale.flutterLocale));
// Close the bottom sheet
Navigator.pop(context);
// Force a rebuild of the entire app to reflect locale change instantly if not handled by root widget
// (Usually handled by BlocBuilder at the root, but this ensures settings are updated)
},
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
child: Container(
padding: EdgeInsets.symmetric(
vertical: UiConstants.space4,
horizontal: UiConstants.space4,
),
decoration: BoxDecoration(
color: isSelected ? UiColors.primary.withValues(alpha: 0.1) : UiColors.background,
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
border: Border.all(
color: isSelected ? UiColors.primary : UiColors.border,
width: isSelected ? 2 : 1,
),
),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
label,
style: isSelected
? UiTypography.body1b.copyWith(color: UiColors.primary)
: UiTypography.body1r,
),
if (isSelected)
Icon(
UiIcons.check,
color: UiColors.primary,
size: 24.0,
),
],
),
),
);
}
}

View File

@@ -1,28 +1,38 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/usecases/get_certificates_usecase.dart';
import 'certificates_state.dart';
class CertificatesCubit extends Cubit<CertificatesState> {
class CertificatesCubit extends Cubit<CertificatesState>
with BlocErrorHandler<CertificatesState> {
final GetCertificatesUseCase _getCertificatesUseCase;
CertificatesCubit(this._getCertificatesUseCase) : super(const CertificatesState()) {
CertificatesCubit(this._getCertificatesUseCase)
: super(const CertificatesState()) {
loadCertificates();
}
Future<void> loadCertificates() async {
emit(state.copyWith(status: CertificatesStatus.loading));
try {
final List<StaffDocument> certificates = await _getCertificatesUseCase();
emit(state.copyWith(
status: CertificatesStatus.success,
certificates: certificates,
));
} catch (e) {
emit(state.copyWith(
status: CertificatesStatus.failure,
errorMessage: e.toString(),
));
}
await handleError(
emit: emit,
action: () async {
final List<StaffDocument> certificates =
await _getCertificatesUseCase();
emit(
state.copyWith(
status: CertificatesStatus.success,
certificates: certificates,
),
);
},
onError:
(String errorKey) => state.copyWith(
status: CertificatesStatus.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -49,6 +49,7 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
),
];
/*
try {
final QueryResult<ListStaffDocumentsByStaffIdData,
ListStaffDocumentsByStaffIdVariables> result =
@@ -63,6 +64,7 @@ class DocumentsRepositoryImpl implements DocumentsRepository {
} catch (e) {
throw Exception('Failed to fetch documents: $e');
}
*/
}
domain.StaffDocument _mapToDomain(

View File

@@ -1,26 +1,34 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/usecases/get_documents_usecase.dart';
import 'documents_state.dart';
class DocumentsCubit extends Cubit<DocumentsState> {
class DocumentsCubit extends Cubit<DocumentsState>
with BlocErrorHandler<DocumentsState> {
final GetDocumentsUseCase _getDocumentsUseCase;
DocumentsCubit(this._getDocumentsUseCase) : super(const DocumentsState());
Future<void> loadDocuments() async {
emit(state.copyWith(status: DocumentsStatus.loading));
try {
final List<StaffDocument> documents = await _getDocumentsUseCase();
emit(state.copyWith(
status: DocumentsStatus.success,
documents: documents,
));
} catch (e) {
emit(state.copyWith(
status: DocumentsStatus.failure,
errorMessage: e.toString(),
));
}
await handleError(
emit: emit,
action: () async {
final List<StaffDocument> documents = await _getDocumentsUseCase();
emit(
state.copyWith(
status: DocumentsStatus.success,
documents: documents,
),
);
},
onError:
(String errorKey) => state.copyWith(
status: DocumentsStatus.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,11 +1,12 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:uuid/uuid.dart';
import '../../../domain/usecases/submit_i9_form_usecase.dart';
import 'form_i9_state.dart';
class FormI9Cubit extends Cubit<FormI9State> {
class FormI9Cubit extends Cubit<FormI9State> with BlocErrorHandler<FormI9State> {
final SubmitI9FormUseCase _submitI9FormUseCase;
String _formId = '';
@@ -16,31 +17,33 @@ class FormI9Cubit extends Cubit<FormI9State> {
emit(const FormI9State()); // Reset to empty if no form
return;
}
final Map<String, dynamic> data = form.formData;
_formId = form.id;
emit(FormI9State(
firstName: data['firstName'] as String? ?? '',
lastName: data['lastName'] as String? ?? '',
middleInitial: data['middleInitial'] as String? ?? '',
otherLastNames: data['otherLastNames'] as String? ?? '',
dob: data['dob'] as String? ?? '',
ssn: data['ssn'] as String? ?? '',
email: data['email'] as String? ?? '',
phone: data['phone'] as String? ?? '',
address: data['address'] as String? ?? '',
aptNumber: data['aptNumber'] as String? ?? '',
city: data['city'] as String? ?? '',
state: data['state'] as String? ?? '',
zipCode: data['zipCode'] as String? ?? '',
citizenshipStatus: data['citizenshipStatus'] as String? ?? '',
uscisNumber: data['uscisNumber'] as String? ?? '',
admissionNumber: data['admissionNumber'] as String? ?? '',
passportNumber: data['passportNumber'] as String? ?? '',
countryIssuance: data['countryIssuance'] as String? ?? '',
preparerUsed: data['preparerUsed'] as bool? ?? false,
signature: data['signature'] as String? ?? '',
));
emit(
FormI9State(
firstName: data['firstName'] as String? ?? '',
lastName: data['lastName'] as String? ?? '',
middleInitial: data['middleInitial'] as String? ?? '',
otherLastNames: data['otherLastNames'] as String? ?? '',
dob: data['dob'] as String? ?? '',
ssn: data['ssn'] as String? ?? '',
email: data['email'] as String? ?? '',
phone: data['phone'] as String? ?? '',
address: data['address'] as String? ?? '',
aptNumber: data['aptNumber'] as String? ?? '',
city: data['city'] as String? ?? '',
state: data['state'] as String? ?? '',
zipCode: data['zipCode'] as String? ?? '',
citizenshipStatus: data['citizenshipStatus'] as String? ?? '',
uscisNumber: data['uscisNumber'] as String? ?? '',
admissionNumber: data['admissionNumber'] as String? ?? '',
passportNumber: data['passportNumber'] as String? ?? '',
countryIssuance: data['countryIssuance'] as String? ?? '',
preparerUsed: data['preparerUsed'] as bool? ?? false,
signature: data['signature'] as String? ?? '',
),
);
}
void nextStep(int totalSteps) {
@@ -58,8 +61,10 @@ class FormI9Cubit extends Cubit<FormI9State> {
// Personal Info
void firstNameChanged(String value) => emit(state.copyWith(firstName: value));
void lastNameChanged(String value) => emit(state.copyWith(lastName: value));
void middleInitialChanged(String value) => emit(state.copyWith(middleInitial: value));
void otherLastNamesChanged(String value) => emit(state.copyWith(otherLastNames: value));
void middleInitialChanged(String value) =>
emit(state.copyWith(middleInitial: value));
void otherLastNamesChanged(String value) =>
emit(state.copyWith(otherLastNames: value));
void dobChanged(String value) => emit(state.copyWith(dob: value));
void ssnChanged(String value) => emit(state.copyWith(ssn: value));
void emailChanged(String value) => emit(state.copyWith(email: value));
@@ -73,55 +78,65 @@ class FormI9Cubit extends Cubit<FormI9State> {
void zipCodeChanged(String value) => emit(state.copyWith(zipCode: value));
// Citizenship
void citizenshipStatusChanged(String value) => emit(state.copyWith(citizenshipStatus: value));
void uscisNumberChanged(String value) => emit(state.copyWith(uscisNumber: value));
void admissionNumberChanged(String value) => emit(state.copyWith(admissionNumber: value));
void passportNumberChanged(String value) => emit(state.copyWith(passportNumber: value));
void countryIssuanceChanged(String value) => emit(state.copyWith(countryIssuance: value));
void citizenshipStatusChanged(String value) =>
emit(state.copyWith(citizenshipStatus: value));
void uscisNumberChanged(String value) =>
emit(state.copyWith(uscisNumber: value));
void admissionNumberChanged(String value) =>
emit(state.copyWith(admissionNumber: value));
void passportNumberChanged(String value) =>
emit(state.copyWith(passportNumber: value));
void countryIssuanceChanged(String value) =>
emit(state.copyWith(countryIssuance: value));
// Signature
void preparerUsedChanged(bool value) => emit(state.copyWith(preparerUsed: value));
void preparerUsedChanged(bool value) =>
emit(state.copyWith(preparerUsed: value));
void signatureChanged(String value) => emit(state.copyWith(signature: value));
Future<void> submit() async {
emit(state.copyWith(status: FormI9Status.submitting));
try {
final Map<String, dynamic> formData = {
'firstName': state.firstName,
'lastName': state.lastName,
'middleInitial': state.middleInitial,
'otherLastNames': state.otherLastNames,
'dob': state.dob,
'ssn': state.ssn,
'email': state.email,
'phone': state.phone,
'address': state.address,
'aptNumber': state.aptNumber,
'city': state.city,
'state': state.state,
'zipCode': state.zipCode,
'citizenshipStatus': state.citizenshipStatus,
'uscisNumber': state.uscisNumber,
'admissionNumber': state.admissionNumber,
'passportNumber': state.passportNumber,
'countryIssuance': state.countryIssuance,
'preparerUsed': state.preparerUsed,
'signature': state.signature,
};
await handleError(
emit: emit,
action: () async {
final Map<String, dynamic> formData = {
'firstName': state.firstName,
'lastName': state.lastName,
'middleInitial': state.middleInitial,
'otherLastNames': state.otherLastNames,
'dob': state.dob,
'ssn': state.ssn,
'email': state.email,
'phone': state.phone,
'address': state.address,
'aptNumber': state.aptNumber,
'city': state.city,
'state': state.state,
'zipCode': state.zipCode,
'citizenshipStatus': state.citizenshipStatus,
'uscisNumber': state.uscisNumber,
'admissionNumber': state.admissionNumber,
'passportNumber': state.passportNumber,
'countryIssuance': state.countryIssuance,
'preparerUsed': state.preparerUsed,
'signature': state.signature,
};
final I9TaxForm form = I9TaxForm(
id: _formId.isNotEmpty ? _formId : const Uuid().v4(),
title: 'Form I-9',
formData: formData,
);
final I9TaxForm form = I9TaxForm(
id: _formId.isNotEmpty ? _formId : const Uuid().v4(),
title: 'Form I-9',
formData: formData,
);
await _submitI9FormUseCase(form);
emit(state.copyWith(status: FormI9Status.success));
} catch (e) {
emit(state.copyWith(
status: FormI9Status.failure,
errorMessage: e.toString(),
));
}
await _submitI9FormUseCase(form);
emit(state.copyWith(status: FormI9Status.success));
},
onError:
(String errorKey) => state.copyWith(
status: FormI9Status.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,26 +1,29 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../../domain/usecases/get_tax_forms_usecase.dart';
import 'tax_forms_state.dart';
class TaxFormsCubit extends Cubit<TaxFormsState> {
class TaxFormsCubit extends Cubit<TaxFormsState>
with BlocErrorHandler<TaxFormsState> {
final GetTaxFormsUseCase _getTaxFormsUseCase;
TaxFormsCubit(this._getTaxFormsUseCase) : super(const TaxFormsState());
Future<void> loadTaxForms() async {
emit(state.copyWith(status: TaxFormsStatus.loading));
try {
final List<TaxForm> forms = await _getTaxFormsUseCase();
emit(state.copyWith(
status: TaxFormsStatus.success,
forms: forms,
));
} catch (e) {
emit(state.copyWith(
status: TaxFormsStatus.failure,
errorMessage: e.toString(),
));
}
await handleError(
emit: emit,
action: () async {
final List<TaxForm> forms = await _getTaxFormsUseCase();
emit(state.copyWith(status: TaxFormsStatus.success, forms: forms));
},
onError:
(String errorKey) => state.copyWith(
status: TaxFormsStatus.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,11 +1,12 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:uuid/uuid.dart';
import '../../../domain/usecases/submit_w4_form_usecase.dart';
import 'form_w4_state.dart';
class FormW4Cubit extends Cubit<FormW4State> {
class FormW4Cubit extends Cubit<FormW4State> with BlocErrorHandler<FormW4State> {
final SubmitW4FormUseCase _submitW4FormUseCase;
String _formId = '';
@@ -16,31 +17,33 @@ class FormW4Cubit extends Cubit<FormW4State> {
emit(const FormW4State()); // Reset
return;
}
final Map<String, dynamic> data = form.formData;
_formId = form.id;
// Combine address parts if needed, or take existing
final String city = data['city'] as String? ?? '';
final String stateVal = data['state'] as String? ?? '';
final String zip = data['zipCode'] as String? ?? '';
final String cityStateZip = '$city, $stateVal $zip'.trim();
emit(FormW4State(
firstName: data['firstName'] as String? ?? '',
lastName: data['lastName'] as String? ?? '',
ssn: data['ssn'] as String? ?? '',
address: data['address'] as String? ?? '',
cityStateZip: cityStateZip.contains(',') ? cityStateZip : '',
filingStatus: data['filingStatus'] as String? ?? '',
multipleJobs: data['multipleJobs'] as bool? ?? false,
qualifyingChildren: data['qualifyingChildren'] as int? ?? 0,
otherDependents: data['otherDependents'] as int? ?? 0,
otherIncome: data['otherIncome'] as String? ?? '',
deductions: data['deductions'] as String? ?? '',
extraWithholding: data['extraWithholding'] as String? ?? '',
signature: data['signature'] as String? ?? '',
));
emit(
FormW4State(
firstName: data['firstName'] as String? ?? '',
lastName: data['lastName'] as String? ?? '',
ssn: data['ssn'] as String? ?? '',
address: data['address'] as String? ?? '',
cityStateZip: cityStateZip.contains(',') ? cityStateZip : '',
filingStatus: data['filingStatus'] as String? ?? '',
multipleJobs: data['multipleJobs'] as bool? ?? false,
qualifyingChildren: data['qualifyingChildren'] as int? ?? 0,
otherDependents: data['otherDependents'] as int? ?? 0,
otherIncome: data['otherIncome'] as String? ?? '',
deductions: data['deductions'] as String? ?? '',
extraWithholding: data['extraWithholding'] as String? ?? '',
signature: data['signature'] as String? ?? '',
),
);
}
void nextStep(int totalSteps) {
@@ -62,52 +65,65 @@ class FormW4Cubit extends Cubit<FormW4State> {
void lastNameChanged(String value) => emit(state.copyWith(lastName: value));
void ssnChanged(String value) => emit(state.copyWith(ssn: value));
void addressChanged(String value) => emit(state.copyWith(address: value));
void cityStateZipChanged(String value) => emit(state.copyWith(cityStateZip: value));
void cityStateZipChanged(String value) =>
emit(state.copyWith(cityStateZip: value));
// Form Data
void filingStatusChanged(String value) => emit(state.copyWith(filingStatus: value));
void multipleJobsChanged(bool value) => emit(state.copyWith(multipleJobs: value));
void qualifyingChildrenChanged(int value) => emit(state.copyWith(qualifyingChildren: value));
void otherDependentsChanged(int value) => emit(state.copyWith(otherDependents: value));
void filingStatusChanged(String value) =>
emit(state.copyWith(filingStatus: value));
void multipleJobsChanged(bool value) =>
emit(state.copyWith(multipleJobs: value));
void qualifyingChildrenChanged(int value) =>
emit(state.copyWith(qualifyingChildren: value));
void otherDependentsChanged(int value) =>
emit(state.copyWith(otherDependents: value));
// Adjustments
void otherIncomeChanged(String value) => emit(state.copyWith(otherIncome: value));
void deductionsChanged(String value) => emit(state.copyWith(deductions: value));
void extraWithholdingChanged(String value) => emit(state.copyWith(extraWithholding: value));
void otherIncomeChanged(String value) =>
emit(state.copyWith(otherIncome: value));
void deductionsChanged(String value) =>
emit(state.copyWith(deductions: value));
void extraWithholdingChanged(String value) =>
emit(state.copyWith(extraWithholding: value));
void signatureChanged(String value) => emit(state.copyWith(signature: value));
Future<void> submit() async {
emit(state.copyWith(status: FormW4Status.submitting));
try {
final Map<String, dynamic> formData = {
'firstName': state.firstName,
'lastName': state.lastName,
'ssn': state.ssn,
'address': state.address,
'cityStateZip': state.cityStateZip, // Note: Repository should split this if needed.
'filingStatus': state.filingStatus,
'multipleJobs': state.multipleJobs,
'qualifyingChildren': state.qualifyingChildren,
'otherDependents': state.otherDependents,
'otherIncome': state.otherIncome,
'deductions': state.deductions,
'extraWithholding': state.extraWithholding,
'signature': state.signature,
};
await handleError(
emit: emit,
action: () async {
final Map<String, dynamic> formData = {
'firstName': state.firstName,
'lastName': state.lastName,
'ssn': state.ssn,
'address': state.address,
'cityStateZip':
state.cityStateZip, // Note: Repository should split this if needed.
'filingStatus': state.filingStatus,
'multipleJobs': state.multipleJobs,
'qualifyingChildren': state.qualifyingChildren,
'otherDependents': state.otherDependents,
'otherIncome': state.otherIncome,
'deductions': state.deductions,
'extraWithholding': state.extraWithholding,
'signature': state.signature,
};
final W4TaxForm form = W4TaxForm(
id: _formId.isNotEmpty ? _formId : const Uuid().v4(),
title: 'Form W-4',
formData: formData,
);
final W4TaxForm form = W4TaxForm(
id: _formId.isNotEmpty ? _formId : const Uuid().v4(),
title: 'Form W-4',
formData: formData,
);
await _submitW4FormUseCase(form);
emit(state.copyWith(status: FormW4Status.success));
} catch (e) {
emit(state.copyWith(
status: FormW4Status.failure,
errorMessage: e.toString(),
));
}
await _submitW4FormUseCase(form);
emit(state.copyWith(status: FormW4Status.success));
},
onError:
(String errorKey) => state.copyWith(
status: FormW4Status.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -147,12 +147,12 @@ class TaxFormsPage extends StatelessWidget {
if (form is I9TaxForm) {
final result = await Modular.to.pushNamed('i9', arguments: form);
if (result == true && context.mounted) {
BlocProvider.of<TaxFormsCubit>(context).loadTaxForms();
await BlocProvider.of<TaxFormsCubit>(context).loadTaxForms();
}
} else if (form is W4TaxForm) {
final result = await Modular.to.pushNamed('w4', arguments: form);
if (result == true && context.mounted) {
BlocProvider.of<TaxFormsCubit>(context).loadTaxForms();
await BlocProvider.of<TaxFormsCubit>(context).loadTaxForms();
}
}
},

View File

@@ -1,35 +1,42 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/add_bank_account_params.dart';
import '../../domain/usecases/add_bank_account_usecase.dart';
import '../../domain/usecases/get_bank_accounts_usecase.dart';
import 'bank_account_state.dart';
class BankAccountCubit extends Cubit<BankAccountState> {
class BankAccountCubit extends Cubit<BankAccountState>
with BlocErrorHandler<BankAccountState> {
final GetBankAccountsUseCase _getBankAccountsUseCase;
final AddBankAccountUseCase _addBankAccountUseCase;
BankAccountCubit({
required GetBankAccountsUseCase getBankAccountsUseCase,
required AddBankAccountUseCase addBankAccountUseCase,
}) : _getBankAccountsUseCase = getBankAccountsUseCase,
_addBankAccountUseCase = addBankAccountUseCase,
super(const BankAccountState());
}) : _getBankAccountsUseCase = getBankAccountsUseCase,
_addBankAccountUseCase = addBankAccountUseCase,
super(const BankAccountState());
Future<void> loadAccounts() async {
emit(state.copyWith(status: BankAccountStatus.loading));
try {
final List<BankAccount> accounts = await _getBankAccountsUseCase();
emit(state.copyWith(
status: BankAccountStatus.loaded,
accounts: accounts,
));
} catch (e) {
emit(state.copyWith(
status: BankAccountStatus.error,
errorMessage: e.toString(),
));
}
await handleError(
emit: emit,
action: () async {
final List<BankAccount> accounts = await _getBankAccountsUseCase();
emit(
state.copyWith(
status: BankAccountStatus.loaded,
accounts: accounts,
),
);
},
onError:
(String errorKey) => state.copyWith(
status: BankAccountStatus.error,
errorMessage: errorKey,
),
);
}
void toggleForm(bool show) {
@@ -43,35 +50,47 @@ class BankAccountCubit extends Cubit<BankAccountState> {
required String type,
}) async {
emit(state.copyWith(status: BankAccountStatus.loading));
// Create domain entity
final BankAccount newAccount = BankAccount(
id: '', // Generated by server usually
userId: '', // Handled by Repo/Auth
bankName: bankName,
accountNumber: accountNumber,
accountName: '',
sortCode: routingNumber,
type: type == 'CHECKING' ? BankAccountType.checking : BankAccountType.savings,
last4: accountNumber.length > 4 ? accountNumber.substring(accountNumber.length - 4) : accountNumber,
isPrimary: false,
id: '', // Generated by server usually
userId: '', // Handled by Repo/Auth
bankName: bankName,
accountNumber: accountNumber,
accountName: '',
sortCode: routingNumber,
type:
type == 'CHECKING'
? BankAccountType.checking
: BankAccountType.savings,
last4:
accountNumber.length > 4
? accountNumber.substring(accountNumber.length - 4)
: accountNumber,
isPrimary: false,
);
try {
await _addBankAccountUseCase(AddBankAccountParams(account: newAccount));
await handleError(
emit: emit,
action: () async {
await _addBankAccountUseCase(AddBankAccountParams(account: newAccount));
// Re-fetch to get latest state including server-generated IDs
await loadAccounts();
emit(state.copyWith(
status: BankAccountStatus.accountAdded,
showForm: false, // Close form on success
));
} catch (e) {
emit(state.copyWith(
status: BankAccountStatus.error,
errorMessage: e.toString(),
));
}
// Re-fetch to get latest state including server-generated IDs
await loadAccounts();
emit(
state.copyWith(
status: BankAccountStatus.accountAdded,
showForm: false, // Close form on success
),
);
},
onError:
(String errorKey) => state.copyWith(
status: BankAccountStatus.error,
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/get_time_cards_arguments.dart';
import '../../domain/usecases/get_time_cards_usecase.dart';
@@ -8,35 +9,55 @@ part 'time_card_event.dart';
part 'time_card_state.dart';
/// BLoC to manage Time Card state.
class TimeCardBloc extends Bloc<TimeCardEvent, TimeCardState> {
class TimeCardBloc extends Bloc<TimeCardEvent, TimeCardState>
with BlocErrorHandler<TimeCardState> {
final GetTimeCardsUseCase getTimeCards;
TimeCardBloc({required this.getTimeCards}) : super(TimeCardInitial()) {
on<LoadTimeCards>(_onLoadTimeCards);
on<ChangeMonth>(_onChangeMonth);
}
/// Handles fetching time cards for the requested month.
Future<void> _onLoadTimeCards(LoadTimeCards event, Emitter<TimeCardState> emit) async {
Future<void> _onLoadTimeCards(
LoadTimeCards event,
Emitter<TimeCardState> emit,
) async {
emit(TimeCardLoading());
try {
final List<TimeCard> cards = await getTimeCards(GetTimeCardsArguments(event.month));
final double totalHours = cards.fold(0.0, (double sum, TimeCard t) => sum + t.totalHours);
final double totalEarnings = cards.fold(0.0, (double sum, TimeCard t) => sum + t.totalPay);
await handleError(
emit: emit,
action: () async {
final List<TimeCard> cards = await getTimeCards(
GetTimeCardsArguments(event.month),
);
emit(TimeCardLoaded(
timeCards: cards,
selectedMonth: event.month,
totalHours: totalHours,
totalEarnings: totalEarnings,
));
} catch (e) {
emit(TimeCardError(e.toString()));
}
final double totalHours = cards.fold(
0.0,
(double sum, TimeCard t) => sum + t.totalHours,
);
final double totalEarnings = cards.fold(
0.0,
(double sum, TimeCard t) => sum + t.totalPay,
);
emit(
TimeCardLoaded(
timeCards: cards,
selectedMonth: event.month,
totalHours: totalHours,
totalEarnings: totalEarnings,
),
);
},
onError: (String errorKey) => TimeCardError(errorKey),
);
}
Future<void> _onChangeMonth(ChangeMonth event, Emitter<TimeCardState> emit) async {
add(LoadTimeCards(event.month));
Future<void> _onChangeMonth(
ChangeMonth event,
Emitter<TimeCardState> emit,
) async {
add(LoadTimeCards(event.month));
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/save_attire_arguments.dart';
@@ -8,7 +9,8 @@ import '../../domain/usecases/save_attire_usecase.dart';
import '../../domain/usecases/upload_attire_photo_usecase.dart';
import 'attire_state.dart';
class AttireCubit extends Cubit<AttireState> {
class AttireCubit extends Cubit<AttireState>
with BlocErrorHandler<AttireState> {
final GetAttireOptionsUseCase _getAttireOptionsUseCase;
final SaveAttireUseCase _saveAttireUseCase;
final UploadAttirePhotoUseCase _uploadAttirePhotoUseCase;
@@ -23,30 +25,41 @@ class AttireCubit extends Cubit<AttireState> {
Future<void> loadOptions() async {
emit(state.copyWith(status: AttireStatus.loading));
try {
final List<AttireItem> options = await _getAttireOptionsUseCase();
// Auto-select mandatory items initially as per prototype
final List<String> mandatoryIds = options
.where((AttireItem e) => e.isMandatory)
.map((AttireItem e) => e.id)
.toList();
final List<String> initialSelection = List<String>.from(state.selectedIds);
for (final String id in mandatoryIds) {
if (!initialSelection.contains(id)) {
initialSelection.add(id);
}
}
await handleError(
emit: emit,
action: () async {
final List<AttireItem> options = await _getAttireOptionsUseCase();
emit(state.copyWith(
status: AttireStatus.success,
options: options,
selectedIds: initialSelection,
));
} catch (e) {
emit(state.copyWith(status: AttireStatus.failure, errorMessage: e.toString()));
}
// Auto-select mandatory items initially as per prototype
final List<String> mandatoryIds =
options
.where((AttireItem e) => e.isMandatory)
.map((AttireItem e) => e.id)
.toList();
final List<String> initialSelection = List<String>.from(
state.selectedIds,
);
for (final String id in mandatoryIds) {
if (!initialSelection.contains(id)) {
initialSelection.add(id);
}
}
emit(
state.copyWith(
status: AttireStatus.success,
options: options,
selectedIds: initialSelection,
),
);
},
onError:
(String errorKey) => state.copyWith(
status: AttireStatus.failure,
errorMessage: errorKey,
),
);
}
void toggleSelection(String id) {
@@ -67,51 +80,81 @@ class AttireCubit extends Cubit<AttireState> {
}
Future<void> uploadPhoto(String itemId) async {
final Map<String, bool> currentUploading = Map<String, bool>.from(state.uploadingStatus);
final Map<String, bool> currentUploading = Map<String, bool>.from(
state.uploadingStatus,
);
currentUploading[itemId] = true;
emit(state.copyWith(uploadingStatus: currentUploading));
try {
final String url = await _uploadAttirePhotoUseCase(
UploadAttirePhotoArguments(itemId: itemId),
);
final Map<String, String> currentPhotos = Map<String, String>.from(state.photoUrls);
currentPhotos[itemId] = url;
// Auto-select item on upload success if not selected
final List<String> currentSelection = List<String>.from(state.selectedIds);
if (!currentSelection.contains(itemId)) {
currentSelection.add(itemId);
}
await handleError(
emit: emit,
action: () async {
final String url = await _uploadAttirePhotoUseCase(
UploadAttirePhotoArguments(itemId: itemId),
);
currentUploading[itemId] = false;
emit(state.copyWith(
uploadingStatus: currentUploading,
photoUrls: currentPhotos,
selectedIds: currentSelection,
));
} catch (e) {
currentUploading[itemId] = false;
emit(state.copyWith(
uploadingStatus: currentUploading,
final Map<String, String> currentPhotos = Map<String, String>.from(
state.photoUrls,
);
currentPhotos[itemId] = url;
// Auto-select item on upload success if not selected
final List<String> currentSelection = List<String>.from(
state.selectedIds,
);
if (!currentSelection.contains(itemId)) {
currentSelection.add(itemId);
}
final Map<String, bool> updatedUploading = Map<String, bool>.from(
state.uploadingStatus,
);
updatedUploading[itemId] = false;
emit(
state.copyWith(
uploadingStatus: updatedUploading,
photoUrls: currentPhotos,
selectedIds: currentSelection,
),
);
},
onError: (String errorKey) {
final Map<String, bool> updatedUploading = Map<String, bool>.from(
state.uploadingStatus,
);
updatedUploading[itemId] = false;
// Could handle error specifically via snackbar event
));
}
// For now, attaching the error message but keeping state generally usable
return state.copyWith(
uploadingStatus: updatedUploading,
errorMessage: errorKey,
);
},
);
}
Future<void> save() async {
if (!state.canSave) return;
emit(state.copyWith(status: AttireStatus.saving));
try {
await _saveAttireUseCase(SaveAttireArguments(
selectedItemIds: state.selectedIds,
photoUrls: state.photoUrls,
));
emit(state.copyWith(status: AttireStatus.saved));
} catch (e) {
emit(state.copyWith(status: AttireStatus.failure, errorMessage: e.toString()));
}
await handleError(
emit: emit,
action: () async {
await _saveAttireUseCase(
SaveAttireArguments(
selectedItemIds: state.selectedIds,
photoUrls: state.photoUrls,
),
);
emit(state.copyWith(status: AttireStatus.saved));
},
onError:
(String errorKey) => state.copyWith(
status: AttireStatus.failure,
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,4 +1,5 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/get_emergency_contacts_arguments.dart';
import '../../domain/arguments/save_emergency_contacts_arguments.dart';
@@ -12,7 +13,8 @@ export 'emergency_contact_state.dart';
// BLoC
class EmergencyContactBloc
extends Bloc<EmergencyContactEvent, EmergencyContactState> {
extends Bloc<EmergencyContactEvent, EmergencyContactState>
with BlocErrorHandler<EmergencyContactState> {
final GetEmergencyContactsUseCase getEmergencyContacts;
final SaveEmergencyContactsUseCase saveEmergencyContacts;
@@ -28,29 +30,30 @@ class EmergencyContactBloc
add(EmergencyContactsLoaded());
}
Future<void> _onLoaded(
EmergencyContactsLoaded event,
Emitter<EmergencyContactState> emit,
) async {
emit(state.copyWith(status: EmergencyContactStatus.loading));
try {
final contacts = await getEmergencyContacts(
const GetEmergencyContactsArguments(),
);
emit(state.copyWith(
status: EmergencyContactStatus.loaded,
contacts: contacts.isNotEmpty
? contacts
: [EmergencyContact.empty()],
));
} catch (e) {
emit(state.copyWith(
await handleError(
emit: emit,
action: () async {
final contacts = await getEmergencyContacts(
const GetEmergencyContactsArguments(),
);
emit(
state.copyWith(
status: EmergencyContactStatus.loaded,
contacts: contacts.isNotEmpty ? contacts : [EmergencyContact.empty()],
),
);
},
onError: (String errorKey) => state.copyWith(
status: EmergencyContactStatus.failure,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
void _onAdded(
@@ -85,18 +88,19 @@ class EmergencyContactBloc
Emitter<EmergencyContactState> emit,
) async {
emit(state.copyWith(status: EmergencyContactStatus.saving));
try {
await saveEmergencyContacts(
SaveEmergencyContactsArguments(
contacts: state.contacts,
),
);
emit(state.copyWith(status: EmergencyContactStatus.saved));
} catch (e) {
emit(state.copyWith(
await handleError(
emit: emit,
action: () async {
await saveEmergencyContacts(
SaveEmergencyContactsArguments(contacts: state.contacts),
);
emit(state.copyWith(status: EmergencyContactStatus.saved));
},
onError: (String errorKey) => state.copyWith(
status: EmergencyContactStatus.failure,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:equatable/equatable.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/arguments/save_experience_arguments.dart';
import '../../domain/usecases/get_staff_industries_usecase.dart';
@@ -92,8 +93,8 @@ class ExperienceState extends Equatable {
}
// BLoC
class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState>
with BlocErrorHandler<ExperienceState> {
final GetStaffIndustriesUseCase getIndustries;
final GetStaffSkillsUseCase getSkills;
final SaveExperienceUseCase saveExperience;
@@ -102,10 +103,12 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
required this.getIndustries,
required this.getSkills,
required this.saveExperience,
}) : super(const ExperienceState(
availableIndustries: Industry.values,
availableSkills: ExperienceSkill.values,
)) {
}) : super(
const ExperienceState(
availableIndustries: Industry.values,
availableSkills: ExperienceSkill.values,
),
) {
on<ExperienceLoaded>(_onLoaded);
on<ExperienceIndustryToggled>(_onIndustryToggled);
on<ExperienceSkillToggled>(_onSkillToggled);
@@ -120,26 +123,28 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
Emitter<ExperienceState> emit,
) async {
emit(state.copyWith(status: ExperienceStatus.loading));
try {
final results = await Future.wait([
getIndustries(),
getSkills(),
]);
await handleError(
emit: emit,
action: () async {
final results = await Future.wait([getIndustries(), getSkills()]);
emit(state.copyWith(
status: ExperienceStatus.initial,
selectedIndustries: results[0]
.map((e) => Industry.fromString(e))
.whereType<Industry>()
.toList(),
selectedSkills: results[1],
));
} catch (e) {
emit(state.copyWith(
emit(
state.copyWith(
status: ExperienceStatus.initial,
selectedIndustries:
results[0]
.map((e) => Industry.fromString(e))
.whereType<Industry>()
.toList(),
selectedSkills: results[1],
),
);
},
onError: (String errorKey) => state.copyWith(
status: ExperienceStatus.failure,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
void _onIndustryToggled(
@@ -183,19 +188,22 @@ class ExperienceBloc extends Bloc<ExperienceEvent, ExperienceState> {
Emitter<ExperienceState> emit,
) async {
emit(state.copyWith(status: ExperienceStatus.loading));
try {
await saveExperience(
SaveExperienceArguments(
industries: state.selectedIndustries.map((e) => e.value).toList(),
skills: state.selectedSkills,
),
);
emit(state.copyWith(status: ExperienceStatus.success));
} catch (e) {
emit(state.copyWith(
await handleError(
emit: emit,
action: () async {
await saveExperience(
SaveExperienceArguments(
industries: state.selectedIndustries.map((e) => e.value).toList(),
skills: state.selectedSkills,
),
);
emit(state.copyWith(status: ExperienceStatus.success));
},
onError: (String errorKey) => state.copyWith(
status: ExperienceStatus.failure,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/usecases/get_personal_info_usecase.dart';
@@ -13,17 +14,17 @@ import 'personal_info_state.dart';
/// during onboarding or profile editing. It delegates business logic to
/// use cases following Clean Architecture principles.
class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
with BlocErrorHandler<PersonalInfoState>
implements Disposable {
/// Creates a [PersonalInfoBloc].
///
/// Requires the use cases to load and update the profile.
PersonalInfoBloc({
required GetPersonalInfoUseCase getPersonalInfoUseCase,
required UpdatePersonalInfoUseCase updatePersonalInfoUseCase,
}) : _getPersonalInfoUseCase = getPersonalInfoUseCase,
_updatePersonalInfoUseCase = updatePersonalInfoUseCase,
super(const PersonalInfoState.initial()) {
}) : _getPersonalInfoUseCase = getPersonalInfoUseCase,
_updatePersonalInfoUseCase = updatePersonalInfoUseCase,
super(const PersonalInfoState.initial()) {
on<PersonalInfoLoadRequested>(_onLoadRequested);
on<PersonalInfoFieldChanged>(_onFieldChanged);
on<PersonalInfoAddressSelected>(_onAddressSelected);
@@ -40,32 +41,37 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
Emitter<PersonalInfoState> emit,
) async {
emit(state.copyWith(status: PersonalInfoStatus.loading));
try {
final Staff staff = await _getPersonalInfoUseCase();
// Initialize form values from staff entity
// Note: Staff entity currently stores address as a string, but we want to map it to 'preferredLocations'
final Map<String, dynamic> initialValues = <String, dynamic>{
'name': staff.name,
'email': staff.email,
'phone': staff.phone,
'preferredLocations': staff.address != null
? <String?>[staff.address]
: <dynamic>[], // TODO: Map correctly when Staff entity supports list
'avatar': staff.avatar,
};
await handleError(
emit: emit,
action: () async {
final Staff staff = await _getPersonalInfoUseCase();
emit(state.copyWith(
status: PersonalInfoStatus.loaded,
staff: staff,
formValues: initialValues,
));
} catch (e) {
emit(state.copyWith(
// Initialize form values from staff entity
// Note: Staff entity currently stores address as a string, but we want to map it to 'preferredLocations'
final Map<String, dynamic> initialValues = <String, dynamic>{
'name': staff.name,
'email': staff.email,
'phone': staff.phone,
'preferredLocations':
staff.address != null
? <String?>[staff.address]
: <dynamic>[], // TODO: Map correctly when Staff entity supports list
'avatar': staff.avatar,
};
emit(
state.copyWith(
status: PersonalInfoStatus.loaded,
staff: staff,
formValues: initialValues,
),
);
},
onError: (String errorKey) => state.copyWith(
status: PersonalInfoStatus.error,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
/// Handles updating a field value in the current staff profile.
@@ -86,43 +92,48 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
if (state.staff == null) return;
emit(state.copyWith(status: PersonalInfoStatus.saving));
try {
final Staff updatedStaff = await _updatePersonalInfoUseCase(
UpdatePersonalInfoParams(
staffId: state.staff!.id,
data: state.formValues,
),
);
// Update local state with the returned staff and keep form values in sync
final Map<String, dynamic> newValues = <String, dynamic>{
'name': updatedStaff.name,
'email': updatedStaff.email,
'phone': updatedStaff.phone,
'preferredLocations': updatedStaff.address != null
? <String?>[updatedStaff.address]
: <dynamic>[],
'avatar': updatedStaff.avatar,
};
await handleError(
emit: emit,
action: () async {
final Staff updatedStaff = await _updatePersonalInfoUseCase(
UpdatePersonalInfoParams(
staffId: state.staff!.id,
data: state.formValues,
),
);
emit(state.copyWith(
status: PersonalInfoStatus.saved,
staff: updatedStaff,
formValues: newValues,
));
} catch (e) {
emit(state.copyWith(
// Update local state with the returned staff and keep form values in sync
final Map<String, dynamic> newValues = <String, dynamic>{
'name': updatedStaff.name,
'email': updatedStaff.email,
'phone': updatedStaff.phone,
'preferredLocations':
updatedStaff.address != null
? <String?>[updatedStaff.address]
: <dynamic>[],
'avatar': updatedStaff.avatar,
};
emit(
state.copyWith(
status: PersonalInfoStatus.saved,
staff: updatedStaff,
formValues: newValues,
),
);
},
onError: (String errorKey) => state.copyWith(
status: PersonalInfoStatus.error,
errorMessage: e.toString(),
));
}
errorMessage: errorKey,
),
);
}
void _onAddressSelected(
PersonalInfoAddressSelected event,
Emitter<PersonalInfoState> emit,
) {
// TODO: Implement Google Places logic if needed
// TODO: Implement Google Places logic if needed
}
/// With _onPhotoUploadRequested and _onSaveRequested removed or renamed,
@@ -133,3 +144,4 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
close();
}
}

View File

@@ -1,4 +1,5 @@
import 'package:bloc/bloc.dart';
import 'package:krow_core/core.dart';
import '../../../domain/usecases/apply_for_shift_usecase.dart';
import '../../../domain/usecases/decline_shift_usecase.dart';
import '../../../domain/usecases/get_shift_details_usecase.dart';
@@ -6,7 +7,8 @@ import '../../../domain/arguments/get_shift_details_arguments.dart';
import 'shift_details_event.dart';
import 'shift_details_state.dart';
class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
with BlocErrorHandler<ShiftDetailsState> {
final GetShiftDetailsUseCase getShiftDetails;
final ApplyForShiftUseCase applyForShift;
final DeclineShiftUseCase declineShift;
@@ -26,47 +28,54 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
Emitter<ShiftDetailsState> emit,
) async {
emit(ShiftDetailsLoading());
try {
final shift = await getShiftDetails(
GetShiftDetailsArguments(shiftId: event.shiftId, roleId: event.roleId),
);
if (shift != null) {
emit(ShiftDetailsLoaded(shift));
} else {
emit(const ShiftDetailsError("Shift not found"));
}
} catch (e) {
emit(ShiftDetailsError(e.toString()));
}
await handleError(
emit: emit,
action: () async {
final shift = await getShiftDetails(
GetShiftDetailsArguments(shiftId: event.shiftId, roleId: event.roleId),
);
if (shift != null) {
emit(ShiftDetailsLoaded(shift));
} else {
emit(const ShiftDetailsError("Shift not found"));
}
},
onError: (String errorKey) => ShiftDetailsError(errorKey),
);
}
Future<void> _onBookShift(
BookShiftDetailsEvent event,
Emitter<ShiftDetailsState> emit,
) async {
try {
await applyForShift(
event.shiftId,
isInstantBook: true,
roleId: event.roleId,
);
emit(
ShiftActionSuccess("Shift successfully booked!", shiftDate: event.date),
);
} catch (e) {
emit(ShiftDetailsError(e.toString()));
}
await handleError(
emit: emit,
action: () async {
await applyForShift(
event.shiftId,
isInstantBook: true,
roleId: event.roleId,
);
emit(
ShiftActionSuccess("Shift successfully booked!", shiftDate: event.date),
);
},
onError: (String errorKey) => ShiftDetailsError(errorKey),
);
}
Future<void> _onDeclineShift(
DeclineShiftDetailsEvent event,
Emitter<ShiftDetailsState> emit,
) async {
try {
await declineShift(event.shiftId);
emit(const ShiftActionSuccess("Shift declined"));
} catch (e) {
emit(ShiftDetailsError(e.toString()));
}
await handleError(
emit: emit,
action: () async {
await declineShift(event.shiftId);
emit(const ShiftActionSuccess("Shift declined"));
},
onError: (String errorKey) => ShiftDetailsError(errorKey),
);
}
}

View File

@@ -1,5 +1,6 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:meta/meta.dart';
@@ -14,7 +15,8 @@ import '../../../domain/usecases/get_pending_assignments_usecase.dart';
part 'shifts_event.dart';
part 'shifts_state.dart';
class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
with BlocErrorHandler<ShiftsState> {
final GetMyShiftsUseCase getMyShifts;
final GetAvailableShiftsUseCase getAvailableShifts;
final GetPendingAssignmentsUseCase getPendingAssignments;
@@ -43,33 +45,32 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
if (state is! ShiftsLoaded) {
emit(ShiftsLoading());
}
// Determine what to load based on current tab?
// Or load all for simplicity as per prototype logic which had them all in memory.
try {
final List<DateTime> days = _getCalendarDaysForOffset(0);
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: days.first, end: days.last),
);
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',
));
} catch (_) {
emit(const ShiftsError('Failed to load shifts'));
}
await handleError(
emit: emit,
action: () async {
final List<DateTime> days = _getCalendarDaysForOffset(0);
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: days.first, end: days.last),
);
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),
);
}
Future<void> _onLoadHistoryShifts(
@@ -81,17 +82,24 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
if (currentState.historyLoading || currentState.historyLoaded) return;
emit(currentState.copyWith(historyLoading: true));
try {
final historyResult = await getHistoryShifts();
emit(currentState.copyWith(
myShiftsLoaded: true,
historyShifts: historyResult,
historyLoading: false,
historyLoaded: true,
));
} catch (_) {
emit(currentState.copyWith(historyLoading: false));
}
await handleError(
emit: emit,
action: () async {
final historyResult = await getHistoryShifts();
emit(currentState.copyWith(
myShiftsLoaded: true,
historyShifts: historyResult,
historyLoading: false,
historyLoaded: true,
));
},
onError: (String errorKey) {
if (state is ShiftsLoaded) {
return (state as ShiftsLoaded).copyWith(historyLoading: false);
}
return ShiftsError(errorKey);
},
);
}
Future<void> _onLoadAvailableShifts(
@@ -103,17 +111,24 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
if (currentState.availableLoading || currentState.availableLoaded) return;
emit(currentState.copyWith(availableLoading: true));
try {
final availableResult =
await getAvailableShifts(const GetAvailableShiftsArguments());
emit(currentState.copyWith(
availableShifts: _filterPastShifts(availableResult),
availableLoading: false,
availableLoaded: true,
));
} catch (_) {
emit(currentState.copyWith(availableLoading: false));
}
await handleError(
emit: emit,
action: () async {
final availableResult =
await getAvailableShifts(const GetAvailableShiftsArguments());
emit(currentState.copyWith(
availableShifts: _filterPastShifts(availableResult),
availableLoading: false,
availableLoaded: true,
));
},
onError: (String errorKey) {
if (state is ShiftsLoaded) {
return (state as ShiftsLoaded).copyWith(availableLoading: false);
}
return ShiftsError(errorKey);
},
);
}
Future<void> _onLoadFindFirst(
@@ -137,81 +152,86 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
));
}
final currentState =
state is ShiftsLoaded ? state as ShiftsLoaded : null;
final currentState = state is ShiftsLoaded ? state as ShiftsLoaded : null;
if (currentState != null && currentState.availableLoaded) return;
if (currentState != null) {
emit(currentState.copyWith(availableLoading: true));
}
try {
final availableResult =
await getAvailableShifts(const GetAvailableShiftsArguments());
final loadedState = state is ShiftsLoaded
? state as ShiftsLoaded
: const ShiftsLoaded(
myShifts: [],
pendingShifts: [],
cancelledShifts: [],
availableShifts: [],
historyShifts: [],
availableLoading: true,
availableLoaded: false,
historyLoading: false,
historyLoaded: false,
myShiftsLoaded: false,
searchQuery: '',
jobType: 'all',
);
emit(loadedState.copyWith(
availableShifts: _filterPastShifts(availableResult),
availableLoading: false,
availableLoaded: true,
));
} catch (_) {
if (state is ShiftsLoaded) {
final current = state as ShiftsLoaded;
emit(current.copyWith(availableLoading: false));
}
}
await handleError(
emit: emit,
action: () async {
final availableResult =
await getAvailableShifts(const GetAvailableShiftsArguments());
final loadedState = state is ShiftsLoaded
? state as ShiftsLoaded
: const ShiftsLoaded(
myShifts: [],
pendingShifts: [],
cancelledShifts: [],
availableShifts: [],
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) {
if (state is ShiftsLoaded) {
return (state as ShiftsLoaded).copyWith(availableLoading: false);
}
return ShiftsError(errorKey);
},
);
}
Future<void> _onLoadShiftsForRange(
LoadShiftsForRangeEvent event,
Emitter<ShiftsState> emit,
) async {
try {
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: event.start, end: event.end),
);
await handleError(
emit: emit,
action: () async {
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: event.start, end: event.end),
);
if (state is ShiftsLoaded) {
final currentState = state as ShiftsLoaded;
emit(currentState.copyWith(
if (state is ShiftsLoaded) {
final currentState = state as ShiftsLoaded;
emit(currentState.copyWith(
myShifts: myShiftsResult,
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',
));
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',
));
} catch (_) {
emit(const ShiftsError('Failed to load shifts'));
}
},
onError: (String errorKey) => ShiftsError(errorKey),
);
}
Future<void> _onFilterAvailableShifts(
@@ -224,23 +244,27 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
add(LoadAvailableShiftsEvent());
return;
}
// Optimistic update or loading indicator?
// Since it's filtering, we can just reload available.
try {
final result = await getAvailableShifts(GetAvailableShiftsArguments(
query: event.query ?? currentState.searchQuery,
type: event.jobType ?? currentState.jobType,
));
emit(currentState.copyWith(
availableShifts: _filterPastShifts(result),
searchQuery: event.query ?? currentState.searchQuery,
jobType: event.jobType ?? currentState.jobType,
));
} catch (_) {
// Error handling if filter fails
}
await handleError(
emit: emit,
action: () async {
final result = await getAvailableShifts(GetAvailableShiftsArguments(
query: event.query ?? currentState.searchQuery,
type: event.jobType ?? currentState.jobType,
));
emit(currentState.copyWith(
availableShifts: _filterPastShifts(result),
searchQuery: event.query ?? currentState.searchQuery,
jobType: event.jobType ?? currentState.jobType,
));
},
onError: (String errorKey) {
// Stay on current state for filtering errors, maybe show a snackbar?
// For now just logging is enough via handleError mixin.
return currentState;
},
);
}
}
@@ -268,3 +292,4 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
}).toList();
}
}