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:
Achintha Isuru
2026-01-31 20:33:35 -05:00
parent eac6c1b778
commit c6128c2332
14 changed files with 623 additions and 526 deletions

View File

@@ -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()));
}
}
}

View File

@@ -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];
}

View File

@@ -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];
}

View File

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

View File

@@ -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];
}

View File

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

View File

@@ -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'),
),
],
),
);
}
}

View File

@@ -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,
),
),
],
),
);
}
}

View File

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

View File

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

View File

@@ -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));
}
}

View File

@@ -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));
}
}

View File

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

View File

@@ -77,6 +77,10 @@ class StaffMainModule extends Module {
'/availability',
module: StaffAvailabilityModule(),
);
r.module(
'/shift-details',
module: ShiftDetailsModule(),
);
}
}