feat: Migrate staff profile features from Data Connect to V2 REST API

- Removed data_connect package from mobile pubspec.yaml.
- Added documentation for V2 profile migration status and QA findings.
- Implemented new session management with ClientSessionStore and StaffSessionStore.
- Created V2SessionService for handling user sessions via the V2 API.
- Developed use cases for cancelling late worker assignments and submitting worker reviews.
- Added arguments and use cases for payment chart retrieval and profile completion checks.
- Implemented repository interfaces and their implementations for staff main and profile features.
- Ensured proper error handling and validation in use cases.
This commit is contained in:
Achintha Isuru
2026-03-16 22:45:06 -04:00
parent 4834266986
commit b31a615092
478 changed files with 10512 additions and 19854 deletions

View File

@@ -1,103 +1,158 @@
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../domain/repositories/shifts_repository_interface.dart';
/// Implementation of [ShiftsRepositoryInterface] that delegates to [dc.ShiftsConnectorRepository].
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// V2 API implementation of [ShiftsRepositoryInterface].
///
/// This implementation follows the "Buffer Layer" pattern by using a dedicated
/// connector repository from the data_connect package.
/// Uses [BaseApiService] with [V2ApiEndpoints] for all network access.
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
final dc.ShiftsConnectorRepository _connectorRepository;
final dc.DataConnectService _service;
/// Creates a [ShiftsRepositoryImpl].
ShiftsRepositoryImpl({required BaseApiService apiService})
: _apiService = apiService;
ShiftsRepositoryImpl({
dc.ShiftsConnectorRepository? connectorRepository,
dc.DataConnectService? service,
}) : _connectorRepository = connectorRepository ??
dc.DataConnectService.instance.getShiftsRepository(),
_service = service ?? dc.DataConnectService.instance;
/// The API service used for network requests.
final BaseApiService _apiService;
/// Extracts a list of items from the API response data.
///
/// Handles both the V2 wrapped `{"items": [...]}` shape and a raw
/// `List<dynamic>` for backwards compatibility.
List<dynamic> _extractItems(dynamic data) {
if (data is List<dynamic>) {
return data;
}
if (data is Map<String, dynamic>) {
return data['items'] as List<dynamic>? ?? <dynamic>[];
}
return <dynamic>[];
}
@override
Future<List<Shift>> getMyShifts({
Future<List<AssignedShift>> getAssignedShifts({
required DateTime start,
required DateTime end,
}) async {
final staffId = await _service.getStaffId();
return _connectorRepository.getMyShifts(
staffId: staffId,
start: start,
end: end,
final ApiResponse response = await _apiService.get(
V2ApiEndpoints.staffShiftsAssigned,
params: <String, dynamic>{
'startDate': start.toIso8601String(),
'endDate': end.toIso8601String(),
},
);
final List<dynamic> items = _extractItems(response.data);
return items
.map((dynamic json) =>
AssignedShift.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<List<Shift>> getPendingAssignments() async {
final staffId = await _service.getStaffId();
return _connectorRepository.getPendingAssignments(staffId: staffId);
}
@override
Future<List<Shift>> getCancelledShifts() async {
final staffId = await _service.getStaffId();
return _connectorRepository.getCancelledShifts(staffId: staffId);
}
@override
Future<List<Shift>> getHistoryShifts() async {
final staffId = await _service.getStaffId();
return _connectorRepository.getHistoryShifts(staffId: staffId);
}
@override
Future<List<Shift>> getAvailableShifts(String query, String type) async {
final staffId = await _service.getStaffId();
return _connectorRepository.getAvailableShifts(
staffId: staffId,
query: query,
type: type,
Future<List<OpenShift>> getOpenShifts({
String? search,
int limit = 20,
}) async {
final Map<String, dynamic> params = <String, dynamic>{
'limit': limit,
};
if (search != null && search.isNotEmpty) {
params['search'] = search;
}
final ApiResponse response = await _apiService.get(
V2ApiEndpoints.staffShiftsOpen,
params: params,
);
final List<dynamic> items = _extractItems(response.data);
return items
.map(
(dynamic json) => OpenShift.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<Shift?> getShiftDetails(String shiftId, {String? roleId}) async {
final staffId = await _service.getStaffId();
return _connectorRepository.getShiftDetails(
shiftId: shiftId,
staffId: staffId,
roleId: roleId,
);
Future<List<PendingAssignment>> getPendingAssignments() async {
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.staffShiftsPending);
final List<dynamic> items = _extractItems(response.data);
return items
.map((dynamic json) =>
PendingAssignment.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<List<CancelledShift>> getCancelledShifts() async {
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.staffShiftsCancelled);
final List<dynamic> items = _extractItems(response.data);
return items
.map((dynamic json) =>
CancelledShift.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<List<CompletedShift>> getCompletedShifts() async {
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.staffShiftsCompleted);
final List<dynamic> items = _extractItems(response.data);
return items
.map((dynamic json) =>
CompletedShift.fromJson(json as Map<String, dynamic>))
.toList();
}
@override
Future<ShiftDetail?> getShiftDetail(String shiftId) async {
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.staffShiftDetails(shiftId));
if (response.data == null) {
return null;
}
return ShiftDetail.fromJson(response.data as Map<String, dynamic>);
}
@override
Future<void> applyForShift(
String shiftId, {
bool isInstantBook = false,
String? roleId,
bool instantBook = false,
}) async {
final staffId = await _service.getStaffId();
return _connectorRepository.applyForShift(
shiftId: shiftId,
staffId: staffId,
isInstantBook: isInstantBook,
roleId: roleId,
await _apiService.post(
V2ApiEndpoints.staffShiftApply(shiftId),
data: <String, dynamic>{
if (roleId != null) 'roleId': roleId,
'instantBook': instantBook,
},
);
}
@override
Future<void> acceptShift(String shiftId) async {
final staffId = await _service.getStaffId();
return _connectorRepository.acceptShift(
shiftId: shiftId,
staffId: staffId,
);
await _apiService.post(V2ApiEndpoints.staffShiftAccept(shiftId));
}
@override
Future<void> declineShift(String shiftId) async {
final staffId = await _service.getStaffId();
return _connectorRepository.declineShift(
shiftId: shiftId,
staffId: staffId,
await _apiService.post(V2ApiEndpoints.staffShiftDecline(shiftId));
}
@override
Future<void> requestSwap(String shiftId, {String? reason}) async {
await _apiService.post(
V2ApiEndpoints.staffShiftRequestSwap(shiftId),
data: <String, dynamic>{
if (reason != null) 'reason': reason,
},
);
}
@override
Future<bool> getProfileCompletion() async {
final ApiResponse response =
await _apiService.get(V2ApiEndpoints.staffProfileCompletion);
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
final ProfileCompletion completion = ProfileCompletion.fromJson(data);
return completion.completed;
}
}

View File

@@ -1,19 +1,19 @@
import 'package:krow_core/core.dart';
/// Arguments for [GetAvailableShiftsUseCase].
class GetAvailableShiftsArguments extends UseCaseArgument {
/// The search query to filter shifts.
final String query;
/// The job type filter (e.g., 'all', 'one-day', 'multi-day', 'long-term').
final String type;
/// Creates a [GetAvailableShiftsArguments] instance.
const GetAvailableShiftsArguments({
this.query = '',
this.type = 'all',
/// Arguments for GetOpenShiftsUseCase.
class GetOpenShiftsArguments extends UseCaseArgument {
/// Creates a [GetOpenShiftsArguments] instance.
const GetOpenShiftsArguments({
this.search,
this.limit = 20,
});
/// Optional search query to filter by role name or location.
final String? search;
/// Maximum number of results to return.
final int limit;
@override
List<Object?> get props => [query, type];
List<Object?> get props => <Object?>[search, limit];
}

View File

@@ -1,11 +1,19 @@
import 'package:krow_core/core.dart';
class GetMyShiftsArguments extends UseCaseArgument {
final DateTime start;
final DateTime end;
const GetMyShiftsArguments({
/// Arguments for GetAssignedShiftsUseCase.
class GetAssignedShiftsArguments extends UseCaseArgument {
/// Creates a [GetAssignedShiftsArguments] instance.
const GetAssignedShiftsArguments({
required this.start,
required this.end,
});
/// Start of the date range.
final DateTime start;
/// End of the date range.
final DateTime end;
@override
List<Object?> get props => <Object?>[start, end];
}

View File

@@ -1,32 +1,39 @@
import 'package:krow_domain/krow_domain.dart';
/// Interface for the Shifts Repository.
/// Contract for accessing shift-related data from the V2 API.
///
/// Defines the contract for accessing and modifying shift-related data.
/// Implementations of this interface should reside in the data layer.
/// Implementations reside in the data layer and use [BaseApiService]
/// with V2ApiEndpoints.
abstract interface class ShiftsRepositoryInterface {
/// Retrieves the list of shifts assigned to the current user.
Future<List<Shift>> getMyShifts({
/// Retrieves assigned shifts for the current staff within a date range.
Future<List<AssignedShift>> getAssignedShifts({
required DateTime start,
required DateTime end,
});
/// Retrieves available shifts matching the given [query] and [type].
Future<List<Shift>> getAvailableShifts(String query, String type);
/// Retrieves open shifts available for the staff to apply.
Future<List<OpenShift>> getOpenShifts({
String? search,
int limit,
});
/// Retrieves shifts that are pending acceptance by the user.
Future<List<Shift>> getPendingAssignments();
/// Retrieves pending assignments awaiting acceptance.
Future<List<PendingAssignment>> getPendingAssignments();
/// Retrieves detailed information for a specific shift by [shiftId].
Future<Shift?> getShiftDetails(String shiftId, {String? roleId});
/// Retrieves cancelled shift assignments.
Future<List<CancelledShift>> getCancelledShifts();
/// Applies for a specific open shift.
///
/// [isInstantBook] determines if the application should be immediately accepted.
/// Retrieves completed shift history.
Future<List<CompletedShift>> getCompletedShifts();
/// Retrieves full details for a specific shift.
Future<ShiftDetail?> getShiftDetail(String shiftId);
/// Applies for an open shift.
Future<void> applyForShift(
String shiftId, {
bool isInstantBook = false,
String? roleId,
bool instantBook,
});
/// Accepts a pending shift assignment.
@@ -35,9 +42,9 @@ abstract interface class ShiftsRepositoryInterface {
/// Declines a pending shift assignment.
Future<void> declineShift(String shiftId);
/// Retrieves shifts that were cancelled for the current user.
Future<List<Shift>> getCancelledShifts();
/// Requests a swap for an accepted shift assignment.
Future<void> requestSwap(String shiftId, {String? reason});
/// Retrieves completed shifts for the current user.
Future<List<Shift>> getHistoryShifts();
/// Returns whether the staff profile is complete.
Future<bool> getProfileCompletion();
}

View File

@@ -1,10 +1,14 @@
import '../repositories/shifts_repository_interface.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Accepts a pending shift assignment.
class AcceptShiftUseCase {
final ShiftsRepositoryInterface repository;
/// Creates an [AcceptShiftUseCase].
AcceptShiftUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
/// Executes the use case.
Future<void> call(String shiftId) async {
return repository.acceptShift(shiftId);
}

View File

@@ -1,18 +1,22 @@
import '../repositories/shifts_repository_interface.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Applies for an open shift.
class ApplyForShiftUseCase {
final ShiftsRepositoryInterface repository;
/// Creates an [ApplyForShiftUseCase].
ApplyForShiftUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
/// Executes the use case.
Future<void> call(
String shiftId, {
bool isInstantBook = false,
bool instantBook = false,
String? roleId,
}) async {
return repository.applyForShift(
shiftId,
isInstantBook: isInstantBook,
instantBook: instantBook,
roleId: roleId,
);
}

View File

@@ -1,10 +1,14 @@
import '../repositories/shifts_repository_interface.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Declines a pending shift assignment.
class DeclineShiftUseCase {
final ShiftsRepositoryInterface repository;
/// Creates a [DeclineShiftUseCase].
DeclineShiftUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
/// Executes the use case.
Future<void> call(String shiftId) async {
return repository.declineShift(shiftId);
}

View File

@@ -1,19 +1,24 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/shifts_repository_interface.dart';
import '../arguments/get_available_shifts_arguments.dart';
/// Use case for retrieving available shifts with filters.
///
/// This use case delegates to [ShiftsRepositoryInterface].
class GetAvailableShiftsUseCase extends UseCase<GetAvailableShiftsArguments, List<Shift>> {
import 'package:staff_shifts/src/domain/arguments/get_available_shifts_arguments.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Retrieves open shifts available for the worker to apply.
class GetOpenShiftsUseCase
extends UseCase<GetOpenShiftsArguments, List<OpenShift>> {
/// Creates a [GetOpenShiftsUseCase].
GetOpenShiftsUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
GetAvailableShiftsUseCase(this.repository);
@override
Future<List<Shift>> call(GetAvailableShiftsArguments arguments) async {
return repository.getAvailableShifts(arguments.query, arguments.type);
Future<List<OpenShift>> call(GetOpenShiftsArguments arguments) async {
return repository.getOpenShifts(
search: arguments.search,
limit: arguments.limit,
);
}
}

View File

@@ -1,12 +1,17 @@
import 'package:krow_domain/krow_domain.dart';
import '../repositories/shifts_repository_interface.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Retrieves cancelled shift assignments.
class GetCancelledShiftsUseCase {
final ShiftsRepositoryInterface repository;
/// Creates a [GetCancelledShiftsUseCase].
GetCancelledShiftsUseCase(this.repository);
Future<List<Shift>> call() async {
/// The shifts repository.
final ShiftsRepositoryInterface repository;
/// Executes the use case.
Future<List<CancelledShift>> call() async {
return repository.getCancelledShifts();
}
}

View File

@@ -1,12 +1,17 @@
import 'package:krow_domain/krow_domain.dart';
import '../repositories/shifts_repository_interface.dart';
class GetHistoryShiftsUseCase {
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Retrieves completed shift history.
class GetCompletedShiftsUseCase {
/// Creates a [GetCompletedShiftsUseCase].
GetCompletedShiftsUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
GetHistoryShiftsUseCase(this.repository);
Future<List<Shift>> call() async {
return repository.getHistoryShifts();
/// Executes the use case.
Future<List<CompletedShift>> call() async {
return repository.getCompletedShifts();
}
}

View File

@@ -1,19 +1,21 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_my_shifts_arguments.dart';
import '../repositories/shifts_repository_interface.dart';
/// Use case for retrieving the user's assigned shifts.
///
/// This use case delegates to [ShiftsRepositoryInterface].
class GetMyShiftsUseCase extends UseCase<GetMyShiftsArguments, List<Shift>> {
import 'package:staff_shifts/src/domain/arguments/get_my_shifts_arguments.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Retrieves assigned shifts within a date range.
class GetAssignedShiftsUseCase
extends UseCase<GetAssignedShiftsArguments, List<AssignedShift>> {
/// Creates a [GetAssignedShiftsUseCase].
GetAssignedShiftsUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
GetMyShiftsUseCase(this.repository);
@override
Future<List<Shift>> call(GetMyShiftsArguments arguments) async {
return repository.getMyShifts(
Future<List<AssignedShift>> call(GetAssignedShiftsArguments arguments) async {
return repository.getAssignedShifts(
start: arguments.start,
end: arguments.end,
);

View File

@@ -1,17 +1,19 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../repositories/shifts_repository_interface.dart';
/// Use case for retrieving pending shift assignments.
///
/// This use case delegates to [ShiftsRepositoryInterface].
class GetPendingAssignmentsUseCase extends NoInputUseCase<List<Shift>> {
final ShiftsRepositoryInterface repository;
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Retrieves pending assignments awaiting acceptance.
class GetPendingAssignmentsUseCase
extends NoInputUseCase<List<PendingAssignment>> {
/// Creates a [GetPendingAssignmentsUseCase].
GetPendingAssignmentsUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
@override
Future<List<Shift>> call() async {
Future<List<PendingAssignment>> call() async {
return repository.getPendingAssignments();
}
}

View File

@@ -0,0 +1,17 @@
import 'package:krow_core/core.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Checks whether the staff member's profile is complete.
class GetProfileCompletionUseCase extends NoInputUseCase<bool> {
/// Creates a [GetProfileCompletionUseCase].
GetProfileCompletionUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
@override
Future<bool> call() {
return repository.getProfileCompletion();
}
}

View File

@@ -1,18 +1,18 @@
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../arguments/get_shift_details_arguments.dart';
import '../repositories/shifts_repository_interface.dart';
class GetShiftDetailsUseCase extends UseCase<GetShiftDetailsArguments, Shift?> {
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
/// Retrieves full details for a specific shift.
class GetShiftDetailUseCase extends UseCase<String, ShiftDetail?> {
/// Creates a [GetShiftDetailUseCase].
GetShiftDetailUseCase(this.repository);
/// The shifts repository.
final ShiftsRepositoryInterface repository;
GetShiftDetailsUseCase(this.repository);
@override
Future<Shift?> call(GetShiftDetailsArguments params) {
return repository.getShiftDetails(
params.shiftId,
roleId: params.roleId,
);
Future<ShiftDetail?> call(String shiftId) {
return repository.getShiftDetail(shiftId);
}
}

View File

@@ -1,22 +1,21 @@
import 'package:bloc/bloc.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import '../../../domain/usecases/apply_for_shift_usecase.dart';
import '../../../domain/usecases/decline_shift_usecase.dart';
import '../../../domain/usecases/get_shift_details_usecase.dart';
import '../../../domain/arguments/get_shift_details_arguments.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_shifts/src/domain/usecases/apply_for_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/decline_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_profile_completion_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_shift_details_usecase.dart';
import 'shift_details_event.dart';
import 'shift_details_state.dart';
/// Manages the state for the shift details page.
class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
with BlocErrorHandler<ShiftDetailsState> {
final GetShiftDetailsUseCase getShiftDetails;
final ApplyForShiftUseCase applyForShift;
final DeclineShiftUseCase declineShift;
final GetProfileCompletionUseCase getProfileCompletion;
/// Creates a [ShiftDetailsBloc].
ShiftDetailsBloc({
required this.getShiftDetails,
required this.getShiftDetail,
required this.applyForShift,
required this.declineShift,
required this.getProfileCompletion,
@@ -26,6 +25,18 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
on<DeclineShiftDetailsEvent>(_onDeclineShift);
}
/// Use case for fetching shift details.
final GetShiftDetailUseCase getShiftDetail;
/// Use case for applying to a shift.
final ApplyForShiftUseCase applyForShift;
/// Use case for declining a shift.
final DeclineShiftUseCase declineShift;
/// Use case for checking profile completion.
final GetProfileCompletionUseCase getProfileCompletion;
Future<void> _onLoadDetails(
LoadShiftDetailsEvent event,
Emitter<ShiftDetailsState> emit,
@@ -34,14 +45,15 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
await handleError(
emit: emit.call,
action: () async {
final shift = await getShiftDetails(
GetShiftDetailsArguments(shiftId: event.shiftId, roleId: event.roleId),
);
final isProfileComplete = await getProfileCompletion();
if (shift != null) {
emit(ShiftDetailsLoaded(shift, isProfileComplete: isProfileComplete));
final ShiftDetail? detail = await getShiftDetail(event.shiftId);
final bool isProfileComplete = await getProfileCompletion();
if (detail != null) {
emit(ShiftDetailsLoaded(
detail,
isProfileComplete: isProfileComplete,
));
} else {
emit(const ShiftDetailsError("Shift not found"));
emit(const ShiftDetailsError('Shift not found'));
}
},
onError: (String errorKey) => ShiftDetailsError(errorKey),
@@ -57,11 +69,14 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
action: () async {
await applyForShift(
event.shiftId,
isInstantBook: true,
instantBook: true,
roleId: event.roleId,
);
emit(
ShiftActionSuccess("Shift successfully booked!", shiftDate: event.date),
ShiftActionSuccess(
'Shift successfully booked!',
shiftDate: event.date,
),
);
},
onError: (String errorKey) => ShiftDetailsError(errorKey),
@@ -76,7 +91,7 @@ class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState>
emit: emit.call,
action: () async {
await declineShift(event.shiftId);
emit(const ShiftActionSuccess("Shift declined"));
emit(const ShiftActionSuccess('Shift declined'));
},
onError: (String errorKey) => ShiftDetailsError(errorKey),
);

View File

@@ -1,39 +1,59 @@
import 'package:equatable/equatable.dart';
import 'package:krow_domain/krow_domain.dart';
/// Base class for shift details states.
abstract class ShiftDetailsState extends Equatable {
/// Creates a [ShiftDetailsState].
const ShiftDetailsState();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
/// Initial state before any data is loaded.
class ShiftDetailsInitial extends ShiftDetailsState {}
/// Loading state while fetching shift details.
class ShiftDetailsLoading extends ShiftDetailsState {}
/// Loaded state containing the full shift detail.
class ShiftDetailsLoaded extends ShiftDetailsState {
final Shift shift;
/// Creates a [ShiftDetailsLoaded].
const ShiftDetailsLoaded(this.detail, {this.isProfileComplete = false});
/// The full shift detail from the V2 API.
final ShiftDetail detail;
/// Whether the staff profile is complete.
final bool isProfileComplete;
const ShiftDetailsLoaded(this.shift, {this.isProfileComplete = false});
@override
List<Object?> get props => [shift, isProfileComplete];
List<Object?> get props => <Object?>[detail, isProfileComplete];
}
/// Error state with a message key.
class ShiftDetailsError extends ShiftDetailsState {
final String message;
/// Creates a [ShiftDetailsError].
const ShiftDetailsError(this.message);
/// The error message key.
final String message;
@override
List<Object?> get props => [message];
List<Object?> get props => <Object?>[message];
}
/// Success state after a shift action (apply, accept, decline).
class ShiftActionSuccess extends ShiftDetailsState {
final String message;
final DateTime? shiftDate;
/// Creates a [ShiftActionSuccess].
const ShiftActionSuccess(this.message, {this.shiftDate});
/// Success message.
final String message;
/// The date of the shift for navigation.
final DateTime? shiftDate;
@override
List<Object?> get props => [message, shiftDate];
List<Object?> get props => <Object?>[message, shiftDate];
}

View File

@@ -1,47 +1,72 @@
import 'package:bloc/bloc.dart';
import 'package:equatable/equatable.dart';
import 'package:krow_core/core.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:meta/meta.dart';
import '../../../domain/arguments/get_available_shifts_arguments.dart';
import '../../../domain/arguments/get_my_shifts_arguments.dart';
import '../../../domain/usecases/get_available_shifts_usecase.dart';
import '../../../domain/usecases/get_cancelled_shifts_usecase.dart';
import '../../../domain/usecases/get_history_shifts_usecase.dart';
import '../../../domain/usecases/get_my_shifts_usecase.dart';
import '../../../domain/usecases/get_pending_assignments_usecase.dart';
import 'package:staff_shifts/src/domain/arguments/get_available_shifts_arguments.dart';
import 'package:staff_shifts/src/domain/arguments/get_my_shifts_arguments.dart';
import 'package:staff_shifts/src/domain/usecases/accept_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/decline_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_available_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_cancelled_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_history_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_my_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_pending_assignments_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_profile_completion_usecase.dart';
part 'shifts_event.dart';
part 'shifts_state.dart';
/// Manages the state for the shifts listing page (My Shifts / Find / History).
class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
with BlocErrorHandler<ShiftsState> {
final GetMyShiftsUseCase getMyShifts;
final GetAvailableShiftsUseCase getAvailableShifts;
final GetPendingAssignmentsUseCase getPendingAssignments;
final GetCancelledShiftsUseCase getCancelledShifts;
final GetHistoryShiftsUseCase getHistoryShifts;
final GetProfileCompletionUseCase getProfileCompletion;
/// Creates a [ShiftsBloc].
ShiftsBloc({
required this.getMyShifts,
required this.getAvailableShifts,
required this.getAssignedShifts,
required this.getOpenShifts,
required this.getPendingAssignments,
required this.getCancelledShifts,
required this.getHistoryShifts,
required this.getCompletedShifts,
required this.getProfileCompletion,
required this.acceptShift,
required this.declineShift,
}) : super(const ShiftsState()) {
on<LoadShiftsEvent>(_onLoadShifts);
on<LoadHistoryShiftsEvent>(_onLoadHistoryShifts);
on<LoadAvailableShiftsEvent>(_onLoadAvailableShifts);
on<LoadFindFirstEvent>(_onLoadFindFirst);
on<LoadShiftsForRangeEvent>(_onLoadShiftsForRange);
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
on<SearchOpenShiftsEvent>(_onSearchOpenShifts);
on<CheckProfileCompletionEvent>(_onCheckProfileCompletion);
on<AcceptShiftEvent>(_onAcceptShift);
on<DeclineShiftEvent>(_onDeclineShift);
}
/// Use case for assigned shifts.
final GetAssignedShiftsUseCase getAssignedShifts;
/// Use case for open shifts.
final GetOpenShiftsUseCase getOpenShifts;
/// Use case for pending assignments.
final GetPendingAssignmentsUseCase getPendingAssignments;
/// Use case for cancelled shifts.
final GetCancelledShiftsUseCase getCancelledShifts;
/// Use case for completed shifts.
final GetCompletedShiftsUseCase getCompletedShifts;
/// Use case for profile completion.
final GetProfileCompletionUseCase getProfileCompletion;
/// Use case for accepting a shift.
final AcceptShiftUseCase acceptShift;
/// Use case for declining a shift.
final DeclineShiftUseCase declineShift;
Future<void> _onLoadShifts(
LoadShiftsEvent event,
Emitter<ShiftsState> emit,
@@ -54,25 +79,24 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
emit: emit.call,
action: () async {
final List<DateTime> days = _getCalendarDaysForOffset(0);
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: days.first, end: days.last),
final List<AssignedShift> myShiftsResult = await getAssignedShifts(
GetAssignedShiftsArguments(start: days.first, end: days.last),
);
emit(
state.copyWith(
status: ShiftsStatus.loaded,
myShifts: myShiftsResult,
pendingShifts: const [],
cancelledShifts: const [],
availableShifts: const [],
historyShifts: const [],
pendingShifts: const <PendingAssignment>[],
cancelledShifts: const <CancelledShift>[],
availableShifts: const <OpenShift>[],
historyShifts: const <CompletedShift>[],
availableLoading: false,
availableLoaded: false,
historyLoading: false,
historyLoaded: false,
myShiftsLoaded: true,
searchQuery: '',
jobType: 'all',
),
);
},
@@ -92,7 +116,7 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
await handleError(
emit: emit.call,
action: () async {
final historyResult = await getHistoryShifts();
final List<CompletedShift> historyResult = await getCompletedShifts();
emit(
state.copyWith(
myShiftsLoaded: true,
@@ -125,12 +149,12 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
await handleError(
emit: emit.call,
action: () async {
final availableResult = await getAvailableShifts(
const GetAvailableShiftsArguments(),
final List<OpenShift> availableResult = await getOpenShifts(
const GetOpenShiftsArguments(),
);
emit(
state.copyWith(
availableShifts: _filterPastShifts(availableResult),
availableShifts: _filterPastOpenShifts(availableResult),
availableLoading: false,
availableLoaded: true,
),
@@ -154,18 +178,17 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
emit(
state.copyWith(
status: ShiftsStatus.loading,
myShifts: const [],
pendingShifts: const [],
cancelledShifts: const [],
availableShifts: const [],
historyShifts: const [],
myShifts: const <AssignedShift>[],
pendingShifts: const <PendingAssignment>[],
cancelledShifts: const <CancelledShift>[],
availableShifts: const <OpenShift>[],
historyShifts: const <CompletedShift>[],
availableLoading: false,
availableLoaded: false,
historyLoading: false,
historyLoaded: false,
myShiftsLoaded: false,
searchQuery: '',
jobType: 'all',
),
);
}
@@ -177,13 +200,13 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
await handleError(
emit: emit.call,
action: () async {
final availableResult = await getAvailableShifts(
const GetAvailableShiftsArguments(),
final List<OpenShift> availableResult = await getOpenShifts(
const GetOpenShiftsArguments(),
);
emit(
state.copyWith(
status: ShiftsStatus.loaded,
availableShifts: _filterPastShifts(availableResult),
availableShifts: _filterPastOpenShifts(availableResult),
availableLoading: false,
availableLoaded: true,
),
@@ -206,8 +229,8 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
await handleError(
emit: emit.call,
action: () async {
final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: event.start, end: event.end),
final List<AssignedShift> myShiftsResult = await getAssignedShifts(
GetAssignedShiftsArguments(start: event.start, end: event.end),
);
emit(
@@ -223,8 +246,8 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
);
}
Future<void> _onFilterAvailableShifts(
FilterAvailableShiftsEvent event,
Future<void> _onSearchOpenShifts(
SearchOpenShiftsEvent event,
Emitter<ShiftsState> emit,
) async {
if (state.status == ShiftsStatus.loaded) {
@@ -236,18 +259,17 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
await handleError(
emit: emit.call,
action: () async {
final result = await getAvailableShifts(
GetAvailableShiftsArguments(
query: event.query ?? state.searchQuery,
type: event.jobType ?? state.jobType,
final String search = event.query ?? state.searchQuery;
final List<OpenShift> result = await getOpenShifts(
GetOpenShiftsArguments(
search: search.isEmpty ? null : search,
),
);
emit(
state.copyWith(
availableShifts: _filterPastShifts(result),
searchQuery: event.query ?? state.searchQuery,
jobType: event.jobType ?? state.jobType,
availableShifts: _filterPastOpenShifts(result),
searchQuery: search,
),
);
},
@@ -277,33 +299,60 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState>
);
}
List<DateTime> _getCalendarDaysForOffset(int weekOffset) {
final now = DateTime.now();
final int reactDayIndex = now.weekday == 7 ? 0 : now.weekday;
final int daysSinceFriday = (reactDayIndex + 2) % 7;
final start = now
.subtract(Duration(days: daysSinceFriday))
.add(Duration(days: weekOffset * 7));
final startDate = DateTime(start.year, start.month, start.day);
return List.generate(7, (index) => startDate.add(Duration(days: index)));
Future<void> _onAcceptShift(
AcceptShiftEvent event,
Emitter<ShiftsState> emit,
) async {
await handleError(
emit: emit.call,
action: () async {
await acceptShift(event.shiftId);
add(LoadShiftsEvent());
},
onError: (String errorKey) =>
state.copyWith(status: ShiftsStatus.error, errorMessage: errorKey),
);
}
List<Shift> _filterPastShifts(List<Shift> shifts) {
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
return shifts.where((shift) {
if (shift.date.isEmpty) return false;
try {
final shiftDate = DateTime.parse(shift.date).toLocal();
final dateOnly = DateTime(
shiftDate.year,
shiftDate.month,
shiftDate.day,
);
return !dateOnly.isBefore(today);
} catch (_) {
return false;
}
Future<void> _onDeclineShift(
DeclineShiftEvent event,
Emitter<ShiftsState> emit,
) async {
await handleError(
emit: emit.call,
action: () async {
await declineShift(event.shiftId);
add(LoadShiftsEvent());
},
onError: (String errorKey) =>
state.copyWith(status: ShiftsStatus.error, errorMessage: errorKey),
);
}
/// Gets calendar days for the given week offset (Friday-based week).
List<DateTime> _getCalendarDaysForOffset(int weekOffset) {
final DateTime now = DateTime.now();
final int reactDayIndex = now.weekday == 7 ? 0 : now.weekday;
final int daysSinceFriday = (reactDayIndex + 2) % 7;
final DateTime start = now
.subtract(Duration(days: daysSinceFriday))
.add(Duration(days: weekOffset * 7));
final DateTime startDate = DateTime(start.year, start.month, start.day);
return List<DateTime>.generate(
7, (int index) => startDate.add(Duration(days: index)));
}
/// Filters out open shifts whose date is in the past.
List<OpenShift> _filterPastOpenShifts(List<OpenShift> shifts) {
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
return shifts.where((OpenShift shift) {
final DateTime dateOnly = DateTime(
shift.date.year,
shift.date.month,
shift.date.day,
);
return !dateOnly.isBefore(today);
}).toList();
}
}

View File

@@ -1,69 +1,95 @@
part of 'shifts_bloc.dart';
/// Base class for all shifts events.
@immutable
sealed class ShiftsEvent extends Equatable {
/// Creates a [ShiftsEvent].
const ShiftsEvent();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}
/// Triggers initial load of assigned shifts for the current week.
class LoadShiftsEvent extends ShiftsEvent {}
/// Triggers lazy load of completed shift history.
class LoadHistoryShiftsEvent extends ShiftsEvent {}
/// Triggers load of open shifts available to apply.
class LoadAvailableShiftsEvent extends ShiftsEvent {
final bool force;
/// Creates a [LoadAvailableShiftsEvent].
const LoadAvailableShiftsEvent({this.force = false});
/// Whether to force reload even if already loaded.
final bool force;
@override
List<Object?> get props => [force];
List<Object?> get props => <Object?>[force];
}
/// Loads open shifts first (for when Find tab is the initial tab).
class LoadFindFirstEvent extends ShiftsEvent {}
/// Loads assigned shifts for a specific date range.
class LoadShiftsForRangeEvent extends ShiftsEvent {
final DateTime start;
final DateTime end;
/// Creates a [LoadShiftsForRangeEvent].
const LoadShiftsForRangeEvent({
required this.start,
required this.end,
});
/// Start of the date range.
final DateTime start;
/// End of the date range.
final DateTime end;
@override
List<Object?> get props => [start, end];
List<Object?> get props => <Object?>[start, end];
}
class FilterAvailableShiftsEvent extends ShiftsEvent {
/// Triggers a server-side search for open shifts.
class SearchOpenShiftsEvent extends ShiftsEvent {
/// Creates a [SearchOpenShiftsEvent].
const SearchOpenShiftsEvent({this.query});
/// The search query string.
final String? query;
final String? jobType;
const FilterAvailableShiftsEvent({this.query, this.jobType});
@override
List<Object?> get props => [query, jobType];
List<Object?> get props => <Object?>[query];
}
/// Accepts a pending shift assignment.
class AcceptShiftEvent extends ShiftsEvent {
final String shiftId;
/// Creates an [AcceptShiftEvent].
const AcceptShiftEvent(this.shiftId);
/// The shift row id to accept.
final String shiftId;
@override
List<Object?> get props => [shiftId];
List<Object?> get props => <Object?>[shiftId];
}
/// Declines a pending shift assignment.
class DeclineShiftEvent extends ShiftsEvent {
final String shiftId;
/// Creates a [DeclineShiftEvent].
const DeclineShiftEvent(this.shiftId);
/// The shift row id to decline.
final String shiftId;
@override
List<Object?> get props => [shiftId];
List<Object?> get props => <Object?>[shiftId];
}
/// Triggers a profile completion check.
class CheckProfileCompletionEvent extends ShiftsEvent {
/// Creates a [CheckProfileCompletionEvent].
const CheckProfileCompletionEvent();
@override
List<Object?> get props => [];
List<Object?> get props => <Object?>[];
}

View File

@@ -1,56 +1,84 @@
part of 'shifts_bloc.dart';
/// Lifecycle status for the shifts page.
enum ShiftsStatus { initial, loading, loaded, error }
/// State for the shifts listing page.
class ShiftsState extends Equatable {
final ShiftsStatus status;
final List<Shift> myShifts;
final List<Shift> pendingShifts;
final List<Shift> cancelledShifts;
final List<Shift> availableShifts;
final List<Shift> historyShifts;
final bool availableLoading;
final bool availableLoaded;
final bool historyLoading;
final bool historyLoaded;
final bool myShiftsLoaded;
final String searchQuery;
final String jobType;
final bool? profileComplete;
final String? errorMessage;
/// Creates a [ShiftsState].
const ShiftsState({
this.status = ShiftsStatus.initial,
this.myShifts = const [],
this.pendingShifts = const [],
this.cancelledShifts = const [],
this.availableShifts = const [],
this.historyShifts = const [],
this.myShifts = const <AssignedShift>[],
this.pendingShifts = const <PendingAssignment>[],
this.cancelledShifts = const <CancelledShift>[],
this.availableShifts = const <OpenShift>[],
this.historyShifts = const <CompletedShift>[],
this.availableLoading = false,
this.availableLoaded = false,
this.historyLoading = false,
this.historyLoaded = false,
this.myShiftsLoaded = false,
this.searchQuery = '',
this.jobType = 'all',
this.profileComplete,
this.errorMessage,
});
/// Current lifecycle status.
final ShiftsStatus status;
/// Assigned shifts for the selected week.
final List<AssignedShift> myShifts;
/// Pending assignments awaiting acceptance.
final List<PendingAssignment> pendingShifts;
/// Cancelled shift assignments.
final List<CancelledShift> cancelledShifts;
/// Open shifts available for application.
final List<OpenShift> availableShifts;
/// Completed shift history.
final List<CompletedShift> historyShifts;
/// Whether open shifts are currently loading.
final bool availableLoading;
/// Whether open shifts have been loaded at least once.
final bool availableLoaded;
/// Whether history is currently loading.
final bool historyLoading;
/// Whether history has been loaded at least once.
final bool historyLoaded;
/// Whether assigned shifts have been loaded at least once.
final bool myShiftsLoaded;
/// Current search query for open shifts.
final String searchQuery;
/// Whether the staff profile is complete.
final bool? profileComplete;
/// Error message key for display.
final String? errorMessage;
/// Creates a copy with the given fields replaced.
ShiftsState copyWith({
ShiftsStatus? status,
List<Shift>? myShifts,
List<Shift>? pendingShifts,
List<Shift>? cancelledShifts,
List<Shift>? availableShifts,
List<Shift>? historyShifts,
List<AssignedShift>? myShifts,
List<PendingAssignment>? pendingShifts,
List<CancelledShift>? cancelledShifts,
List<OpenShift>? availableShifts,
List<CompletedShift>? historyShifts,
bool? availableLoading,
bool? availableLoaded,
bool? historyLoading,
bool? historyLoaded,
bool? myShiftsLoaded,
String? searchQuery,
String? jobType,
bool? profileComplete,
String? errorMessage,
}) {
@@ -67,28 +95,26 @@ class ShiftsState extends Equatable {
historyLoaded: historyLoaded ?? this.historyLoaded,
myShiftsLoaded: myShiftsLoaded ?? this.myShiftsLoaded,
searchQuery: searchQuery ?? this.searchQuery,
jobType: jobType ?? this.jobType,
profileComplete: profileComplete ?? this.profileComplete,
errorMessage: errorMessage ?? this.errorMessage,
);
}
@override
List<Object?> get props => [
status,
myShifts,
pendingShifts,
cancelledShifts,
availableShifts,
historyShifts,
availableLoading,
availableLoaded,
historyLoading,
historyLoaded,
myShiftsLoaded,
searchQuery,
jobType,
profileComplete,
errorMessage,
];
List<Object?> get props => <Object?>[
status,
myShifts,
pendingShifts,
cancelledShifts,
availableShifts,
historyShifts,
availableLoading,
availableLoaded,
historyLoading,
historyLoaded,
myShiftsLoaded,
searchQuery,
profileComplete,
errorMessage,
];
}

View File

@@ -7,29 +7,30 @@ import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/shift_details/shift_details_bloc.dart';
import '../blocs/shift_details/shift_details_event.dart';
import '../blocs/shift_details/shift_details_state.dart';
import '../widgets/shift_details/shift_break_section.dart';
import '../widgets/shift_details/shift_date_time_section.dart';
import '../widgets/shift_details/shift_description_section.dart';
import '../widgets/shift_details/shift_details_bottom_bar.dart';
import '../widgets/shift_details/shift_details_header.dart';
import '../widgets/shift_details_page_skeleton.dart';
import '../widgets/shift_details/shift_location_section.dart';
import '../widgets/shift_details/shift_schedule_summary_section.dart';
import '../widgets/shift_details/shift_stats_row.dart';
import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_bloc.dart';
import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_event.dart';
import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_state.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_date_time_section.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_description_section.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_details_header.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_details_page_skeleton.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_location_section.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_details/shift_stats_row.dart';
/// Page displaying full details for a single shift.
///
/// Loads data via [ShiftDetailsBloc] from the V2 API.
class ShiftDetailsPage extends StatefulWidget {
final String shiftId;
final Shift shift;
/// Creates a [ShiftDetailsPage].
const ShiftDetailsPage({
super.key,
required this.shiftId,
required this.shift,
});
/// The shift row ID to load details for.
final String shiftId;
@override
State<ShiftDetailsPage> createState() => _ShiftDetailsPageState();
}
@@ -38,53 +39,27 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
bool _actionDialogOpen = false;
bool _isApplying = false;
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return time;
}
String _formatTime(DateTime dt) {
return DateFormat('h:mm a').format(dt);
}
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
return DateFormat('EEEE, MMMM d, y').format(date);
} catch (e) {
return dateStr;
}
String _formatDate(DateTime dt) {
return DateFormat('EEEE, MMMM d, y').format(dt);
}
double _calculateDuration(Shift shift) {
if (shift.startTime.isEmpty || shift.endTime.isEmpty) {
return 0;
}
try {
final s = shift.startTime.split(':').map(int.parse).toList();
final e = shift.endTime.split(':').map(int.parse).toList();
double hours = ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60;
if (hours < 0) hours += 24;
return hours.roundToDouble();
} catch (_) {
return 0;
}
double _calculateDuration(ShiftDetail detail) {
final int minutes = detail.endTime.difference(detail.startTime).inMinutes;
final double hours = minutes / 60;
return hours < 0 ? hours + 24 : hours;
}
@override
Widget build(BuildContext context) {
return BlocProvider<ShiftDetailsBloc>(
create: (_) => Modular.get<ShiftDetailsBloc>()
..add(
LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId),
),
..add(LoadShiftDetailsEvent(widget.shiftId)),
child: BlocConsumer<ShiftDetailsBloc, ShiftDetailsState>(
listener: (context, state) {
listener: (BuildContext context, ShiftDetailsState state) {
if (state is ShiftActionSuccess || state is ShiftDetailsError) {
_closeActionDialog(context);
}
@@ -117,20 +92,19 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
_isApplying = false;
}
},
builder: (context, state) {
if (state is ShiftDetailsLoading) {
builder: (BuildContext context, ShiftDetailsState state) {
if (state is! ShiftDetailsLoaded) {
return const ShiftDetailsPageSkeleton();
}
final Shift displayShift = widget.shift;
final i18n = Translations.of(context).staff_shifts.shift_details;
final isProfileComplete = state is ShiftDetailsLoaded
? state.isProfileComplete
: false;
final ShiftDetail detail = state.detail;
final dynamic i18n =
Translations.of(context).staff_shifts.shift_details;
final bool isProfileComplete = state.isProfileComplete;
final duration = _calculateDuration(displayShift);
final estimatedTotal =
displayShift.totalValue ?? (displayShift.hourlyRate * duration);
final double duration = _calculateDuration(detail);
final double hourlyRate = detail.hourlyRateCents / 100;
final double estimatedTotal = hourlyRate * duration;
return Scaffold(
appBar: UiAppBar(
@@ -138,12 +112,12 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
onLeadingPressed: () => Modular.to.toShifts(),
),
body: Column(
children: [
children: <Widget>[
Expanded(
child: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
if (!isProfileComplete)
Padding(
padding: const EdgeInsets.all(UiConstants.space6),
@@ -154,56 +128,38 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
icon: UiIcons.sparkles,
),
),
ShiftDetailsHeader(shift: displayShift),
ShiftDetailsHeader(detail: detail),
const Divider(height: 1, thickness: 0.5),
ShiftStatsRow(
estimatedTotal: estimatedTotal,
hourlyRate: displayShift.hourlyRate,
hourlyRate: hourlyRate,
duration: duration,
totalLabel: i18n.est_total,
hourlyRateLabel: i18n.hourly_rate,
hoursLabel: i18n.hours,
),
const Divider(height: 1, thickness: 0.5),
ShiftDateTimeSection(
date: displayShift.date,
endDate: displayShift.endDate,
startTime: displayShift.startTime,
endTime: displayShift.endTime,
date: detail.date,
startTime: detail.startTime,
endTime: detail.endTime,
shiftDateLabel: i18n.shift_date,
clockInLabel: i18n.start_time,
clockOutLabel: i18n.end_time,
),
const Divider(height: 1, thickness: 0.5),
ShiftScheduleSummarySection(shift: displayShift),
const Divider(height: 1, thickness: 0.5),
if (displayShift.breakInfo != null &&
displayShift.breakInfo!.duration !=
BreakDuration.none) ...[
ShiftBreakSection(
breakInfo: displayShift.breakInfo!,
breakTitle: i18n.break_title,
paidLabel: i18n.paid,
unpaidLabel: i18n.unpaid,
minLabel: i18n.min,
),
const Divider(height: 1, thickness: 0.5),
],
ShiftLocationSection(
shift: displayShift,
location: detail.location,
address: detail.address ?? '',
locationLabel: i18n.location,
tbdLabel: i18n.tbd,
getDirectionLabel: i18n.get_direction,
),
const Divider(height: 1, thickness: 0.5),
if (displayShift.description != null &&
displayShift.description!.isNotEmpty)
if (detail.description != null &&
detail.description!.isNotEmpty)
ShiftDescriptionSection(
description: displayShift.description!,
description: detail.description!,
descriptionLabel: i18n.job_description,
),
],
@@ -212,18 +168,18 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
),
if (isProfileComplete)
ShiftDetailsBottomBar(
shift: displayShift,
onApply: () => _bookShift(context, displayShift),
detail: detail,
onApply: () => _bookShift(context, detail),
onDecline: () => BlocProvider.of<ShiftDetailsBloc>(
context,
).add(DeclineShiftDetailsEvent(displayShift.id)),
).add(DeclineShiftDetailsEvent(detail.shiftId)),
onAccept: () =>
BlocProvider.of<ShiftDetailsBloc>(context).add(
BookShiftDetailsEvent(
displayShift.id,
roleId: displayShift.roleId,
),
),
BookShiftDetailsEvent(
detail.shiftId,
roleId: detail.roleId,
),
),
),
],
),
@@ -233,16 +189,15 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
);
}
void _bookShift(BuildContext context, Shift shift) {
final i18n = Translations.of(
context,
).staff_shifts.shift_details.book_dialog;
showDialog(
void _bookShift(BuildContext context, ShiftDetail detail) {
final dynamic i18n =
Translations.of(context).staff_shifts.shift_details.book_dialog;
showDialog<void>(
context: context,
builder: (ctx) => AlertDialog(
title: Text(i18n.title),
content: Text(i18n.message),
actions: [
builder: (BuildContext ctx) => AlertDialog(
title: Text(i18n.title as String),
content: Text(i18n.message as String),
actions: <Widget>[
TextButton(
onPressed: () => Modular.to.popSafe(),
child: Text(Translations.of(context).common.cancel),
@@ -250,12 +205,12 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
TextButton(
onPressed: () {
Modular.to.popSafe();
_showApplyingDialog(context, shift);
_showApplyingDialog(context, detail);
BlocProvider.of<ShiftDetailsBloc>(context).add(
BookShiftDetailsEvent(
shift.id,
roleId: shift.roleId,
date: DateTime.tryParse(shift.date),
detail.shiftId,
roleId: detail.roleId,
date: detail.date,
),
);
},
@@ -269,22 +224,21 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
);
}
void _showApplyingDialog(BuildContext context, Shift shift) {
void _showApplyingDialog(BuildContext context, ShiftDetail detail) {
if (_actionDialogOpen) return;
_actionDialogOpen = true;
_isApplying = true;
final i18n = Translations.of(
context,
).staff_shifts.shift_details.applying_dialog;
showDialog(
final dynamic i18n =
Translations.of(context).staff_shifts.shift_details.applying_dialog;
showDialog<void>(
context: context,
useRootNavigator: true,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: Text(i18n.title),
builder: (BuildContext ctx) => AlertDialog(
title: Text(i18n.title as String),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
const SizedBox(
height: 36,
width: 36,
@@ -292,24 +246,16 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
),
const SizedBox(height: UiConstants.space4),
Text(
shift.title,
detail.title,
style: UiTypography.body2b.textPrimary,
textAlign: TextAlign.center,
),
const SizedBox(height: 6),
Text(
'${_formatDate(shift.date)} ${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}',
'${_formatDate(detail.date)} \u2022 ${_formatTime(detail.startTime)} - ${_formatTime(detail.endTime)}',
style: UiTypography.body3r.textSecondary,
textAlign: TextAlign.center,
),
if (shift.clientName.isNotEmpty) ...[
const SizedBox(height: 6),
Text(
shift.clientName,
style: UiTypography.body3r.textSecondary,
textAlign: TextAlign.center,
),
],
],
),
),
@@ -325,14 +271,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
}
void _showEligibilityErrorDialog(BuildContext context) {
showDialog(
showDialog<void>(
context: context,
builder: (BuildContext ctx) => AlertDialog(
backgroundColor: UiColors.bgPopup,
shape: RoundedRectangleBorder(borderRadius: UiConstants.radiusLg),
title: Row(
spacing: UiConstants.space2,
children: [
children: <Widget>[
const Icon(UiIcons.warning, color: UiColors.error),
Expanded(
child: Text(
@@ -342,16 +288,16 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
],
),
content: Text(
"You are missing required certifications or documents to claim this shift. Please upload them to continue.",
'You are missing required certifications or documents to claim this shift. Please upload them to continue.',
style: UiTypography.body2r.textSecondary,
),
actions: [
actions: <Widget>[
UiButton.secondary(
text: "Cancel",
text: 'Cancel',
onPressed: () => Navigator.of(ctx).pop(),
),
UiButton.primary(
text: "Go to Certificates",
text: 'Go to Certificates',
onPressed: () {
Modular.to.popSafe();
Modular.to.toCertificates();

View File

@@ -4,12 +4,13 @@ import 'package:flutter_modular/flutter_modular.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
import 'package:krow_domain/krow_domain.dart';
import '../blocs/shifts/shifts_bloc.dart';
import '../utils/shift_tab_type.dart';
import '../widgets/shifts_page_skeleton.dart';
import '../widgets/tabs/my_shifts_tab.dart';
import '../widgets/tabs/find_shifts_tab.dart';
import '../widgets/tabs/history_shifts_tab.dart';
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
import 'package:staff_shifts/src/presentation/utils/shift_tab_type.dart';
import 'package:staff_shifts/src/presentation/widgets/shifts_page_skeleton.dart';
import 'package:staff_shifts/src/presentation/widgets/tabs/my_shifts_tab.dart';
import 'package:staff_shifts/src/presentation/widgets/tabs/find_shifts_tab.dart';
import 'package:staff_shifts/src/presentation/widgets/tabs/history_shifts_tab.dart';
class ShiftsPage extends StatefulWidget {
final ShiftTabType? initialTab;
@@ -102,13 +103,13 @@ class _ShiftsPageState extends State<ShiftsPage> {
_bloc.add(const LoadAvailableShiftsEvent(force: true));
}
final bool baseLoaded = state.status == ShiftsStatus.loaded;
final List<Shift> myShifts = state.myShifts;
final List<Shift> availableJobs = state.availableShifts;
final List<AssignedShift> myShifts = state.myShifts;
final List<OpenShift> availableJobs = state.availableShifts;
final bool availableLoading = state.availableLoading;
final bool availableLoaded = state.availableLoaded;
final List<Shift> pendingAssignments = state.pendingShifts;
final List<Shift> cancelledShifts = state.cancelledShifts;
final List<Shift> historyShifts = state.historyShifts;
final List<PendingAssignment> pendingAssignments = state.pendingShifts;
final List<CancelledShift> cancelledShifts = state.cancelledShifts;
final List<CompletedShift> historyShifts = state.historyShifts;
final bool historyLoading = state.historyLoading;
final bool historyLoaded = state.historyLoaded;
final bool myShiftsLoaded = state.myShiftsLoaded;
@@ -235,11 +236,11 @@ class _ShiftsPageState extends State<ShiftsPage> {
Widget _buildTabContent(
ShiftsState state,
List<Shift> myShifts,
List<Shift> pendingAssignments,
List<Shift> cancelledShifts,
List<Shift> availableJobs,
List<Shift> historyShifts,
List<AssignedShift> myShifts,
List<PendingAssignment> pendingAssignments,
List<CancelledShift> cancelledShifts,
List<OpenShift> availableJobs,
List<CompletedShift> historyShifts,
bool availableLoading,
bool historyLoading,
) {

View File

@@ -4,24 +4,31 @@ import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
import 'package:krow_core/core.dart'; // For modular navigation
import 'package:krow_core/core.dart';
/// Card widget displaying an assigned shift summary.
class MyShiftCard extends StatefulWidget {
final Shift shift;
final bool historyMode;
final VoidCallback? onAccept;
final VoidCallback? onDecline;
final VoidCallback? onRequestSwap;
/// Creates a [MyShiftCard].
const MyShiftCard({
super.key,
required this.shift,
this.historyMode = false,
this.onAccept,
this.onDecline,
this.onRequestSwap,
});
/// The assigned shift entity.
final AssignedShift shift;
/// Callback when the shift is accepted.
final VoidCallback? onAccept;
/// Callback when the shift is declined.
final VoidCallback? onDecline;
/// Callback when a swap is requested.
final VoidCallback? onRequestSwap;
@override
State<MyShiftCard> createState() => _MyShiftCardState();
}
@@ -29,141 +36,88 @@ class MyShiftCard extends StatefulWidget {
class _MyShiftCardState extends State<MyShiftCard> {
bool _isSubmitted = false;
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
// Date doesn't matter for time formatting
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return time;
}
}
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final d = DateTime(date.year, date.month, date.day);
if (d == today) return 'Today';
if (d == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
} catch (e) {
return dateStr;
}
String _formatDate(DateTime date) {
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime tomorrow = today.add(const Duration(days: 1));
final DateTime d = DateTime(date.year, date.month, date.day);
if (d == today) return 'Today';
if (d == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
}
double _calculateDuration() {
if (widget.shift.startTime.isEmpty || widget.shift.endTime.isEmpty) {
return 0;
}
try {
final s = widget.shift.startTime.split(':').map(int.parse).toList();
final e = widget.shift.endTime.split(':').map(int.parse).toList();
double hours = ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60;
if (hours < 0) hours += 24;
return hours.roundToDouble();
} catch (_) {
return 0;
}
final int minutes =
widget.shift.endTime.difference(widget.shift.startTime).inMinutes;
double hours = minutes / 60;
if (hours < 0) hours += 24;
return hours.roundToDouble();
}
String _getShiftType() {
// Handling potential localization key availability
try {
final String orderType = (widget.shift.orderType ?? '').toUpperCase();
if (orderType == 'PERMANENT') {
return t.staff_shifts.filter.long_term;
switch (widget.shift.orderType) {
case OrderType.permanent:
return t.staff_shifts.filter.long_term;
case OrderType.recurring:
return t.staff_shifts.filter.multi_day;
case OrderType.oneTime:
default:
return t.staff_shifts.filter.one_day;
}
if (orderType == 'RECURRING') {
return t.staff_shifts.filter.multi_day;
}
if (widget.shift.durationDays != null &&
widget.shift.durationDays! > 30) {
return t.staff_shifts.filter.long_term;
}
if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) {
return t.staff_shifts.filter.multi_day;
}
return t.staff_shifts.filter.one_day;
} catch (_) {
return "One Day";
return 'One Day';
}
}
@override
Widget build(BuildContext context) {
final duration = _calculateDuration();
final estimatedTotal = (widget.shift.hourlyRate) * duration;
final double duration = _calculateDuration();
final double hourlyRate = widget.shift.hourlyRateCents / 100;
final double estimatedTotal = hourlyRate * duration;
// Status Logic
String? status = widget.shift.status;
final AssignmentStatus status = widget.shift.status;
Color statusColor = UiColors.primary;
Color statusBg = UiColors.primary;
String statusText = '';
IconData? statusIcon;
// Fallback localization if keys missing
try {
if (status == 'confirmed') {
statusText = t.staff_shifts.status.confirmed;
statusColor = UiColors.textLink;
statusBg = UiColors.primary;
} else if (status == 'checked_in') {
statusText = context.t.staff_shifts.my_shift_card.checked_in;
statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess;
} else if (status == 'pending' || status == 'open') {
statusText = t.staff_shifts.status.act_now;
statusColor = UiColors.destructive;
statusBg = UiColors.destructive;
} else if (status == 'swap') {
statusText = t.staff_shifts.status.swap_requested;
statusColor = UiColors.textWarning;
statusBg = UiColors.textWarning;
statusIcon = UiIcons.swap;
} else if (status == 'completed') {
statusText = t.staff_shifts.status.completed;
statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess;
} else if (status == 'no_show') {
statusText = t.staff_shifts.status.no_show;
statusColor = UiColors.destructive;
statusBg = UiColors.destructive;
switch (status) {
case AssignmentStatus.accepted:
statusText = t.staff_shifts.status.confirmed;
statusColor = UiColors.textLink;
statusBg = UiColors.primary;
case AssignmentStatus.checkedIn:
statusText = context.t.staff_shifts.my_shift_card.checked_in;
statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess;
case AssignmentStatus.assigned:
statusText = t.staff_shifts.status.act_now;
statusColor = UiColors.destructive;
statusBg = UiColors.destructive;
case AssignmentStatus.swapRequested:
statusText = t.staff_shifts.status.swap_requested;
statusColor = UiColors.textWarning;
statusBg = UiColors.textWarning;
statusIcon = UiIcons.swap;
case AssignmentStatus.completed:
statusText = t.staff_shifts.status.completed;
statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess;
default:
statusText = status.toJson().toUpperCase();
}
} catch (_) {
statusText = status?.toUpperCase() ?? "";
statusText = status.toJson().toUpperCase();
}
final schedules = widget.shift.schedules ?? <ShiftSchedule>[];
final hasSchedules = schedules.isNotEmpty;
final List<ShiftSchedule> visibleSchedules = schedules.length <= 5
? schedules
: schedules.take(3).toList();
final int remainingSchedules = schedules.length <= 5
? 0
: schedules.length - 3;
final String scheduleRange = hasSchedules
? () {
final first = schedules.first.date;
final last = schedules.last.date;
if (first == last) {
return _formatDate(first);
}
return '${_formatDate(first)} ${_formatDate(last)}';
}()
: '';
return GestureDetector(
onTap: () {
Modular.to.toShiftDetails(widget.shift);
Modular.to.toShiftDetailsById(widget.shift.shiftId);
},
child: Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3),
@@ -265,23 +219,13 @@ class _MyShiftCardState extends State<MyShiftCard> {
color: UiColors.primary.withValues(alpha: 0.09),
),
),
child: widget.shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
child: Image.network(
widget.shift.logoUrl!,
fit: BoxFit.contain,
),
)
: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: UiConstants.iconMd,
),
),
child: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: UiConstants.iconMd,
),
),
),
const SizedBox(width: UiConstants.space3),
@@ -298,12 +242,12 @@ class _MyShiftCardState extends State<MyShiftCard> {
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
widget.shift.title,
widget.shift.roleName,
style: UiTypography.body2m.textPrimary,
overflow: TextOverflow.ellipsis,
),
Text(
widget.shift.clientName,
widget.shift.location,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
),
@@ -315,11 +259,11 @@ class _MyShiftCardState extends State<MyShiftCard> {
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"\$${estimatedTotal.toStringAsFixed(0)}",
'\$${estimatedTotal.toStringAsFixed(0)}',
style: UiTypography.title1m.textPrimary,
),
Text(
"\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h",
'\$${hourlyRate.toInt()}/hr \u00b7 ${duration.toInt()}h',
style: UiTypography.footnote2r.textSecondary,
),
],
@@ -329,134 +273,36 @@ class _MyShiftCardState extends State<MyShiftCard> {
const SizedBox(height: UiConstants.space2),
// Date & Time
if (hasSchedules) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
crossAxisAlignment: CrossAxisAlignment.center,
children: [
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
scheduleRange,
style:
UiTypography.footnote2r.textSecondary,
),
],
),
const SizedBox(height: UiConstants.space2),
Text(
'${schedules.length} schedules',
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
const SizedBox(height: UiConstants.space1),
...visibleSchedules.map(
(schedule) => Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
'${_formatDate(schedule.date)}, ${_formatTime(schedule.startTime)} ${_formatTime(schedule.endTime)}',
style: UiTypography.footnote2r.copyWith(
color: UiColors.primary,
),
),
),
),
if (remainingSchedules > 0)
Text(
'+$remainingSchedules more schedules',
style: UiTypography.footnote2r.copyWith(
color: UiColors.primary.withValues(
alpha: 0.7,
),
),
),
],
),
] else if (widget.shift.durationDays != null &&
widget.shift.durationDays! > 1) ...[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.primary,
),
const SizedBox(width: UiConstants.space1),
Text(
t.staff_shifts.details.days(
days: widget.shift.durationDays!,
),
style: UiTypography.footnote2m.copyWith(
color: UiColors.primary,
),
),
],
),
const SizedBox(height: UiConstants.space1),
Padding(
padding: const EdgeInsets.only(bottom: 2),
child: Text(
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)} ${_formatTime(widget.shift.endTime)}',
style: UiTypography.footnote2r.copyWith(
color: UiColors.primary,
),
),
),
if (widget.shift.durationDays! > 1)
Text(
'... +${widget.shift.durationDays! - 1} more days',
style: UiTypography.footnote2r.copyWith(
color: UiColors.primary.withValues(
alpha: 0.7,
),
),
),
],
),
] else ...[
Row(
children: [
const Icon(
UiIcons.calendar,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
_formatDate(widget.shift.date),
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
"${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}",
style: UiTypography.footnote1r.textSecondary,
),
],
),
],
Row(
children: <Widget>[
const Icon(
UiIcons.calendar,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
_formatDate(widget.shift.date),
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary,
),
const SizedBox(width: UiConstants.space1),
Text(
'${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}',
style: UiTypography.footnote1r.textSecondary,
),
],
),
const SizedBox(height: UiConstants.space1),
// Location
Row(
children: [
children: <Widget>[
const Icon(
UiIcons.mapPin,
size: UiConstants.iconXs,
@@ -465,9 +311,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(
widget.shift.locationAddress.isNotEmpty
? widget.shift.locationAddress
: widget.shift.location,
widget.shift.location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis,
),
@@ -479,7 +323,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
),
],
),
if (status == 'completed') ...[
if (status == AssignmentStatus.completed) ...[
const SizedBox(height: UiConstants.space4),
const Divider(),
const SizedBox(height: UiConstants.space2),

View File

@@ -3,72 +3,49 @@ import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:design_system/design_system.dart';
/// Card displaying a pending assignment with accept/decline actions.
class ShiftAssignmentCard extends StatelessWidget {
final Shift shift;
final VoidCallback onConfirm;
final VoidCallback onDecline;
final bool isConfirming;
/// Creates a [ShiftAssignmentCard].
const ShiftAssignmentCard({
super.key,
required this.shift,
required this.assignment,
required this.onConfirm,
required this.onDecline,
this.isConfirming = false,
});
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return time;
}
}
/// The pending assignment entity.
final PendingAssignment assignment;
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final tomorrow = today.add(const Duration(days: 1));
final d = DateTime(date.year, date.month, date.day);
/// Callback for accepting the assignment.
final VoidCallback onConfirm;
if (d == today) return 'Today';
if (d == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
} catch (e) {
return dateStr;
}
}
/// Callback for declining the assignment.
final VoidCallback onDecline;
double _calculateHours(String start, String end) {
if (start.isEmpty || end.isEmpty) return 0;
try {
final s = start.split(':').map(int.parse).toList();
final e = end.split(':').map(int.parse).toList();
return ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60;
} catch (_) {
return 0;
}
/// Whether the confirm action is in progress.
final bool isConfirming;
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
String _formatDate(DateTime date) {
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime tomorrow = today.add(const Duration(days: 1));
final DateTime d = DateTime(date.year, date.month, date.day);
if (d == today) return 'Today';
if (d == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
}
@override
Widget build(BuildContext context) {
final hours = _calculateHours(shift.startTime, shift.endTime);
final totalPay = shift.hourlyRate * hours;
return Container(
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
boxShadow: [
boxShadow: <BoxShadow>[
BoxShadow(
color: UiColors.black.withValues(alpha: 0.05),
blurRadius: 2,
@@ -77,155 +54,95 @@ class ShiftAssignmentCard extends StatelessWidget {
],
),
child: Column(
children: [
// Header
children: <Widget>[
Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Card content starts directly as per prototype
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Logo
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
UiColors.primary.withValues(alpha: 0.09),
UiColors.primary.withValues(alpha: 0.03),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: UiColors.primary.withValues(alpha: 0.09),
),
),
child: shift.logoUrl != null
? ClipRRect(
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
child: Image.network(
shift.logoUrl!,
fit: BoxFit.contain,
),
)
: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: 20,
),
),
children: <Widget>[
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.primary.withValues(alpha: 0.09),
UiColors.primary.withValues(alpha: 0.03),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
const SizedBox(width: UiConstants.space3),
// Details
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
shift.title,
style: UiTypography.body2m.textPrimary,
overflow: TextOverflow.ellipsis,
),
Text(
shift.clientName,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
),
],
),
),
const SizedBox(width: UiConstants.space2),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
"\$${totalPay.toStringAsFixed(0)}",
style: UiTypography.title1m.textPrimary,
),
Text(
"\$${shift.hourlyRate.toInt()}/hr · ${hours.toInt()}h",
style: UiTypography.footnote2r.textSecondary,
),
],
),
],
borderRadius:
BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(
color: UiColors.primary.withValues(alpha: 0.09),
),
),
child: const Center(
child: Icon(
UiIcons.briefcase,
color: UiColors.primary,
size: 20,
),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
assignment.roleName,
style: UiTypography.body2m.textPrimary,
overflow: TextOverflow.ellipsis,
),
if (assignment.title.isNotEmpty)
Text(
assignment.title,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
),
const SizedBox(height: UiConstants.space3),
Row(
children: <Widget>[
const Icon(UiIcons.calendar,
size: 12, color: UiColors.iconSecondary),
const SizedBox(width: 4),
Text(
_formatDate(assignment.startTime),
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(height: UiConstants.space3),
// Date & Time
Row(
children: [
const Icon(
UiIcons.calendar,
size: 12,
color: UiColors.iconSecondary,
),
const SizedBox(width: 4),
Text(
_formatDate(shift.date),
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: 12,
color: UiColors.iconSecondary,
),
const SizedBox(width: 4),
Text(
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
style: UiTypography.footnote1r.textSecondary,
),
],
),
const SizedBox(height: 4),
// Location
Row(
children: [
const Icon(
UiIcons.mapPin,
size: 12,
color: UiColors.iconSecondary,
),
const SizedBox(width: 4),
Expanded(
child: Text(
shift.locationAddress.isNotEmpty
? shift.locationAddress
: shift.location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis,
),
),
],
const SizedBox(width: UiConstants.space3),
const Icon(UiIcons.clock,
size: 12, color: UiColors.iconSecondary),
const SizedBox(width: 4),
Text(
'${_formatTime(assignment.startTime)} - ${_formatTime(assignment.endTime)}',
style: UiTypography.footnote1r.textSecondary,
),
],
),
),
],
const SizedBox(height: 4),
Row(
children: <Widget>[
const Icon(UiIcons.mapPin,
size: 12, color: UiColors.iconSecondary),
const SizedBox(width: 4),
Expanded(
child: Text(
assignment.location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis,
),
),
],
),
],
),
),
],
),
),
// Actions
Container(
padding: const EdgeInsets.all(UiConstants.space2),
decoration: const BoxDecoration(
@@ -236,17 +153,14 @@ class ShiftAssignmentCard extends StatelessWidget {
),
),
child: Row(
children: [
children: <Widget>[
Expanded(
child: TextButton(
onPressed: onDecline,
style: TextButton.styleFrom(
foregroundColor: UiColors.destructive,
),
child: Text(
"Decline", // Fallback if translation is broken
style: UiTypography.body2m.textError,
),
child: Text('Decline', style: UiTypography.body2m.textError),
),
),
const SizedBox(width: UiConstants.space2),
@@ -258,7 +172,8 @@ class ShiftAssignmentCard extends StatelessWidget {
foregroundColor: UiColors.white,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(UiConstants.radiusMdValue),
borderRadius: BorderRadius.circular(
UiConstants.radiusMdValue),
),
),
child: isConfirming
@@ -270,10 +185,7 @@ class ShiftAssignmentCard extends StatelessWidget {
color: UiColors.white,
),
)
: Text(
"Accept", // Fallback
style: UiTypography.body2m.white,
),
: Text('Accept', style: UiTypography.body2m.white),
),
),
],

View File

@@ -1,62 +0,0 @@
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
/// A section displaying shift break details (duration and payment status).
class ShiftBreakSection extends StatelessWidget {
/// The break information.
final Break breakInfo;
/// Localization string for break section title.
final String breakTitle;
/// Localization string for paid status.
final String paidLabel;
/// Localization string for unpaid status.
final String unpaidLabel;
/// Localization string for minutes ("min").
final String minLabel;
/// Creates a [ShiftBreakSection].
const ShiftBreakSection({
super.key,
required this.breakInfo,
required this.breakTitle,
required this.paidLabel,
required this.unpaidLabel,
required this.minLabel,
});
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
breakTitle,
style: UiTypography.titleUppercase4b.textSecondary,
),
const SizedBox(height: UiConstants.space2),
Row(
children: [
const Icon(
UiIcons.breakIcon,
size: 20,
color: UiColors.primary,
),
const SizedBox(width: UiConstants.space2),
Text(
"${breakInfo.duration.minutes} $minLabel (${breakInfo.isBreakPaid ? paidLabel : unpaidLabel})",
style: UiTypography.headline5m.textPrimary,
),
],
),
],
),
);
}
}

View File

@@ -4,17 +4,25 @@ import 'package:intl/intl.dart';
/// A section displaying the date and the shift's start/end times.
class ShiftDateTimeSection extends StatelessWidget {
/// The ISO string of the date.
final String date;
/// Creates a [ShiftDateTimeSection].
const ShiftDateTimeSection({
super.key,
required this.date,
required this.startTime,
required this.endTime,
required this.shiftDateLabel,
required this.clockInLabel,
required this.clockOutLabel,
});
/// The end date string (ISO).
final String? endDate;
/// The shift date.
final DateTime date;
/// The start time string (HH:mm).
final String startTime;
/// Scheduled start time.
final DateTime startTime;
/// The end time string (HH:mm).
final String endTime;
/// Scheduled end time.
final DateTime endTime;
/// Localization string for shift date.
final String shiftDateLabel;
@@ -25,40 +33,9 @@ class ShiftDateTimeSection extends StatelessWidget {
/// Localization string for clock out time.
final String clockOutLabel;
/// Creates a [ShiftDateTimeSection].
const ShiftDateTimeSection({
super.key,
required this.date,
required this.endDate,
required this.startTime,
required this.endTime,
required this.shiftDateLabel,
required this.clockInLabel,
required this.clockOutLabel,
});
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
String _formatTime(String time) {
if (time.isEmpty) return '';
try {
final parts = time.split(':');
final hour = int.parse(parts[0]);
final minute = int.parse(parts[1]);
final dt = DateTime(2022, 1, 1, hour, minute);
return DateFormat('h:mm a').format(dt);
} catch (e) {
return time;
}
}
String _formatDate(String dateStr) {
if (dateStr.isEmpty) return '';
try {
final date = DateTime.parse(dateStr);
return DateFormat('EEEE, MMMM d, y').format(date);
} catch (e) {
return dateStr;
}
}
String _formatDate(DateTime dt) => DateFormat('EEEE, MMMM d, y').format(dt);
@override
Widget build(BuildContext context) {
@@ -66,17 +43,17 @@ class ShiftDateTimeSection extends StatelessWidget {
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
shiftDateLabel,
style: UiTypography.titleUppercase4b.textSecondary,
),
const SizedBox(height: UiConstants.space2),
Row(
children: [
children: <Widget>[
const Icon(
UiIcons.calendar,
size: 20,
@@ -91,36 +68,9 @@ class ShiftDateTimeSection extends StatelessWidget {
),
],
),
if (endDate != null) ...[
const SizedBox(height: UiConstants.space6),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'SHIFT END DATE',
style: UiTypography.titleUppercase4b.textSecondary,
),
const SizedBox(height: UiConstants.space2),
Row(
children: [
const Icon(
UiIcons.calendar,
size: 20,
color: UiColors.primary,
),
const SizedBox(width: UiConstants.space2),
Text(
_formatDate(endDate!),
style: UiTypography.headline5m.textPrimary,
),
],
),
],
),
],
const SizedBox(height: UiConstants.space6),
Row(
children: [
children: <Widget>[
Expanded(child: _buildTimeBox(clockInLabel, startTime)),
const SizedBox(width: UiConstants.space4),
Expanded(child: _buildTimeBox(clockOutLabel, endTime)),
@@ -131,7 +81,7 @@ class ShiftDateTimeSection extends StatelessWidget {
);
}
Widget _buildTimeBox(String label, String time) {
Widget _buildTimeBox(String label, DateTime time) {
return Container(
padding: const EdgeInsets.all(UiConstants.space3),
decoration: BoxDecoration(
@@ -139,7 +89,7 @@ class ShiftDateTimeSection extends StatelessWidget {
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: Column(
children: [
children: <Widget>[
Text(
label,
style: UiTypography.footnote2b.copyWith(

View File

@@ -7,8 +7,17 @@ import 'package:krow_domain/krow_domain.dart';
/// A bottom action bar containing contextual buttons based on shift status.
class ShiftDetailsBottomBar extends StatelessWidget {
/// The current shift.
final Shift shift;
/// Creates a [ShiftDetailsBottomBar].
const ShiftDetailsBottomBar({
super.key,
required this.detail,
required this.onApply,
required this.onDecline,
required this.onAccept,
});
/// The shift detail entity.
final ShiftDetail detail;
/// Callback for applying/booking a shift.
final VoidCallback onApply;
@@ -19,19 +28,9 @@ class ShiftDetailsBottomBar extends StatelessWidget {
/// Callback for accepting a shift.
final VoidCallback onAccept;
/// Creates a [ShiftDetailsBottomBar].
const ShiftDetailsBottomBar({
super.key,
required this.shift,
required this.onApply,
required this.onDecline,
required this.onAccept,
});
@override
Widget build(BuildContext context) {
final String status = shift.status ?? 'open';
final i18n = Translations.of(context).staff_shifts.shift_details;
final dynamic i18n = Translations.of(context).staff_shifts.shift_details;
return Container(
padding: EdgeInsets.fromLTRB(
@@ -40,16 +39,17 @@ class ShiftDetailsBottomBar extends StatelessWidget {
UiConstants.space5,
MediaQuery.of(context).padding.bottom + UiConstants.space4,
),
decoration: BoxDecoration(
decoration: const BoxDecoration(
color: UiColors.white,
border: Border(top: BorderSide(color: UiColors.border)),
),
child: _buildButtons(status, i18n, context),
child: _buildButtons(i18n, context),
);
}
Widget _buildButtons(String status, dynamic i18n, BuildContext context) {
if (status == 'confirmed') {
Widget _buildButtons(dynamic i18n, BuildContext context) {
// If worker has an accepted assignment, show clock-in
if (detail.assignmentStatus == AssignmentStatus.accepted) {
return UiButton.primary(
onPressed: () => Modular.to.toClockIn(),
fullWidth: true,
@@ -57,9 +57,10 @@ class ShiftDetailsBottomBar extends StatelessWidget {
);
}
if (status == 'pending') {
// If worker has a pending (assigned) assignment, show accept/decline
if (detail.assignmentStatus == AssignmentStatus.assigned) {
return Row(
children: [
children: <Widget>[
Expanded(
child: UiButton.secondary(
onPressed: onDecline,
@@ -70,14 +71,17 @@ class ShiftDetailsBottomBar extends StatelessWidget {
Expanded(
child: UiButton.primary(
onPressed: onAccept,
child: Text(i18n.accept_shift, style: UiTypography.body2b.white),
child:
Text(i18n.accept_shift, style: UiTypography.body2b.white),
),
),
],
);
}
if (status == 'open' || status == 'available') {
// If worker has no assignment and no pending application, show apply
if (detail.assignmentStatus == null &&
detail.applicationStatus == null) {
return UiButton.primary(
onPressed: onApply,
fullWidth: true,

View File

@@ -2,13 +2,16 @@ import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
/// A header widget for the shift details page displaying the role, client name, and address.
class ShiftDetailsHeader extends StatelessWidget {
/// The shift entity containing the header information.
final Shift shift;
/// Size of the role icon container in the shift details header.
const double _kIconContainerSize = 68.0;
/// A header widget for the shift details page displaying the role and address.
class ShiftDetailsHeader extends StatelessWidget {
/// Creates a [ShiftDetailsHeader].
const ShiftDetailsHeader({super.key, required this.shift});
const ShiftDetailsHeader({super.key, required this.detail});
/// The shift detail entity.
final ShiftDetail detail;
@override
Widget build(BuildContext context) {
@@ -17,15 +20,14 @@ class ShiftDetailsHeader extends StatelessWidget {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
spacing: UiConstants.space4,
children: [
// Icon + role name + client name
children: <Widget>[
Row(
crossAxisAlignment: CrossAxisAlignment.center,
spacing: UiConstants.space4,
children: [
children: <Widget>[
Container(
width: 68,
height: 68,
width: _kIconContainerSize,
height: _kIconContainerSize,
decoration: BoxDecoration(
color: UiColors.primary.withAlpha(20),
borderRadius: UiConstants.radiusLg,
@@ -42,19 +44,17 @@ class ShiftDetailsHeader extends StatelessWidget {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(shift.title, style: UiTypography.headline1b.textPrimary),
Text(shift.clientName, style: UiTypography.body1m.textSecondary),
children: <Widget>[
Text(detail.title, style: UiTypography.headline1b.textPrimary),
Text(detail.roleName, style: UiTypography.body1m.textSecondary),
],
),
),
],
),
// Location address
Row(
spacing: UiConstants.space1,
children: [
children: <Widget>[
const Icon(
UiIcons.mapPin,
size: 16,
@@ -62,7 +62,7 @@ class ShiftDetailsHeader extends StatelessWidget {
),
Expanded(
child: Text(
shift.locationAddress,
detail.address ?? detail.location,
style: UiTypography.body2r.textSecondary,
),
),

View File

@@ -1,121 +0,0 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:google_maps_flutter/google_maps_flutter.dart';
/// A widget that displays the shift location on an interactive Google Map.
class ShiftLocationMap extends StatefulWidget {
/// The shift entity containing location and coordinates.
final Shift shift;
/// The height of the map widget.
final double height;
/// The border radius for the map container.
final double borderRadius;
/// Creates a [ShiftLocationMap].
const ShiftLocationMap({
super.key,
required this.shift,
this.height = 120,
this.borderRadius = 8,
});
@override
State<ShiftLocationMap> createState() => _ShiftLocationMapState();
}
class _ShiftLocationMapState extends State<ShiftLocationMap> {
late final CameraPosition _initialPosition;
final Set<Marker> _markers = {};
@override
void initState() {
super.initState();
// Default to a fallback coordinate if latitude/longitude are null.
// In a real app, you might want to geocode the address if coordinates are missing.
final double lat = widget.shift.latitude ?? 0.0;
final double lng = widget.shift.longitude ?? 0.0;
final LatLng position = LatLng(lat, lng);
_initialPosition = CameraPosition(
target: position,
zoom: 15,
);
_markers.add(
Marker(
markerId: MarkerId(widget.shift.id),
position: position,
infoWindow: InfoWindow(
title: widget.shift.location,
snippet: widget.shift.locationAddress,
),
),
);
}
@override
Widget build(BuildContext context) {
// If coordinates are missing, we show a placeholder.
if (widget.shift.latitude == null || widget.shift.longitude == null) {
return _buildPlaceholder(context, "Coordinates unavailable");
}
return Container(
height: widget.height * 1.25, // Slightly taller to accommodate map controls
width: double.infinity,
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(color: UiColors.border),
),
clipBehavior: Clip.antiAlias,
child: GoogleMap(
initialCameraPosition: _initialPosition,
markers: _markers,
liteModeEnabled: true, // Optimized for static-like display in details page
scrollGesturesEnabled: false,
zoomGesturesEnabled: true,
tiltGesturesEnabled: false,
rotateGesturesEnabled: false,
myLocationButtonEnabled: false,
mapToolbarEnabled: false,
compassEnabled: false,
),
);
}
Widget _buildPlaceholder(BuildContext context, String message) {
return Container(
height: widget.height,
width: double.infinity,
decoration: BoxDecoration(
color: UiColors.bgThird,
borderRadius: BorderRadius.circular(widget.borderRadius),
border: Border.all(color: UiColors.border),
),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
UiIcons.mapPin,
size: 32,
color: UiColors.primary,
),
if (message.isNotEmpty) ...[
const SizedBox(height: UiConstants.space2),
Text(
message,
style: UiTypography.footnote2r.textSecondary,
),
],
],
),
),
);
}
}

View File

@@ -1,14 +1,25 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:url_launcher/url_launcher.dart';
import 'shift_location_map.dart';
/// A section displaying the shift's location, address, map, and "Get direction" action.
/// A section displaying the shift's location, address, and "Get direction" action.
class ShiftLocationSection extends StatelessWidget {
/// The shift entity containing location data.
final Shift shift;
/// Creates a [ShiftLocationSection].
const ShiftLocationSection({
super.key,
required this.location,
required this.address,
required this.locationLabel,
required this.tbdLabel,
required this.getDirectionLabel,
});
/// Human-readable location label.
final String location;
/// Street address.
final String address;
/// Localization string for location section title.
final String locationLabel;
@@ -19,15 +30,6 @@ class ShiftLocationSection extends StatelessWidget {
/// Localization string for "Get direction".
final String getDirectionLabel;
/// Creates a [ShiftLocationSection].
const ShiftLocationSection({
super.key,
required this.shift,
required this.locationLabel,
required this.tbdLabel,
required this.getDirectionLabel,
});
@override
Widget build(BuildContext context) {
return Padding(
@@ -36,33 +38,32 @@ class ShiftLocationSection extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: UiConstants.space4,
children: [
children: <Widget>[
Column(
spacing: UiConstants.space2,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
children: <Widget>[
Text(
locationLabel,
style: UiTypography.titleUppercase4b.textSecondary,
),
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
spacing: UiConstants.space4,
children: [
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Text(
shift.location.isEmpty ? tbdLabel : shift.location,
location.isEmpty ? tbdLabel : location,
style: UiTypography.title1m.textPrimary,
overflow: TextOverflow.ellipsis,
),
if (shift.locationAddress.isNotEmpty)
if (address.isNotEmpty)
Text(
shift.locationAddress,
address,
style: UiTypography.body2r.textSecondary,
maxLines: 2,
overflow: TextOverflow.ellipsis,
@@ -96,28 +97,19 @@ class ShiftLocationSection extends StatelessWidget {
),
],
),
ShiftLocationMap(
shift: shift,
borderRadius: UiConstants.radiusBase,
),
],
),
);
}
Future<void> _openDirections(BuildContext context) async {
final destination = (shift.latitude != null && shift.longitude != null)
? '${shift.latitude},${shift.longitude}'
: Uri.encodeComponent(
shift.locationAddress.isNotEmpty
? shift.locationAddress
: shift.location,
);
final String destination = Uri.encodeComponent(
address.isNotEmpty ? address : location,
);
final String url =
'https://www.google.com/maps/dir/?api=1&destination=$destination';
final uri = Uri.parse(url);
final Uri uri = Uri.parse(url);
if (await canLaunchUrl(uri)) {
await launchUrl(uri, mode: LaunchMode.externalApplication);

View File

@@ -1,162 +0,0 @@
import 'package:core_localization/core_localization.dart';
import 'package:design_system/design_system.dart';
import 'package:flutter/material.dart';
import 'package:intl/intl.dart';
import 'package:krow_domain/krow_domain.dart';
/// A section displaying the shift type, date range, and weekday schedule summary.
class ShiftScheduleSummarySection extends StatelessWidget {
/// The shift entity.
final Shift shift;
/// Creates a [ShiftScheduleSummarySection].
const ShiftScheduleSummarySection({super.key, required this.shift});
String _getShiftTypeLabel(Translations t) {
final String type = (shift.orderType ?? '').toUpperCase();
if (type == 'PERMANENT') {
return t.staff_shifts.filter.long_term;
}
if (type == 'RECURRING') {
return t.staff_shifts.filter.multi_day;
}
return t.staff_shifts.filter.one_day;
}
bool _isMultiDayOrLongTerm() {
final String type = (shift.orderType ?? '').toUpperCase();
return type == 'RECURRING' || type == 'PERMANENT';
}
@override
Widget build(BuildContext context) {
final t = Translations.of(context);
final isMultiDay = _isMultiDayOrLongTerm();
final typeLabel = _getShiftTypeLabel(t);
final String orderType = (shift.orderType ?? '').toUpperCase();
return Padding(
padding: const EdgeInsets.all(UiConstants.space5),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// Shift Type Title
UiChip(label: typeLabel, variant: UiChipVariant.secondary),
const SizedBox(height: UiConstants.space2),
if (isMultiDay) ...[
// Date Range
if (shift.startDate != null && shift.endDate != null)
Row(
children: [
const Icon(
UiIcons.calendar,
size: 16,
color: UiColors.textSecondary,
),
const SizedBox(width: UiConstants.space2),
Text(
'${_formatDate(shift.startDate!)} ${_formatDate(shift.endDate!)}',
style: UiTypography.body2m.textPrimary,
),
],
),
const SizedBox(height: UiConstants.space4),
// Weekday Circles
_buildWeekdaySchedule(context),
// Available Shifts Count (Only for RECURRING/Multi-Day)
if (orderType == 'RECURRING' && shift.schedules != null) ...[
const SizedBox(height: UiConstants.space4),
Row(
children: [
Container(
width: 8,
height: 8,
decoration: const BoxDecoration(
color: UiColors.success,
shape: BoxShape.circle,
),
),
const SizedBox(width: UiConstants.space2),
Text(
'${shift.schedules!.length} available shifts',
style: UiTypography.body2b.copyWith(
color: UiColors.textSuccess,
),
),
],
),
],
],
],
),
);
}
String _formatDate(String dateStr) {
try {
final date = DateTime.parse(dateStr);
return DateFormat('MMM d, y').format(date);
} catch (_) {
return dateStr;
}
}
Widget _buildWeekdaySchedule(BuildContext context) {
final List<String> weekDays = ['S', 'M', 'T', 'W', 'T', 'F', 'S'];
final Set<int> activeDays = _getActiveWeekdayIndices();
return Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: List.generate(weekDays.length, (index) {
final bool isActive = activeDays.contains(index); // 1-7 (Mon-Sun)
return Container(
width: 38,
height: 38,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: isActive ? UiColors.primaryInverse : UiColors.bgThird,
border: Border.all(
color: isActive ? UiColors.primary : UiColors.border,
),
),
child: Center(
child: Text(
weekDays[index],
style: UiTypography.body2b.copyWith(
color: isActive ? UiColors.primary : UiColors.textSecondary,
),
),
),
);
}),
);
}
Set<int> _getActiveWeekdayIndices() {
final List<String> days = shift.recurringDays ?? shift.permanentDays ?? [];
return days.map((day) {
switch (day.toUpperCase()) {
case 'MON':
return DateTime.monday;
case 'TUE':
return DateTime.tuesday;
case 'WED':
return DateTime.wednesday;
case 'THU':
return DateTime.thursday;
case 'FRI':
return DateTime.friday;
case 'SAT':
return DateTime.saturday;
case 'SUN':
return DateTime.sunday;
default:
return -1;
}
}).toSet();
}
}

View File

@@ -3,27 +3,28 @@ 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';
import 'package:geolocator/geolocator.dart';
import 'package:intl/intl.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../blocs/shifts/shifts_bloc.dart';
import '../my_shift_card.dart';
import '../shared/empty_state_view.dart';
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
import 'package:staff_shifts/src/presentation/widgets/shared/empty_state_view.dart';
/// Tab showing open shifts available for the worker to browse and apply.
class FindShiftsTab extends StatefulWidget {
final List<Shift> availableJobs;
/// Whether the worker's profile is complete. When false, shows incomplete
/// profile banner and disables apply actions.
final bool profileComplete;
/// Creates a [FindShiftsTab].
const FindShiftsTab({
super.key,
required this.availableJobs,
this.profileComplete = true,
});
/// Open shifts loaded from the V2 API.
final List<OpenShift> availableJobs;
/// Whether the worker's profile is complete.
final bool profileComplete;
@override
State<FindShiftsTab> createState() => _FindShiftsTabState();
}
@@ -31,230 +32,21 @@ class FindShiftsTab extends StatefulWidget {
class _FindShiftsTabState extends State<FindShiftsTab> {
String _searchQuery = '';
String _jobType = 'all';
double? _maxDistance; // miles
Position? _currentPosition;
@override
void initState() {
super.initState();
_initLocation();
}
String _formatTime(DateTime dt) => DateFormat('h:mm a').format(dt);
Future<void> _initLocation() async {
try {
final LocationPermission permission = await Geolocator.checkPermission();
if (permission == LocationPermission.always ||
permission == LocationPermission.whileInUse) {
final Position pos = await Geolocator.getCurrentPosition();
if (mounted) {
setState(() => _currentPosition = pos);
}
}
} catch (_) {}
}
double _calculateDistance(double lat, double lng) {
if (_currentPosition == null) return -1;
final double distMeters = Geolocator.distanceBetween(
_currentPosition!.latitude,
_currentPosition!.longitude,
lat,
lng,
);
return distMeters / 1609.34; // meters to miles
}
void _showDistanceFilter() {
showModalBottomSheet<void>(
context: context,
backgroundColor: UiColors.bgPopup,
shape: const RoundedRectangleBorder(
borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
),
builder: (BuildContext context) {
return StatefulBuilder(
builder: (BuildContext context, StateSetter setModalState) {
return Container(
padding: const EdgeInsets.all(24),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
context.t.staff_shifts.find_shifts.radius_filter_title,
style: UiTypography.headline4m.textPrimary,
),
const SizedBox(height: 16),
Text(
_maxDistance == null
? context.t.staff_shifts.find_shifts.unlimited_distance
: context.t.staff_shifts.find_shifts.within_miles(
miles: _maxDistance!.round().toString(),
),
style: UiTypography.body2m.textSecondary,
),
Slider(
value: _maxDistance ?? 100,
min: 5,
max: 100,
divisions: 19,
activeColor: UiColors.primary,
onChanged: (double val) {
setModalState(() => _maxDistance = val);
setState(() => _maxDistance = val);
},
),
const SizedBox(height: 24),
Row(
children: <Widget>[
Expanded(
child: UiButton.secondary(
text: context.t.staff_shifts.find_shifts.clear,
onPressed: () {
setModalState(() => _maxDistance = null);
setState(() => _maxDistance = null);
Navigator.pop(context);
},
),
),
const SizedBox(width: 12),
Expanded(
child: UiButton.primary(
text: context.t.staff_shifts.find_shifts.apply,
onPressed: () => Navigator.pop(context),
),
),
],
),
],
),
);
},
);
},
);
}
bool _isRecurring(Shift shift) =>
(shift.orderType ?? '').toUpperCase() == 'RECURRING';
bool _isPermanent(Shift shift) =>
(shift.orderType ?? '').toUpperCase() == 'PERMANENT';
DateTime? _parseShiftDate(String date) {
if (date.isEmpty) return null;
try {
return DateTime.parse(date);
} catch (_) {
return null;
}
}
List<Shift> _groupMultiDayShifts(List<Shift> shifts) {
final Map<String, List<Shift>> grouped = <String, List<Shift>>{};
for (final shift in shifts) {
if (!_isRecurring(shift) && !_isPermanent(shift)) {
continue;
}
final orderId = shift.orderId;
final roleId = shift.roleId;
if (orderId == null || roleId == null) {
continue;
}
final key = '$orderId::$roleId';
grouped.putIfAbsent(key, () => <Shift>[]).add(shift);
}
final Set<String> addedGroups = <String>{};
final List<Shift> result = <Shift>[];
for (final shift in shifts) {
if (!_isRecurring(shift) && !_isPermanent(shift)) {
result.add(shift);
continue;
}
final orderId = shift.orderId;
final roleId = shift.roleId;
if (orderId == null || roleId == null) {
result.add(shift);
continue;
}
final key = '$orderId::$roleId';
if (addedGroups.contains(key)) {
continue;
}
addedGroups.add(key);
final List<Shift> group = grouped[key] ?? <Shift>[];
if (group.isEmpty) {
result.add(shift);
continue;
}
group.sort((a, b) {
final ad = _parseShiftDate(a.date);
final bd = _parseShiftDate(b.date);
if (ad == null && bd == null) return 0;
if (ad == null) return 1;
if (bd == null) return -1;
return ad.compareTo(bd);
});
final Shift first = group.first;
final List<ShiftSchedule> schedules = group
.map(
(s) => ShiftSchedule(
date: s.date,
startTime: s.startTime,
endTime: s.endTime,
),
)
.toList();
result.add(
Shift(
id: first.id,
roleId: first.roleId,
title: first.title,
clientName: first.clientName,
logoUrl: first.logoUrl,
hourlyRate: first.hourlyRate,
location: first.location,
locationAddress: first.locationAddress,
date: first.date,
endDate: first.endDate,
startTime: first.startTime,
endTime: first.endTime,
createdDate: first.createdDate,
tipsAvailable: first.tipsAvailable,
travelTime: first.travelTime,
mealProvided: first.mealProvided,
parkingAvailable: first.parkingAvailable,
gasCompensation: first.gasCompensation,
description: first.description,
instructions: first.instructions,
managers: first.managers,
latitude: first.latitude,
longitude: first.longitude,
status: first.status,
durationDays: schedules.length,
requiredSlots: first.requiredSlots,
filledSlots: first.filledSlots,
hasApplied: first.hasApplied,
totalValue: first.totalValue,
breakInfo: first.breakInfo,
orderId: first.orderId,
orderType: first.orderType,
schedules: schedules,
recurringDays: first.recurringDays,
permanentDays: first.permanentDays,
),
);
}
return result;
String _formatDate(DateTime date) {
final DateTime now = DateTime.now();
final DateTime today = DateTime(now.year, now.month, now.day);
final DateTime tomorrow = today.add(const Duration(days: 1));
final DateTime d = DateTime(date.year, date.month, date.day);
if (d == today) return 'Today';
if (d == tomorrow) return 'Tomorrow';
return DateFormat('EEE, MMM d').format(date);
}
Widget _buildFilterTab(String id, String label) {
final isSelected = _jobType == id;
final bool isSelected = _jobType == id;
return GestureDetector(
onTap: () => setState(() => _jobType = id),
child: Container(
@@ -280,43 +72,184 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
);
}
List<OpenShift> _filterByType(List<OpenShift> shifts) {
if (_jobType == 'all') return shifts;
return shifts.where((OpenShift s) {
if (_jobType == 'one-day') return s.orderType == OrderType.oneTime;
if (_jobType == 'multi-day') return s.orderType == OrderType.recurring;
if (_jobType == 'long-term') return s.orderType == OrderType.permanent;
return true;
}).toList();
}
/// Builds an open shift card.
Widget _buildOpenShiftCard(BuildContext context, OpenShift shift) {
final double hourlyRate = shift.hourlyRateCents / 100;
final int minutes = shift.endTime.difference(shift.startTime).inMinutes;
final double duration = minutes / 60;
final double estimatedTotal = hourlyRate * duration;
String typeLabel;
switch (shift.orderType) {
case OrderType.permanent:
typeLabel = t.staff_shifts.filter.long_term;
case OrderType.recurring:
typeLabel = t.staff_shifts.filter.multi_day;
case OrderType.oneTime:
default:
typeLabel = t.staff_shifts.filter.one_day;
}
return GestureDetector(
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
child: Container(
margin: const EdgeInsets.only(bottom: UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
// Type badge
Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space2),
child: Container(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space2,
vertical: 2,
),
decoration: BoxDecoration(
color: UiColors.background,
borderRadius: UiConstants.radiusSm,
border: Border.all(color: UiColors.border),
),
child: Text(
typeLabel,
style: UiTypography.footnote2m
.copyWith(color: UiColors.textSecondary),
),
),
),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: <Color>[
UiColors.primary.withValues(alpha: 0.09),
UiColors.primary.withValues(alpha: 0.03),
],
),
borderRadius:
BorderRadius.circular(UiConstants.radiusBase),
),
child: const Center(
child: Icon(UiIcons.briefcase,
color: UiColors.primary, size: UiConstants.iconMd),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(shift.roleName,
style: UiTypography.body2m.textPrimary,
overflow: TextOverflow.ellipsis),
Text(shift.location,
style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis),
],
),
),
const SizedBox(width: UiConstants.space2),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Text('\$${estimatedTotal.toStringAsFixed(0)}',
style: UiTypography.title1m.textPrimary),
Text(
'\$${hourlyRate.toInt()}/hr \u00b7 ${duration.toInt()}h',
style:
UiTypography.footnote2r.textSecondary),
],
),
],
),
const SizedBox(height: UiConstants.space2),
Row(
children: <Widget>[
const Icon(UiIcons.calendar,
size: UiConstants.iconXs,
color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Text(_formatDate(shift.date),
style: UiTypography.footnote1r.textSecondary),
const SizedBox(width: UiConstants.space3),
const Icon(UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Text(
'${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}',
style: UiTypography.footnote1r.textSecondary),
],
),
const SizedBox(height: UiConstants.space1),
Row(
children: <Widget>[
const Icon(UiIcons.mapPin,
size: UiConstants.iconXs,
color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(shift.location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis),
),
],
),
],
),
),
],
),
],
),
),
),
);
}
@override
Widget build(BuildContext context) {
final groupedJobs = _groupMultiDayShifts(widget.availableJobs);
// Filter logic
final filteredJobs = groupedJobs.where((s) {
final matchesSearch =
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
s.clientName.toLowerCase().contains(_searchQuery.toLowerCase());
if (!matchesSearch) return false;
if (_maxDistance != null && s.latitude != null && s.longitude != null) {
final double dist = _calculateDistance(s.latitude!, s.longitude!);
if (dist > _maxDistance!) return false;
}
if (_jobType == 'all') return true;
if (_jobType == 'one-day') {
if (_isRecurring(s) || _isPermanent(s)) return false;
return s.durationDays == null || s.durationDays! <= 1;
}
if (_jobType == 'multi-day') {
return _isRecurring(s) ||
(s.durationDays != null && s.durationDays! > 1);
}
if (_jobType == 'long-term') {
return _isPermanent(s);
}
return true;
// Client-side filter by order type
final List<OpenShift> filteredJobs =
_filterByType(widget.availableJobs).where((OpenShift s) {
if (_searchQuery.isEmpty) return true;
final String q = _searchQuery.toLowerCase();
return s.roleName.toLowerCase().contains(q) ||
s.location.toLowerCase().contains(q);
}).toList();
return Column(
children: [
children: <Widget>[
// Incomplete profile banner
if (!widget.profileComplete) ...[
if (!widget.profileComplete)
GestureDetector(
onTap: () => Modular.to.toProfile(),
child: Container(
@@ -324,19 +257,12 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
child: UiNoticeBanner(
icon: UiIcons.sparkles,
title: context
.t
.staff_shifts
.find_shifts
.incomplete_profile_banner_title,
description: context
.t
.staff_shifts
.find_shifts
.t.staff_shifts.find_shifts.incomplete_profile_banner_title,
description: context.t.staff_shifts.find_shifts
.incomplete_profile_banner_message,
),
),
),
],
// Search and Filters
Container(
color: UiColors.white,
@@ -345,151 +271,76 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
vertical: UiConstants.space4,
),
child: Column(
children: [
// Search Bar
Row(
children: [
Expanded(
child: Container(
height: 48,
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space3,
),
decoration: BoxDecoration(
color: UiColors.background,
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
children: <Widget>[
Container(
height: 48,
padding:
const EdgeInsets.symmetric(horizontal: UiConstants.space3),
decoration: BoxDecoration(
color: UiColors.background,
borderRadius:
BorderRadius.circular(UiConstants.radiusBase),
border: Border.all(color: UiColors.border),
),
child: Row(
children: <Widget>[
const Icon(UiIcons.search,
size: 20, color: UiColors.textInactive),
const SizedBox(width: UiConstants.space2),
Expanded(
child: TextField(
onChanged: (String v) =>
setState(() => _searchQuery = v),
decoration: InputDecoration(
border: InputBorder.none,
hintText:
context.t.staff_shifts.find_shifts.search_hint,
hintStyle: UiTypography.body2r.textPlaceholder,
),
border: Border.all(color: UiColors.border),
),
child: Row(
children: [
const Icon(
UiIcons.search,
size: 20,
color: UiColors.textInactive,
),
const SizedBox(width: UiConstants.space2),
Expanded(
child: TextField(
onChanged: (v) =>
setState(() => _searchQuery = v),
decoration: InputDecoration(
border: InputBorder.none,
hintText: context
.t
.staff_shifts
.find_shifts
.search_hint,
hintStyle: UiTypography.body2r.textPlaceholder,
),
),
),
],
),
),
),
const SizedBox(width: UiConstants.space2),
GestureDetector(
onTap: _showDistanceFilter,
child: Container(
height: 48,
width: 48,
decoration: BoxDecoration(
color: _maxDistance != null
? UiColors.primary.withValues(alpha: 0.1)
: UiColors.white,
borderRadius: BorderRadius.circular(
UiConstants.radiusBase,
),
border: Border.all(
color: _maxDistance != null
? UiColors.primary
: UiColors.border,
),
),
child: Icon(
UiIcons.filter,
size: 18,
color: _maxDistance != null
? UiColors.primary
: UiColors.textSecondary,
),
),
),
],
],
),
),
const SizedBox(height: UiConstants.space4),
// Filter Tabs
SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: [
children: <Widget>[
_buildFilterTab(
'all',
context.t.staff_shifts.find_shifts.filter_all,
),
'all', context.t.staff_shifts.find_shifts.filter_all),
const SizedBox(width: UiConstants.space2),
_buildFilterTab(
'one-day',
context.t.staff_shifts.find_shifts.filter_one_day,
),
_buildFilterTab('one-day',
context.t.staff_shifts.find_shifts.filter_one_day),
const SizedBox(width: UiConstants.space2),
_buildFilterTab(
'multi-day',
context.t.staff_shifts.find_shifts.filter_multi_day,
),
_buildFilterTab('multi-day',
context.t.staff_shifts.find_shifts.filter_multi_day),
const SizedBox(width: UiConstants.space2),
_buildFilterTab(
'long-term',
context.t.staff_shifts.find_shifts.filter_long_term,
),
_buildFilterTab('long-term',
context.t.staff_shifts.find_shifts.filter_long_term),
],
),
),
],
),
),
Expanded(
child: filteredJobs.isEmpty
? EmptyStateView(
icon: UiIcons.search,
title: context.t.staff_shifts.find_shifts.no_jobs_title,
subtitle: context.t.staff_shifts.find_shifts.no_jobs_subtitle,
subtitle:
context.t.staff_shifts.find_shifts.no_jobs_subtitle,
)
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(
horizontal: UiConstants.space5,
),
horizontal: UiConstants.space5),
child: Column(
children: [
children: <Widget>[
const SizedBox(height: UiConstants.space5),
...filteredJobs.map(
(shift) => Padding(
padding: const EdgeInsets.only(
bottom: UiConstants.space3,
),
child: MyShiftCard(
shift: shift,
onAccept: widget.profileComplete
? () {
BlocProvider.of<ShiftsBloc>(
context,
).add(AcceptShiftEvent(shift.id));
UiSnackbar.show(
context,
message: context
.t
.staff_shifts
.find_shifts
.application_submitted,
type: UiSnackbarType.success,
);
}
: null,
),
),
(OpenShift shift) =>
_buildOpenShiftCard(context, shift),
),
const SizedBox(height: UiConstants.space32),
],

View File

@@ -1,37 +1,41 @@
import 'package:flutter/material.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:flutter_modular/flutter_modular.dart';
import 'package:intl/intl.dart';
import 'package:design_system/design_system.dart';
import 'package:krow_core/core.dart';
import '../my_shift_card.dart';
import '../shared/empty_state_view.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_shifts/src/presentation/widgets/shared/empty_state_view.dart';
/// Tab displaying completed shift history.
class HistoryShiftsTab extends StatelessWidget {
final List<Shift> historyShifts;
/// Creates a [HistoryShiftsTab].
const HistoryShiftsTab({super.key, required this.historyShifts});
/// Completed shifts.
final List<CompletedShift> historyShifts;
@override
Widget build(BuildContext context) {
if (historyShifts.isEmpty) {
return const EmptyStateView(
icon: UiIcons.clock,
title: "No shift history",
subtitle: "Completed shifts appear here",
title: 'No shift history',
subtitle: 'Completed shifts appear here',
);
}
return SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space5),
child: Column(
children: [
children: <Widget>[
const SizedBox(height: UiConstants.space5),
...historyShifts.map(
(shift) => Padding(
(CompletedShift shift) => Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: GestureDetector(
onTap: () => Modular.to.toShiftDetails(shift),
child: MyShiftCard(shift: shift),
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
child: _CompletedShiftCard(shift: shift),
),
),
),
@@ -41,3 +45,89 @@ class HistoryShiftsTab extends StatelessWidget {
);
}
}
/// Card displaying a completed shift summary.
class _CompletedShiftCard extends StatelessWidget {
const _CompletedShiftCard({required this.shift});
final CompletedShift shift;
@override
Widget build(BuildContext context) {
final int hours = shift.minutesWorked ~/ 60;
final int mins = shift.minutesWorked % 60;
final String workedLabel =
mins > 0 ? '${hours}h ${mins}m' : '${hours}h';
return Container(
decoration: BoxDecoration(
color: UiColors.white,
borderRadius: UiConstants.radiusLg,
border: Border.all(color: UiColors.border),
),
child: Padding(
padding: const EdgeInsets.all(UiConstants.space4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
),
child: const Center(
child: Icon(UiIcons.briefcase,
color: UiColors.primary, size: UiConstants.iconMd),
),
),
const SizedBox(width: UiConstants.space3),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(shift.title,
style: UiTypography.body2m.textPrimary,
overflow: TextOverflow.ellipsis),
const SizedBox(height: UiConstants.space1),
Row(
children: <Widget>[
const Icon(UiIcons.calendar,
size: UiConstants.iconXs,
color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Text(DateFormat('EEE, MMM d').format(shift.date),
style: UiTypography.footnote1r.textSecondary),
const SizedBox(width: UiConstants.space3),
const Icon(UiIcons.clock,
size: UiConstants.iconXs,
color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Text(workedLabel,
style: UiTypography.footnote1r.textSecondary),
],
),
const SizedBox(height: UiConstants.space1),
Row(
children: <Widget>[
const Icon(UiIcons.mapPin,
size: UiConstants.iconXs,
color: UiColors.iconSecondary),
const SizedBox(width: UiConstants.space1),
Expanded(
child: Text(shift.location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis),
),
],
),
],
),
),
],
),
),
);
}
}

View File

@@ -4,17 +4,15 @@ import 'package:intl/intl.dart';
import 'package:design_system/design_system.dart';
import 'package:core_localization/core_localization.dart';
import 'package:krow_domain/krow_domain.dart';
import '../../blocs/shifts/shifts_bloc.dart';
import '../my_shift_card.dart';
import '../shift_assignment_card.dart';
import '../shared/empty_state_view.dart';
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
import 'package:staff_shifts/src/presentation/widgets/my_shift_card.dart';
import 'package:staff_shifts/src/presentation/widgets/shift_assignment_card.dart';
import 'package:staff_shifts/src/presentation/widgets/shared/empty_state_view.dart';
/// Tab displaying the worker's assigned, pending, and cancelled shifts.
class MyShiftsTab extends StatefulWidget {
final List<Shift> myShifts;
final List<Shift> pendingAssignments;
final List<Shift> cancelledShifts;
final DateTime? initialDate;
/// Creates a [MyShiftsTab].
const MyShiftsTab({
super.key,
required this.myShifts,
@@ -23,6 +21,18 @@ class MyShiftsTab extends StatefulWidget {
this.initialDate,
});
/// Assigned shifts for the current week.
final List<AssignedShift> myShifts;
/// Pending assignments awaiting acceptance.
final List<PendingAssignment> pendingAssignments;
/// Cancelled shift assignments.
final List<CancelledShift> cancelledShifts;
/// Initial date to select in the calendar.
final DateTime? initialDate;
@override
State<MyShiftsTab> createState() => _MyShiftsTabState();
}
@@ -165,17 +175,16 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
);
}
String _formatDateStr(String dateStr) {
try {
final date = DateTime.parse(dateStr);
final now = DateTime.now();
if (_isSameDay(date, now)) return context.t.staff_shifts.my_shifts_tab.date.today;
final tomorrow = now.add(const Duration(days: 1));
if (_isSameDay(date, tomorrow)) return context.t.staff_shifts.my_shifts_tab.date.tomorrow;
return DateFormat('EEE, MMM d').format(date);
} catch (_) {
return dateStr;
String _formatDateFromDateTime(DateTime date) {
final DateTime now = DateTime.now();
if (_isSameDay(date, now)) {
return context.t.staff_shifts.my_shifts_tab.date.today;
}
final DateTime tomorrow = now.add(const Duration(days: 1));
if (_isSameDay(date, tomorrow)) {
return context.t.staff_shifts.my_shifts_tab.date.tomorrow;
}
return DateFormat('EEE, MMM d').format(date);
}
@override
@@ -184,25 +193,15 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
final weekStartDate = calendarDays.first;
final weekEndDate = calendarDays.last;
final visibleMyShifts = widget.myShifts.where((s) {
try {
final date = DateTime.parse(s.date);
return _isSameDay(date, _selectedDate);
} catch (_) {
return false;
}
}).toList();
final List<AssignedShift> visibleMyShifts = widget.myShifts.where(
(AssignedShift s) => _isSameDay(s.date, _selectedDate),
).toList();
final visibleCancelledShifts = widget.cancelledShifts.where((s) {
try {
final date = DateTime.parse(s.date);
return date.isAfter(
weekStartDate.subtract(const Duration(seconds: 1)),
) &&
date.isBefore(weekEndDate.add(const Duration(days: 1)));
} catch (_) {
return false;
}
final List<CancelledShift> visibleCancelledShifts =
widget.cancelledShifts.where((CancelledShift s) {
return s.date.isAfter(
weekStartDate.subtract(const Duration(seconds: 1))) &&
s.date.isBefore(weekEndDate.add(const Duration(days: 1)));
}).toList();
return Column(
@@ -263,13 +262,9 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
final isSelected = _isSameDay(date, _selectedDate);
// ignore: unused_local_variable
final dateStr = DateFormat('yyyy-MM-dd').format(date);
final hasShifts = widget.myShifts.any((s) {
try {
return _isSameDay(DateTime.parse(s.date), date);
} catch (_) {
return false;
}
});
final bool hasShifts = widget.myShifts.any(
(AssignedShift s) => _isSameDay(s.date, date),
);
return GestureDetector(
onTap: () => setState(() => _selectedDate = date),
@@ -342,12 +337,12 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
UiColors.textWarning,
),
...widget.pendingAssignments.map(
(shift) => Padding(
(PendingAssignment assignment) => Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: ShiftAssignmentCard(
shift: shift,
onConfirm: () => _confirmShift(shift.id),
onDecline: () => _declineShift(shift.id),
assignment: assignment,
onConfirm: () => _confirmShift(assignment.shiftId),
onDecline: () => _declineShift(assignment.shiftId),
isConfirming: true,
),
),
@@ -358,17 +353,13 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
if (visibleCancelledShifts.isNotEmpty) ...[
_buildSectionHeader(context.t.staff_shifts.my_shifts_tab.sections.cancelled, UiColors.textSecondary),
...visibleCancelledShifts.map(
(shift) => Padding(
(CancelledShift cs) => Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space4),
child: _buildCancelledCard(
title: shift.title,
client: shift.clientName,
pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}",
rate: "\$${shift.hourlyRate}/hr · 8h",
date: _formatDateStr(shift.date),
time: "${shift.startTime} - ${shift.endTime}",
address: shift.locationAddress,
isLastMinute: true,
title: cs.title,
location: cs.location,
date: DateFormat('EEE, MMM d').format(cs.date),
reason: cs.cancellationReason,
onTap: () {},
),
),
@@ -380,11 +371,11 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
if (visibleMyShifts.isNotEmpty) ...[
_buildSectionHeader(context.t.staff_shifts.my_shifts_tab.sections.confirmed, UiColors.textSecondary),
...visibleMyShifts.map(
(shift) => Padding(
(AssignedShift shift) => Padding(
padding: const EdgeInsets.only(bottom: UiConstants.space3),
child: MyShiftCard(
shift: shift,
onDecline: () => _declineShift(shift.id),
onDecline: () => _declineShift(shift.shiftId),
onRequestSwap: () {
UiSnackbar.show(
context,
@@ -439,13 +430,9 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
Widget _buildCancelledCard({
required String title,
required String client,
required String pay,
required String rate,
required String location,
required String date,
required String time,
required String address,
required bool isLastMinute,
String? reason,
required VoidCallback onTap,
}) {
return GestureDetector(
@@ -459,9 +446,9 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Row(
children: [
children: <Widget>[
Container(
width: 6,
height: 6,
@@ -475,25 +462,19 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
context.t.staff_shifts.my_shifts_tab.card.cancelled,
style: UiTypography.footnote2b.textError,
),
if (isLastMinute) ...[
const SizedBox(width: 4),
Text(
context.t.staff_shifts.my_shifts_tab.card.compensation,
style: UiTypography.footnote2m.textSuccess,
),
],
],
),
const SizedBox(height: UiConstants.space3),
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
children: <Widget>[
Container(
width: 44,
height: 44,
decoration: BoxDecoration(
color: UiColors.primary.withValues(alpha: 0.05),
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
borderRadius:
BorderRadius.circular(UiConstants.radiusBase),
),
child: const Center(
child: Icon(
@@ -507,84 +488,42 @@ class _MyShiftsTabState extends State<MyShiftsTab> {
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: UiTypography.body2b.textPrimary,
),
Text(
client,
style: UiTypography.footnote1r.textSecondary,
),
],
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Text(
pay,
style: UiTypography.headline4m.textPrimary,
),
Text(
rate,
style: UiTypography.footnote2r.textSecondary,
),
],
),
],
),
children: <Widget>[
Text(title, style: UiTypography.body2b.textPrimary),
const SizedBox(height: UiConstants.space2),
Row(
children: [
const Icon(
UiIcons.calendar,
size: 12,
color: UiColors.textSecondary,
),
children: <Widget>[
const Icon(UiIcons.calendar,
size: 12, color: UiColors.textSecondary),
const SizedBox(width: 4),
Text(
date,
style: UiTypography.footnote1r.textSecondary,
),
const SizedBox(width: UiConstants.space3),
const Icon(
UiIcons.clock,
size: 12,
color: UiColors.textSecondary,
),
const SizedBox(width: 4),
Text(
time,
style: UiTypography.footnote1r.textSecondary,
),
Text(date,
style: UiTypography.footnote1r.textSecondary),
],
),
const SizedBox(height: 4),
Row(
children: [
const Icon(
UiIcons.mapPin,
size: 12,
color: UiColors.textSecondary,
),
children: <Widget>[
const Icon(UiIcons.mapPin,
size: 12, color: UiColors.textSecondary),
const SizedBox(width: 4),
Expanded(
child: Text(
address,
location,
style: UiTypography.footnote1r.textSecondary,
overflow: TextOverflow.ellipsis,
),
),
],
),
if (reason != null && reason.isNotEmpty) ...[
const SizedBox(height: 4),
Text(
reason,
style: UiTypography.footnote2r.textSecondary,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
],
),
),

View File

@@ -1,46 +1,55 @@
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'domain/repositories/shifts_repository_interface.dart';
import 'data/repositories_impl/shifts_repository_impl.dart';
import 'domain/usecases/get_shift_details_usecase.dart';
import 'domain/usecases/accept_shift_usecase.dart';
import 'domain/usecases/decline_shift_usecase.dart';
import 'domain/usecases/apply_for_shift_usecase.dart';
import 'presentation/blocs/shift_details/shift_details_bloc.dart';
import 'presentation/pages/shift_details_page.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_shifts/src/data/repositories_impl/shifts_repository_impl.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
import 'package:staff_shifts/src/domain/usecases/apply_for_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/decline_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_profile_completion_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_shift_details_usecase.dart';
import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_bloc.dart';
import 'package:staff_shifts/src/presentation/pages/shift_details_page.dart';
/// DI module for the shift details page.
///
/// Registers the detail-specific repository, use cases, and BLoC using
/// the V2 API via [BaseApiService].
class ShiftDetailsModule extends Module {
@override
List<Module> get imports => <Module>[CoreModule()];
@override
void binds(Injector i) {
// Repository
i.add<ShiftsRepositoryInterface>(ShiftsRepositoryImpl.new);
// StaffConnectorRepository for profile completion
i.addLazySingleton<StaffConnectorRepository>(
() => StaffConnectorRepositoryImpl(),
i.add<ShiftsRepositoryInterface>(
() => ShiftsRepositoryImpl(apiService: i.get<BaseApiService>()),
);
// UseCases
i.add(GetShiftDetailsUseCase.new);
i.add(AcceptShiftUseCase.new);
i.add(DeclineShiftUseCase.new);
// Use cases
i.add(GetShiftDetailUseCase.new);
i.add(ApplyForShiftUseCase.new);
i.addLazySingleton<GetProfileCompletionUseCase>(
() => GetProfileCompletionUseCase(
repository: i.get<StaffConnectorRepository>(),
i.add(DeclineShiftUseCase.new);
i.add(GetProfileCompletionUseCase.new);
// BLoC
i.add(
() => ShiftDetailsBloc(
getShiftDetail: i.get(),
applyForShift: i.get(),
declineShift: i.get(),
getProfileCompletion: i.get(),
),
);
// Bloc
i.add(ShiftDetailsBloc.new);
}
@override
void routes(RouteManager r) {
r.child(
'/:id',
child: (_) =>
ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data),
child: (_) => ShiftDetailsPage(
shiftId: r.args.params['id'] ?? '',
),
);
}
}

View File

@@ -1,62 +1,72 @@
import 'package:flutter_modular/flutter_modular.dart';
import 'package:krow_data_connect/krow_data_connect.dart';
import 'domain/repositories/shifts_repository_interface.dart';
import 'data/repositories_impl/shifts_repository_impl.dart';
import 'domain/usecases/get_my_shifts_usecase.dart';
import 'domain/usecases/get_available_shifts_usecase.dart';
import 'domain/usecases/get_pending_assignments_usecase.dart';
import 'domain/usecases/get_cancelled_shifts_usecase.dart';
import 'domain/usecases/get_history_shifts_usecase.dart';
import 'domain/usecases/accept_shift_usecase.dart';
import 'domain/usecases/decline_shift_usecase.dart';
import 'domain/usecases/apply_for_shift_usecase.dart';
import 'domain/usecases/get_shift_details_usecase.dart';
import 'presentation/blocs/shifts/shifts_bloc.dart';
import 'presentation/blocs/shift_details/shift_details_bloc.dart';
import 'presentation/utils/shift_tab_type.dart';
import 'presentation/pages/shifts_page.dart';
import 'package:krow_core/core.dart';
import 'package:krow_domain/krow_domain.dart';
import 'package:staff_shifts/src/data/repositories_impl/shifts_repository_impl.dart';
import 'package:staff_shifts/src/domain/repositories/shifts_repository_interface.dart';
import 'package:staff_shifts/src/domain/usecases/apply_for_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_available_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_cancelled_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_history_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_my_shifts_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_pending_assignments_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_profile_completion_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/get_shift_details_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/accept_shift_usecase.dart';
import 'package:staff_shifts/src/domain/usecases/decline_shift_usecase.dart';
import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart';
import 'package:staff_shifts/src/presentation/blocs/shift_details/shift_details_bloc.dart';
import 'package:staff_shifts/src/presentation/utils/shift_tab_type.dart';
import 'package:staff_shifts/src/presentation/pages/shifts_page.dart';
/// DI module for the staff shifts feature.
///
/// Registers repository, use cases, and BLoCs using the V2 API
/// via [BaseApiService].
class StaffShiftsModule extends Module {
@override
List<Module> get imports => <Module>[CoreModule()];
@override
void binds(Injector i) {
// StaffConnectorRepository for profile completion
i.addLazySingleton<StaffConnectorRepository>(
() => StaffConnectorRepositoryImpl(),
);
// Profile completion use case
i.addLazySingleton<GetProfileCompletionUseCase>(
() => GetProfileCompletionUseCase(
repository: i.get<StaffConnectorRepository>(),
),
);
// Repository
i.addLazySingleton<ShiftsRepositoryInterface>(ShiftsRepositoryImpl.new);
i.addLazySingleton<ShiftsRepositoryInterface>(
() => ShiftsRepositoryImpl(apiService: i.get<BaseApiService>()),
);
// UseCases
i.addLazySingleton(GetMyShiftsUseCase.new);
i.addLazySingleton(GetAvailableShiftsUseCase.new);
// Use cases
i.addLazySingleton(GetAssignedShiftsUseCase.new);
i.addLazySingleton(GetOpenShiftsUseCase.new);
i.addLazySingleton(GetPendingAssignmentsUseCase.new);
i.addLazySingleton(GetCancelledShiftsUseCase.new);
i.addLazySingleton(GetHistoryShiftsUseCase.new);
i.addLazySingleton(GetCompletedShiftsUseCase.new);
i.addLazySingleton(AcceptShiftUseCase.new);
i.addLazySingleton(DeclineShiftUseCase.new);
i.addLazySingleton(ApplyForShiftUseCase.new);
i.addLazySingleton(GetShiftDetailsUseCase.new);
i.addLazySingleton(GetShiftDetailUseCase.new);
i.addLazySingleton(GetProfileCompletionUseCase.new);
// Bloc
// BLoC
i.add(
() => ShiftsBloc(
getMyShifts: i.get(),
getAvailableShifts: i.get(),
getAssignedShifts: i.get(),
getOpenShifts: i.get(),
getPendingAssignments: i.get(),
getCancelledShifts: i.get(),
getHistoryShifts: i.get(),
getCompletedShifts: i.get(),
getProfileCompletion: i.get(),
acceptShift: i.get(),
declineShift: i.get(),
),
);
i.add(
() => ShiftDetailsBloc(
getShiftDetail: i.get(),
applyForShift: i.get(),
declineShift: i.get(),
getProfileCompletion: i.get(),
),
);
i.add(ShiftDetailsBloc.new);
}
@override
@@ -64,12 +74,14 @@ class StaffShiftsModule extends Module {
r.child(
'/',
child: (_) {
final args = r.args.data as Map?;
final queryParams = r.args.queryParams;
final initialTabStr = queryParams['tab'] ?? args?['initialTab'];
final Map<dynamic, dynamic>? args =
r.args.data as Map<dynamic, dynamic>?;
final Map<String, String> queryParams = r.args.queryParams;
final dynamic initialTabStr =
queryParams['tab'] ?? args?['initialTab'];
return ShiftsPage(
initialTab: ShiftTabType.fromString(initialTabStr),
selectedDate: args?['selectedDate'],
selectedDate: args?['selectedDate'] as DateTime?,
refreshAvailable: args?['refreshAvailable'] == true,
);
},

View File

@@ -12,15 +12,13 @@ dependencies:
flutter:
sdk: flutter
# Internal packages
# Architecture packages
krow_core:
path: ../../../core
design_system:
path: ../../../design_system
krow_domain:
path: ../../../domain
krow_data_connect:
path: ../../../data_connect
core_localization:
path: ../../../core_localization
@@ -28,11 +26,7 @@ dependencies:
flutter_bloc: ^8.1.3
equatable: ^2.0.5
intl: ^0.20.2
google_maps_flutter: ^2.14.2
url_launcher: ^6.3.1
firebase_auth: ^6.1.4
firebase_data_connect: ^0.2.2+2
meta: ^1.17.0
bloc: ^8.1.4
dev_dependencies: