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

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

View File

@@ -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: <Widget>[
Text(
shift.roleName,
style: UiTypography.body1m.textPrimary,
overflow: TextOverflow.ellipsis,
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(
'\$${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: <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,
),
),
],
),
],
),

View File

@@ -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(
'\$${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,
),
],
),
),