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:
@@ -1,187 +1,33 @@
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||
|
||||
class HomeRepositoryImpl
|
||||
implements HomeRepository {
|
||||
HomeRepositoryImpl() : _service = DataConnectService.instance;
|
||||
/// V2 API implementation of [HomeRepository].
|
||||
///
|
||||
/// Fetches staff dashboard data from `GET /staff/dashboard` and profile
|
||||
/// completion from `GET /staff/profile-completion`.
|
||||
class HomeRepositoryImpl implements HomeRepository {
|
||||
/// Creates a [HomeRepositoryImpl].
|
||||
HomeRepositoryImpl({required BaseApiService apiService})
|
||||
: _apiService = apiService;
|
||||
|
||||
final DataConnectService _service;
|
||||
/// The API service used for network requests.
|
||||
final BaseApiService _apiService;
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getTodayShifts() async {
|
||||
return _getShiftsForDate(DateTime.now());
|
||||
Future<StaffDashboard> getDashboard() async {
|
||||
final ApiResponse response =
|
||||
await _apiService.get(V2ApiEndpoints.staffDashboard);
|
||||
final Map<String, dynamic> data = response.data as Map<String, dynamic>;
|
||||
return StaffDashboard.fromJson(data);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getTomorrowShifts() async {
|
||||
return _getShiftsForDate(DateTime.now().add(const Duration(days: 1)));
|
||||
}
|
||||
|
||||
Future<List<Shift>> _getShiftsForDate(DateTime date) async {
|
||||
return _service.run(() async {
|
||||
final staffId = await _service.getStaffId();
|
||||
|
||||
// Create start and end timestamps for the target date
|
||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||
final DateTime end =
|
||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
||||
|
||||
final response = await _service.run(() => _service.connector
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(_service.toTimestamp(start))
|
||||
.dayEnd(_service.toTimestamp(end))
|
||||
.execute());
|
||||
|
||||
// Filter for CONFIRMED applications (same logic as shifts_repository_impl)
|
||||
final apps = response.data.applications.where((app) =>
|
||||
(app.status is Known &&
|
||||
(app.status as Known).value == ApplicationStatus.CONFIRMED));
|
||||
|
||||
final List<Shift> shifts = [];
|
||||
for (final app in apps) {
|
||||
shifts.add(_mapApplicationToShift(app));
|
||||
}
|
||||
|
||||
return shifts;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getRecommendedShifts() async {
|
||||
// Logic: List ALL open shifts (simple recommendation engine)
|
||||
// Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED.
|
||||
return _service.run(() async {
|
||||
final response =
|
||||
await _service.run(() => _service.connector.listShifts().execute());
|
||||
|
||||
return response.data.shifts
|
||||
.where((s) {
|
||||
final isOpen = s.status is Known &&
|
||||
(s.status as Known).value == ShiftStatus.OPEN;
|
||||
if (!isOpen) return false;
|
||||
|
||||
final start = _service.toDateTime(s.startTime);
|
||||
if (start == null) return false;
|
||||
|
||||
return start.isAfter(DateTime.now());
|
||||
})
|
||||
.take(10)
|
||||
.map((s) => _mapConnectorShiftToDomain(s))
|
||||
.toList();
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<String?> getStaffName() async {
|
||||
final session = StaffSessionStore.instance.session;
|
||||
|
||||
// If session data is available, return staff name immediately
|
||||
if (session?.staff?.name != null) {
|
||||
return session!.staff!.name;
|
||||
}
|
||||
|
||||
// If session is not initialized, attempt to fetch staff data to populate session
|
||||
return await _service.run(() async {
|
||||
final staffId = await _service.getStaffId();
|
||||
final response = await _service.connector
|
||||
.getStaffById(id: staffId)
|
||||
.execute();
|
||||
|
||||
if (response.data.staff == null) {
|
||||
throw Exception('Staff data not found for ID: $staffId');
|
||||
}
|
||||
|
||||
final staff = response.data.staff!;
|
||||
final updatedSession = StaffSession(
|
||||
staff: Staff(
|
||||
id: staff.id,
|
||||
authProviderId: staff.userId,
|
||||
name: staff.fullName,
|
||||
email: staff.email ?? '',
|
||||
phone: staff.phone,
|
||||
status: StaffStatus.completedProfile,
|
||||
address: staff.addres,
|
||||
avatar: staff.photoUrl,
|
||||
),
|
||||
ownerId: staff.ownerId,
|
||||
);
|
||||
StaffSessionStore.instance.setSession(updatedSession);
|
||||
|
||||
return staff.fullName;
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Benefit>> getBenefits() async {
|
||||
return _service.run(() async {
|
||||
final staffId = await _service.getStaffId();
|
||||
final response = await _service.connector
|
||||
.listBenefitsDataByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
|
||||
return response.data.benefitsDatas.map((data) {
|
||||
final plan = data.vendorBenefitPlan;
|
||||
final total = plan.total?.toDouble() ?? 0.0;
|
||||
final remaining = data.current.toDouble();
|
||||
return Benefit(
|
||||
title: plan.title,
|
||||
entitlementHours: total,
|
||||
usedHours: (total - remaining).clamp(0.0, total),
|
||||
);
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
// Mappers specific to Home's Domain Entity 'Shift'
|
||||
// Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift.
|
||||
|
||||
Shift _mapApplicationToShift(GetApplicationsByStaffIdApplications app) {
|
||||
final s = app.shift;
|
||||
final r = app.shiftRole;
|
||||
|
||||
return ShiftAdapter.fromApplicationData(
|
||||
shiftId: s.id,
|
||||
roleId: r.roleId,
|
||||
roleName: r.role.name,
|
||||
businessName: s.order.business.businessName,
|
||||
companyLogoUrl: s.order.business.companyLogoUrl,
|
||||
costPerHour: r.role.costPerHour,
|
||||
shiftLocation: s.location,
|
||||
teamHubName: s.order.teamHub.hubName,
|
||||
shiftDate: _service.toDateTime(s.date),
|
||||
startTime: _service.toDateTime(r.startTime),
|
||||
endTime: _service.toDateTime(r.endTime),
|
||||
createdAt: _service.toDateTime(app.createdAt),
|
||||
status: 'confirmed',
|
||||
description: s.description,
|
||||
durationDays: s.durationDays,
|
||||
count: r.count,
|
||||
assigned: r.assigned,
|
||||
eventName: s.order.eventName,
|
||||
hasApplied: true,
|
||||
);
|
||||
}
|
||||
|
||||
Shift _mapConnectorShiftToDomain(ListShiftsShifts s) {
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
clientName: s.order.business.businessName,
|
||||
hourlyRate: s.cost ?? 0.0,
|
||||
location: s.location ?? 'Unknown',
|
||||
locationAddress: s.locationAddress ?? '',
|
||||
date: _service.toDateTime(s.date)?.toIso8601String() ?? '',
|
||||
startTime: DateFormat('HH:mm')
|
||||
.format(_service.toDateTime(s.startTime) ?? DateTime.now()),
|
||||
endTime: DateFormat('HH:mm')
|
||||
.format(_service.toDateTime(s.endTime) ?? DateTime.now()),
|
||||
createdDate: _service.toDateTime(s.createdAt)?.toIso8601String() ?? '',
|
||||
tipsAvailable: false,
|
||||
mealProvided: false,
|
||||
managers: [],
|
||||
description: s.description,
|
||||
);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
/// Entity representing a shift for the staff home screen.
|
||||
///
|
||||
/// This entity aggregates essential shift details needed for display cards.
|
||||
class Shift extends Equatable {
|
||||
const Shift({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.clientName,
|
||||
this.logoUrl,
|
||||
required this.hourlyRate,
|
||||
required this.location,
|
||||
this.locationAddress,
|
||||
required this.date,
|
||||
required this.startTime,
|
||||
required this.endTime,
|
||||
required this.createdDate,
|
||||
this.tipsAvailable,
|
||||
this.travelTime,
|
||||
this.mealProvided,
|
||||
this.parkingAvailable,
|
||||
this.gasCompensation,
|
||||
this.description,
|
||||
this.instructions,
|
||||
this.managers,
|
||||
this.latitude,
|
||||
this.longitude,
|
||||
this.status,
|
||||
this.durationDays,
|
||||
});
|
||||
|
||||
final String id;
|
||||
final String title;
|
||||
final String clientName;
|
||||
final String? logoUrl;
|
||||
final double hourlyRate;
|
||||
final String location;
|
||||
final String? locationAddress;
|
||||
final String date;
|
||||
final String startTime;
|
||||
final String endTime;
|
||||
final String createdDate;
|
||||
final bool? tipsAvailable;
|
||||
final bool? travelTime;
|
||||
final bool? mealProvided;
|
||||
final bool? parkingAvailable;
|
||||
final bool? gasCompensation;
|
||||
final String? description;
|
||||
final String? instructions;
|
||||
final List<ShiftManager>? managers;
|
||||
final double? latitude;
|
||||
final double? longitude;
|
||||
final String? status;
|
||||
final int? durationDays;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
id,
|
||||
title,
|
||||
clientName,
|
||||
logoUrl,
|
||||
hourlyRate,
|
||||
location,
|
||||
locationAddress,
|
||||
date,
|
||||
startTime,
|
||||
endTime,
|
||||
createdDate,
|
||||
tipsAvailable,
|
||||
travelTime,
|
||||
mealProvided,
|
||||
parkingAvailable,
|
||||
gasCompensation,
|
||||
description,
|
||||
instructions,
|
||||
managers,
|
||||
latitude,
|
||||
longitude,
|
||||
status,
|
||||
durationDays,
|
||||
];
|
||||
}
|
||||
|
||||
class ShiftManager extends Equatable {
|
||||
const ShiftManager({required this.name, required this.phone, this.avatar});
|
||||
|
||||
final String name;
|
||||
final String phone;
|
||||
final String? avatar;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [name, phone, avatar];
|
||||
}
|
||||
@@ -2,22 +2,14 @@ import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Repository interface for home screen data operations.
|
||||
///
|
||||
/// This interface defines the contract for fetching shift data
|
||||
/// displayed on the worker home screen. Implementations should
|
||||
/// handle data retrieval from appropriate data sources.
|
||||
/// This interface defines the contract for fetching dashboard data
|
||||
/// displayed on the worker home screen. The V2 API returns all data
|
||||
/// in a single `/staff/dashboard` call.
|
||||
abstract class HomeRepository {
|
||||
/// Retrieves the list of shifts scheduled for today.
|
||||
Future<List<Shift>> getTodayShifts();
|
||||
/// Retrieves the staff dashboard containing today's shifts, tomorrow's
|
||||
/// shifts, recommended shifts, benefits, and the staff member's name.
|
||||
Future<StaffDashboard> getDashboard();
|
||||
|
||||
/// Retrieves the list of shifts scheduled for tomorrow.
|
||||
Future<List<Shift>> getTomorrowShifts();
|
||||
|
||||
/// Retrieves shifts recommended for the worker based on their profile.
|
||||
Future<List<Shift>> getRecommendedShifts();
|
||||
|
||||
/// Retrieves the current staff member's name.
|
||||
Future<String?> getStaffName();
|
||||
|
||||
/// Retrieves the list of benefits for the staff member.
|
||||
Future<List<Benefit>> getBenefits();
|
||||
/// Retrieves whether the staff member's profile is complete.
|
||||
Future<bool> getProfileCompletion();
|
||||
}
|
||||
|
||||
@@ -1,42 +1,31 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||
|
||||
/// Use case for fetching all shifts displayed on the home screen.
|
||||
/// Use case for fetching the staff dashboard data.
|
||||
///
|
||||
/// This use case aggregates shift data from multiple time periods
|
||||
/// (today, tomorrow, and recommended) into a single response.
|
||||
class GetHomeShifts {
|
||||
final HomeRepository repository;
|
||||
/// Wraps the repository call and returns the full [StaffDashboard]
|
||||
/// containing shifts, benefits, and the staff member's name.
|
||||
class GetDashboardUseCase {
|
||||
/// Creates a [GetDashboardUseCase].
|
||||
GetDashboardUseCase(this._repository);
|
||||
|
||||
GetHomeShifts(this.repository);
|
||||
/// The repository used for data access.
|
||||
final HomeRepository _repository;
|
||||
|
||||
/// Executes the use case to fetch all home screen shift data.
|
||||
///
|
||||
/// Returns a [HomeShifts] object containing today's shifts,
|
||||
/// tomorrow's shifts, and recommended shifts.
|
||||
Future<HomeShifts> call() async {
|
||||
final today = await repository.getTodayShifts();
|
||||
final tomorrow = await repository.getTomorrowShifts();
|
||||
final recommended = await repository.getRecommendedShifts();
|
||||
return HomeShifts(
|
||||
today: today,
|
||||
tomorrow: tomorrow,
|
||||
recommended: recommended,
|
||||
);
|
||||
}
|
||||
/// Executes the use case to fetch dashboard data.
|
||||
Future<StaffDashboard> call() => _repository.getDashboard();
|
||||
}
|
||||
|
||||
/// Data transfer object containing all shifts for the home screen.
|
||||
/// Use case for checking staff profile completion status.
|
||||
///
|
||||
/// Groups shifts by time period for easy presentation layer consumption.
|
||||
class HomeShifts {
|
||||
final List<Shift> today;
|
||||
final List<Shift> tomorrow;
|
||||
final List<Shift> recommended;
|
||||
/// Returns `true` when all required profile fields are filled.
|
||||
class GetProfileCompletionUseCase {
|
||||
/// Creates a [GetProfileCompletionUseCase].
|
||||
GetProfileCompletionUseCase(this._repository);
|
||||
|
||||
HomeShifts({
|
||||
required this.today,
|
||||
required this.tomorrow,
|
||||
required this.recommended,
|
||||
});
|
||||
/// The repository used for data access.
|
||||
final HomeRepository _repository;
|
||||
|
||||
/// Executes the use case to check profile completion.
|
||||
Future<bool> call() => _repository.getProfileCompletion();
|
||||
}
|
||||
|
||||
@@ -6,27 +6,32 @@ import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||
|
||||
part 'benefits_overview_state.dart';
|
||||
|
||||
/// Cubit to manage benefits overview page state.
|
||||
/// Cubit managing the benefits overview page state.
|
||||
///
|
||||
/// Fetches the dashboard and extracts benefits for the detail page.
|
||||
class BenefitsOverviewCubit extends Cubit<BenefitsOverviewState>
|
||||
with BlocErrorHandler<BenefitsOverviewState> {
|
||||
final HomeRepository _repository;
|
||||
|
||||
/// Creates a [BenefitsOverviewCubit].
|
||||
BenefitsOverviewCubit({required HomeRepository repository})
|
||||
: _repository = repository,
|
||||
super(const BenefitsOverviewState.initial());
|
||||
|
||||
/// The repository used for data access.
|
||||
final HomeRepository _repository;
|
||||
|
||||
/// Loads benefits from the dashboard endpoint.
|
||||
Future<void> loadBenefits() async {
|
||||
if (isClosed) return;
|
||||
emit(state.copyWith(status: BenefitsOverviewStatus.loading));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
final benefits = await _repository.getBenefits();
|
||||
final StaffDashboard dashboard = await _repository.getDashboard();
|
||||
if (isClosed) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: BenefitsOverviewStatus.loaded,
|
||||
benefits: benefits,
|
||||
benefits: dashboard.benefits,
|
||||
),
|
||||
);
|
||||
},
|
||||
|
||||
@@ -1,61 +1,56 @@
|
||||
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:staff_home/src/domain/repositories/home_repository.dart';
|
||||
import 'package:staff_home/src/domain/usecases/get_home_shifts.dart';
|
||||
|
||||
part 'home_state.dart';
|
||||
|
||||
/// Simple Cubit to manage home page state (shifts + loading/error).
|
||||
/// Cubit managing the staff home page state.
|
||||
///
|
||||
/// Fetches the dashboard and profile-completion status concurrently
|
||||
/// using the V2 API via [GetDashboardUseCase] and
|
||||
/// [GetProfileCompletionUseCase].
|
||||
class HomeCubit extends Cubit<HomeState> with BlocErrorHandler<HomeState> {
|
||||
final GetHomeShifts _getHomeShifts;
|
||||
final HomeRepository _repository;
|
||||
/// Creates a [HomeCubit].
|
||||
HomeCubit({
|
||||
required GetDashboardUseCase getDashboard,
|
||||
required GetProfileCompletionUseCase getProfileCompletion,
|
||||
}) : _getDashboard = getDashboard,
|
||||
_getProfileCompletion = getProfileCompletion,
|
||||
super(const HomeState.initial());
|
||||
|
||||
/// Use case that fetches the full staff dashboard.
|
||||
final GetDashboardUseCase _getDashboard;
|
||||
|
||||
/// Use case that checks whether the staff member's profile is complete.
|
||||
///
|
||||
/// Used to determine whether profile-gated features (such as shift browsing)
|
||||
/// should be enabled on the home screen.
|
||||
final GetProfileCompletionUseCase _getProfileCompletion;
|
||||
|
||||
HomeCubit({
|
||||
required HomeRepository repository,
|
||||
required GetProfileCompletionUseCase getProfileCompletion,
|
||||
}) : _getHomeShifts = GetHomeShifts(repository),
|
||||
_repository = repository,
|
||||
_getProfileCompletion = getProfileCompletion,
|
||||
super(const HomeState.initial());
|
||||
|
||||
/// Loads dashboard data and profile completion concurrently.
|
||||
Future<void> loadShifts() async {
|
||||
if (isClosed) return;
|
||||
emit(state.copyWith(status: HomeStatus.loading));
|
||||
await handleError(
|
||||
emit: emit,
|
||||
action: () async {
|
||||
// Fetch shifts, name, benefits and profile completion status concurrently
|
||||
final results = await Future.wait([
|
||||
_getHomeShifts.call(),
|
||||
final List<Object> results = await Future.wait(<Future<Object>>[
|
||||
_getDashboard.call(),
|
||||
_getProfileCompletion.call(),
|
||||
_repository.getBenefits(),
|
||||
_repository.getStaffName(),
|
||||
]);
|
||||
|
||||
final homeResult = results[0] as HomeShifts;
|
||||
final isProfileComplete = results[1] as bool;
|
||||
final benefits = results[2] as List<Benefit>;
|
||||
final name = results[3] as String?;
|
||||
|
||||
final StaffDashboard dashboard = results[0] as StaffDashboard;
|
||||
final bool isProfileComplete = results[1] as bool;
|
||||
|
||||
if (isClosed) return;
|
||||
emit(
|
||||
state.copyWith(
|
||||
status: HomeStatus.loaded,
|
||||
todayShifts: homeResult.today,
|
||||
tomorrowShifts: homeResult.tomorrow,
|
||||
recommendedShifts: homeResult.recommended,
|
||||
staffName: name,
|
||||
todayShifts: dashboard.todaysShifts,
|
||||
tomorrowShifts: dashboard.tomorrowsShifts,
|
||||
recommendedShifts: dashboard.recommendedShifts,
|
||||
staffName: dashboard.staffName,
|
||||
isProfileComplete: isProfileComplete,
|
||||
benefits: benefits,
|
||||
benefits: dashboard.benefits,
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -66,6 +61,7 @@ class HomeCubit extends Cubit<HomeState> with BlocErrorHandler<HomeState> {
|
||||
);
|
||||
}
|
||||
|
||||
/// Toggles the auto-match preference.
|
||||
void toggleAutoMatch(bool enabled) {
|
||||
emit(state.copyWith(autoMatchEnabled: enabled));
|
||||
}
|
||||
|
||||
@@ -1,37 +1,62 @@
|
||||
part of 'home_cubit.dart';
|
||||
|
||||
/// Status of the home page data loading.
|
||||
enum HomeStatus { initial, loading, loaded, error }
|
||||
|
||||
/// State for the staff home page.
|
||||
///
|
||||
/// Contains today's shifts, tomorrow's shifts, recommended shifts, benefits,
|
||||
/// and profile-completion status from the V2 dashboard API.
|
||||
class HomeState extends Equatable {
|
||||
final HomeStatus status;
|
||||
final List<Shift> todayShifts;
|
||||
final List<Shift> tomorrowShifts;
|
||||
final List<Shift> recommendedShifts;
|
||||
final bool autoMatchEnabled;
|
||||
final bool isProfileComplete;
|
||||
final String? staffName;
|
||||
final String? errorMessage;
|
||||
final List<Benefit> benefits;
|
||||
|
||||
/// Creates a [HomeState].
|
||||
const HomeState({
|
||||
required this.status,
|
||||
this.todayShifts = const [],
|
||||
this.tomorrowShifts = const [],
|
||||
this.recommendedShifts = const [],
|
||||
this.todayShifts = const <TodayShift>[],
|
||||
this.tomorrowShifts = const <AssignedShift>[],
|
||||
this.recommendedShifts = const <OpenShift>[],
|
||||
this.autoMatchEnabled = false,
|
||||
this.isProfileComplete = false,
|
||||
this.staffName,
|
||||
this.errorMessage,
|
||||
this.benefits = const [],
|
||||
this.benefits = const <Benefit>[],
|
||||
});
|
||||
|
||||
/// Initial state with no data loaded.
|
||||
const HomeState.initial() : this(status: HomeStatus.initial);
|
||||
|
||||
/// Current loading status.
|
||||
final HomeStatus status;
|
||||
|
||||
/// Shifts assigned for today.
|
||||
final List<TodayShift> todayShifts;
|
||||
|
||||
/// Shifts assigned for tomorrow.
|
||||
final List<AssignedShift> tomorrowShifts;
|
||||
|
||||
/// Recommended open shifts.
|
||||
final List<OpenShift> recommendedShifts;
|
||||
|
||||
/// Whether auto-match is enabled.
|
||||
final bool autoMatchEnabled;
|
||||
|
||||
/// Whether the staff profile is complete.
|
||||
final bool isProfileComplete;
|
||||
|
||||
/// The staff member's display name.
|
||||
final String? staffName;
|
||||
|
||||
/// Error message if loading failed.
|
||||
final String? errorMessage;
|
||||
|
||||
/// Active benefits.
|
||||
final List<Benefit> benefits;
|
||||
|
||||
/// Creates a copy with the given fields replaced.
|
||||
HomeState copyWith({
|
||||
HomeStatus? status,
|
||||
List<Shift>? todayShifts,
|
||||
List<Shift>? tomorrowShifts,
|
||||
List<Shift>? recommendedShifts,
|
||||
List<TodayShift>? todayShifts,
|
||||
List<AssignedShift>? tomorrowShifts,
|
||||
List<OpenShift>? recommendedShifts,
|
||||
bool? autoMatchEnabled,
|
||||
bool? isProfileComplete,
|
||||
String? staffName,
|
||||
@@ -52,7 +77,7 @@ class HomeState extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
todayShifts,
|
||||
tomorrowShifts,
|
||||
@@ -63,4 +88,4 @@ class HomeState extends Equatable {
|
||||
errorMessage,
|
||||
benefits,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
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';
|
||||
@@ -6,20 +5,14 @@ import 'package:staff_home/src/presentation/widgets/benefits_overview/benefit_ca
|
||||
|
||||
/// Card widget displaying detailed benefit information.
|
||||
class BenefitCard extends StatelessWidget {
|
||||
/// The benefit to display.
|
||||
final Benefit benefit;
|
||||
|
||||
/// Creates a [BenefitCard].
|
||||
const BenefitCard({required this.benefit, super.key});
|
||||
|
||||
/// The benefit to display.
|
||||
final Benefit benefit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final bool isSickLeave = benefit.title.toLowerCase().contains('sick');
|
||||
final bool isVacation = benefit.title.toLowerCase().contains('vacation');
|
||||
final bool isHolidays = benefit.title.toLowerCase().contains('holiday');
|
||||
|
||||
final i18n = t.staff.home.benefits.overview;
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space6),
|
||||
decoration: BoxDecoration(
|
||||
@@ -29,17 +22,8 @@ class BenefitCard extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
BenefitCardHeader(benefit: benefit),
|
||||
// const SizedBox(height: UiConstants.space6),
|
||||
// if (isSickLeave) ...[
|
||||
// AccordionHistory(label: i18n.sick_leave_history),
|
||||
// const SizedBox(height: UiConstants.space6),
|
||||
// ],
|
||||
// if (isVacation || isHolidays) ...[
|
||||
// ComplianceBanner(text: i18n.compliance_banner),
|
||||
// const SizedBox(height: UiConstants.space6),
|
||||
// ],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
@@ -6,30 +6,33 @@ import 'package:staff_home/src/presentation/widgets/benefits_overview/circular_p
|
||||
import 'package:staff_home/src/presentation/widgets/benefits_overview/stat_chip.dart';
|
||||
|
||||
/// Header section of a benefit card showing progress circle, title, and stats.
|
||||
///
|
||||
/// Uses V2 [Benefit] entity fields: [Benefit.targetHours],
|
||||
/// [Benefit.trackedHours], and [Benefit.remainingHours].
|
||||
class BenefitCardHeader extends StatelessWidget {
|
||||
/// The benefit to display.
|
||||
final Benefit benefit;
|
||||
|
||||
/// Creates a [BenefitCardHeader].
|
||||
const BenefitCardHeader({required this.benefit, super.key});
|
||||
|
||||
/// The benefit to display.
|
||||
final Benefit benefit;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final i18n = t.staff.home.benefits.overview;
|
||||
final dynamic i18n = t.staff.home.benefits.overview;
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
_buildProgressCircle(),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
benefit.title,
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
),
|
||||
if (_getSubtitle(benefit.title).isNotEmpty) ...[
|
||||
if (_getSubtitle(benefit.title).isNotEmpty) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
Text(
|
||||
_getSubtitle(benefit.title),
|
||||
@@ -46,8 +49,8 @@ class BenefitCardHeader extends StatelessWidget {
|
||||
}
|
||||
|
||||
Widget _buildProgressCircle() {
|
||||
final double progress = benefit.entitlementHours > 0
|
||||
? (benefit.remainingHours / benefit.entitlementHours)
|
||||
final double progress = benefit.targetHours > 0
|
||||
? (benefit.remainingHours / benefit.targetHours)
|
||||
: 0.0;
|
||||
|
||||
return SizedBox(
|
||||
@@ -60,14 +63,14 @@ class BenefitCardHeader extends StatelessWidget {
|
||||
child: Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'${benefit.remainingHours.toInt()}/${benefit.entitlementHours.toInt()}',
|
||||
'${benefit.remainingHours}/${benefit.targetHours}',
|
||||
style: UiTypography.body2b.textPrimary.copyWith(fontSize: 14),
|
||||
),
|
||||
Text(
|
||||
t.client_billing.hours_suffix,
|
||||
style: UiTypography.footnote1r.textSecondary
|
||||
style: UiTypography.footnote1r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -78,27 +81,27 @@ class BenefitCardHeader extends StatelessWidget {
|
||||
|
||||
Widget _buildStatsRow(dynamic i18n) {
|
||||
return Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
StatChip(
|
||||
label: i18n.entitlement,
|
||||
value: '${benefit.entitlementHours.toInt()}',
|
||||
value: '${benefit.targetHours}',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatChip(
|
||||
label: i18n.used,
|
||||
value: '${benefit.usedHours.toInt()}',
|
||||
value: '${benefit.trackedHours}',
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
StatChip(
|
||||
label: i18n.remaining,
|
||||
value: '${benefit.remainingHours.toInt()}',
|
||||
value: '${benefit.remainingHours}',
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
String _getSubtitle(String title) {
|
||||
final i18n = t.staff.home.benefits.overview;
|
||||
final dynamic i18n = t.staff.home.benefits.overview;
|
||||
if (title.toLowerCase().contains('sick')) {
|
||||
return i18n.sick_leave_subtitle;
|
||||
} else if (title.toLowerCase().contains('vacation')) {
|
||||
|
||||
@@ -2,23 +2,33 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
/// Card widget for a recommended open shift.
|
||||
///
|
||||
/// Displays the role name, pay rate, time range, and location
|
||||
/// from an [OpenShift] entity.
|
||||
class RecommendedShiftCard extends StatelessWidget {
|
||||
final Shift shift;
|
||||
/// Creates a [RecommendedShiftCard].
|
||||
const RecommendedShiftCard({required this.shift, super.key});
|
||||
|
||||
const RecommendedShiftCard({super.key, required this.shift});
|
||||
/// The open shift to display.
|
||||
final OpenShift shift;
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
return DateFormat('h:mma').format(time).toLowerCase();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final recI18n = t.staff.home.recommended_card;
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final dynamic recI18n = t.staff.home.recommended_card;
|
||||
final Size size = MediaQuery.sizeOf(context);
|
||||
final double hourlyRate = shift.hourlyRateCents / 100;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
Modular.to.toShiftDetails(shift);
|
||||
},
|
||||
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
|
||||
child: Container(
|
||||
width: size.width * 0.8,
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
@@ -31,10 +41,10 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: UiConstants.space10,
|
||||
height: UiConstants.space10,
|
||||
@@ -52,20 +62,20 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: UiConstants.space1,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Text(
|
||||
shift.title,
|
||||
shift.roleName,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'\$${shift.hourlyRate}/h',
|
||||
'\$${hourlyRate.toStringAsFixed(0)}/h',
|
||||
style: UiTypography.headline4b,
|
||||
),
|
||||
],
|
||||
@@ -73,13 +83,13 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
spacing: UiConstants.space1,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
shift.clientName,
|
||||
shift.orderType.toJson(),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
Text(
|
||||
'\$${shift.hourlyRate.toStringAsFixed(0)}/hr',
|
||||
'\$${hourlyRate.toStringAsFixed(0)}/hr',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
@@ -91,14 +101,17 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space3),
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.calendar,
|
||||
size: UiConstants.space3,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(recI18n.today, style: UiTypography.body3r.textSecondary),
|
||||
Text(
|
||||
recI18n.today,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
const Icon(
|
||||
UiIcons.clock,
|
||||
@@ -108,8 +121,8 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Text(
|
||||
recI18n.time_range(
|
||||
start: shift.startTime,
|
||||
end: shift.endTime,
|
||||
start: _formatTime(shift.startTime),
|
||||
end: _formatTime(shift.endTime),
|
||||
),
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
@@ -117,7 +130,7 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
UiIcons.mapPin,
|
||||
size: UiConstants.space3,
|
||||
@@ -126,7 +139,7 @@ class RecommendedShiftCard extends StatelessWidget {
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Expanded(
|
||||
child: Text(
|
||||
shift.locationAddress,
|
||||
shift.location,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
|
||||
@@ -2,6 +2,7 @@ import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart';
|
||||
@@ -10,23 +11,23 @@ import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dar
|
||||
|
||||
/// A widget that displays recommended shifts section.
|
||||
///
|
||||
/// Shows a horizontal scrolling list of shifts recommended for the worker
|
||||
/// based on their profile and preferences.
|
||||
/// Shows a horizontal scrolling list of [OpenShift] entities recommended
|
||||
/// for the worker based on their profile and preferences.
|
||||
class RecommendedShiftsSection extends StatelessWidget {
|
||||
/// Creates a [RecommendedShiftsSection].
|
||||
const RecommendedShiftsSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final sectionsI18n = t.staff.home.sections;
|
||||
final emptyI18n = t.staff.home.empty_states;
|
||||
final size = MediaQuery.sizeOf(context);
|
||||
final Translations i18nRoot = Translations.of(context);
|
||||
final dynamic sectionsI18n = i18nRoot.staff.home.sections;
|
||||
final dynamic emptyI18n = i18nRoot.staff.home.empty_states;
|
||||
final Size size = MediaQuery.sizeOf(context);
|
||||
|
||||
return SectionLayout(
|
||||
title: sectionsI18n.recommended_for_you,
|
||||
child: BlocBuilder<HomeCubit, HomeState>(
|
||||
builder: (context, state) {
|
||||
builder: (BuildContext context, HomeState state) {
|
||||
if (state.recommendedShifts.isEmpty) {
|
||||
return EmptyStateWidget(message: emptyI18n.no_recommended_shifts);
|
||||
}
|
||||
@@ -36,7 +37,7 @@ class RecommendedShiftsSection extends StatelessWidget {
|
||||
scrollDirection: Axis.horizontal,
|
||||
itemCount: state.recommendedShifts.length,
|
||||
clipBehavior: Clip.none,
|
||||
itemBuilder: (context, index) => Padding(
|
||||
itemBuilder: (BuildContext context, int index) => Padding(
|
||||
padding: const EdgeInsets.only(right: UiConstants.space3),
|
||||
child: RecommendedShiftCard(
|
||||
shift: state.recommendedShifts[index],
|
||||
|
||||
@@ -3,36 +3,35 @@ 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:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/shift_card.dart';
|
||||
|
||||
/// A widget that displays today's shifts section.
|
||||
///
|
||||
/// Shows a list of shifts scheduled for today, with loading state
|
||||
/// and empty state handling.
|
||||
/// Shows a list of shifts scheduled for today using [TodayShift] entities
|
||||
/// from the V2 dashboard API.
|
||||
class TodaysShiftsSection extends StatelessWidget {
|
||||
/// Creates a [TodaysShiftsSection].
|
||||
const TodaysShiftsSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final sectionsI18n = t.staff.home.sections;
|
||||
final emptyI18n = t.staff.home.empty_states;
|
||||
final Translations i18nRoot = Translations.of(context);
|
||||
final dynamic sectionsI18n = i18nRoot.staff.home.sections;
|
||||
final dynamic emptyI18n = i18nRoot.staff.home.empty_states;
|
||||
|
||||
return BlocBuilder<HomeCubit, HomeState>(
|
||||
builder: (context, state) {
|
||||
final shifts = state.todayShifts;
|
||||
builder: (BuildContext context, HomeState state) {
|
||||
final List<TodayShift> shifts = state.todayShifts;
|
||||
return SectionLayout(
|
||||
title: sectionsI18n.todays_shift,
|
||||
action: shifts.isNotEmpty
|
||||
? sectionsI18n.scheduled_count(
|
||||
count: shifts.length,
|
||||
)
|
||||
? sectionsI18n.scheduled_count(count: shifts.length)
|
||||
: null,
|
||||
child: state.status == HomeStatus.loading
|
||||
? const _ShiftsSectionSkeleton()
|
||||
@@ -46,10 +45,7 @@ class TodaysShiftsSection extends StatelessWidget {
|
||||
: Column(
|
||||
children: shifts
|
||||
.map(
|
||||
(shift) => ShiftCard(
|
||||
shift: shift,
|
||||
compact: true,
|
||||
),
|
||||
(TodayShift shift) => _TodayShiftCard(shift: shift),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
@@ -59,6 +55,70 @@ class TodaysShiftsSection extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact card for a today's shift.
|
||||
class _TodayShiftCard extends StatelessWidget {
|
||||
const _TodayShiftCard({required this.shift});
|
||||
|
||||
/// The today-shift to display.
|
||||
final TodayShift shift;
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
return DateFormat('h:mma').format(time).toLowerCase();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return GestureDetector(
|
||||
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: UiConstants.space12,
|
||||
height: UiConstants.space12,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Icon(
|
||||
UiIcons.building,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
shift.roleName,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)} \u2022 ${shift.location}',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Inline shimmer skeleton for the shifts section loading state.
|
||||
class _ShiftsSectionSkeleton extends StatelessWidget {
|
||||
const _ShiftsSectionSkeleton();
|
||||
@@ -68,20 +128,20 @@ class _ShiftsSectionSkeleton extends StatelessWidget {
|
||||
return UiShimmer(
|
||||
child: UiShimmerList(
|
||||
itemCount: 2,
|
||||
itemBuilder: (index) => Container(
|
||||
itemBuilder: (int index) => Container(
|
||||
padding: const EdgeInsets.all(UiConstants.space3),
|
||||
decoration: BoxDecoration(
|
||||
border: Border.all(color: UiColors.border),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
),
|
||||
child: const Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
UiShimmerBox(width: 48, height: 48),
|
||||
SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
UiShimmerLine(width: 160, height: 14),
|
||||
SizedBox(height: UiConstants.space2),
|
||||
UiShimmerLine(width: 120, height: 12),
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/empty_state_widget.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/shift_card.dart';
|
||||
|
||||
/// A widget that displays tomorrow's shifts section.
|
||||
///
|
||||
/// Shows a list of shifts scheduled for tomorrow with empty state handling.
|
||||
/// Shows a list of [AssignedShift] entities scheduled for tomorrow.
|
||||
class TomorrowsShiftsSection extends StatelessWidget {
|
||||
/// Creates a [TomorrowsShiftsSection].
|
||||
const TomorrowsShiftsSection({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final t = Translations.of(context);
|
||||
final sectionsI18n = t.staff.home.sections;
|
||||
final emptyI18n = t.staff.home.empty_states;
|
||||
final Translations i18nRoot = Translations.of(context);
|
||||
final dynamic sectionsI18n = i18nRoot.staff.home.sections;
|
||||
final dynamic emptyI18n = i18nRoot.staff.home.empty_states;
|
||||
|
||||
return BlocBuilder<HomeCubit, HomeState>(
|
||||
builder: (context, state) {
|
||||
final shifts = state.tomorrowShifts;
|
||||
|
||||
builder: (BuildContext context, HomeState state) {
|
||||
final List<AssignedShift> shifts = state.tomorrowShifts;
|
||||
|
||||
return SectionLayout(
|
||||
title: sectionsI18n.tomorrow,
|
||||
child: shifts.isEmpty
|
||||
? EmptyStateWidget(
|
||||
message: emptyI18n.no_shifts_tomorrow,
|
||||
)
|
||||
? EmptyStateWidget(message: emptyI18n.no_shifts_tomorrow)
|
||||
: Column(
|
||||
children: shifts
|
||||
.map(
|
||||
(shift) => ShiftCard(
|
||||
shift: shift,
|
||||
compact: true,
|
||||
),
|
||||
(AssignedShift shift) =>
|
||||
_TomorrowShiftCard(shift: shift),
|
||||
)
|
||||
.toList(),
|
||||
),
|
||||
@@ -45,3 +45,89 @@ class TomorrowsShiftsSection extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact card for a tomorrow's shift.
|
||||
class _TomorrowShiftCard extends StatelessWidget {
|
||||
const _TomorrowShiftCard({required this.shift});
|
||||
|
||||
/// The assigned shift to display.
|
||||
final AssignedShift shift;
|
||||
|
||||
String _formatTime(DateTime time) {
|
||||
return DateFormat('h:mma').format(time).toLowerCase();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double hourlyRate = shift.hourlyRateCents / 100;
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: UiConstants.space12,
|
||||
height: UiConstants.space12,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius:
|
||||
BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Icon(
|
||||
UiIcons.building,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
Flexible(
|
||||
child: Text(
|
||||
shift.roleName,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text:
|
||||
'\$${hourlyRate % 1 == 0 ? hourlyRate.toInt() : hourlyRate.toStringAsFixed(2)}',
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
children: <InlineSpan>[
|
||||
TextSpan(
|
||||
text: '/h',
|
||||
style: UiTypography.body3r,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)} \u2022 ${shift.location}',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,395 +0,0 @@
|
||||
import 'package:core_localization/core_localization.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
|
||||
class ShiftCard extends StatefulWidget {
|
||||
final Shift shift;
|
||||
final VoidCallback? onApply;
|
||||
final VoidCallback? onDecline;
|
||||
final bool compact;
|
||||
final bool disableTapNavigation; // Added property
|
||||
|
||||
const ShiftCard({
|
||||
super.key,
|
||||
required this.shift,
|
||||
this.onApply,
|
||||
this.onDecline,
|
||||
this.compact = false,
|
||||
this.disableTapNavigation = false,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ShiftCard> createState() => _ShiftCardState();
|
||||
}
|
||||
|
||||
class _ShiftCardState extends State<ShiftCard> {
|
||||
bool isExpanded = 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:mma').format(dt).toLowerCase();
|
||||
} catch (e) {
|
||||
return time;
|
||||
}
|
||||
}
|
||||
|
||||
String _formatDate(String dateStr) {
|
||||
if (dateStr.isEmpty) return '';
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
return DateFormat('MMMM d').format(date);
|
||||
} catch (e) {
|
||||
return dateStr;
|
||||
}
|
||||
}
|
||||
|
||||
String _getTimeAgo(String dateStr) {
|
||||
if (dateStr.isEmpty) return '';
|
||||
try {
|
||||
final date = DateTime.parse(dateStr);
|
||||
final diff = DateTime.now().difference(date);
|
||||
if (diff.inHours < 1) return t.staff_shifts.card.just_now;
|
||||
if (diff.inHours < 24)
|
||||
return t.staff_shifts.details.pending_time(time: '${diff.inHours}h');
|
||||
return t.staff_shifts.details.pending_time(time: '${diff.inDays}d');
|
||||
} catch (e) {
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (widget.compact) {
|
||||
return GestureDetector(
|
||||
onTap: widget.disableTapNavigation
|
||||
? null
|
||||
: () {
|
||||
setState(() => isExpanded = !isExpanded);
|
||||
Modular.to.toShiftDetails(widget.shift);
|
||||
},
|
||||
child: Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space3),
|
||||
padding: const EdgeInsets.all(UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
width: UiConstants.space12,
|
||||
height: UiConstants.space12,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: widget.shift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
child: Image.network(
|
||||
widget.shift.logoUrl!,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
: Icon(UiIcons.building, color: UiColors.mutedForeground),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space3),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Flexible(
|
||||
child: Text(
|
||||
widget.shift.title,
|
||||
style: UiTypography.body1m.textPrimary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '\$${widget.shift.hourlyRate % 1 == 0 ? widget.shift.hourlyRate.toInt() : widget.shift.hourlyRate.toStringAsFixed(2)}',
|
||||
style: UiTypography.body1b.textPrimary,
|
||||
children: [
|
||||
TextSpan(text: '/h', style: UiTypography.body3r),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Text(
|
||||
widget.shift.clientName,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'${_formatTime(widget.shift.startTime)} • ${widget.shift.location}',
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return Container(
|
||||
margin: const EdgeInsets.only(bottom: UiConstants.space4),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(UiConstants.radiusBase),
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: UiColors.black.withValues(alpha: 0.05),
|
||||
blurRadius: 4,
|
||||
offset: const Offset(0, 2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(UiConstants.space5),
|
||||
child: Column(
|
||||
children: [
|
||||
// Header
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Container(
|
||||
width: UiConstants.space14,
|
||||
height: UiConstants.space14,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.white,
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: widget.shift.logoUrl != null
|
||||
? ClipRRect(
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
child: Image.network(
|
||||
widget.shift.logoUrl!,
|
||||
fit: BoxFit.contain,
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
UiIcons.building,
|
||||
size: UiConstants.iconXl - 4, // 28px
|
||||
color: UiColors.primary,
|
||||
),
|
||||
),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space4,
|
||||
vertical: 6,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Text(
|
||||
t.staff_shifts.card.assigned(
|
||||
time: _getTimeAgo(widget.shift.createdDate)
|
||||
.replaceAll('Pending ', '')
|
||||
.replaceAll('Just now', 'just now'),
|
||||
),
|
||||
style: UiTypography.body3m.white,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Title and Rate
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
widget.shift.title,
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
),
|
||||
Text(
|
||||
widget.shift.clientName,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text.rich(
|
||||
TextSpan(
|
||||
text: '\$${widget.shift.hourlyRate % 1 == 0 ? widget.shift.hourlyRate.toInt() : widget.shift.hourlyRate.toStringAsFixed(2)}',
|
||||
style: UiTypography.headline3m.textPrimary,
|
||||
children: [
|
||||
TextSpan(text: '/h', style: UiTypography.body1r),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Location and Date
|
||||
Row(
|
||||
children: [
|
||||
Icon(
|
||||
UiIcons.mapPin,
|
||||
size: UiConstants.iconSm,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.shift.location,
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: UiConstants.space4),
|
||||
Icon(
|
||||
UiIcons.calendar,
|
||||
size: UiConstants.iconSm,
|
||||
color: UiColors.mutedForeground,
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
Text(
|
||||
'${_formatDate(widget.shift.date)}, ${_formatTime(widget.shift.startTime)}',
|
||||
style: UiTypography.body2r.textSecondary,
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
// Tags
|
||||
Wrap(
|
||||
spacing: 8,
|
||||
runSpacing: 8,
|
||||
children: [
|
||||
_buildTag(
|
||||
UiIcons.zap,
|
||||
t.staff_shifts.tags.immediate_start,
|
||||
UiColors.accent.withValues(alpha: 0.3),
|
||||
UiColors.foreground,
|
||||
),
|
||||
_buildTag(
|
||||
UiIcons.timer,
|
||||
t.staff_shifts.tags.no_experience,
|
||||
UiColors.tagError,
|
||||
UiColors.textError,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Actions
|
||||
if (!widget.compact)
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: UiConstants.space5,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: UiConstants.space12,
|
||||
child: ElevatedButton(
|
||||
onPressed: widget.onApply,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: UiColors.white,
|
||||
elevation: 0,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(t.staff_shifts.card.accept_shift),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space2),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: UiConstants.space12,
|
||||
child: OutlinedButton(
|
||||
onPressed: widget.onDecline,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
side: BorderSide(
|
||||
color: UiColors.destructive.withValues(alpha: 0.3),
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
UiConstants.radiusBase,
|
||||
),
|
||||
),
|
||||
),
|
||||
child: Text(t.staff_shifts.card.decline_shift),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: UiConstants.space5),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTag(IconData icon, String label, Color bg, Color text) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
|
||||
decoration: BoxDecoration(
|
||||
color: bg,
|
||||
borderRadius: UiConstants.radiusFull,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Icon(icon, size: UiConstants.iconSm - 2, color: text),
|
||||
const SizedBox(width: UiConstants.space1),
|
||||
Flexible(
|
||||
child: Text(
|
||||
label,
|
||||
style: UiTypography.body3m.copyWith(color: text),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -2,16 +2,17 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:staff_home/src/presentation/widgets/worker/worker_benefits/benefit_item.dart';
|
||||
|
||||
/// Widget for displaying staff benefits, using design system tokens.
|
||||
///
|
||||
/// Shows a list of benefits with circular progress indicators.
|
||||
/// Shows a list of V2 [Benefit] entities with circular progress indicators.
|
||||
class BenefitsWidget extends StatelessWidget {
|
||||
/// The list of benefits to display.
|
||||
final List<Benefit> benefits;
|
||||
|
||||
/// Creates a [BenefitsWidget].
|
||||
const BenefitsWidget({required this.benefits, super.key});
|
||||
|
||||
/// The list of benefits to display.
|
||||
final List<Benefit> benefits;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (benefits.isEmpty) {
|
||||
@@ -26,9 +27,9 @@ class BenefitsWidget extends StatelessWidget {
|
||||
return Expanded(
|
||||
child: BenefitItem(
|
||||
label: benefit.title,
|
||||
remaining: benefit.remainingHours,
|
||||
total: benefit.entitlementHours,
|
||||
used: benefit.usedHours,
|
||||
remaining: benefit.remainingHours.toDouble(),
|
||||
total: benefit.targetHours.toDouble(),
|
||||
used: benefit.trackedHours.toDouble(),
|
||||
),
|
||||
);
|
||||
}).toList(),
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_modular/flutter_modular.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:staff_home/src/data/repositories/home_repository_impl.dart';
|
||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||
import 'package:staff_home/src/domain/usecases/get_home_shifts.dart';
|
||||
import 'package:staff_home/src/presentation/blocs/benefits_overview/benefits_overview_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/blocs/home/home_cubit.dart';
|
||||
import 'package:staff_home/src/presentation/pages/benefits_overview_page.dart';
|
||||
@@ -12,36 +13,37 @@ import 'package:staff_home/src/presentation/pages/worker_home_page.dart';
|
||||
/// The module for the staff home feature.
|
||||
///
|
||||
/// This module provides dependency injection bindings for the home feature
|
||||
/// following Clean Architecture principles. It injects the repository
|
||||
/// implementation and state management components.
|
||||
/// following Clean Architecture principles. It uses the V2 REST API via
|
||||
/// [BaseApiService] for all backend access.
|
||||
class StaffHomeModule extends Module {
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository - provides home data (shifts, staff name)
|
||||
i.addLazySingleton<HomeRepository>(() => HomeRepositoryImpl());
|
||||
List<Module> get imports => <Module>[CoreModule()];
|
||||
|
||||
// StaffConnectorRepository for profile completion queries
|
||||
i.addLazySingleton<StaffConnectorRepository>(
|
||||
() => StaffConnectorRepositoryImpl(),
|
||||
@override
|
||||
void binds(Injector i) {
|
||||
// Repository - uses V2 API for dashboard data
|
||||
i.addLazySingleton<HomeRepository>(
|
||||
() => HomeRepositoryImpl(apiService: i.get<BaseApiService>()),
|
||||
);
|
||||
|
||||
// Use case for checking profile completion
|
||||
// Use cases
|
||||
i.addLazySingleton<GetDashboardUseCase>(
|
||||
() => GetDashboardUseCase(i.get<HomeRepository>()),
|
||||
);
|
||||
i.addLazySingleton<GetProfileCompletionUseCase>(
|
||||
() => GetProfileCompletionUseCase(
|
||||
repository: i.get<StaffConnectorRepository>(),
|
||||
),
|
||||
() => GetProfileCompletionUseCase(i.get<HomeRepository>()),
|
||||
);
|
||||
|
||||
// Presentation layer - Cubits
|
||||
i.addLazySingleton(
|
||||
i.addLazySingleton<HomeCubit>(
|
||||
() => HomeCubit(
|
||||
repository: i.get<HomeRepository>(),
|
||||
getDashboard: i.get<GetDashboardUseCase>(),
|
||||
getProfileCompletion: i.get<GetProfileCompletionUseCase>(),
|
||||
),
|
||||
);
|
||||
|
||||
// Cubit for benefits overview page
|
||||
i.addLazySingleton(
|
||||
i.addLazySingleton<BenefitsOverviewCubit>(
|
||||
() => BenefitsOverviewCubit(repository: i.get<HomeRepository>()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,11 +18,7 @@ dependencies:
|
||||
path: ../../../core
|
||||
krow_domain:
|
||||
path: ../../../domain
|
||||
staff_shifts:
|
||||
path: ../shifts
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
|
||||
|
||||
flutter:
|
||||
sdk: flutter
|
||||
flutter_bloc: ^8.1.0
|
||||
@@ -30,8 +26,6 @@ dependencies:
|
||||
flutter_modular: ^6.3.0
|
||||
equatable: ^2.0.5
|
||||
intl: ^0.20.0
|
||||
google_fonts: ^7.0.0
|
||||
firebase_data_connect:
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user