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

View File

@@ -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];
}

View File

@@ -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();
}

View File

@@ -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();
}

View File

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

View File

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

View File

@@ -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,
];
}
}

View File

@@ -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),
// ],
],
),
);

View File

@@ -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')) {

View File

@@ -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,

View File

@@ -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],

View File

@@ -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),

View File

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

View File

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

View File

@@ -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(),

View File

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