diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart index daab8913..e4563de1 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -10,7 +10,13 @@ import 'package:krow_domain/krow_domain.dart'; import '../blocs/shift_details/shift_details_bloc.dart'; import '../blocs/shift_details/shift_details_event.dart'; import '../blocs/shift_details/shift_details_state.dart'; -import '../widgets/shift_location_map.dart'; +import '../widgets/shift_details/shift_break_section.dart'; +import '../widgets/shift_details/shift_date_time_section.dart'; +import '../widgets/shift_details/shift_description_section.dart'; +import '../widgets/shift_details/shift_details_bottom_bar.dart'; +import '../widgets/shift_details/shift_details_header.dart'; +import '../widgets/shift_details/shift_location_section.dart'; +import '../widgets/shift_details/shift_stats_row.dart'; class ShiftDetailsPage extends StatefulWidget { final String shiftId; @@ -68,65 +74,6 @@ class _ShiftDetailsPageState extends State { } } - Widget _buildStatCard(IconData icon, String value, String label) { - return Container( - padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.bgThird, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - child: Column( - children: [ - Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: UiColors.white, - shape: BoxShape.circle, - ), - child: Icon(icon, size: 20, color: UiColors.textSecondary), - ), - const SizedBox(height: UiConstants.space2), - Text( - value, - style: UiTypography.title1m - .copyWith(fontWeight: FontWeight.w700) - .textPrimary, - ), - Text(label, style: UiTypography.footnote2r.textSecondary), - ], - ), - ); - } - - Widget _buildTimeBox(String label, String time) { - return Container( - padding: const EdgeInsets.all(UiConstants.space3), - decoration: BoxDecoration( - color: UiColors.bgThird, - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - child: Column( - children: [ - Text( - label, - style: UiTypography.footnote2b.copyWith( - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: UiConstants.space1), - Text( - _formatTime(time), - style: UiTypography.title1m - .copyWith(fontWeight: FontWeight.w700) - .textPrimary, - ), - ], - ), - ); - } - @override Widget build(BuildContext context) { return BlocProvider( @@ -134,7 +81,7 @@ class _ShiftDetailsPageState extends State { ..add( LoadShiftDetailsEvent(widget.shiftId, roleId: widget.shift.roleId), ), - child: BlocListener( + child: BlocConsumer( listener: (context, state) { if (state is ShiftActionSuccess || state is ShiftDetailsError) { _closeActionDialog(context); @@ -158,345 +105,99 @@ class _ShiftDetailsPageState extends State { _isApplying = false; } }, - child: BlocBuilder( - builder: (context, state) { - if (state is ShiftDetailsLoading) { - return const Scaffold( - body: Center(child: CircularProgressIndicator()), - ); - } + builder: (context, state) { + if (state is ShiftDetailsLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } - Shift? displayShift = widget.shift; + Shift displayShift = widget.shift; + final i18n = Translations.of(context).staff_shifts.shift_details; - final i18n = Translations.of(context).staff_shifts.shift_details; + final duration = _calculateDuration(displayShift); + final estimatedTotal = + displayShift.totalValue ?? (displayShift.hourlyRate * duration); - final duration = _calculateDuration(displayShift); - final estimatedTotal = - displayShift.totalValue ?? (displayShift.hourlyRate * duration); - - return Scaffold( - appBar: UiAppBar( - centerTitle: false, - onLeadingPressed: () => Modular.to.toShifts(), - ), - body: Column( - children: [ - Expanded( - child: SingleChildScrollView( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Role & Client Section - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - crossAxisAlignment: CrossAxisAlignment.start, - spacing: UiConstants.space4, - children: [ - Container( - width: 48, - height: 48, - decoration: BoxDecoration( - color: UiColors.background, - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), - border: Border.all(color: UiColors.border), - ), - child: const Center( - child: Icon( - UiIcons.briefcase, - color: UiColors.primary, - size: 24, - ), - ), - ), - Expanded( - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - displayShift.title, - style: - UiTypography.headline1b.textPrimary, - ), - Text( - displayShift.clientName, - style: - UiTypography.body1m.textSecondary, - ), - Text( - displayShift.locationAddress, - style: - UiTypography.body2r.textSecondary, - ), - ], - ), - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Stats Row (New) - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - i18n.est_total, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${displayShift.hourlyRate.toStringAsFixed(0)}", - i18n.hourly_rate, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildStatCard( - UiIcons.clock, - duration.toStringAsFixed(1), - i18n.hours, - ), - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Date & Time Section - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.shift_date, - style: UiTypography - .titleUppercase4b - .textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - const Icon( - UiIcons.calendar, - size: 20, - color: UiColors.primary, - ), - const SizedBox(width: UiConstants.space2), - Text( - _formatDate(displayShift.date), - style: - UiTypography.headline5m.textPrimary, - ), - ], - ), - const SizedBox(height: UiConstants.space4), - Row( - children: [ - Expanded( - child: _buildTimeBox( - i18n.start_time, - displayShift.startTime, - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: _buildTimeBox( - i18n.end_time, - displayShift.endTime, - ), - ), - ], - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Break Section - if (displayShift.breakInfo != null && - displayShift.breakInfo!.duration != - BreakDuration.none) ...[ - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.break_title, - style: UiTypography.titleUppercase4b - .textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Row( - children: [ - const Icon( - UiIcons.breakIcon, - size: 20, - color: UiColors.primary, - ), - const SizedBox(width: UiConstants.space2), - Text( - "${displayShift.breakInfo!.duration.minutes} ${i18n.min} (${displayShift.breakInfo!.isBreakPaid ? i18n.paid : i18n.unpaid})", - style: - UiTypography.headline5m.textPrimary, - ), - ], - ), - ], - ), - ), - const Divider(height: 1, thickness: 0.5), - ], - - // Location Section (New with Map) - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.location, - style: UiTypography - .titleUppercase4b - .textSecondary, - ), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - displayShift.location.isEmpty - ? i18n.tbd - : displayShift.location, - style: UiTypography.title1m.textPrimary, - overflow: TextOverflow.ellipsis, - ), - ), - const SizedBox(width: UiConstants.space3), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - displayShift - .locationAddress - .isNotEmpty - ? displayShift.locationAddress - : displayShift.location, - ), - duration: const Duration( - seconds: 3, - ), - ), - ); - }, - icon: const Icon( - UiIcons.navigation, - size: UiConstants.iconXs, - ), - label: Text(i18n.get_direction), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.textPrimary, - side: const BorderSide( - color: UiColors.border, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - UiConstants.radiusBase, - ), - ), - padding: const EdgeInsets.symmetric( - horizontal: UiConstants.space3, - vertical: 0, - ), - minimumSize: const Size(0, 32), - ), - ), - ], - ), - const SizedBox(height: UiConstants.space3), - ShiftLocationMap( - shift: displayShift, - height: 160, - borderRadius: UiConstants.radiusBase, - ), - ], - ), - ), - - const Divider(height: 1, thickness: 0.5), - - // Description / Instructions - if ((displayShift.description ?? '').isNotEmpty) ...[ - Padding( - padding: const EdgeInsets.all(UiConstants.space5), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - i18n.job_description, - style: UiTypography - .titleUppercase4b - .textSecondary, - ), - const SizedBox(height: UiConstants.space2), - Text( - displayShift.description!, - style: UiTypography.body2r.textSecondary, - ), - ], - ), - ), - ], - ], - ), - ), - ), - - // Bottom Action Bar - Container( - padding: EdgeInsets.fromLTRB( - UiConstants.space5, - UiConstants.space4, - UiConstants.space5, - MediaQuery.of(context).padding.bottom + - UiConstants.space4, - ), - decoration: BoxDecoration( - color: UiColors.white, - border: Border(top: BorderSide(color: UiColors.border)), - boxShadow: [ - BoxShadow( - color: UiColors.popupShadow.withValues(alpha: 0.05), - blurRadius: 10, - offset: const Offset(0, -4), + return Scaffold( + appBar: UiAppBar( + centerTitle: false, + onLeadingPressed: () => Modular.to.toShifts(), + ), + body: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ShiftDetailsHeader(shift: displayShift), + const Divider(height: 1, thickness: 0.5), + ShiftStatsRow( + estimatedTotal: estimatedTotal, + hourlyRate: displayShift.hourlyRate, + duration: duration, + totalLabel: i18n.est_total, + hourlyRateLabel: i18n.hourly_rate, + hoursLabel: i18n.hours, ), + const Divider(height: 1, thickness: 0.5), + ShiftDateTimeSection( + date: displayShift.date, + startTime: displayShift.startTime, + endTime: displayShift.endTime, + shiftDateLabel: i18n.shift_date, + clockInLabel: i18n.start_time, + clockOutLabel: i18n.end_time, + ), + const Divider(height: 1, thickness: 0.5), + if (displayShift.breakInfo != null && + displayShift.breakInfo!.duration != + BreakDuration.none) ...[ + ShiftBreakSection( + breakInfo: displayShift.breakInfo!, + breakTitle: i18n.break_title, + paidLabel: i18n.paid, + unpaidLabel: i18n.unpaid, + minLabel: i18n.min, + ), + const Divider(height: 1, thickness: 0.5), + ], + ShiftLocationSection( + shift: displayShift, + locationLabel: i18n.location, + tbdLabel: i18n.tbd, + getDirectionLabel: i18n.get_direction, + ), + const Divider(height: 1, thickness: 0.5), + if (displayShift.description != null && + displayShift.description!.isNotEmpty) + ShiftDescriptionSection( + description: displayShift.description!, + descriptionLabel: i18n.job_description, + ), ], ), - child: _buildBottomButton(displayShift, context), ), - ], - ), - ); - }, - ), + ), + ShiftDetailsBottomBar( + shift: displayShift, + onApply: () => _bookShift(context, displayShift), + onDecline: () => BlocProvider.of( + context, + ).add(DeclineShiftDetailsEvent(displayShift.id)), + onAccept: () => + BlocProvider.of(context).add( + BookShiftDetailsEvent( + displayShift.id, + roleId: displayShift.roleId, + ), + ), + ), + ], + ), + ); + }, ), ); } @@ -591,91 +292,4 @@ class _ShiftDetailsPageState extends State { Navigator.of(context, rootNavigator: true).pop(); _actionDialogOpen = false; } - - Widget _buildBottomButton(Shift shift, BuildContext context) { - final String status = shift.status ?? 'open'; - - final i18n = Translations.of(context).staff_shifts.shift_details; - if (status == 'confirmed') { - return SizedBox( - width: double.infinity, - child: ElevatedButton( - onPressed: () => Modular.to.toClockIn(), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.success, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.clock_in, style: UiTypography.body2b.white), - ), - ); - } - - if (status == 'pending') { - return Row( - children: [ - Expanded( - child: OutlinedButton( - onPressed: () => BlocProvider.of( - context, - ).add(DeclineShiftDetailsEvent(shift.id)), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.destructive, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - side: const BorderSide(color: UiColors.destructive), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - ), - child: Text(i18n.decline, style: UiTypography.body2b.textError), - ), - ), - const SizedBox(width: UiConstants.space4), - Expanded( - child: ElevatedButton( - onPressed: () => BlocProvider.of( - context, - ).add(BookShiftDetailsEvent(shift.id, roleId: shift.roleId)), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric( - vertical: UiConstants.space4, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.accept_shift, style: UiTypography.body2b.white), - ), - ), - ], - ); - } - - if (status == 'open' || status == 'available') { - return ElevatedButton( - onPressed: () => _bookShift(context, shift), - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: UiColors.white, - padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(UiConstants.radiusBase), - ), - elevation: 0, - ), - child: Text(i18n.apply_now, style: UiTypography.body2b.white), - ); - } - - return const SizedBox(); - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_break_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_break_section.dart new file mode 100644 index 00000000..50288460 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_break_section.dart @@ -0,0 +1,62 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A section displaying shift break details (duration and payment status). +class ShiftBreakSection extends StatelessWidget { + /// The break information. + final Break breakInfo; + + /// Localization string for break section title. + final String breakTitle; + + /// Localization string for paid status. + final String paidLabel; + + /// Localization string for unpaid status. + final String unpaidLabel; + + /// Localization string for minutes ("min"). + final String minLabel; + + /// Creates a [ShiftBreakSection]. + const ShiftBreakSection({ + super.key, + required this.breakInfo, + required this.breakTitle, + required this.paidLabel, + required this.unpaidLabel, + required this.minLabel, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + breakTitle, + style: UiTypography.titleUppercase4b.textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Row( + children: [ + const Icon( + UiIcons.breakIcon, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space2), + Text( + "${breakInfo.duration.minutes} $minLabel (${breakInfo.isBreakPaid ? paidLabel : unpaidLabel})", + style: UiTypography.headline5m.textPrimary, + ), + ], + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart new file mode 100644 index 00000000..47eded2f --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_date_time_section.dart @@ -0,0 +1,135 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; + +/// A section displaying the date and the shift's start/end times. +class ShiftDateTimeSection extends StatelessWidget { + /// The ISO string of the date. + final String date; + + /// The start time string (HH:mm). + final String startTime; + + /// The end time string (HH:mm). + final String endTime; + + /// Localization string for shift date. + final String shiftDateLabel; + + /// Localization string for clock in time. + final String clockInLabel; + + /// Localization string for clock out time. + final String clockOutLabel; + + /// Creates a [ShiftDateTimeSection]. + const ShiftDateTimeSection({ + super.key, + required this.date, + required this.startTime, + required this.endTime, + required this.shiftDateLabel, + required this.clockInLabel, + required this.clockOutLabel, + }); + + String _formatTime(String time) { + if (time.isEmpty) return ''; + try { + final parts = time.split(':'); + final hour = int.parse(parts[0]); + final minute = int.parse(parts[1]); + final dt = DateTime(2022, 1, 1, hour, minute); + return DateFormat('h:mm a').format(dt); + } catch (e) { + return time; + } + } + + String _formatDate(String dateStr) { + if (dateStr.isEmpty) return ''; + try { + final date = DateTime.parse(dateStr); + return DateFormat('EEEE, MMMM d, y').format(date); + } catch (e) { + return dateStr; + } + } + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + shiftDateLabel, + style: UiTypography.titleUppercase4b.textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Row( + children: [ + const Icon( + UiIcons.calendar, + size: 20, + color: UiColors.primary, + ), + const SizedBox(width: UiConstants.space2), + Text( + _formatDate(date), + style: UiTypography.headline5m.textPrimary, + ), + ], + ), + const SizedBox(height: UiConstants.space4), + Row( + children: [ + Expanded( + child: _buildTimeBox( + clockInLabel, + startTime, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildTimeBox( + clockOutLabel, + endTime, + ), + ), + ], + ), + ], + ), + ); + } + + Widget _buildTimeBox(String label, String time) { + return Container( + padding: const EdgeInsets.all(UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgThird, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Column( + children: [ + Text( + label, + style: UiTypography.footnote2b.copyWith( + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: UiConstants.space1), + Text( + _formatTime(time), + style: UiTypography.title1m + .copyWith(fontWeight: FontWeight.w700) + .textPrimary, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_description_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_description_section.dart new file mode 100644 index 00000000..770fc3f9 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_description_section.dart @@ -0,0 +1,41 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A section displaying the job description for the shift. +class ShiftDescriptionSection extends StatelessWidget { + /// The description text. + final String description; + + /// Localization string for description section title. + final String descriptionLabel; + + /// Creates a [ShiftDescriptionSection]. + const ShiftDescriptionSection({ + super.key, + required this.description, + required this.descriptionLabel, + }); + + @override + Widget build(BuildContext context) { + if (description.isEmpty) return const SizedBox.shrink(); + + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + descriptionLabel, + style: UiTypography.titleUppercase4b.textSecondary, + ), + const SizedBox(height: UiConstants.space2), + Text( + description, + style: UiTypography.body2r.textSecondary, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart new file mode 100644 index 00000000..00eb9578 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_bottom_bar.dart @@ -0,0 +1,137 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:core_localization/core_localization.dart'; +import 'package:krow_core/core.dart'; + +/// A bottom action bar containing contextual buttons based on shift status. +class ShiftDetailsBottomBar extends StatelessWidget { + /// The current shift. + final Shift shift; + + /// Callback for applying/booking a shift. + final VoidCallback onApply; + + /// Callback for declining a shift. + final VoidCallback onDecline; + + /// Callback for accepting a shift. + final VoidCallback onAccept; + + /// Creates a [ShiftDetailsBottomBar]. + const ShiftDetailsBottomBar({ + super.key, + required this.shift, + required this.onApply, + required this.onDecline, + required this.onAccept, + }); + + @override + Widget build(BuildContext context) { + final String status = shift.status ?? 'open'; + final i18n = Translations.of(context).staff_shifts.shift_details; + + return Container( + padding: EdgeInsets.fromLTRB( + UiConstants.space5, + UiConstants.space4, + UiConstants.space5, + MediaQuery.of(context).padding.bottom + UiConstants.space4, + ), + decoration: BoxDecoration( + color: UiColors.white, + border: Border(top: BorderSide(color: UiColors.border)), + boxShadow: [ + BoxShadow( + color: UiColors.popupShadow.withValues(alpha: 0.05), + blurRadius: 10, + offset: const Offset(0, -4), + ), + ], + ), + child: _buildButtons(status, i18n, context), + ); + } + + Widget _buildButtons(String status, dynamic i18n, BuildContext context) { + if (status == 'confirmed') { + return SizedBox( + width: double.infinity, + child: ElevatedButton( + onPressed: () => Modular.to.toClockIn(), + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.success, + foregroundColor: UiColors.white, + padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + elevation: 0, + ), + child: Text(i18n.clock_in, style: UiTypography.body2b.white), + ), + ); + } + + if (status == 'pending') { + return Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: onDecline, + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.destructive, + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), + side: const BorderSide(color: UiColors.destructive), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + ), + child: Text(i18n.decline, style: UiTypography.body2b.textError), + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: ElevatedButton( + onPressed: onAccept, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + padding: const EdgeInsets.symmetric( + vertical: UiConstants.space4, + ), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + elevation: 0, + ), + child: Text(i18n.accept_shift, style: UiTypography.body2b.white), + ), + ), + ], + ); + } + + if (status == 'open' || status == 'available') { + return ElevatedButton( + onPressed: onApply, + style: ElevatedButton.styleFrom( + backgroundColor: UiColors.primary, + foregroundColor: UiColors.white, + padding: const EdgeInsets.symmetric(vertical: UiConstants.space4), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + elevation: 0, + ), + child: Text(i18n.apply_now, style: UiTypography.body2b.white), + ); + } + + return const SizedBox.shrink(); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_header.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_header.dart new file mode 100644 index 00000000..ea8d70db --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_details_header.dart @@ -0,0 +1,65 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; + +/// A header widget for the shift details page displaying the role, client name, and address. +class ShiftDetailsHeader extends StatelessWidget { + /// The shift entity containing the header information. + final Shift shift; + + /// Creates a [ShiftDetailsHeader]. + const ShiftDetailsHeader({ + super.key, + required this.shift, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + spacing: UiConstants.space4, + children: [ + Container( + width: 48, + height: 48, + decoration: BoxDecoration( + color: UiColors.background, + borderRadius: BorderRadius.circular( + UiConstants.radiusBase, + ), + border: Border.all(color: UiColors.border), + ), + child: const Center( + child: Icon( + UiIcons.briefcase, + color: UiColors.primary, + size: 24, + ), + ), + ), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + shift.title, + style: UiTypography.headline1b.textPrimary, + ), + Text( + shift.clientName, + style: UiTypography.body1m.textSecondary, + ), + Text( + shift.locationAddress, + style: UiTypography.body2r.textSecondary, + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_location_section.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_location_section.dart new file mode 100644 index 00000000..35656dfd --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_location_section.dart @@ -0,0 +1,94 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; +import 'package:krow_domain/krow_domain.dart'; +import '../shift_location_map.dart'; + +/// A section displaying the shift's location, address, map, and "Get direction" action. +class ShiftLocationSection extends StatelessWidget { + /// The shift entity containing location data. + final Shift shift; + + /// Localization string for location section title. + final String locationLabel; + + /// Localization string for "TBD". + final String tbdLabel; + + /// Localization string for "Get direction". + final String getDirectionLabel; + + /// Creates a [ShiftLocationSection]. + const ShiftLocationSection({ + super.key, + required this.shift, + required this.locationLabel, + required this.tbdLabel, + required this.getDirectionLabel, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + locationLabel, + style: UiTypography.titleUppercase4b.textSecondary, + ), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Expanded( + child: Text( + shift.location.isEmpty ? tbdLabel : shift.location, + style: UiTypography.title1m.textPrimary, + overflow: TextOverflow.ellipsis, + ), + ), + const SizedBox(width: UiConstants.space3), + OutlinedButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + shift.locationAddress.isNotEmpty + ? shift.locationAddress + : shift.location, + ), + duration: const Duration(seconds: 3), + ), + ); + }, + icon: const Icon( + UiIcons.navigation, + size: UiConstants.iconXs, + ), + label: Text(getDirectionLabel), + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.textPrimary, + side: const BorderSide(color: UiColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + padding: const EdgeInsets.symmetric( + horizontal: UiConstants.space3, + vertical: 0, + ), + minimumSize: const Size(0, 32), + ), + ), + ], + ), + const SizedBox(height: UiConstants.space3), + ShiftLocationMap( + shift: shift, + height: 160, + borderRadius: UiConstants.radiusBase, + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_stats_row.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_stats_row.dart new file mode 100644 index 00000000..49d8a8c6 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/shift_details/shift_stats_row.dart @@ -0,0 +1,99 @@ +import 'package:design_system/design_system.dart'; +import 'package:flutter/material.dart'; + +/// A row of statistic cards for shift details (Total Pay, Rate, Hours). +class ShiftStatsRow extends StatelessWidget { + /// Estimated total pay for the shift. + final double estimatedTotal; + + /// Hourly rate for the shift. + final double hourlyRate; + + /// Total duration of the shift in hours. + final double duration; + + /// Localization string for total. + final String totalLabel; + + /// Localization string for hourly rate. + final String hourlyRateLabel; + + /// Localization string for hours. + final String hoursLabel; + + /// Creates a [ShiftStatsRow]. + const ShiftStatsRow({ + super.key, + required this.estimatedTotal, + required this.hourlyRate, + required this.duration, + required this.totalLabel, + required this.hourlyRateLabel, + required this.hoursLabel, + }); + + @override + Widget build(BuildContext context) { + return Padding( + padding: const EdgeInsets.all(UiConstants.space5), + child: Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + totalLabel, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${hourlyRate.toStringAsFixed(0)}", + hourlyRateLabel, + ), + ), + const SizedBox(width: UiConstants.space4), + Expanded( + child: _buildStatCard( + UiIcons.clock, + duration.toStringAsFixed(1), + hoursLabel, + ), + ), + ], + ), + ); + } + + Widget _buildStatCard(IconData icon, String value, String label) { + return Container( + padding: const EdgeInsets.symmetric(vertical: UiConstants.space3), + decoration: BoxDecoration( + color: UiColors.bgThird, + borderRadius: BorderRadius.circular(UiConstants.radiusBase), + ), + child: Column( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: UiColors.white, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 20, color: UiColors.textSecondary), + ), + const SizedBox(height: UiConstants.space2), + Text( + value, + style: UiTypography.title1m + .copyWith(fontWeight: FontWeight.w700) + .textPrimary, + ), + Text(label, style: UiTypography.footnote2r.textSecondary), + ], + ), + ); + } +}