diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart index 584a4bcd..6f76b2a7 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/data/repositories_impl/shifts_repository_impl.dart @@ -223,7 +223,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } @override - Future applyForShift(String shiftId) async { + Future applyForShift(String shiftId, {bool isInstantBook = false}) async { final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute(); if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift'); @@ -234,7 +234,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { shiftId: shiftId, staffId: staffId, roleId: role.id, - status: dc.ApplicationStatus.PENDING, + status: isInstantBook ? dc.ApplicationStatus.ACCEPTED : dc.ApplicationStatus.PENDING, origin: dc.ApplicationOrigin.STAFF, ).execute(); } @@ -273,6 +273,22 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface { } if (appId == null || roleId == null) { + // If we are rejecting and can't find an application, create one as rejected (declining an available shift) + if (newStatus == dc.ApplicationStatus.REJECTED) { + final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute(); + if (rolesResult.data.shiftRoles.isNotEmpty) { + final role = rolesResult.data.shiftRoles.first; + final staffId = await _getStaffId(); + await _dataConnect.createApplication( + shiftId: shiftId, + staffId: staffId, + roleId: role.id, + status: dc.ApplicationStatus.REJECTED, + origin: dc.ApplicationOrigin.STAFF, + ).execute(); + return; + } + } throw Exception("Application not found for shift $shiftId"); } diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart index f77844e5..c3767fd0 100644 --- a/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/repositories/shifts_repository_interface.dart @@ -18,7 +18,9 @@ abstract interface class ShiftsRepositoryInterface { Future getShiftDetails(String shiftId); /// Applies for a specific open shift. - Future applyForShift(String shiftId); + /// + /// [isInstantBook] determines if the application should be immediately accepted. + Future applyForShift(String shiftId, {bool isInstantBook = false}); /// Accepts a pending shift assignment. Future acceptShift(String shiftId); diff --git a/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart new file mode 100644 index 00000000..a637be4c --- /dev/null +++ b/apps/mobile/packages/features/staff/shifts/lib/src/domain/usecases/apply_for_shift_usecase.dart @@ -0,0 +1,11 @@ +import '../repositories/shifts_repository_interface.dart'; + +class ApplyForShiftUseCase { + final ShiftsRepositoryInterface repository; + + ApplyForShiftUseCase(this.repository); + + Future call(String shiftId, {bool isInstantBook = false}) async { + return repository.applyForShift(shiftId, isInstantBook: isInstantBook); + } +} 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 d2f26c17..0fb6f979 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 @@ -11,6 +11,7 @@ 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'; part 'shifts_event.dart'; part 'shifts_state.dart'; @@ -23,6 +24,7 @@ class ShiftsBloc extends Bloc { final GetHistoryShiftsUseCase getHistoryShifts; final AcceptShiftUseCase acceptShift; final DeclineShiftUseCase declineShift; + final ApplyForShiftUseCase applyForShift; ShiftsBloc({ required this.getMyShifts, @@ -32,11 +34,13 @@ class ShiftsBloc extends Bloc { 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( @@ -122,4 +126,16 @@ class ShiftsBloc extends Bloc { // 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 0ea1a6b1..eeab5787 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,3 +35,11 @@ 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/pages/shift_details_page.dart b/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart deleted file mode 100644 index c62b3b15..00000000 --- a/apps/mobile/packages/features/staff/shifts/lib/src/presentation/pages/shift_details_page.dart +++ /dev/null @@ -1,869 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_modular/flutter_modular.dart'; -import 'package:lucide_icons/lucide_icons.dart'; -import 'package:intl/intl.dart'; -import 'package:design_system/design_system.dart'; -import 'package:krow_domain/krow_domain.dart'; -import 'package:staff_shifts/src/presentation/blocs/shifts/shifts_bloc.dart'; -import '../../domain/usecases/get_shift_details_usecase.dart'; -import '../../domain/usecases/accept_shift_usecase.dart'; -import '../../domain/usecases/decline_shift_usecase.dart'; - -// Shim to match POC styles locally -class AppColors { - static const Color krowBlue = UiColors.primary; - static const Color krowYellow = Color(0xFFFFED4A); - static const Color krowCharcoal = UiColors.textPrimary; // 121826 - static const Color krowMuted = UiColors.textSecondary; // 6A7382 - static const Color krowBorder = UiColors.border; // E3E6E9 - static const Color krowBackground = UiColors.background; // FAFBFC - static const Color white = Colors.white; -} - -class ShiftDetailsPage extends StatefulWidget { - final String shiftId; - final Shift? shift; - - const ShiftDetailsPage({super.key, required this.shiftId, this.shift}); - - @override - State createState() => _ShiftDetailsPageState(); -} - -class _ShiftDetailsPageState extends State { - late Shift _shift; - bool _isLoading = true; - bool _showDetails = true; - bool _isApplying = false; - - - - @override - void initState() { - super.initState(); - _loadShift(); - } - - void _loadShift() async { - if (widget.shift != null) { - _shift = widget.shift!; - setState(() => _isLoading = false); - } else { - try { - final useCase = Modular.get(); - final shift = await useCase(widget.shiftId); - if (mounted) { - if (shift != null) { - setState(() { - _shift = shift; - _isLoading = false; - }); - } else { - // Handle case where shift is not found - Navigator.of(context).pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('Shift not found')), - ); - } - } - } catch (e) { - if (mounted) { - setState(() => _isLoading = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar(content: Text('Error loading shift: $e')), - ); - } - } - } - } - - 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:mma').format(dt).toLowerCase(); - } catch (e) { - return time; - } - } - - String _formatDate(String dateStr) { - if (dateStr.isEmpty) return ''; - try { - final date = DateTime.parse(dateStr); - return DateFormat('MMMM d').format(date); - } catch (e) { - return dateStr; - } - } - - double _calculateHours(String start, String end) { - try { - final startParts = start.split(':').map(int.parse).toList(); - final endParts = end.split(':').map(int.parse).toList(); - double h = (endParts[0] - startParts[0]) + (endParts[1] - startParts[1]) / 60; - if (h < 0) h += 24; - return h; - } catch (e) { - return 0; - } - } - - @override - Widget build(BuildContext context) { - if (_isLoading) { - return const Scaffold( - backgroundColor: AppColors.krowBackground, - body: Center(child: CircularProgressIndicator()), - ); - } - - final hours = _calculateHours(_shift.startTime, _shift.endTime); - final totalPay = _shift.hourlyRate * hours; - - return Scaffold( - backgroundColor: AppColors.krowBackground, - appBar: AppBar( - backgroundColor: Colors.white, - elevation: 0, - leading: IconButton( - icon: const Icon(LucideIcons.chevronLeft, color: AppColors.krowMuted), - onPressed: () => Modular.to.pop(), - ), - bottom: PreferredSize( - preferredSize: const Size.fromHeight(1.0), - child: Container(color: AppColors.krowBorder, height: 1.0), - ), - ), - body: Stack( - children: [ - SingleChildScrollView( - padding: const EdgeInsets.fromLTRB(20, 20, 20, 120), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - // Pending Badge - // Status Badge - Align( - alignment: Alignment.centerRight, - child: Container( - padding: const EdgeInsets.symmetric( - horizontal: 12, - vertical: 4, - ), - decoration: BoxDecoration( - color: _getStatusColor(_shift.status ?? 'open').withOpacity(0.1), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - (_shift.status ?? 'open').toUpperCase(), - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w500, - color: _getStatusColor(_shift.status ?? 'open'), - ), - ), - ), - ), - const SizedBox(height: 16), - - // Header - Row( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Container( - width: 56, - height: 56, - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: _shift.logoUrl != null - ? ClipRRect( - borderRadius: BorderRadius.circular(12), - child: Image.network( - _shift.logoUrl!, - fit: BoxFit.contain, - ), - ) - : Center( - child: Text( - _shift.clientName.isNotEmpty ? _shift.clientName[0] : 'K', - style: const TextStyle( - fontSize: 24, - fontWeight: FontWeight.bold, - color: AppColors.krowBlue, - ), - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Text( - _shift.title, - style: const TextStyle( - fontSize: 20, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.end, - children: [ - Text( - '\$${_shift.hourlyRate.toStringAsFixed(0)}/h', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - '(exp.total \$${totalPay.toStringAsFixed(0)})', - style: const TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - Text( - _shift.clientName, - style: const TextStyle(color: AppColors.krowMuted), - ), - ], - ), - ), - ], - ), - const SizedBox(height: 16), - - - - // Additional Details Collapsible - Container( - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - children: [ - InkWell( - onTap: () => - setState(() => _showDetails = !_showDetails), - child: Padding( - padding: const EdgeInsets.all(16), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - const Text( - 'ADDITIONAL DETAILS', - style: TextStyle( - fontSize: 12, - fontWeight: FontWeight.w600, - letterSpacing: 0.5, - color: AppColors.krowMuted, - ), - ), - Icon( - _showDetails - ? LucideIcons.chevronUp - : LucideIcons.chevronDown, - color: AppColors.krowMuted, - size: 20, - ), - ], - ), - ), - ), - if (_showDetails) - Padding( - padding: const EdgeInsets.fromLTRB(16, 0, 16, 16), - child: Column( - children: [ - _buildDetailRow('Tips', _shift.tipsAvailable == true ? 'Yes' : 'No', _shift.tipsAvailable == true), - _buildDetailRow('Travel Time', _shift.travelTime == true ? 'Yes' : 'No', _shift.travelTime == true), - _buildDetailRow('Meal Provided', _shift.mealProvided == true ? 'Yes' : 'No', _shift.mealProvided == true), - _buildDetailRow('Parking Available', _shift.parkingAvailable == true ? 'Yes' : 'No', _shift.parkingAvailable == true), - _buildDetailRow('Gas Compensation', _shift.gasCompensation == true ? 'Yes' : 'No', _shift.gasCompensation == true), - ], - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Date & Duration Grid - Row( - children: [ - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'START', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 8), - Text( - _formatDate(_shift.date), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Date', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Text( - _formatTime(_shift.startTime), - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Time', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ), - ), - const SizedBox(width: 16), - Expanded( - child: Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'DURATION', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 8), - Text( - '${hours.toStringAsFixed(0)} hours', - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Shift duration', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - const Text( - '1 hour', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - const Text( - 'Break duration', - style: TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ), - ), - ], - ), - const SizedBox(height: 16), - - // Location - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'LOCATION', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - _shift.location, - style: const TextStyle( - fontSize: 18, - fontWeight: FontWeight.bold, - color: AppColors.krowCharcoal, - ), - ), - Text( - _shift.locationAddress, - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - ), - ), - ], - ), - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text( - _shift.locationAddress, - ), - duration: const Duration(seconds: 3), - ), - ); - }, - icon: const Icon(LucideIcons.navigation, size: 14), - label: const Text('Get direction'), - style: OutlinedButton.styleFrom( - foregroundColor: AppColors.krowCharcoal, - side: const BorderSide( - color: AppColors.krowBorder, - ), - textStyle: const TextStyle(fontSize: 12), - ), - ), - ], - ), - const SizedBox(height: 16), - Container( - height: 160, - width: double.infinity, - decoration: BoxDecoration( - color: const Color(0xFFF1F3F5), - borderRadius: BorderRadius.circular(12), - ), - child: const Center( - child: Icon( - LucideIcons.map, - color: AppColors.krowMuted, - size: 48, - ), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - - // Manager Contact - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'MANAGER CONTACT DETAILS', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 16), - ...(_shift.managers ?? []) - .map( - (manager) => Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Row( - mainAxisAlignment: - MainAxisAlignment.spaceBetween, - children: [ - Row( - children: [ - Container( - width: 40, - height: 40, - decoration: BoxDecoration( - gradient: const LinearGradient( - colors: [ - AppColors.krowBlue, - Color(0xFF0830B8), - ], - ), - borderRadius: BorderRadius.circular( - 8, - ), - ), - child: _buildAvatar(manager), - ), - const SizedBox(width: 12), - Column( - crossAxisAlignment: - CrossAxisAlignment.start, - children: [ - Text( - manager.name, - style: const TextStyle( - fontWeight: FontWeight.w600, - color: AppColors.krowCharcoal, - ), - ), - Text( - manager.phone, - style: const TextStyle( - fontSize: 12, - color: AppColors.krowMuted, - ), - ), - ], - ), - ], - ), - OutlinedButton.icon( - onPressed: () { - ScaffoldMessenger.of( - context, - ).showSnackBar( - SnackBar( - content: Text(manager.phone), - duration: const Duration(seconds: 3), - ), - ); - }, - icon: const Icon( - LucideIcons.phone, - size: 14, - color: Color(0xFF059669), - ), - label: const Text( - 'Call', - style: TextStyle( - color: Color(0xFF059669), - ), - ), - style: OutlinedButton.styleFrom( - side: const BorderSide( - color: Color(0xFFA7F3D0), - ), - backgroundColor: const Color(0xFFECFDF5), - textStyle: const TextStyle(fontSize: 12), - ), - ), - ], - ), - ), - ) - .toList(), - ], - ), - ), - const SizedBox(height: 16), - - // Additional Info - Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Colors.white, - borderRadius: BorderRadius.circular(12), - border: Border.all(color: AppColors.krowBorder), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - const Text( - 'ADDITIONAL INFO', - style: TextStyle( - fontSize: 10, - fontWeight: FontWeight.bold, - color: AppColors.krowMuted, - ), - ), - const SizedBox(height: 12), - Text( - _shift.description ?? - 'Providing Exceptional Customer Service.', - style: const TextStyle( - fontSize: 14, - color: AppColors.krowMuted, - height: 1.5, - ), - ), - ], - ), - ), - ], - ), - ), - - // Bottom Actions - Positioned( - bottom: 0, - left: 0, - right: 0, - child: Container( - padding: const EdgeInsets.all(20), - decoration: const BoxDecoration( - color: Colors.white, - border: Border(top: BorderSide(color: AppColors.krowBorder)), - ), - child: SafeArea( - top: false, - child: Column( - children: [ - SizedBox( - width: double.infinity, - height: 56, - child: ElevatedButton( - onPressed: () async { - setState(() => _isApplying = true); - try { - final acceptUseCase = Modular.get(); - await acceptUseCase(_shift.id); - - if (mounted) { - setState(() => _isApplying = false); - Modular.to.pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Accepted!'), - backgroundColor: Color(0xFF10B981), - ), - ); - // Ideally, trigger a refresh on the previous screen - Modular.get().add(LoadShiftsEvent()); - } - } catch (e) { - if (mounted) { - setState(() => _isApplying = false); - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to accept shift: $e'), - backgroundColor: const Color(0xFFEF4444), - ), - ); - } - } - }, - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.krowBlue, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - ), - elevation: 0, - ), - child: _isApplying - ? const SizedBox( - width: 24, - height: 24, - child: CircularProgressIndicator( - color: Colors.white, - ), - ) - : const Text( - 'Accept shift', - style: TextStyle( - fontSize: 18, - fontWeight: FontWeight.w600, - color: Colors.white, - ), - ), - ), - ), - const SizedBox(height: 12), - SizedBox( - width: double.infinity, - height: 48, - child: TextButton( - onPressed: () async { - try { - final declineUseCase = Modular.get(); - await declineUseCase(_shift.id); - - if (mounted) { - Modular.to.pop(); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Declined'), - backgroundColor: Color(0xFFEF4444), - ), - ); - // Refresh list - Modular.get().add(LoadShiftsEvent()); - } - } catch (e) { - if (mounted) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text('Failed to decline shift: $e'), - backgroundColor: const Color(0xFFEF4444), - ), - ); - } - } - }, - child: const Text( - 'Decline shift', - style: TextStyle( - color: Color(0xFFEF4444), - fontSize: 16, - fontWeight: FontWeight.w500, - ), - ), - ), - ), - ], - ), - ), - ), - ), - ], - ), - ); - } - - Widget _buildTag(IconData icon, String label, Color bg, Color text) { - return Container( - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), - decoration: BoxDecoration( - color: bg, - borderRadius: BorderRadius.circular(20), - ), - child: Row( - children: [ - Icon(icon, size: 14, color: text), - const SizedBox(width: 4), - Text( - label, - style: TextStyle( - color: text, - fontSize: 12, - fontWeight: FontWeight.w600, - ), - ), - ], - ), - ); - } - - Color _getStatusColor(String status) { - switch (status.toLowerCase()) { - case 'confirmed': - case 'accepted': - return const Color(0xFF10B981); // Green - case 'pending': - return const Color(0xFFF59E0B); // Yellow - case 'cancelled': - case 'rejected': - return const Color(0xFFEF4444); // Red - case 'completed': - return const Color(0xFF10B981); - default: - return AppColors.krowBlue; - } - } - - Widget _buildAvatar(ShiftManager manager) { - if (manager.avatar != null && manager.avatar!.isNotEmpty) { - return ClipRRect( - borderRadius: BorderRadius.circular(8), - child: Image.network(manager.avatar!, fit: BoxFit.cover), - ); - } - return const Center( - child: Icon( - LucideIcons.user, - color: Colors.white, - size: 20, - ), - ); - } - - Widget _buildDetailRow(String label, String value, bool isPositive) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - label, - style: const TextStyle(fontSize: 14, color: AppColors.krowMuted), - ), - Text( - value, - style: TextStyle( - fontSize: 14, - fontWeight: FontWeight.w500, - color: isPositive ? const Color(0xFF059669) : AppColors.krowMuted, - ), - ), - ], - ), - ); - } -} 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 3f9fb459..215da086 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,6 +1,8 @@ import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:design_system/design_system.dart'; import 'package:krow_domain/krow_domain.dart'; +import '../../blocs/shifts/shifts_bloc.dart'; import '../../styles/shifts_styles.dart'; import '../my_shift_card.dart'; import '../shared/empty_state_view.dart'; @@ -21,6 +23,74 @@ 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( @@ -171,22 +241,8 @@ class _FindShiftsTabState extends State { padding: const EdgeInsets.only(bottom: 12), child: MyShiftCard( shift: shift, - onAccept: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Booked!'), - backgroundColor: Color(0xFF10B981), - ), - ); - }, - onDecline: () { - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar( - content: Text('Shift Declined'), - backgroundColor: Color(0xFFEF4444), - ), - ); - }, + onAccept: () => _bookShift(shift.id), + onDecline: () => _declineShift(shift.id), ), ), ), 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 8b979692..bb8e9f9a 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 @@ -8,6 +8,7 @@ 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_shift_details_usecase.dart'; import 'presentation/blocs/shifts/shifts_bloc.dart'; import 'presentation/pages/shifts_page.dart'; @@ -27,6 +28,7 @@ class StaffShiftsModule extends Module { i.add(GetHistoryShiftsUseCase.new); i.add(AcceptShiftUseCase.new); i.add(DeclineShiftUseCase.new); + i.add(ApplyForShiftUseCase.new); i.add(GetShiftDetailsUseCase.new); // Bloc