From c6128c2332117ce1730d5590dec0c3c07ad8b87f Mon Sep 17 00:00:00 2001 From: Achintha Isuru Date: Sat, 31 Jan 2026 20:33:35 -0500 Subject: [PATCH] feat: Integrate Google Maps Places Autocomplete for Hub Address Validation - Refactored ShiftsBloc to remove unused shift-related events and use cases. - Updated navigation paths in ShiftsNavigator to reflect new structure. - Simplified MyShiftCard widget by removing unnecessary parameters and logic. - Modified FindShiftsTab and HistoryShiftsTab to utilize new navigation for shift details. - Created ShiftDetailsModule with necessary bindings and routes for shift details. - Implemented ShiftDetailsBloc, ShiftDetailsEvent, and ShiftDetailsState for managing shift details. - Developed ShiftDetailsPage to display detailed information about a shift and handle booking/declining actions. - Added necessary imports and adjusted existing files to accommodate new shift details functionality. --- .../shift_details/shift_details_bloc.dart | 63 +++ .../shift_details/shift_details_event.dart | 32 ++ .../shift_details/shift_details_state.dart | 37 ++ .../blocs/shifts/shifts_bloc.dart | 54 +-- .../blocs/shifts/shifts_event.dart | 8 - .../navigation/shifts_navigator.dart | 2 +- .../pages/shift_details_page.dart | 433 ++++++++++++++++++ .../presentation/widgets/my_shift_card.dart | 396 +--------------- .../widgets/tabs/find_shifts_tab.dart | 74 +-- .../widgets/tabs/history_shifts_tab.dart | 10 +- .../shifts/lib/src/shift_details_module.dart | 31 ++ .../shifts/lib/src/staff_shifts_module.dart | 3 +- .../staff/shifts/lib/staff_shifts.dart | 2 + .../staff_main/lib/src/staff_main_module.dart | 4 + 14 files changed, 623 insertions(+), 526 deletions(-) create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart create mode 100644 apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart new file mode 100644 index 00000000..5b225f06 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_bloc.dart @@ -0,0 +1,63 @@ +import 'package:bloc/bloc.dart'; +import '../../../domain/usecases/apply_for_shift_usecase.dart'; +import '../../../domain/usecases/decline_shift_usecase.dart'; +import '../../../domain/usecases/get_shift_details_usecase.dart'; +import 'shift_details_event.dart'; +import 'shift_details_state.dart'; + +class ShiftDetailsBloc extends Bloc { + final GetShiftDetailsUseCase getShiftDetails; + final ApplyForShiftUseCase applyForShift; + final DeclineShiftUseCase declineShift; + + ShiftDetailsBloc({ + required this.getShiftDetails, + required this.applyForShift, + required this.declineShift, + }) : super(ShiftDetailsInitial()) { + on(_onLoadDetails); + on(_onBookShift); + on(_onDeclineShift); + } + + Future _onLoadDetails( + LoadShiftDetailsEvent event, + Emitter emit, + ) async { + emit(ShiftDetailsLoading()); + try { + final shift = await getShiftDetails(event.shiftId); + if (shift != null) { + emit(ShiftDetailsLoaded(shift)); + } else { + emit(const ShiftDetailsError("Shift not found")); + } + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } + + Future _onBookShift( + BookShiftDetailsEvent event, + Emitter emit, + ) async { + try { + await applyForShift(event.shiftId, isInstantBook: true); + emit(const ShiftActionSuccess("Shift successfully booked!")); + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } + + Future _onDeclineShift( + DeclineShiftDetailsEvent event, + Emitter emit, + ) async { + try { + await declineShift(event.shiftId); + emit(const ShiftActionSuccess("Shift declined")); + } catch (e) { + emit(ShiftDetailsError(e.toString())); + } + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart new file mode 100644 index 00000000..1080065c --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_event.dart @@ -0,0 +1,32 @@ +import 'package:equatable/equatable.dart'; + +abstract class ShiftDetailsEvent extends Equatable { + const ShiftDetailsEvent(); + + @override + List get props => []; +} + +class LoadShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + const LoadShiftDetailsEvent(this.shiftId); + + @override + List get props => [shiftId]; +} + +class BookShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + const BookShiftDetailsEvent(this.shiftId); + + @override + List get props => [shiftId]; +} + +class DeclineShiftDetailsEvent extends ShiftDetailsEvent { + final String shiftId; + const DeclineShiftDetailsEvent(this.shiftId); + + @override + List get props => [shiftId]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart new file mode 100644 index 00000000..b1a239c4 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shift_details/shift_details_state.dart @@ -0,0 +1,37 @@ +import 'package:equatable/equatable.dart'; +import 'package:krow_domain/krow_domain.dart'; + +abstract class ShiftDetailsState extends Equatable { + const ShiftDetailsState(); + + @override + List get props => []; +} + +class ShiftDetailsInitial extends ShiftDetailsState {} + +class ShiftDetailsLoading extends ShiftDetailsState {} + +class ShiftDetailsLoaded extends ShiftDetailsState { + final Shift shift; + const ShiftDetailsLoaded(this.shift); + + @override + List get props => [shift]; +} + +class ShiftDetailsError extends ShiftDetailsState { + final String message; + const ShiftDetailsError(this.message); + + @override + List get props => [message]; +} + +class ShiftActionSuccess extends ShiftDetailsState { + final String message; + const ShiftActionSuccess(this.message); + + @override + List get props => [message]; +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart index 0fb6f979..1b21f68b 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_bloc.dart @@ -3,15 +3,12 @@ import 'package:equatable/equatable.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:meta/meta.dart'; -import '../../../domain/usecases/get_available_shifts_usecase.dart'; import '../../../domain/arguments/get_available_shifts_arguments.dart'; -import '../../../domain/usecases/get_my_shifts_usecase.dart'; -import '../../../domain/usecases/get_pending_assignments_usecase.dart'; +import '../../../domain/usecases/get_available_shifts_usecase.dart'; import '../../../domain/usecases/get_cancelled_shifts_usecase.dart'; import '../../../domain/usecases/get_history_shifts_usecase.dart'; -import '../../../domain/usecases/accept_shift_usecase.dart'; -import '../../../domain/usecases/decline_shift_usecase.dart'; -import '../../../domain/usecases/apply_for_shift_usecase.dart'; +import '../../../domain/usecases/get_my_shifts_usecase.dart'; +import '../../../domain/usecases/get_pending_assignments_usecase.dart'; part 'shifts_event.dart'; part 'shifts_state.dart'; @@ -22,9 +19,6 @@ class ShiftsBloc extends Bloc { final GetPendingAssignmentsUseCase getPendingAssignments; final GetCancelledShiftsUseCase getCancelledShifts; final GetHistoryShiftsUseCase getHistoryShifts; - final AcceptShiftUseCase acceptShift; - final DeclineShiftUseCase declineShift; - final ApplyForShiftUseCase applyForShift; ShiftsBloc({ required this.getMyShifts, @@ -32,15 +26,9 @@ class ShiftsBloc extends Bloc { required this.getPendingAssignments, required this.getCancelledShifts, required this.getHistoryShifts, - required this.acceptShift, - required this.declineShift, - required this.applyForShift, }) : super(ShiftsInitial()) { on(_onLoadShifts); on(_onFilterAvailableShifts); - on(_onAcceptShift); - on(_onDeclineShift); - on(_onBookShift); } Future _onLoadShifts( @@ -102,40 +90,4 @@ class ShiftsBloc extends Bloc { } } } - - Future _onAcceptShift( - AcceptShiftEvent event, - Emitter emit, - ) async { - try { - await acceptShift(event.shiftId); - add(LoadShiftsEvent()); // Reload lists - } catch (_) { - // Handle error - } - } - - Future _onDeclineShift( - DeclineShiftEvent event, - Emitter emit, - ) async { - try { - await declineShift(event.shiftId); - add(LoadShiftsEvent()); // Reload lists - } catch (_) { - // Handle error - } - } - - Future _onBookShift( - BookShiftEvent event, - Emitter emit, - ) async { - try { - await applyForShift(event.shiftId, isInstantBook: true); - add(LoadShiftsEvent()); // Reload to move from Available to My Shifts - } catch (_) { - // Handle error - } - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart index eeab5787..0ea1a6b1 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/blocs/shifts/shifts_event.dart @@ -35,11 +35,3 @@ class DeclineShiftEvent extends ShiftsEvent { @override List get props => [shiftId]; } - -class BookShiftEvent extends ShiftsEvent { - final String shiftId; - const BookShiftEvent(this.shiftId); - - @override - List get props => [shiftId]; -} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart index 4832055b..306caa5a 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/navigation/shifts_navigator.dart @@ -3,7 +3,7 @@ import 'package:krow_domain/krow_domain.dart'; extension ShiftsNavigator on IModularNavigator { void pushShiftDetails(Shift shift) { - pushNamed('/shifts/details/${shift.id}', arguments: shift); + pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift); } // Example for going back or internal navigation if needed 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 new file mode 100644 index 00000000..7ade6d31 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart @@ -0,0 +1,433 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import 'package:krow_domain/krow_domain.dart'; +import 'package:design_system/design_system.dart'; // Re-added for UiIcons/Colors as they are used in expanded logic +import 'package:intl/intl.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 '../styles/shifts_styles.dart'; +import '../widgets/my_shift_card.dart'; + +class ShiftDetailsPage extends StatelessWidget { + final String shiftId; + final Shift? shift; + + const ShiftDetailsPage({ + super.key, + required this.shiftId, + this.shift, + }); + + 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; + } + } + + double _calculateDuration(Shift shift) { + if (shift.startTime.isEmpty || shift.endTime.isEmpty) { + return 0; + } + try { + final s = shift.startTime.split(':').map(int.parse).toList(); + final e = shift.endTime.split(':').map(int.parse).toList(); + double hours = ((e[0] * 60 + e[1]) - (s[0] * 60 + s[1])) / 60; + if (hours < 0) hours += 24; + return hours.roundToDouble(); + } catch (_) { + return 0; + } + } + + Widget _buildStatCard(IconData icon, String value, String label) { + return Container( + padding: const EdgeInsets.symmetric(vertical: 16), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: UiColors.border), + ), + child: Column( + children: [ + Container( + width: 40, + height: 40, + decoration: const BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: Icon(icon, size: 20, color: UiColors.iconSecondary), + ), + const SizedBox(height: 8), + Text( + value, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + Text( + label, + style: UiTypography.footnote2r.copyWith( + color: UiColors.textSecondary, + ), + ), + ], + ), + ); + } + + Widget _buildTimeBox(String label, String time) { + return Container( + padding: const EdgeInsets.all(16), + decoration: BoxDecoration( + color: const Color(0xFFF8FAFC), + borderRadius: BorderRadius.circular(16), + ), + child: Column( + children: [ + Text( + label, + style: const TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 4), + Text( + _formatTime(time), + style: UiTypography.display2m.copyWith( + fontSize: 20, + color: UiColors.textPrimary, + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (_) => Modular.get() + ..add(LoadShiftDetailsEvent(shiftId)), + child: BlocListener( + listener: (context, state) { + if (state is ShiftActionSuccess) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: const Color(0xFF10B981), + ), + ); + Modular.to.pop(true); // Return outcome + } else if (state is ShiftDetailsError) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.message), + backgroundColor: const Color(0xFFEF4444), + ), + ); + } + }, + child: BlocBuilder( + builder: (context, state) { + if (state is ShiftDetailsLoading) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } + + Shift? displayShift; + if (state is ShiftDetailsLoaded) { + displayShift = state.shift; + } else { + displayShift = shift; + } + + if (displayShift == null) { + return const Scaffold( + body: Center(child: Text("Shift not found")), + ); + } + + final duration = _calculateDuration(displayShift); + final estimatedTotal = (displayShift.hourlyRate) * duration; + + return Scaffold( + backgroundColor: AppColors.krowBackground, + appBar: AppBar( + title: const Text("Shift Details"), + backgroundColor: Colors.white, + foregroundColor: AppColors.krowCharcoal, + elevation: 0.5, + ), + body: Padding( + padding: const EdgeInsets.all(20.0), + child: Column( + children: [ + Expanded( + child: SingleChildScrollView( + child: Column( + children: [ + MyShiftCard( + shift: displayShift, + // No direct actions on the card, handled by page buttons + ), + const SizedBox(height: 24), + + // Stats Row + Row( + children: [ + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${estimatedTotal.toStringAsFixed(0)}", + "Total", + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + UiIcons.dollar, + "\$${displayShift.hourlyRate.toInt()}", + "Hourly Rate", + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildStatCard( + UiIcons.clock, + "${duration.toInt()}", + "Hours", + ), + ), + ], + ), + const SizedBox(height: 24), + + // In/Out Time + Row( + children: [ + Expanded( + child: _buildTimeBox( + "CLOCK IN TIME", + displayShift.startTime, + ), + ), + const SizedBox(width: 12), + Expanded( + child: _buildTimeBox( + "CLOCK OUT TIME", + displayShift.endTime, + ), + ), + ], + ), + const SizedBox(height: 24), + + // Location + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "LOCATION", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + displayShift.location.isEmpty + ? "TBD" + : displayShift.location, + style: UiTypography.title1m.copyWith( + color: UiColors.textPrimary, + ), + ), + OutlinedButton.icon( + onPressed: () { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(displayShift!.locationAddress), + duration: const Duration(seconds: 3), + ), + ); + }, + icon: const Icon(UiIcons.navigation, size: 14), + label: const Text( + "Get direction", + style: TextStyle(fontSize: 12), + ), + style: OutlinedButton.styleFrom( + foregroundColor: UiColors.textPrimary, + side: const BorderSide(color: UiColors.border), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + ), + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0), + minimumSize: const Size(0, 32), + ), + ), + ], + ), + const SizedBox(height: 12), + Container( + height: 128, + width: double.infinity, + decoration: BoxDecoration( + color: Colors.grey.shade100, + borderRadius: BorderRadius.circular(12), + ), + child: const Center( + child: Icon( + UiIcons.mapPin, + color: UiColors.iconSecondary, + size: 32, + ), + ), + // Placeholder for Map + ), + ], + ), + const SizedBox(height: 24), + + // Additional Info + if (displayShift.description != null) ...[ + SizedBox( + width: double.infinity, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "ADDITIONAL INFO", + style: TextStyle( + fontSize: 10, + fontWeight: FontWeight.bold, + color: UiColors.textSecondary, + letterSpacing: 0.5, + ), + ), + const SizedBox(height: 8), + Text( + displayShift.description!, + style: UiTypography.body2m.copyWith( + color: UiColors.textPrimary, + ), + ), + ], + ), + ), + ], + ], + ), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Expanded( + child: OutlinedButton( + onPressed: () => _declineShift(context, displayShift!.id), + style: OutlinedButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + side: const BorderSide(color: Color(0xFFEF4444)), + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text("Decline"), + ), + ), + const SizedBox(width: 16), + Expanded( + child: ElevatedButton( + onPressed: () => _bookShift(context, displayShift!.id), + style: ElevatedButton.styleFrom( + backgroundColor: const Color(0xFF10B981), + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 16), + ), + child: const Text("Book Shift"), + ), + ), + ], + ), + SizedBox(height: MediaQuery.of(context).padding.bottom + 10), + ], + ), + ), + ); + }, + ), + ), + ); + } + + void _bookShift(BuildContext context, String id) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Book Shift'), + content: const Text('Do you want to instantly book this shift?'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + BlocProvider.of(context).add(BookShiftDetailsEvent(id)); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFF10B981), + ), + child: const Text('Book'), + ), + ], + ), + ); + } + + void _declineShift(BuildContext context, String id) { + showDialog( + context: context, + builder: (ctx) => AlertDialog( + title: const Text('Decline Shift'), + content: const Text( + 'Are you sure you want to decline this shift? It will be hidden from your available jobs.'), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(), + child: const Text('Cancel'), + ), + TextButton( + onPressed: () { + Navigator.of(ctx).pop(); + BlocProvider.of(context).add(DeclineShiftDetailsEvent(id)); + }, + style: TextButton.styleFrom( + foregroundColor: const Color(0xFFEF4444), + ), + child: const Text('Decline'), + ), + ], + ), + ); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart index c24fa6c1..7175e004 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/my_shift_card.dart @@ -1,25 +1,17 @@ import 'package:flutter/material.dart'; +import 'package:flutter_modular/flutter_modular.dart'; import 'package:intl/intl.dart'; import 'package:krow_domain/krow_domain.dart'; import 'package:design_system/design_system.dart'; import 'package:core_localization/core_localization.dart'; +import 'package:staff_shifts/src/presentation/navigation/shifts_navigator.dart'; class MyShiftCard extends StatefulWidget { final Shift shift; - final bool historyMode; - final VoidCallback? onAccept; - final VoidCallback? onDecline; - final VoidCallback? onRequestSwap; - final int index; const MyShiftCard({ super.key, required this.shift, - this.historyMode = false, - this.onAccept, - this.onDecline, - this.onRequestSwap, - this.index = 0, }); @override @@ -27,8 +19,6 @@ class MyShiftCard extends StatefulWidget { } class _MyShiftCardState extends State { - bool _isExpanded = false; - String _formatTime(String time) { if (time.isEmpty) return ''; try { @@ -120,9 +110,10 @@ class _MyShiftCardState extends State { } return GestureDetector( - onTap: () => setState(() => _isExpanded = !_isExpanded), - child: AnimatedContainer( - duration: const Duration(milliseconds: 300), + onTap: () { + Modular.to.pushShiftDetails(widget.shift); + }, + child: Container( margin: const EdgeInsets.only(bottom: 12), decoration: BoxDecoration( color: Colors.white, @@ -389,384 +380,9 @@ class _MyShiftCardState extends State { ], ), ), - - // Expanded Content - AnimatedSize( - duration: const Duration(milliseconds: 300), - child: _isExpanded - ? Column( - children: [ - const Divider(height: 1, color: UiColors.border), - Padding( - padding: const EdgeInsets.all(16), - child: Column( - children: [ - // Stats Row - Row( - children: [ - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${estimatedTotal.toStringAsFixed(0)}", - "Total", - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.dollar, - "\$${widget.shift.hourlyRate.toInt()}", - "Hourly Rate", - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildStatCard( - UiIcons.clock, - "${duration.toInt()}", - "Hours", - ), - ), - ], - ), - const SizedBox(height: 24), - - // In/Out Time - Row( - children: [ - Expanded( - child: _buildTimeBox( - "CLOCK IN TIME", - widget.shift.startTime, - ), - ), - const SizedBox(width: 12), - Expanded( - child: _buildTimeBox( - "CLOCK OUT TIME", - widget.shift.endTime, - ), - ), - ], - ), - const SizedBox(height: 24), - - // Location - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - "LOCATION", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 8), - Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Text( - widget.shift.location.isEmpty - ? "TBD" - : widget.shift.location, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), - ), - OutlinedButton.icon( - onPressed: () { - // Show snackbar with the address - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text( - widget.shift.locationAddress, - ), - duration: const Duration( - seconds: 3, - ), - ), - ); - }, - icon: const Icon( - UiIcons.navigation, - size: 14, - ), - label: const Text( - "Get direction", - style: TextStyle(fontSize: 12), - ), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.textPrimary, - side: const BorderSide( - color: UiColors.border, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 20, - ), - ), - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 0, - ), - minimumSize: const Size(0, 32), - ), - ), - ], - ), - const SizedBox(height: 12), - Container( - height: 128, - width: double.infinity, - decoration: BoxDecoration( - color: Colors.grey.shade100, - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon( - UiIcons.mapPin, - color: UiColors.iconSecondary, - size: 32, - ), - ), - // Placeholder for Map - ), - ], - ), - const SizedBox(height: 24), - - // Additional Info - if (widget.shift.description != null) ...[ - SizedBox( - width: double.infinity, - child: Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - const Text( - "ADDITIONAL INFO", - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 8), - Text( - widget.shift.description!.split('.')[0], - style: UiTypography.body2m.copyWith( - color: UiColors.textPrimary, - ), - ), - Text( - widget.shift.description!, - style: UiTypography.body3r.copyWith( - color: UiColors.textSecondary, - ), - ), - ], - ), - ), - const SizedBox(height: 24), - ], - - // Actions - if (!widget.historyMode) - if (status == 'confirmed') - SizedBox( - width: double.infinity, - height: 48, - child: OutlinedButton.icon( - onPressed: widget.onRequestSwap, - icon: const Icon( - UiIcons.swap, - size: 16, - ), - label: Text( - t.staff_shifts.action.request_swap), - style: OutlinedButton.styleFrom( - foregroundColor: UiColors.primary, - side: const BorderSide( - color: UiColors.primary, - ), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular( - 12, - ), - ), - ), - ), - ) - else if (status == 'swap') - Container( - width: double.infinity, - height: 48, - decoration: BoxDecoration( - color: const Color( - 0xFFFFFBEB, - ), // amber-50 - border: Border.all( - color: const Color(0xFFFDE68A), - ), // amber-200 - borderRadius: BorderRadius.circular(12), - ), - child: Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - const Icon( - UiIcons.swap, - size: 16, - color: Color(0xFFB45309), - ), // amber-700 - const SizedBox(width: 8), - Text( - t.staff_shifts.status.swap_requested, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: Color(0xFFB45309), - ), - ), - ], - ), - ) - else - Column( - children: [ - SizedBox( - width: double.infinity, - height: 48, - child: ElevatedButton( - onPressed: widget.onAccept, - style: ElevatedButton.styleFrom( - backgroundColor: UiColors.primary, - foregroundColor: Colors.white, - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - ), - child: Text( - status == 'pending' - ? t.staff_shifts.action.confirm - : "Book Shift", - style: const TextStyle( - fontWeight: FontWeight.w600, - ), - ), - ), - ), - if (status == 'pending' || - status == 'open') ...[ - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - height: 48, - child: OutlinedButton( - onPressed: widget.onDecline, - style: OutlinedButton.styleFrom( - foregroundColor: - UiColors.destructive, - side: const BorderSide( - color: UiColors.border, - ), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.circular(12), - ), - ), - child: Text( - t.staff_shifts.action.decline), - ), - ), - ], - ], - ), - ], - ), - ), - ], - ) - : const SizedBox.shrink(), - ), ], ), ), ); } - - Widget _buildStatCard(IconData icon, String value, String label) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 16), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFC), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: UiColors.border), - ), - child: Column( - children: [ - Container( - width: 40, - height: 40, - decoration: const BoxDecoration( - color: Colors.white, - shape: BoxShape.circle, - ), - child: Icon(icon, size: 20, color: UiColors.iconSecondary), - ), - const SizedBox(height: 8), - Text( - value, - style: UiTypography.title1m.copyWith( - color: UiColors.textPrimary, - ), - ), - Text( - label, - style: UiTypography.footnote2r.copyWith( - color: UiColors.textSecondary, - ), - ), - ], - ), - ); - } - - Widget _buildTimeBox(String label, String time) { - return Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: const Color(0xFFF8FAFC), - borderRadius: BorderRadius.circular(16), - ), - child: Column( - children: [ - Text( - label, - style: const TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: UiColors.textSecondary, - letterSpacing: 0.5, - ), - ), - const SizedBox(height: 4), - Text( - _formatTime(time), - style: UiTypography.display2m.copyWith( - fontSize: 20, - color: UiColors.textPrimary, - ), - ), - ], - ), - ); - } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart index 215da086..6524b050 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/find_shifts_tab.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; -import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:flutter_modular/flutter_modular.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; -import '../../blocs/shifts/shifts_bloc.dart'; +import '../../navigation/shifts_navigator.dart'; import '../../styles/shifts_styles.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; @@ -23,74 +23,6 @@ class _FindShiftsTabState extends State { String _searchQuery = ''; String _jobType = 'all'; - void _bookShift(String id) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Book Shift'), - content: const Text( - 'Do you want to instantly book this shift?', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - context.read().add(BookShiftEvent(id)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Booking processed!'), - backgroundColor: Color(0xFF10B981), - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: const Color(0xFF10B981), - ), - child: const Text('Book'), - ), - ], - ), - ); - } - - void _declineShift(String id) { - showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Decline Shift'), - content: const Text( - 'Are you sure you want to decline this shift? It will be hidden from your available jobs.', - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () { - Navigator.of(context).pop(); - context.read().add(DeclineShiftEvent(id)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Declined'), - backgroundColor: Color(0xFFEF4444), - ), - ); - }, - style: TextButton.styleFrom( - foregroundColor: const Color(0xFFEF4444), - ), - child: const Text('Decline'), - ), - ], - ), - ); - } - Widget _buildFilterTab(String id, String label) { final isSelected = _jobType == id; return GestureDetector( @@ -241,8 +173,6 @@ class _FindShiftsTabState extends State { padding: const EdgeInsets.only(bottom: 12), child: MyShiftCard( shift: shift, - onAccept: () => _bookShift(shift.id), - onDecline: () => _declineShift(shift.id), ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart index 5edb6eff..b89783ba 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/widgets/tabs/history_shifts_tab.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; +import 'package:flutter_modular/flutter_modular.dart'; +import '../../navigation/shifts_navigator.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; @@ -30,9 +32,11 @@ class HistoryShiftsTab extends StatelessWidget { ...historyShifts.map( (shift) => Padding( padding: const EdgeInsets.only(bottom: 12), - child: MyShiftCard( - shift: shift, - historyMode: true, + child: GestureDetector( + onTap: () => Modular.to.pushShiftDetails(shift), + child: MyShiftCard( + shift: shift, + ), ), ), ), diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart new file mode 100644 index 00000000..78fddf80 --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/shift_details_module.dart @@ -0,0 +1,31 @@ +import 'package:flutter_modular/flutter_modular.dart'; +import 'domain/repositories/shifts_repository_interface.dart'; +import 'data/repositories_impl/shifts_repository_impl.dart'; +import 'domain/usecases/get_shift_details_usecase.dart'; +import 'domain/usecases/accept_shift_usecase.dart'; +import 'domain/usecases/decline_shift_usecase.dart'; +import 'domain/usecases/apply_for_shift_usecase.dart'; +import 'presentation/blocs/shift_details/shift_details_bloc.dart'; +import 'presentation/pages/shift_details_page.dart'; + +class ShiftDetailsModule extends Module { + @override + void binds(Injector i) { + // Repository + i.add(ShiftsRepositoryImpl.new); + + // UseCases + i.add(GetShiftDetailsUseCase.new); + i.add(AcceptShiftUseCase.new); + i.add(DeclineShiftUseCase.new); + i.add(ApplyForShiftUseCase.new); + + // Bloc + i.add(ShiftDetailsBloc.new); + } + + @override + void routes(RouteManager r) { + r.child('/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data)); + } +} diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart index bb8e9f9a..a1adddc4 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/staff_shifts_module.dart @@ -11,6 +11,7 @@ import 'domain/usecases/decline_shift_usecase.dart'; import 'domain/usecases/apply_for_shift_usecase.dart'; import 'domain/usecases/get_shift_details_usecase.dart'; import 'presentation/blocs/shifts/shifts_bloc.dart'; +import 'presentation/blocs/shift_details/shift_details_bloc.dart'; import 'presentation/pages/shifts_page.dart'; import 'presentation/pages/shift_details_page.dart'; @@ -33,11 +34,11 @@ class StaffShiftsModule extends Module { // Bloc i.add(ShiftsBloc.new); + i.add(ShiftDetailsBloc.new); } @override void routes(RouteManager r) { r.child('/', child: (_) => const ShiftsPage()); - r.child('/details/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data)); } } diff --git a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart index 28ae0ac4..7d0a0518 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/staff_shifts.dart @@ -1,4 +1,6 @@ library staff_shifts; export 'src/staff_shifts_module.dart'; +export 'src/shift_details_module.dart'; +export 'src/presentation/navigation/shifts_navigator.dart'; diff --git a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart index d7f5e3e0..661aa05d 100644 --- a/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart +++ b/apps/mobile/packages/features/staff/staff_main/lib/src/staff_main_module.dart @@ -77,6 +77,10 @@ class StaffMainModule extends Module { '/availability', module: StaffAvailabilityModule(), ); + r.module( + '/shift-details', + module: ShiftDetailsModule(), + ); } }