feat(shifts): enhance TodayShift and shift card components with client details and pay calculations
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
@@ -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,16 +122,66 @@ class _TodayShiftCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: <Widget>[
|
||||
Text(
|
||||
shift.roleName,
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: <Widget>[
|
||||
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(
|
||||
'${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)} \u2022 ${shift.location}',
|
||||
'\$${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(
|
||||
displayLocation,
|
||||
style: UiTypography.body3r.textSecondary,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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: <Widget>[
|
||||
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: <InlineSpan>[
|
||||
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) ...<Widget>[
|
||||
const SizedBox(height: UiConstants.space1),
|
||||
Text(
|
||||
'${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)} \u2022 ${shift.location}',
|
||||
'\$${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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user