feat(shifts): enhance TodayShift and shift card components with client details and pay calculations

This commit is contained in:
Achintha Isuru
2026-03-18 15:39:05 -04:00
parent 77f8b8511c
commit 47508f54e4
4 changed files with 202 additions and 30 deletions

View File

@@ -17,6 +17,12 @@ class TodayShift extends Equatable {
required this.startTime, required this.startTime,
required this.endTime, required this.endTime,
required this.attendanceStatus, required this.attendanceStatus,
this.clientName = '',
this.hourlyRateCents = 0,
this.hourlyRate = 0.0,
this.totalRateCents = 0,
this.totalRate = 0.0,
this.locationAddress,
this.clockInAt, this.clockInAt,
}); });
@@ -30,6 +36,12 @@ class TodayShift extends Equatable {
startTime: DateTime.parse(json['startTime'] as String), startTime: DateTime.parse(json['startTime'] as String),
endTime: DateTime.parse(json['endTime'] as String), endTime: DateTime.parse(json['endTime'] as String),
attendanceStatus: AttendanceStatusType.fromJson(json['attendanceStatus'] 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 clockInAt: json['clockInAt'] != null
? DateTime.parse(json['clockInAt'] as String) ? DateTime.parse(json['clockInAt'] as String)
: null, : null,
@@ -48,6 +60,24 @@ class TodayShift extends Equatable {
/// Human-readable location label (clock-point or shift location). /// Human-readable location label (clock-point or shift location).
final String 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. /// Scheduled start time.
final DateTime startTime; final DateTime startTime;
@@ -67,6 +97,12 @@ class TodayShift extends Equatable {
'shiftId': shiftId, 'shiftId': shiftId,
'roleName': roleName, 'roleName': roleName,
'location': location, 'location': location,
'clientName': clientName,
'hourlyRateCents': hourlyRateCents,
'hourlyRate': hourlyRate,
'totalRateCents': totalRateCents,
'totalRate': totalRate,
'locationAddress': locationAddress,
'startTime': startTime.toIso8601String(), 'startTime': startTime.toIso8601String(),
'endTime': endTime.toIso8601String(), 'endTime': endTime.toIso8601String(),
'attendanceStatus': attendanceStatus.toJson(), 'attendanceStatus': attendanceStatus.toJson(),
@@ -80,6 +116,12 @@ class TodayShift extends Equatable {
shiftId, shiftId,
roleName, roleName,
location, location,
clientName,
hourlyRateCents,
hourlyRate,
totalRateCents,
totalRate,
locationAddress,
startTime, startTime,
endTime, endTime,
attendanceStatus, attendanceStatus,

View File

@@ -21,11 +21,20 @@ class RecommendedShiftCard extends StatelessWidget {
return DateFormat('h:mma').format(time).toLowerCase(); 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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final dynamic recI18n = t.staff.home.recommended_card; final dynamic recI18n = t.staff.home.recommended_card;
final Size size = MediaQuery.sizeOf(context); final Size size = MediaQuery.sizeOf(context);
final double hourlyRate = shift.hourlyRateCents / 100; final double durationHours = _durationHours();
final double estimatedTotal = shift.hourlyRate * durationHours;
return GestureDetector( return GestureDetector(
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId), onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
@@ -75,8 +84,8 @@ class RecommendedShiftCard extends StatelessWidget {
), ),
), ),
Text( Text(
'\$${hourlyRate.toStringAsFixed(0)}/h', '\$${estimatedTotal.toStringAsFixed(0)}',
style: UiTypography.headline4b, style: UiTypography.title1m.textPrimary,
), ),
], ],
), ),
@@ -89,8 +98,8 @@ class RecommendedShiftCard extends StatelessWidget {
style: UiTypography.body3r.textSecondary, style: UiTypography.body3r.textSecondary,
), ),
Text( Text(
'\$${hourlyRate.toStringAsFixed(0)}/hr', '\$${shift.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h',
style: UiTypography.body3r.textSecondary, style: UiTypography.footnote2r.textSecondary,
), ),
], ],
), ),

View File

@@ -66,8 +66,31 @@ class _TodayShiftCard extends StatelessWidget {
return DateFormat('h:mma').format(time).toLowerCase(); 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 @override
Widget build(BuildContext context) { 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( return GestureDetector(
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId), onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
child: Container( child: Container(
@@ -99,15 +122,65 @@ class _TodayShiftCard extends StatelessWidget {
child: Column( child: Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Text( Row(
shift.roleName, mainAxisAlignment: MainAxisAlignment.spaceBetween,
style: UiTypography.body1m.textPrimary, children: <Widget>[
overflow: TextOverflow.ellipsis, 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) ...<Widget>[
const SizedBox(height: UiConstants.space1),
Text(
'\$${shift.hourlyRate.toInt()}/hr \u00b7 ${durationHours.toInt()}h',
style: UiTypography.footnote2r.textSecondary,
),
],
const SizedBox(height: UiConstants.space1), const SizedBox(height: UiConstants.space1),
Text( Row(
'${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)} \u2022 ${shift.location}', children: <Widget>[
style: UiTypography.body3r.textSecondary, 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,
),
),
],
), ),
], ],
), ),

View File

@@ -57,9 +57,27 @@ class _TomorrowShiftCard extends StatelessWidget {
return DateFormat('h:mma').format(time).toLowerCase(); 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 @override
Widget build(BuildContext context) { 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( return GestureDetector(
onTap: () => Modular.to.toShiftDetailsById(shift.shiftId), onTap: () => Modular.to.toShiftDetailsById(shift.shiftId),
@@ -97,31 +115,61 @@ class _TomorrowShiftCard extends StatelessWidget {
children: <Widget>[ children: <Widget>[
Flexible( Flexible(
child: Text( child: Text(
shift.roleName, title,
style: UiTypography.body1m.textPrimary, style: UiTypography.body1m.textPrimary,
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),
), ),
Text.rich( if (hasRate)
TextSpan( Text(
text: '\$${estimatedTotal.toStringAsFixed(0)}',
'\$${hourlyRate % 1 == 0 ? hourlyRate.toInt() : hourlyRate.toStringAsFixed(2)}', style: UiTypography.title1m.textPrimary,
style: UiTypography.body1b.textPrimary, ),
children: <InlineSpan>[ ],
TextSpan( ),
text: '/h', if (subtitle != null)
style: UiTypography.body3r, Text(
), subtitle,
], style: UiTypography.body3r.textSecondary,
overflow: TextOverflow.ellipsis,
),
if (hasRate) ...<Widget>[
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: <Widget>[
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,
),
], ],
), ),
), ),