feat: Enhance shift application process with instant booking option and implement shift booking and decline dialogs

This commit is contained in:
Achintha Isuru
2026-01-31 19:47:29 -05:00
parent 3e156565c8
commit eac6c1b778
8 changed files with 130 additions and 888 deletions

View File

@@ -223,7 +223,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
}
@override
Future<void> applyForShift(String shiftId) async {
Future<void> 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");
}

View File

@@ -18,7 +18,9 @@ abstract interface class ShiftsRepositoryInterface {
Future<Shift?> getShiftDetails(String shiftId);
/// Applies for a specific open shift.
Future<void> applyForShift(String shiftId);
///
/// [isInstantBook] determines if the application should be immediately accepted.
Future<void> applyForShift(String shiftId, {bool isInstantBook = false});
/// Accepts a pending shift assignment.
Future<void> acceptShift(String shiftId);

View File

@@ -0,0 +1,11 @@
import '../repositories/shifts_repository_interface.dart';
class ApplyForShiftUseCase {
final ShiftsRepositoryInterface repository;
ApplyForShiftUseCase(this.repository);
Future<void> call(String shiftId, {bool isInstantBook = false}) async {
return repository.applyForShift(shiftId, isInstantBook: isInstantBook);
}
}

View File

@@ -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<ShiftsEvent, ShiftsState> {
final GetHistoryShiftsUseCase getHistoryShifts;
final AcceptShiftUseCase acceptShift;
final DeclineShiftUseCase declineShift;
final ApplyForShiftUseCase applyForShift;
ShiftsBloc({
required this.getMyShifts,
@@ -32,11 +34,13 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
required this.getHistoryShifts,
required this.acceptShift,
required this.declineShift,
required this.applyForShift,
}) : super(ShiftsInitial()) {
on<LoadShiftsEvent>(_onLoadShifts);
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
on<AcceptShiftEvent>(_onAcceptShift);
on<DeclineShiftEvent>(_onDeclineShift);
on<BookShiftEvent>(_onBookShift);
}
Future<void> _onLoadShifts(
@@ -122,4 +126,16 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
// Handle error
}
}
Future<void> _onBookShift(
BookShiftEvent event,
Emitter<ShiftsState> emit,
) async {
try {
await applyForShift(event.shiftId, isInstantBook: true);
add(LoadShiftsEvent()); // Reload to move from Available to My Shifts
} catch (_) {
// Handle error
}
}
}

View File

@@ -35,3 +35,11 @@ class DeclineShiftEvent extends ShiftsEvent {
@override
List<Object?> get props => [shiftId];
}
class BookShiftEvent extends ShiftsEvent {
final String shiftId;
const BookShiftEvent(this.shiftId);
@override
List<Object?> get props => [shiftId];
}

View File

@@ -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<ShiftDetailsPage> createState() => _ShiftDetailsPageState();
}
class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
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<GetShiftDetailsUseCase>();
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<AcceptShiftUseCase>();
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<ShiftsBloc>().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<DeclineShiftUseCase>();
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<ShiftsBloc>().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,
),
),
],
),
);
}
}

View File

@@ -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<FindShiftsTab> {
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<ShiftsBloc>().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<ShiftsBloc>().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<FindShiftsTab> {
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),
),
),
),

View File

@@ -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