diff --git a/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_shift_card.dart b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_shift_card.dart new file mode 100644 index 00000000..14ff8d4a --- /dev/null +++ b/apps/mobile/packages/features/staff/home/lib/src/presentation/widgets/home_page/home_shift_card.dart @@ -0,0 +1,193 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// A reusable compact card for displaying shift information on the home page. +/// +/// Accepts display-ready primitive fields so it works with any shift type +/// (today shifts, tomorrow shifts, etc.). +class HomeShiftCard extends StatelessWidget { + /// Creates a [HomeShiftCard]. + const HomeShiftCard({ + super.key, + required this.shiftId, + required this.title, + this.subtitle, + required this.location, + required this.startTime, + required this.endTime, + this.hourlyRate, + this.totalRate, + this.onTap, + }); + + /// Unique identifier of the shift. + final String shiftId; + + /// Primary display text (client name or role name). + final String title; + + /// Secondary display text (role name when title is client name). + final String? subtitle; + + /// Location address to display. + final String location; + + /// Shift start time. + final DateTime startTime; + + /// Shift end time. + final DateTime endTime; + + /// Hourly rate in dollars, null if not available. + final double? hourlyRate; + + /// Total rate in dollars, null if not available. + final double? totalRate; + + /// Callback when the card is tapped. + final VoidCallback? onTap; + + /// Formats a [DateTime] as a lowercase 12-hour time string (e.g. "9:00am"). + String _formatTime(DateTime time) { + return DateFormat('h:mma').format(time).toLowerCase(); + } + + /// Computes the shift duration in whole hours. + double _durationHours() { + final int minutes = endTime.difference(startTime).inMinutes; + double hours = minutes / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } + + @override + Widget build(BuildContext context) { + final bool hasRate = hourlyRate != null && hourlyRate! > 0; + final double durationHours = _durationHours(); + final double estimatedTotal = (totalRate != null && totalRate! > 0) + ? totalRate! + : (hourlyRate ?? 0) * durationHours; + + return GestureDetector( + onTap: onTap, + 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: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.start, + spacing: UiConstants.space3, + children: [ + Container( + width: UiConstants.space10, + height: UiConstants.space10, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + border: Border.all(color: UiColors.border), + ), + child: Icon( + UiIcons.building, + size: UiConstants.space5, + color: UiColors.mutedForeground, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: UiTypography.body1m.textPrimary, + overflow: TextOverflow.ellipsis, + ), + + if (subtitle != null) + Text( + subtitle!, + style: UiTypography.body3r.textSecondary, + overflow: TextOverflow.ellipsis, + ), + ], + ), + ], + ), + + if (hasRate) ...[ + Column( + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + Text( + '\$${estimatedTotal.toStringAsFixed(0)}', + style: UiTypography.title1m.textPrimary, + ), + Text( + '\$${hourlyRate!.toInt()}/hr \u00b7 ${durationHours.toInt()}h', + style: UiTypography.footnote2r.textSecondary, + ), + ], + ), + ], + ], + ), + + const SizedBox(height: UiConstants.space3), + + // Time and location row + Column( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space1, + children: [ + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + UiIcons.clock, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Text( + '${_formatTime(startTime)} - ${_formatTime(endTime)}', + style: UiTypography.body3r.textSecondary, + ), + ], + ), + Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon( + UiIcons.mapPin, + size: UiConstants.space3, + color: UiColors.mutedForeground, + ), + const SizedBox(width: UiConstants.space1), + Expanded( + child: Text( + location, + style: UiTypography.body3r.textSecondary, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + ), + ], + ), + ], + ), + ], + ), + ), + ); + } +} 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 4f819dad..fb3278c5 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 @@ -3,12 +3,12 @@ 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/home_shift_card.dart'; import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; /// A widget that displays today's shifts section. @@ -45,7 +45,29 @@ class TodaysShiftsSection extends StatelessWidget { : Column( children: shifts .map( - (TodayShift shift) => _TodayShiftCard(shift: shift), + (TodayShift shift) => HomeShiftCard( + shiftId: shift.shiftId, + title: shift.roleName.isNotEmpty + ? shift.roleName + : shift.clientName, + subtitle: shift.clientName.isNotEmpty + ? shift.clientName + : null, + location: + shift.locationAddress?.isNotEmpty == true + ? shift.locationAddress! + : shift.location, + startTime: shift.startTime, + endTime: shift.endTime, + hourlyRate: shift.hourlyRate > 0 + ? shift.hourlyRate + : null, + totalRate: shift.totalRate > 0 + ? shift.totalRate + : null, + onTap: () => Modular.to + .toShiftDetailsById(shift.shiftId), + ), ) .toList(), ), @@ -55,143 +77,6 @@ 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(); - } - - /// 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( - 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: 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( - 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), - 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, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } -} - /// Inline shimmer skeleton for the shifts section loading state. class _ShiftsSectionSkeleton extends StatelessWidget { const _ShiftsSectionSkeleton(); 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 56f28460..0c045f7f 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 @@ -1,14 +1,13 @@ 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/home_shift_card.dart'; import 'package:staff_home/src/presentation/widgets/home_page/section_layout.dart'; /// A widget that displays tomorrow's shifts section. @@ -35,8 +34,26 @@ class TomorrowsShiftsSection extends StatelessWidget { : Column( children: shifts .map( - (AssignedShift shift) => - _TomorrowShiftCard(shift: shift), + (AssignedShift shift) => HomeShiftCard( + shiftId: shift.shiftId, + title: shift.clientName.isNotEmpty + ? shift.clientName + : shift.roleName, + subtitle: shift.clientName.isNotEmpty + ? shift.roleName + : null, + location: shift.location, + startTime: shift.startTime, + endTime: shift.endTime, + hourlyRate: shift.hourlyRate > 0 + ? shift.hourlyRate + : null, + totalRate: shift.totalRate > 0 + ? shift.totalRate + : null, + onTap: () => Modular.to + .toShiftDetailsById(shift.shiftId), + ), ) .toList(), ), @@ -45,137 +62,3 @@ 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(); - } - - /// 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; - - 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: [ - 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: [ - 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), - 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, - ), - ), - ], - ), - ], - ), - ), - ], - ), - ), - ); - } -}