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.
This commit is contained in:
@@ -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<ShiftDetailsEvent, ShiftDetailsState> {
|
||||
final GetShiftDetailsUseCase getShiftDetails;
|
||||
final ApplyForShiftUseCase applyForShift;
|
||||
final DeclineShiftUseCase declineShift;
|
||||
|
||||
ShiftDetailsBloc({
|
||||
required this.getShiftDetails,
|
||||
required this.applyForShift,
|
||||
required this.declineShift,
|
||||
}) : super(ShiftDetailsInitial()) {
|
||||
on<LoadShiftDetailsEvent>(_onLoadDetails);
|
||||
on<BookShiftDetailsEvent>(_onBookShift);
|
||||
on<DeclineShiftDetailsEvent>(_onDeclineShift);
|
||||
}
|
||||
|
||||
Future<void> _onLoadDetails(
|
||||
LoadShiftDetailsEvent event,
|
||||
Emitter<ShiftDetailsState> 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<void> _onBookShift(
|
||||
BookShiftDetailsEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
try {
|
||||
await applyForShift(event.shiftId, isInstantBook: true);
|
||||
emit(const ShiftActionSuccess("Shift successfully booked!"));
|
||||
} catch (e) {
|
||||
emit(ShiftDetailsError(e.toString()));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDeclineShift(
|
||||
DeclineShiftDetailsEvent event,
|
||||
Emitter<ShiftDetailsState> emit,
|
||||
) async {
|
||||
try {
|
||||
await declineShift(event.shiftId);
|
||||
emit(const ShiftActionSuccess("Shift declined"));
|
||||
} catch (e) {
|
||||
emit(ShiftDetailsError(e.toString()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class ShiftDetailsEvent extends Equatable {
|
||||
const ShiftDetailsEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
}
|
||||
|
||||
class LoadShiftDetailsEvent extends ShiftDetailsEvent {
|
||||
final String shiftId;
|
||||
const LoadShiftDetailsEvent(this.shiftId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shiftId];
|
||||
}
|
||||
|
||||
class BookShiftDetailsEvent extends ShiftDetailsEvent {
|
||||
final String shiftId;
|
||||
const BookShiftDetailsEvent(this.shiftId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shiftId];
|
||||
}
|
||||
|
||||
class DeclineShiftDetailsEvent extends ShiftDetailsEvent {
|
||||
final String shiftId;
|
||||
const DeclineShiftDetailsEvent(this.shiftId);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shiftId];
|
||||
}
|
||||
@@ -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<Object?> get props => [];
|
||||
}
|
||||
|
||||
class ShiftDetailsInitial extends ShiftDetailsState {}
|
||||
|
||||
class ShiftDetailsLoading extends ShiftDetailsState {}
|
||||
|
||||
class ShiftDetailsLoaded extends ShiftDetailsState {
|
||||
final Shift shift;
|
||||
const ShiftDetailsLoaded(this.shift);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shift];
|
||||
}
|
||||
|
||||
class ShiftDetailsError extends ShiftDetailsState {
|
||||
final String message;
|
||||
const ShiftDetailsError(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
|
||||
class ShiftActionSuccess extends ShiftDetailsState {
|
||||
final String message;
|
||||
const ShiftActionSuccess(this.message);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
}
|
||||
@@ -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<ShiftsEvent, ShiftsState> {
|
||||
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<ShiftsEvent, ShiftsState> {
|
||||
required this.getPendingAssignments,
|
||||
required this.getCancelledShifts,
|
||||
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(
|
||||
@@ -102,40 +90,4 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onAcceptShift(
|
||||
AcceptShiftEvent event,
|
||||
Emitter<ShiftsState> emit,
|
||||
) async {
|
||||
try {
|
||||
await acceptShift(event.shiftId);
|
||||
add(LoadShiftsEvent()); // Reload lists
|
||||
} catch (_) {
|
||||
// Handle error
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _onDeclineShift(
|
||||
DeclineShiftEvent event,
|
||||
Emitter<ShiftsState> emit,
|
||||
) async {
|
||||
try {
|
||||
await declineShift(event.shiftId);
|
||||
add(LoadShiftsEvent()); // Reload lists
|
||||
} catch (_) {
|
||||
// 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,11 +35,3 @@ 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];
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<ShiftDetailsBloc>()
|
||||
..add(LoadShiftDetailsEvent(shiftId)),
|
||||
child: BlocListener<ShiftDetailsBloc, ShiftDetailsState>(
|
||||
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<ShiftDetailsBloc, ShiftDetailsState>(
|
||||
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<ShiftDetailsBloc>(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<ShiftDetailsBloc>(context).add(DeclineShiftDetailsEvent(id));
|
||||
},
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: const Color(0xFFEF4444),
|
||||
),
|
||||
child: const Text('Decline'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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<MyShiftCard> {
|
||||
bool _isExpanded = false;
|
||||
|
||||
String _formatTime(String time) {
|
||||
if (time.isEmpty) return '';
|
||||
try {
|
||||
@@ -120,9 +110,10 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
}
|
||||
|
||||
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<MyShiftCard> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// 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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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(
|
||||
@@ -241,8 +173,6 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
onAccept: () => _bookShift(shift.id),
|
||||
onDecline: () => _declineShift(shift.id),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -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<ShiftsRepositoryInterface>(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));
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -77,6 +77,10 @@ class StaffMainModule extends Module {
|
||||
'/availability',
|
||||
module: StaffAvailabilityModule(),
|
||||
);
|
||||
r.module(
|
||||
'/shift-details',
|
||||
module: ShiftDetailsModule(),
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user