diff --git a/apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart b/apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart index e1520964..01248ff3 100644 --- a/apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart +++ b/apps/mobile/packages/domain/lib/src/entities/shifts/today_shift.dart @@ -17,6 +17,12 @@ class TodayShift extends Equatable { required this.startTime, required this.endTime, required this.attendanceStatus, + this.clientName = '', + this.hourlyRateCents = 0, + this.hourlyRate = 0.0, + this.totalRateCents = 0, + this.totalRate = 0.0, + this.locationAddress, this.clockInAt, }); @@ -30,6 +36,12 @@ class TodayShift extends Equatable { startTime: DateTime.parse(json['startTime'] as String), endTime: DateTime.parse(json['endTime'] as String), attendanceStatus: AttendanceStatusType.fromJson(json['attendanceStatus'] as String?), + clientName: json['clientName'] as String? ?? '', + hourlyRateCents: json['hourlyRateCents'] as int? ?? 0, + hourlyRate: (json['hourlyRate'] as num?)?.toDouble() ?? 0.0, + totalRateCents: json['totalRateCents'] as int? ?? 0, + totalRate: (json['totalRate'] as num?)?.toDouble() ?? 0.0, + locationAddress: json['locationAddress'] as String?, clockInAt: json['clockInAt'] != null ? DateTime.parse(json['clockInAt'] as String) : null, @@ -48,6 +60,24 @@ class TodayShift extends Equatable { /// Human-readable location label (clock-point or shift location). final String location; + /// Name of the client / business for this shift. + final String clientName; + + /// Pay rate in cents per hour. + final int hourlyRateCents; + + /// Pay rate in dollars per hour. + final double hourlyRate; + + /// Total pay for this shift in cents. + final int totalRateCents; + + /// Total pay for this shift in dollars. + final double totalRate; + + /// Full street address of the shift location, if available. + final String? locationAddress; + /// Scheduled start time. final DateTime startTime; @@ -67,6 +97,12 @@ class TodayShift extends Equatable { 'shiftId': shiftId, 'roleName': roleName, 'location': location, + 'clientName': clientName, + 'hourlyRateCents': hourlyRateCents, + 'hourlyRate': hourlyRate, + 'totalRateCents': totalRateCents, + 'totalRate': totalRate, + 'locationAddress': locationAddress, 'startTime': startTime.toIso8601String(), 'endTime': endTime.toIso8601String(), 'attendanceStatus': attendanceStatus.toJson(), @@ -80,6 +116,12 @@ class TodayShift extends Equatable { shiftId, roleName, location, + clientName, + hourlyRateCents, + hourlyRate, + totalRateCents, + totalRate, + locationAddress, startTime, endTime, attendanceStatus, diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart index 0f518d9d..2257e679 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/recommended_shift_card.dart @@ -21,11 +21,20 @@ class RecommendedShiftCard extends StatelessWidget { return DateFormat('h:mma').format(time).toLowerCase(); } + /// Computes the shift duration in whole hours. + double _durationHours() { + final int minutes = shift.endTime.difference(shift.startTime).inMinutes; + double hours = minutes / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } + @override Widget build(BuildContext context) { final dynamic recI18n = t.staff.home.recommended_card; final Size size = MediaQuery.sizeOf(context); - final double hourlyRate = shift.hourlyRateCents / 100; + final double durationHours = _durationHours(); + final double estimatedTotal = shift.hourlyRate * durationHours; return GestureDetector( onTap: () => Modular.to.toShiftDetailsById(shift.shiftId), @@ -75,8 +84,8 @@ class RecommendedShiftCard extends StatelessWidget { ), ), Text( - '\$${hourlyRate.toStringAsFixed(0)}/h', - style: UiTypography.headline4b, + '\$${estimatedTotal.toStringAsFixed(0)}', + style: UiTypography.title1m.textPrimary, ), ], ), @@ -89,8 +98,8 @@ class RecommendedShiftCard extends StatelessWidget { style: UiTypography.body3r.textSecondary, ), Text( - '\$${hourlyRate.toStringAsFixed(0)}/hr', - style: UiTypography.body3r.textSecondary, + '\$${shift.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', + style: UiTypography.footnote2r.textSecondary, ), ], ), diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart index ea0e376c..4f819dad 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/todays_shifts_section.dart @@ -66,8 +66,31 @@ class _TodayShiftCard extends StatelessWidget { return DateFormat('h:mma').format(time).toLowerCase(); } + /// Computes the shift duration in whole hours. + double _durationHours() { + final int minutes = shift.endTime.difference(shift.startTime).inMinutes; + double hours = minutes / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } + @override Widget build(BuildContext context) { + final bool hasRate = shift.hourlyRate > 0; + final String title = shift.clientName.isNotEmpty + ? shift.clientName + : shift.roleName; + final String? subtitle = shift.clientName.isNotEmpty + ? shift.roleName + : null; + final double durationHours = _durationHours(); + final double estimatedTotal = shift.totalRate > 0 + ? shift.totalRate + : shift.hourlyRate * durationHours; + final String displayLocation = shift.locationAddress?.isNotEmpty == true + ? shift.locationAddress! + : shift.location; + return GestureDetector( onTap: () => Modular.to.toShiftDetailsById(shift.shiftId), child: Container( @@ -99,15 +122,65 @@ class _TodayShiftCard extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text( - shift.roleName, - style: UiTypography.body1m.textPrimary, - overflow: TextOverflow.ellipsis, + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Flexible( + child: Text( + title, + style: UiTypography.body1m.textPrimary, + overflow: TextOverflow.ellipsis, + ), + ), + if (hasRate) + Text( + '\$${estimatedTotal.toStringAsFixed(0)}', + style: UiTypography.title1m.textPrimary, + ), + ], ), + if (subtitle != null) + Text( + subtitle, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + ), + if (hasRate) ...[ + const SizedBox(height: UiConstants.space1), + Text( + '\$${shift.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', + style: UiTypography.footnote2r.textSecondary, + ), + ], const SizedBox(height: UiConstants.space1), - Text( - '${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)} \u2022 ${shift.location}', - style: UiTypography.body3r.textSecondary, + Row( + children: [ + Icon( + UiIcons.clock, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Text( + '${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}', + style: UiTypography.body3r.textSecondary, + ), + const SizedBox(width: UiConstants.space3), + Icon( + UiIcons.mapPin, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + displayLocation, + style: UiTypography.body3r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], ), ], ), diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart index da46d3cf..56f28460 100644 --- a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/tomorrows_shifts_section.dart @@ -57,9 +57,27 @@ class _TomorrowShiftCard extends StatelessWidget { return DateFormat('h:mma').format(time).toLowerCase(); } + /// Computes the shift duration in whole hours. + double _durationHours() { + final int minutes = shift.endTime.difference(shift.startTime).inMinutes; + double hours = minutes / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } + @override Widget build(BuildContext context) { - final double hourlyRate = shift.hourlyRateCents / 100; + final bool hasRate = shift.hourlyRate > 0; + final String title = shift.clientName.isNotEmpty + ? shift.clientName + : shift.roleName; + final String? subtitle = shift.clientName.isNotEmpty + ? shift.roleName + : null; + final double durationHours = _durationHours(); + final double estimatedTotal = shift.totalRate > 0 + ? shift.totalRate + : shift.hourlyRate * durationHours; return GestureDetector( onTap: () => Modular.to.toShiftDetailsById(shift.shiftId), @@ -97,31 +115,61 @@ class _TomorrowShiftCard extends StatelessWidget { children: [ Flexible( child: Text( - shift.roleName, + title, style: UiTypography.body1m.textPrimary, overflow: TextOverflow.ellipsis, ), ), - Text.rich( - TextSpan( - text: - '\$${hourlyRate % 1 == 0 ? hourlyRate.toInt() : hourlyRate.toStringAsFixed(2)}', - style: UiTypography.body1b.textPrimary, - children: [ - TextSpan( - text: '/h', - style: UiTypography.body3r, - ), - ], + if (hasRate) + Text( + '\$${estimatedTotal.toStringAsFixed(0)}', + style: UiTypography.title1m.textPrimary, + ), + ], + ), + if (subtitle != null) + Text( + subtitle, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + ), + if (hasRate) ...[ + const SizedBox(height: UiConstants.space1), + Text( + '\$${shift.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h', + style: UiTypography.footnote2r.textSecondary, + ), + ], + const SizedBox(height: UiConstants.space1), + Row( + children: [ + Icon( + UiIcons.clock, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Text( + '${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}', + style: UiTypography.body3r.textSecondary, + ), + const SizedBox(width: UiConstants.space3), + Icon( + UiIcons.mapPin, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + shift.location, + style: UiTypography.body3r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, ), ), ], ), - const SizedBox(height: UiConstants.space1), - Text( - '${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)} \u2022 ${shift.location}', - style: UiTypography.body3r.textSecondary, - ), ], ), ),