feat: Enhance shift application process with instant booking option and implement shift booking and decline dialogs
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user