Merge pull request #341 from Oloodi/Issues-on-payments-timecard-availability-screens-01-02-03-04
fix: Clock-In business logic, and refines the Shifts UI for proper Data Connect integration
This commit is contained in:
@@ -1,17 +1,10 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:staff_authentication/staff_authentication.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
/// A widget that displays the primary action buttons (Sign Up and Log In)
|
|
||||||
/// for the Get Started page.
|
|
||||||
class GetStartedActions extends StatelessWidget {
|
class GetStartedActions extends StatelessWidget {
|
||||||
/// Void callback for when the Sign Up button is pressed.
|
|
||||||
final VoidCallback onSignUpPressed;
|
final VoidCallback onSignUpPressed;
|
||||||
|
|
||||||
/// Void callback for when the Log In button is pressed.
|
|
||||||
final VoidCallback onLoginPressed;
|
final VoidCallback onLoginPressed;
|
||||||
|
|
||||||
/// Creates a [GetStartedActions].
|
|
||||||
const GetStartedActions({
|
const GetStartedActions({
|
||||||
super.key,
|
super.key,
|
||||||
required this.onSignUpPressed,
|
required this.onSignUpPressed,
|
||||||
@@ -22,19 +15,37 @@ class GetStartedActions extends StatelessWidget {
|
|||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: <Widget>[
|
children: [
|
||||||
// Sign Up Button
|
ElevatedButton(
|
||||||
UiButton.primary(
|
|
||||||
text: t.staff_authentication.get_started_page.sign_up_button,
|
|
||||||
onPressed: onSignUpPressed,
|
onPressed: onSignUpPressed,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: UiColors.primary,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Create Account',
|
||||||
|
style: UiTypography.buttonL.copyWith(color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
const SizedBox(height: 12),
|
OutlinedButton(
|
||||||
|
|
||||||
// Log In Button
|
|
||||||
UiButton.secondary(
|
|
||||||
text: t.staff_authentication.get_started_page.log_in_button,
|
|
||||||
onPressed: onLoginPressed,
|
onPressed: onLoginPressed,
|
||||||
|
style: OutlinedButton.styleFrom(
|
||||||
|
foregroundColor: UiColors.primary,
|
||||||
|
side: const BorderSide(color: UiColors.primary),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
'Log In',
|
||||||
|
style: UiTypography.buttonL.copyWith(color: UiColors.primary),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,49 +1,75 @@
|
|||||||
import 'package:design_system/design_system.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
/// A widget that displays the background for the Get Started page.
|
|
||||||
class GetStartedBackground extends StatelessWidget {
|
class GetStartedBackground extends StatelessWidget {
|
||||||
/// Creates a [GetStartedBackground].
|
|
||||||
const GetStartedBackground({super.key});
|
const GetStartedBackground({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
return Container(
|
||||||
padding: const EdgeInsets.only(top: 24.0),
|
color: Colors.white,
|
||||||
child: Column(
|
child: Column(
|
||||||
children: <Widget>[
|
children: [
|
||||||
|
const SizedBox(height: 32),
|
||||||
// Logo
|
// Logo
|
||||||
Image.asset(UiImageAssets.logoBlue, height: 40),
|
Image.asset(
|
||||||
|
UiImageAssets.logoBlue,
|
||||||
|
height: 40,
|
||||||
|
),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Center(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
child: Container(
|
||||||
children: <Widget>[
|
width: 288,
|
||||||
// Hero Image
|
height: 288,
|
||||||
Container(
|
decoration: BoxDecoration(
|
||||||
width: 288,
|
shape: BoxShape.circle,
|
||||||
height: 288,
|
color: const Color(0xFF3A4A5A).withOpacity(0.05),
|
||||||
margin: const EdgeInsets.only(bottom: 24),
|
),
|
||||||
decoration: BoxDecoration(
|
padding: const EdgeInsets.all(8.0),
|
||||||
shape: BoxShape.circle,
|
child: ClipOval(
|
||||||
color: UiColors.secondaryForeground.withAlpha(
|
child: Image.network(
|
||||||
64,
|
'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=400&h=400&fit=crop&crop=faces',
|
||||||
), // 0.5 opacity
|
fit: BoxFit.cover,
|
||||||
),
|
errorBuilder: (context, error, stackTrace) {
|
||||||
child: Padding(
|
return Image.asset(UiImageAssets.logoBlue);
|
||||||
padding: const EdgeInsets.all(8.0),
|
},
|
||||||
child: ClipOval(
|
|
||||||
child: Image.network(
|
|
||||||
'https://images.unsplash.com/photo-1577219491135-ce391730fb2c?w=400&h=400&fit=crop&crop=faces',
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
),
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Pagination dots (Visual only)
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 24,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary,
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
width: 8,
|
||||||
|
height: 8,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: UiColors.primary.withOpacity(0.2),
|
||||||
|
borderRadius: BorderRadius.circular(4),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,37 +1,24 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:staff_authentication/staff_authentication.dart';
|
|
||||||
|
|
||||||
/// A widget that displays the welcome text and description on the Get Started page.
|
|
||||||
class GetStartedHeader extends StatelessWidget {
|
class GetStartedHeader extends StatelessWidget {
|
||||||
/// Creates a [GetStartedHeader].
|
|
||||||
const GetStartedHeader({super.key});
|
const GetStartedHeader({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
children: <Widget>[
|
children: [
|
||||||
RichText(
|
Text(
|
||||||
|
'Krow Workforce',
|
||||||
|
style: UiTypography.display1b.copyWith(color: UiColors.textPrimary),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
text: TextSpan(
|
|
||||||
style: UiTypography.displayM,
|
|
||||||
children: <InlineSpan>[
|
|
||||||
TextSpan(
|
|
||||||
text: t.staff_authentication.get_started_page.title_part1,
|
|
||||||
),
|
|
||||||
TextSpan(
|
|
||||||
text: t.staff_authentication.get_started_page.title_part2,
|
|
||||||
style: UiTypography.displayMb.textLink,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
t.staff_authentication.get_started_page.subtitle,
|
'Find flexible shifts that fit your schedule.',
|
||||||
|
style: UiTypography.body1r.copyWith(color: UiColors.textSecondary),
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
style: UiTypography.body1r.textSecondary,
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
import '../../domain/usecases/get_todays_shift_usecase.dart';
|
import '../../domain/usecases/get_todays_shift_usecase.dart';
|
||||||
import '../../domain/usecases/get_attendance_status_usecase.dart';
|
import '../../domain/usecases/get_attendance_status_usecase.dart';
|
||||||
import '../../domain/usecases/clock_in_usecase.dart';
|
import '../../domain/usecases/clock_in_usecase.dart';
|
||||||
@@ -16,6 +17,11 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
final ClockOutUseCase _clockOut;
|
final ClockOutUseCase _clockOut;
|
||||||
final GetActivityLogUseCase _getActivityLog;
|
final GetActivityLogUseCase _getActivityLog;
|
||||||
|
|
||||||
|
// Mock Venue Location (e.g., Grand Hotel, NYC)
|
||||||
|
static const double venueLat = 40.7128;
|
||||||
|
static const double venueLng = -74.0060;
|
||||||
|
static const double allowedRadiusMeters = 500;
|
||||||
|
|
||||||
ClockInBloc({
|
ClockInBloc({
|
||||||
required GetTodaysShiftUseCase getTodaysShift,
|
required GetTodaysShiftUseCase getTodaysShift,
|
||||||
required GetAttendanceStatusUseCase getAttendanceStatus,
|
required GetAttendanceStatusUseCase getAttendanceStatus,
|
||||||
@@ -33,6 +39,9 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
on<CheckInRequested>(_onCheckIn);
|
on<CheckInRequested>(_onCheckIn);
|
||||||
on<CheckOutRequested>(_onCheckOut);
|
on<CheckOutRequested>(_onCheckOut);
|
||||||
on<CheckInModeChanged>(_onModeChanged);
|
on<CheckInModeChanged>(_onModeChanged);
|
||||||
|
on<RequestLocationPermission>(_onRequestLocationPermission);
|
||||||
|
on<CommuteModeToggled>(_onCommuteModeToggled);
|
||||||
|
on<LocationUpdated>(_onLocationUpdated);
|
||||||
|
|
||||||
add(ClockInPageLoaded());
|
add(ClockInPageLoaded());
|
||||||
}
|
}
|
||||||
@@ -47,12 +56,20 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
final status = await _getAttendanceStatus();
|
final status = await _getAttendanceStatus();
|
||||||
final activity = await _getActivityLog();
|
final activity = await _getActivityLog();
|
||||||
|
|
||||||
|
// Check permissions silently on load? Maybe better to wait for user interaction or specific event
|
||||||
|
// However, if shift exists, we might want to check permission state
|
||||||
|
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ClockInStatus.success,
|
status: ClockInStatus.success,
|
||||||
todayShift: shift,
|
todayShift: shift,
|
||||||
attendance: status,
|
attendance: status,
|
||||||
activityLog: activity,
|
activityLog: activity,
|
||||||
));
|
));
|
||||||
|
|
||||||
|
if (shift != null && !status.isCheckedIn) {
|
||||||
|
add(RequestLocationPermission());
|
||||||
|
}
|
||||||
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
emit(state.copyWith(
|
emit(state.copyWith(
|
||||||
status: ClockInStatus.failure,
|
status: ClockInStatus.failure,
|
||||||
@@ -61,6 +78,69 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _onRequestLocationPermission(
|
||||||
|
RequestLocationPermission event,
|
||||||
|
Emitter<ClockInState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
LocationPermission permission = await Geolocator.checkPermission();
|
||||||
|
if (permission == LocationPermission.denied) {
|
||||||
|
permission = await Geolocator.requestPermission();
|
||||||
|
}
|
||||||
|
|
||||||
|
final hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
|
||||||
|
|
||||||
|
emit(state.copyWith(hasLocationConsent: hasConsent));
|
||||||
|
|
||||||
|
if (hasConsent) {
|
||||||
|
_startLocationUpdates();
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(state.copyWith(errorMessage: "Location error: $e"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startLocationUpdates() async {
|
||||||
|
try {
|
||||||
|
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||||
|
final distance = Geolocator.distanceBetween(
|
||||||
|
position.latitude,
|
||||||
|
position.longitude,
|
||||||
|
venueLat,
|
||||||
|
venueLng,
|
||||||
|
);
|
||||||
|
final isVerified = distance <= allowedRadiusMeters;
|
||||||
|
|
||||||
|
if (!isClosed) {
|
||||||
|
add(LocationUpdated(position: position, distance: distance, isVerified: isVerified));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Handle error silently or via state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onLocationUpdated(
|
||||||
|
LocationUpdated event,
|
||||||
|
Emitter<ClockInState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(
|
||||||
|
currentLocation: event.position,
|
||||||
|
distanceFromVenue: event.distance,
|
||||||
|
isLocationVerified: event.isVerified,
|
||||||
|
etaMinutes: (event.distance / 80).round(), // Rough estimate: 80m/min walking speed
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
void _onCommuteModeToggled(
|
||||||
|
CommuteModeToggled event,
|
||||||
|
Emitter<ClockInState> emit,
|
||||||
|
) {
|
||||||
|
emit(state.copyWith(isCommuteModeOn: event.isEnabled));
|
||||||
|
if (event.isEnabled) {
|
||||||
|
add(RequestLocationPermission());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void _onDateSelected(
|
void _onDateSelected(
|
||||||
DateSelected event,
|
DateSelected event,
|
||||||
Emitter<ClockInState> emit,
|
Emitter<ClockInState> emit,
|
||||||
@@ -79,6 +159,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
|||||||
CheckInRequested event,
|
CheckInRequested event,
|
||||||
Emitter<ClockInState> emit,
|
Emitter<ClockInState> emit,
|
||||||
) async {
|
) async {
|
||||||
|
// Only verify location if not using NFC (or depending on requirements) - enforcing for swipe
|
||||||
|
if (state.checkInMode == 'swipe' && !state.isLocationVerified) {
|
||||||
|
// Allow for now since coordinates are hardcoded and might not match user location
|
||||||
|
// emit(state.copyWith(errorMessage: "You must be at the location to clock in."));
|
||||||
|
// return;
|
||||||
|
}
|
||||||
|
|
||||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||||
try {
|
try {
|
||||||
final newStatus = await _clockIn(
|
final newStatus = await _clockIn(
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
|
||||||
abstract class ClockInEvent extends Equatable {
|
abstract class ClockInEvent extends Equatable {
|
||||||
const ClockInEvent();
|
const ClockInEvent();
|
||||||
@@ -46,3 +47,25 @@ class CheckInModeChanged extends ClockInEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [mode];
|
List<Object?> get props => [mode];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class CommuteModeToggled extends ClockInEvent {
|
||||||
|
final bool isEnabled;
|
||||||
|
|
||||||
|
const CommuteModeToggled(this.isEnabled);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [isEnabled];
|
||||||
|
}
|
||||||
|
|
||||||
|
class RequestLocationPermission extends ClockInEvent {}
|
||||||
|
|
||||||
|
class LocationUpdated extends ClockInEvent {
|
||||||
|
final Position position;
|
||||||
|
final double distance;
|
||||||
|
final bool isVerified;
|
||||||
|
|
||||||
|
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [position, distance, isVerified];
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:geolocator/geolocator.dart';
|
||||||
|
|
||||||
|
|
||||||
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
||||||
|
|
||||||
@@ -12,6 +14,13 @@ class ClockInState extends Equatable {
|
|||||||
final String checkInMode;
|
final String checkInMode;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
|
final Position? currentLocation;
|
||||||
|
final double? distanceFromVenue;
|
||||||
|
final bool isLocationVerified;
|
||||||
|
final bool isCommuteModeOn;
|
||||||
|
final bool hasLocationConsent;
|
||||||
|
final int? etaMinutes;
|
||||||
|
|
||||||
const ClockInState({
|
const ClockInState({
|
||||||
this.status = ClockInStatus.initial,
|
this.status = ClockInStatus.initial,
|
||||||
this.todayShift,
|
this.todayShift,
|
||||||
@@ -20,6 +29,12 @@ class ClockInState extends Equatable {
|
|||||||
required this.selectedDate,
|
required this.selectedDate,
|
||||||
this.checkInMode = 'swipe',
|
this.checkInMode = 'swipe',
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
|
this.currentLocation,
|
||||||
|
this.distanceFromVenue,
|
||||||
|
this.isLocationVerified = false,
|
||||||
|
this.isCommuteModeOn = false,
|
||||||
|
this.hasLocationConsent = false,
|
||||||
|
this.etaMinutes,
|
||||||
});
|
});
|
||||||
|
|
||||||
ClockInState copyWith({
|
ClockInState copyWith({
|
||||||
@@ -30,6 +45,12 @@ class ClockInState extends Equatable {
|
|||||||
DateTime? selectedDate,
|
DateTime? selectedDate,
|
||||||
String? checkInMode,
|
String? checkInMode,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
|
Position? currentLocation,
|
||||||
|
double? distanceFromVenue,
|
||||||
|
bool? isLocationVerified,
|
||||||
|
bool? isCommuteModeOn,
|
||||||
|
bool? hasLocationConsent,
|
||||||
|
int? etaMinutes,
|
||||||
}) {
|
}) {
|
||||||
return ClockInState(
|
return ClockInState(
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
@@ -39,6 +60,12 @@ class ClockInState extends Equatable {
|
|||||||
selectedDate: selectedDate ?? this.selectedDate,
|
selectedDate: selectedDate ?? this.selectedDate,
|
||||||
checkInMode: checkInMode ?? this.checkInMode,
|
checkInMode: checkInMode ?? this.checkInMode,
|
||||||
errorMessage: errorMessage,
|
errorMessage: errorMessage,
|
||||||
|
currentLocation: currentLocation ?? this.currentLocation,
|
||||||
|
distanceFromVenue: distanceFromVenue ?? this.distanceFromVenue,
|
||||||
|
isLocationVerified: isLocationVerified ?? this.isLocationVerified,
|
||||||
|
isCommuteModeOn: isCommuteModeOn ?? this.isCommuteModeOn,
|
||||||
|
hasLocationConsent: hasLocationConsent ?? this.hasLocationConsent,
|
||||||
|
etaMinutes: etaMinutes ?? this.etaMinutes,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,5 +78,11 @@ class ClockInState extends Equatable {
|
|||||||
selectedDate,
|
selectedDate,
|
||||||
checkInMode,
|
checkInMode,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
|
currentLocation,
|
||||||
|
distanceFromVenue,
|
||||||
|
isLocationVerified,
|
||||||
|
isCommuteModeOn,
|
||||||
|
hasLocationConsent,
|
||||||
|
etaMinutes,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,10 +92,13 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
if (todayShift != null)
|
if (todayShift != null)
|
||||||
CommuteTracker(
|
CommuteTracker(
|
||||||
shift: todayShift,
|
shift: todayShift,
|
||||||
hasLocationConsent: false, // Mock value
|
hasLocationConsent: state.hasLocationConsent,
|
||||||
isCommuteModeOn: false, // Mock value
|
isCommuteModeOn: state.isCommuteModeOn,
|
||||||
distanceMeters: 500, // Mock value for demo
|
distanceMeters: state.distanceFromVenue,
|
||||||
etaMinutes: 8, // Mock value for demo
|
etaMinutes: state.etaMinutes,
|
||||||
|
onCommuteToggled: (value) {
|
||||||
|
_bloc.add(CommuteModeToggled(value));
|
||||||
|
},
|
||||||
),
|
),
|
||||||
// Date Selector
|
// Date Selector
|
||||||
DateSelector(
|
DateSelector(
|
||||||
@@ -183,9 +186,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
Column(
|
Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
children: [
|
children: [
|
||||||
const Text(
|
Text(
|
||||||
"9:00 AM - 5:00 PM",
|
"${_formatTime(todayShift.startTime)} - ${_formatTime(todayShift.endTime)}",
|
||||||
style: TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: Color(0xFF475569), // slate-600
|
color: Color(0xFF475569), // slate-600
|
||||||
@@ -207,36 +210,73 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
|
|
||||||
// Swipe To Check In / Checked Out State / No Shift State
|
// Swipe To Check In / Checked Out State / No Shift State
|
||||||
if (todayShift != null && checkOutTime == null) ...[
|
if (todayShift != null && checkOutTime == null) ...[
|
||||||
SwipeToCheckIn(
|
if (!isCheckedIn && !_isCheckInAllowed(todayShift))
|
||||||
isCheckedIn: isCheckedIn,
|
Container(
|
||||||
mode: state.checkInMode,
|
width: double.infinity,
|
||||||
isLoading:
|
padding: const EdgeInsets.all(24),
|
||||||
state.status ==
|
decoration: BoxDecoration(
|
||||||
ClockInStatus.actionInProgress,
|
color: const Color(0xFFF1F5F9), // slate-100
|
||||||
onCheckIn: () async {
|
borderRadius: BorderRadius.circular(16),
|
||||||
// Show NFC dialog if mode is 'nfc'
|
),
|
||||||
if (state.checkInMode == 'nfc') {
|
child: Column(
|
||||||
await _showNFCDialog(context);
|
children: [
|
||||||
} else {
|
const Icon(
|
||||||
_bloc.add(
|
LucideIcons.clock,
|
||||||
CheckInRequested(shiftId: todayShift.id),
|
size: 48,
|
||||||
|
color: Color(0xFF94A3B8), // slate-400
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
const Text(
|
||||||
|
"You're early!",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: Color(0xFF475569), // slate-600
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Check-in available at ${_getCheckInAvailabilityTime(todayShift)}",
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
color: Color(0xFF64748B), // slate-500
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
)
|
||||||
|
else
|
||||||
|
SwipeToCheckIn(
|
||||||
|
isCheckedIn: isCheckedIn,
|
||||||
|
mode: state.checkInMode,
|
||||||
|
isLoading:
|
||||||
|
state.status ==
|
||||||
|
ClockInStatus.actionInProgress,
|
||||||
|
onCheckIn: () async {
|
||||||
|
// Show NFC dialog if mode is 'nfc'
|
||||||
|
if (state.checkInMode == 'nfc') {
|
||||||
|
await _showNFCDialog(context);
|
||||||
|
} else {
|
||||||
|
_bloc.add(
|
||||||
|
CheckInRequested(shiftId: todayShift.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onCheckOut: () {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => LunchBreakDialog(
|
||||||
|
onComplete: () {
|
||||||
|
Navigator.of(
|
||||||
|
context,
|
||||||
|
).pop(); // Close dialog first
|
||||||
|
_bloc.add(const CheckOutRequested());
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
},
|
),
|
||||||
onCheckOut: () {
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => LunchBreakDialog(
|
|
||||||
onComplete: () {
|
|
||||||
Navigator.of(
|
|
||||||
context,
|
|
||||||
).pop(); // Close dialog first
|
|
||||||
_bloc.add(const CheckOutRequested());
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
] else if (todayShift != null &&
|
] else if (todayShift != null &&
|
||||||
checkOutTime != null) ...[
|
checkOutTime != null) ...[
|
||||||
// Shift Completed State
|
// Shift Completed State
|
||||||
@@ -695,4 +735,43 @@ class _ClockInPageState extends State<ClockInPage> {
|
|||||||
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id));
|
_bloc.add(CheckInRequested(shiftId: _bloc.state.todayShift!.id));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// --- Helper Methods ---
|
||||||
|
|
||||||
|
String _formatTime(String timeStr) {
|
||||||
|
// Expecting HH:mm or HH:mm:ss
|
||||||
|
try {
|
||||||
|
if (timeStr.isEmpty) return '';
|
||||||
|
final parts = timeStr.split(':');
|
||||||
|
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
|
||||||
|
return DateFormat('h:mm a').format(dt);
|
||||||
|
} catch (e) {
|
||||||
|
return timeStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isCheckInAllowed(dynamic shift) {
|
||||||
|
if (shift == null) return false;
|
||||||
|
try {
|
||||||
|
// Parse shift date (e.g. 2024-01-31T09:00:00)
|
||||||
|
// The Shift entity has 'date' which is the start DateTime string
|
||||||
|
final shiftStart = DateTime.parse(shift.date);
|
||||||
|
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||||
|
return DateTime.now().isAfter(windowStart);
|
||||||
|
} catch (e) {
|
||||||
|
// Fallback: If parsing fails, allow check in to avoid blocking.
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _getCheckInAvailabilityTime(dynamic shift) {
|
||||||
|
if (shift == null) return '';
|
||||||
|
try {
|
||||||
|
final shiftStart = DateTime.parse(shift.date);
|
||||||
|
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||||
|
return DateFormat('h:mm a').format(windowStart);
|
||||||
|
} catch (e) {
|
||||||
|
return 'soon';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ enum CommuteMode {
|
|||||||
class CommuteTracker extends StatefulWidget {
|
class CommuteTracker extends StatefulWidget {
|
||||||
final Shift? shift;
|
final Shift? shift;
|
||||||
final Function(CommuteMode)? onModeChange;
|
final Function(CommuteMode)? onModeChange;
|
||||||
|
final ValueChanged<bool>? onCommuteToggled;
|
||||||
final bool hasLocationConsent;
|
final bool hasLocationConsent;
|
||||||
final bool isCommuteModeOn;
|
final bool isCommuteModeOn;
|
||||||
final double? distanceMeters;
|
final double? distanceMeters;
|
||||||
@@ -23,6 +24,7 @@ class CommuteTracker extends StatefulWidget {
|
|||||||
super.key,
|
super.key,
|
||||||
this.shift,
|
this.shift,
|
||||||
this.onModeChange,
|
this.onModeChange,
|
||||||
|
this.onCommuteToggled,
|
||||||
this.hasLocationConsent = false,
|
this.hasLocationConsent = false,
|
||||||
this.isCommuteModeOn = false,
|
this.isCommuteModeOn = false,
|
||||||
this.distanceMeters,
|
this.distanceMeters,
|
||||||
@@ -44,6 +46,21 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
|||||||
_localIsCommuteOn = widget.isCommuteModeOn;
|
_localIsCommuteOn = widget.isCommuteModeOn;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void didUpdateWidget(CommuteTracker oldWidget) {
|
||||||
|
super.didUpdateWidget(oldWidget);
|
||||||
|
if (widget.isCommuteModeOn != oldWidget.isCommuteModeOn) {
|
||||||
|
setState(() {
|
||||||
|
_localIsCommuteOn = widget.isCommuteModeOn;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (widget.hasLocationConsent != oldWidget.hasLocationConsent) {
|
||||||
|
setState(() {
|
||||||
|
_localHasConsent = widget.hasLocationConsent;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CommuteMode _getAppMode() {
|
CommuteMode _getAppMode() {
|
||||||
if (widget.shift == null) return CommuteMode.lockedNoShift;
|
if (widget.shift == null) return CommuteMode.lockedNoShift;
|
||||||
|
|
||||||
@@ -299,6 +316,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
|||||||
value: _localIsCommuteOn,
|
value: _localIsCommuteOn,
|
||||||
onChanged: (value) {
|
onChanged: (value) {
|
||||||
setState(() => _localIsCommuteOn = value);
|
setState(() => _localIsCommuteOn = value);
|
||||||
|
widget.onCommuteToggled?.call(value);
|
||||||
},
|
},
|
||||||
activeColor: AppColors.krowBlue,
|
activeColor: AppColors.krowBlue,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -2,41 +2,75 @@ import 'package:firebase_data_connect/firebase_data_connect.dart';
|
|||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import '../../domain/repositories/payments_repository.dart';
|
import '../../domain/repositories/payments_repository.dart';
|
||||||
|
|
||||||
class PaymentsRepositoryImpl implements PaymentsRepository {
|
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||||
final dc.ExampleConnector _dataConnect;
|
final dc.ExampleConnector _dataConnect;
|
||||||
|
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||||
|
|
||||||
PaymentsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance;
|
PaymentsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance;
|
||||||
|
|
||||||
|
String? _cachedStaffId;
|
||||||
|
|
||||||
|
Future<String> _getStaffId() async {
|
||||||
|
// 1. Check Session Store
|
||||||
|
final StaffSession? session = StaffSessionStore.instance.session;
|
||||||
|
if (session?.staff?.id != null) {
|
||||||
|
return session!.staff!.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Check Cache
|
||||||
|
if (_cachedStaffId != null) return _cachedStaffId!;
|
||||||
|
|
||||||
|
// 3. Fetch from Data Connect using Firebase UID
|
||||||
|
final user = _auth.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User is not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||||
|
if (response.data.staffs.isNotEmpty) {
|
||||||
|
_cachedStaffId = response.data.staffs.first.id;
|
||||||
|
return _cachedStaffId!;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Log or handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback
|
||||||
|
return user.uid;
|
||||||
|
}
|
||||||
|
|
||||||
/// Helper to convert Data Connect Timestamp to DateTime
|
/// Helper to convert Data Connect Timestamp to DateTime
|
||||||
DateTime? _toDateTime(dynamic t) {
|
DateTime? _toDateTime(dynamic t) {
|
||||||
if (t == null) return null;
|
if (t == null) return null;
|
||||||
|
if (t is DateTime) return t;
|
||||||
|
if (t is String) return DateTime.tryParse(t);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Attempt to deserialize via standard methods
|
if (t is Timestamp) {
|
||||||
return DateTime.tryParse(t.toJson() as String);
|
return t.toDateTime();
|
||||||
} catch (_) {
|
|
||||||
try {
|
|
||||||
return DateTime.tryParse(t.toString());
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (t.runtimeType.toString().contains('Timestamp')) {
|
||||||
|
return (t as dynamic).toDate();
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return DateTime.tryParse(t.toString());
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<PaymentSummary> getPaymentSummary() async {
|
Future<PaymentSummary> getPaymentSummary() async {
|
||||||
final StaffSession? session = StaffSessionStore.instance.session;
|
final String currentStaffId = await _getStaffId();
|
||||||
if (session?.staff?.id == null) {
|
|
||||||
return const PaymentSummary(
|
|
||||||
weeklyEarnings: 0,
|
|
||||||
monthlyEarnings: 0,
|
|
||||||
pendingEarnings: 0,
|
|
||||||
totalEarnings: 0,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
final String currentStaffId = session!.staff!.id;
|
|
||||||
|
|
||||||
// Fetch recent payments with a limit
|
// Fetch recent payments with a limit
|
||||||
// Note: limit is chained on the query builder
|
// Note: limit is chained on the query builder
|
||||||
@@ -82,10 +116,7 @@ class PaymentsRepositoryImpl implements PaymentsRepository {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<StaffPayment>> getPaymentHistory(String period) async {
|
Future<List<StaffPayment>> getPaymentHistory(String period) async {
|
||||||
final StaffSession? session = StaffSessionStore.instance.session;
|
final String currentStaffId = await _getStaffId();
|
||||||
if (session?.staff?.id == null) return <StaffPayment>[];
|
|
||||||
|
|
||||||
final String currentStaffId = session!.staff!.id;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final QueryResult<dc.ListRecentPaymentsByStaffIdData, dc.ListRecentPaymentsByStaffIdVariables> response =
|
final QueryResult<dc.ListRecentPaymentsByStaffIdData, dc.ListRecentPaymentsByStaffIdVariables> response =
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 100),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
import '../../domain/repositories/shifts_repository_interface.dart';
|
import '../../domain/repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||||
@@ -16,31 +17,62 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
// Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation)
|
// Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation)
|
||||||
final Map<String, String> _appToRoleIdMap = {};
|
final Map<String, String> _appToRoleIdMap = {};
|
||||||
|
|
||||||
String get _currentStaffId {
|
String? _cachedStaffId;
|
||||||
|
|
||||||
|
Future<String> _getStaffId() async {
|
||||||
|
// 1. Check Session Store
|
||||||
final StaffSession? session = StaffSessionStore.instance.session;
|
final StaffSession? session = StaffSessionStore.instance.session;
|
||||||
if (session?.staff?.id != null) {
|
if (session?.staff?.id != null) {
|
||||||
return session!.staff!.id;
|
return session!.staff!.id;
|
||||||
}
|
}
|
||||||
// Fallback? Or throw.
|
|
||||||
// If not logged in, we shouldn't be here.
|
// 2. Check Cache
|
||||||
return _auth.currentUser?.uid ?? 'STAFF_123';
|
if (_cachedStaffId != null) return _cachedStaffId!;
|
||||||
|
|
||||||
|
// 3. Fetch from Data Connect using Firebase UID
|
||||||
|
final user = _auth.currentUser;
|
||||||
|
if (user == null) {
|
||||||
|
throw Exception('User is not authenticated');
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||||
|
if (response.data.staffs.isNotEmpty) {
|
||||||
|
_cachedStaffId = response.data.staffs.first.id;
|
||||||
|
return _cachedStaffId!;
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Log or handle error
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Fallback (should ideally not happen if DB is seeded)
|
||||||
|
return user.uid;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Helper to convert Data Connect Timestamp to DateTime
|
|
||||||
DateTime? _toDateTime(dynamic t) {
|
DateTime? _toDateTime(dynamic t) {
|
||||||
if (t == null) return null;
|
if (t == null) return null;
|
||||||
|
if (t is DateTime) return t;
|
||||||
|
if (t is String) return DateTime.tryParse(t);
|
||||||
|
|
||||||
|
// Data Connect Timestamp handling
|
||||||
try {
|
try {
|
||||||
if (t is String) return DateTime.tryParse(t);
|
if (t is Timestamp) {
|
||||||
// If it accepts toJson
|
return t.toDateTime();
|
||||||
try {
|
}
|
||||||
return DateTime.tryParse(t.toJson() as String);
|
} catch (_) {}
|
||||||
} catch (_) {}
|
|
||||||
// If it's a Timestamp object (depends on SDK), usually .toDate() exists but 'dynamic' hides it.
|
try {
|
||||||
// Assuming toString or toJson covers it, or using helper.
|
// Fallback for any object that might have a toDate or similar
|
||||||
return DateTime.now(); // Placeholder if type unknown, but ideally fetch correct value
|
if (t.runtimeType.toString().contains('Timestamp')) {
|
||||||
} catch (_) {
|
return (t as dynamic).toDate();
|
||||||
return null;
|
}
|
||||||
}
|
} catch (_) {}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return DateTime.tryParse(t.toString());
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -53,22 +85,49 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
return _fetchApplications(dc.ApplicationStatus.PENDING);
|
return _fetchApplications(dc.ApplicationStatus.PENDING);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Shift>> getCancelledShifts() async {
|
||||||
|
return _fetchApplications(dc.ApplicationStatus.REJECTED);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<List<Shift>> getHistoryShifts() async {
|
||||||
|
return _fetchApplications(dc.ApplicationStatus.CHECKED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<Shift>> _fetchApplications(dc.ApplicationStatus status) async {
|
Future<List<Shift>> _fetchApplications(dc.ApplicationStatus status) async {
|
||||||
try {
|
try {
|
||||||
|
final staffId = await _getStaffId();
|
||||||
final response = await _dataConnect
|
final response = await _dataConnect
|
||||||
.getApplicationsByStaffId(staffId: _currentStaffId)
|
.getApplicationsByStaffId(staffId: staffId)
|
||||||
.execute();
|
.execute();
|
||||||
|
|
||||||
final apps = response.data.applications.where((app) => app.status == status);
|
final apps = response.data.applications.where((app) => app.status.stringValue == status.name);
|
||||||
final List<Shift> shifts = [];
|
final List<Shift> shifts = [];
|
||||||
|
|
||||||
for (final app in apps) {
|
for (final app in apps) {
|
||||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||||
|
|
||||||
final shiftTuple = await _getShiftDetails(app.shift.id);
|
final shift = await _getShiftDetails(app.shift.id);
|
||||||
if (shiftTuple != null) {
|
if (shift != null) {
|
||||||
shifts.add(shiftTuple);
|
// Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED)
|
||||||
|
shifts.add(Shift(
|
||||||
|
id: shift.id,
|
||||||
|
title: shift.title,
|
||||||
|
clientName: shift.clientName,
|
||||||
|
logoUrl: shift.logoUrl,
|
||||||
|
hourlyRate: shift.hourlyRate,
|
||||||
|
location: shift.location,
|
||||||
|
locationAddress: shift.locationAddress,
|
||||||
|
date: shift.date,
|
||||||
|
startTime: shift.startTime,
|
||||||
|
endTime: shift.endTime,
|
||||||
|
createdDate: shift.createdDate,
|
||||||
|
status: _mapStatus(status),
|
||||||
|
description: shift.description,
|
||||||
|
durationDays: shift.durationDays,
|
||||||
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return shifts;
|
return shifts;
|
||||||
@@ -77,6 +136,22 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _mapStatus(dc.ApplicationStatus status) {
|
||||||
|
switch (status) {
|
||||||
|
case dc.ApplicationStatus.ACCEPTED:
|
||||||
|
case dc.ApplicationStatus.CONFIRMED:
|
||||||
|
return 'confirmed';
|
||||||
|
case dc.ApplicationStatus.PENDING:
|
||||||
|
return 'pending';
|
||||||
|
case dc.ApplicationStatus.CHECKED_OUT:
|
||||||
|
return 'completed';
|
||||||
|
case dc.ApplicationStatus.REJECTED:
|
||||||
|
return 'cancelled';
|
||||||
|
default:
|
||||||
|
return 'open';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
||||||
try {
|
try {
|
||||||
@@ -104,8 +179,9 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||||
createdDate: createdDt?.toIso8601String() ?? '',
|
createdDate: createdDt?.toIso8601String() ?? '',
|
||||||
status: s.status?.stringValue ?? 'OPEN',
|
status: s.status?.stringValue.toLowerCase() ?? 'open',
|
||||||
description: s.description,
|
description: s.description,
|
||||||
|
durationDays: s.durationDays,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,6 +228,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
createdDate: createdDt?.toIso8601String() ?? '',
|
createdDate: createdDt?.toIso8601String() ?? '',
|
||||||
status: s.status?.stringValue ?? 'OPEN',
|
status: s.status?.stringValue ?? 'OPEN',
|
||||||
description: s.description,
|
description: s.description,
|
||||||
|
durationDays: s.durationDays,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -165,9 +242,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
|
|
||||||
final role = rolesResult.data.shiftRoles.first;
|
final role = rolesResult.data.shiftRoles.first;
|
||||||
|
|
||||||
|
final staffId = await _getStaffId();
|
||||||
await _dataConnect.createApplication(
|
await _dataConnect.createApplication(
|
||||||
shiftId: shiftId,
|
shiftId: shiftId,
|
||||||
staffId: _currentStaffId,
|
staffId: staffId,
|
||||||
roleId: role.id,
|
roleId: role.id,
|
||||||
status: dc.ApplicationStatus.PENDING,
|
status: dc.ApplicationStatus.PENDING,
|
||||||
origin: dc.ApplicationOrigin.STAFF,
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
@@ -198,7 +276,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
roleId = _appToRoleIdMap[appId];
|
roleId = _appToRoleIdMap[appId];
|
||||||
} else {
|
} else {
|
||||||
// Fallback fetch
|
// Fallback fetch
|
||||||
final apps = await _dataConnect.getApplicationsByStaffId(staffId: _currentStaffId).execute();
|
final staffId = await _getStaffId();
|
||||||
|
final apps = await _dataConnect.getApplicationsByStaffId(staffId: staffId).execute();
|
||||||
final app = apps.data.applications.where((a) => a.shiftId == shiftId).firstOrNull;
|
final app = apps.data.applications.where((a) => a.shiftId == shiftId).firstOrNull;
|
||||||
if (app != null) {
|
if (app != null) {
|
||||||
appId = app.id;
|
appId = app.id;
|
||||||
|
|||||||
@@ -25,4 +25,10 @@ abstract interface class ShiftsRepositoryInterface {
|
|||||||
|
|
||||||
/// Declines a pending shift assignment.
|
/// Declines a pending shift assignment.
|
||||||
Future<void> declineShift(String shiftId);
|
Future<void> declineShift(String shiftId);
|
||||||
|
|
||||||
|
/// Retrieves shifts that were cancelled for the current user.
|
||||||
|
Future<List<Shift>> getCancelledShifts();
|
||||||
|
|
||||||
|
/// Retrieves completed shifts for the current user.
|
||||||
|
Future<List<Shift>> getHistoryShifts();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
class AcceptShiftUseCase {
|
||||||
|
final ShiftsRepositoryInterface repository;
|
||||||
|
|
||||||
|
AcceptShiftUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<void> call(String shiftId) async {
|
||||||
|
return repository.acceptShift(shiftId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
class DeclineShiftUseCase {
|
||||||
|
final ShiftsRepositoryInterface repository;
|
||||||
|
|
||||||
|
DeclineShiftUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<void> call(String shiftId) async {
|
||||||
|
return repository.declineShift(shiftId);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
class GetCancelledShiftsUseCase {
|
||||||
|
final ShiftsRepositoryInterface repository;
|
||||||
|
|
||||||
|
GetCancelledShiftsUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<List<Shift>> call() async {
|
||||||
|
return repository.getCancelledShifts();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
class GetHistoryShiftsUseCase {
|
||||||
|
final ShiftsRepositoryInterface repository;
|
||||||
|
|
||||||
|
GetHistoryShiftsUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<List<Shift>> call() async {
|
||||||
|
return repository.getHistoryShifts();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import 'package:krow_core/core.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
class GetShiftDetailsUseCase extends UseCase<String, Shift?> {
|
||||||
|
final ShiftsRepositoryInterface repository;
|
||||||
|
|
||||||
|
GetShiftDetailsUseCase(this.repository);
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<Shift?> call(String params) {
|
||||||
|
return repository.getShiftDetails(params);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,6 +7,10 @@ import '../../../domain/usecases/get_available_shifts_usecase.dart';
|
|||||||
import '../../../domain/arguments/get_available_shifts_arguments.dart';
|
import '../../../domain/arguments/get_available_shifts_arguments.dart';
|
||||||
import '../../../domain/usecases/get_my_shifts_usecase.dart';
|
import '../../../domain/usecases/get_my_shifts_usecase.dart';
|
||||||
import '../../../domain/usecases/get_pending_assignments_usecase.dart';
|
import '../../../domain/usecases/get_pending_assignments_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';
|
||||||
|
|
||||||
part 'shifts_event.dart';
|
part 'shifts_event.dart';
|
||||||
part 'shifts_state.dart';
|
part 'shifts_state.dart';
|
||||||
@@ -15,14 +19,24 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
final GetMyShiftsUseCase getMyShifts;
|
final GetMyShiftsUseCase getMyShifts;
|
||||||
final GetAvailableShiftsUseCase getAvailableShifts;
|
final GetAvailableShiftsUseCase getAvailableShifts;
|
||||||
final GetPendingAssignmentsUseCase getPendingAssignments;
|
final GetPendingAssignmentsUseCase getPendingAssignments;
|
||||||
|
final GetCancelledShiftsUseCase getCancelledShifts;
|
||||||
|
final GetHistoryShiftsUseCase getHistoryShifts;
|
||||||
|
final AcceptShiftUseCase acceptShift;
|
||||||
|
final DeclineShiftUseCase declineShift;
|
||||||
|
|
||||||
ShiftsBloc({
|
ShiftsBloc({
|
||||||
required this.getMyShifts,
|
required this.getMyShifts,
|
||||||
required this.getAvailableShifts,
|
required this.getAvailableShifts,
|
||||||
required this.getPendingAssignments,
|
required this.getPendingAssignments,
|
||||||
|
required this.getCancelledShifts,
|
||||||
|
required this.getHistoryShifts,
|
||||||
|
required this.acceptShift,
|
||||||
|
required this.declineShift,
|
||||||
}) : super(ShiftsInitial()) {
|
}) : super(ShiftsInitial()) {
|
||||||
on<LoadShiftsEvent>(_onLoadShifts);
|
on<LoadShiftsEvent>(_onLoadShifts);
|
||||||
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
|
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
|
||||||
|
on<AcceptShiftEvent>(_onAcceptShift);
|
||||||
|
on<DeclineShiftEvent>(_onDeclineShift);
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLoadShifts(
|
Future<void> _onLoadShifts(
|
||||||
@@ -39,6 +53,8 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
try {
|
try {
|
||||||
final myShiftsResult = await getMyShifts();
|
final myShiftsResult = await getMyShifts();
|
||||||
final pendingResult = await getPendingAssignments();
|
final pendingResult = await getPendingAssignments();
|
||||||
|
final cancelledResult = await getCancelledShifts();
|
||||||
|
final historyResult = await getHistoryShifts();
|
||||||
|
|
||||||
// Initial available with defaults
|
// Initial available with defaults
|
||||||
final availableResult = await getAvailableShifts(const GetAvailableShiftsArguments());
|
final availableResult = await getAvailableShifts(const GetAvailableShiftsArguments());
|
||||||
@@ -46,7 +62,9 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
emit(ShiftsLoaded(
|
emit(ShiftsLoaded(
|
||||||
myShifts: myShiftsResult,
|
myShifts: myShiftsResult,
|
||||||
pendingShifts: pendingResult,
|
pendingShifts: pendingResult,
|
||||||
|
cancelledShifts: cancelledResult,
|
||||||
availableShifts: availableResult,
|
availableShifts: availableResult,
|
||||||
|
historyShifts: historyResult,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
jobType: 'all',
|
jobType: 'all',
|
||||||
));
|
));
|
||||||
@@ -80,4 +98,28 @@ 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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,3 +19,19 @@ class FilterAvailableShiftsEvent extends ShiftsEvent {
|
|||||||
@override
|
@override
|
||||||
List<Object?> get props => [query, jobType];
|
List<Object?> get props => [query, jobType];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class AcceptShiftEvent extends ShiftsEvent {
|
||||||
|
final String shiftId;
|
||||||
|
const AcceptShiftEvent(this.shiftId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [shiftId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeclineShiftEvent extends ShiftsEvent {
|
||||||
|
final String shiftId;
|
||||||
|
const DeclineShiftEvent(this.shiftId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [shiftId];
|
||||||
|
}
|
||||||
|
|||||||
@@ -15,14 +15,18 @@ class ShiftsLoading extends ShiftsState {}
|
|||||||
class ShiftsLoaded extends ShiftsState {
|
class ShiftsLoaded extends ShiftsState {
|
||||||
final List<Shift> myShifts;
|
final List<Shift> myShifts;
|
||||||
final List<Shift> pendingShifts;
|
final List<Shift> pendingShifts;
|
||||||
|
final List<Shift> cancelledShifts;
|
||||||
final List<Shift> availableShifts;
|
final List<Shift> availableShifts;
|
||||||
|
final List<Shift> historyShifts;
|
||||||
final String searchQuery;
|
final String searchQuery;
|
||||||
final String jobType;
|
final String jobType;
|
||||||
|
|
||||||
const ShiftsLoaded({
|
const ShiftsLoaded({
|
||||||
required this.myShifts,
|
required this.myShifts,
|
||||||
required this.pendingShifts,
|
required this.pendingShifts,
|
||||||
|
required this.cancelledShifts,
|
||||||
required this.availableShifts,
|
required this.availableShifts,
|
||||||
|
required this.historyShifts,
|
||||||
required this.searchQuery,
|
required this.searchQuery,
|
||||||
required this.jobType,
|
required this.jobType,
|
||||||
});
|
});
|
||||||
@@ -30,21 +34,33 @@ class ShiftsLoaded extends ShiftsState {
|
|||||||
ShiftsLoaded copyWith({
|
ShiftsLoaded copyWith({
|
||||||
List<Shift>? myShifts,
|
List<Shift>? myShifts,
|
||||||
List<Shift>? pendingShifts,
|
List<Shift>? pendingShifts,
|
||||||
|
List<Shift>? cancelledShifts,
|
||||||
List<Shift>? availableShifts,
|
List<Shift>? availableShifts,
|
||||||
|
List<Shift>? historyShifts,
|
||||||
String? searchQuery,
|
String? searchQuery,
|
||||||
String? jobType,
|
String? jobType,
|
||||||
}) {
|
}) {
|
||||||
return ShiftsLoaded(
|
return ShiftsLoaded(
|
||||||
myShifts: myShifts ?? this.myShifts,
|
myShifts: myShifts ?? this.myShifts,
|
||||||
pendingShifts: pendingShifts ?? this.pendingShifts,
|
pendingShifts: pendingShifts ?? this.pendingShifts,
|
||||||
|
cancelledShifts: cancelledShifts ?? this.cancelledShifts,
|
||||||
availableShifts: availableShifts ?? this.availableShifts,
|
availableShifts: availableShifts ?? this.availableShifts,
|
||||||
|
historyShifts: historyShifts ?? this.historyShifts,
|
||||||
searchQuery: searchQuery ?? this.searchQuery,
|
searchQuery: searchQuery ?? this.searchQuery,
|
||||||
jobType: jobType ?? this.jobType,
|
jobType: jobType ?? this.jobType,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object> get props => [myShifts, pendingShifts, availableShifts, searchQuery, jobType];
|
List<Object> get props => [
|
||||||
|
myShifts,
|
||||||
|
pendingShifts,
|
||||||
|
cancelledShifts,
|
||||||
|
availableShifts,
|
||||||
|
historyShifts,
|
||||||
|
searchQuery,
|
||||||
|
jobType,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ShiftsError extends ShiftsState {
|
class ShiftsError extends ShiftsState {
|
||||||
|
|||||||
@@ -4,6 +4,9 @@ import 'package:lucide_icons/lucide_icons.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.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
|
// Shim to match POC styles locally
|
||||||
class AppColors {
|
class AppColors {
|
||||||
@@ -32,11 +35,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
bool _showDetails = true;
|
bool _showDetails = true;
|
||||||
bool _isApplying = false;
|
bool _isApplying = false;
|
||||||
|
|
||||||
// Mock Managers
|
|
||||||
final List<Map<String, String>> _managers = [
|
|
||||||
{'name': 'John Smith', 'phone': '+1 123 456 7890'},
|
|
||||||
{'name': 'Jane Doe', 'phone': '+1 123 456 7890'},
|
|
||||||
];
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
@@ -49,27 +48,30 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
_shift = widget.shift!;
|
_shift = widget.shift!;
|
||||||
setState(() => _isLoading = false);
|
setState(() => _isLoading = false);
|
||||||
} else {
|
} else {
|
||||||
await Future.delayed(const Duration(milliseconds: 500));
|
try {
|
||||||
if (mounted) {
|
final useCase = Modular.get<GetShiftDetailsUseCase>();
|
||||||
// Fallback mock shift
|
final shift = await useCase(widget.shiftId);
|
||||||
setState(() {
|
if (mounted) {
|
||||||
_shift = Shift(
|
if (shift != null) {
|
||||||
id: widget.shiftId,
|
setState(() {
|
||||||
title: 'Event Server',
|
_shift = shift;
|
||||||
clientName: 'Grand Hotel',
|
_isLoading = false;
|
||||||
logoUrl: null,
|
});
|
||||||
hourlyRate: 25.0,
|
} else {
|
||||||
date: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
// Handle case where shift is not found
|
||||||
startTime: '16:00',
|
Navigator.of(context).pop();
|
||||||
endTime: '22:00',
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
location: 'Downtown',
|
const SnackBar(content: Text('Shift not found')),
|
||||||
locationAddress: '123 Main St, New York, NY',
|
);
|
||||||
status: 'open',
|
}
|
||||||
createdDate: DateTime.now().toIso8601String(),
|
}
|
||||||
description: 'Provide exceptional customer service. Respond to guest requests or concerns promptly and professionally.',
|
} catch (e) {
|
||||||
);
|
if (mounted) {
|
||||||
_isLoading = false;
|
setState(() => _isLoading = false);
|
||||||
});
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error loading shift: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -143,6 +145,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Pending Badge
|
// Pending Badge
|
||||||
|
// Status Badge
|
||||||
Align(
|
Align(
|
||||||
alignment: Alignment.centerRight,
|
alignment: Alignment.centerRight,
|
||||||
child: Container(
|
child: Container(
|
||||||
@@ -151,15 +154,15 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
vertical: 4,
|
vertical: 4,
|
||||||
),
|
),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: AppColors.krowYellow.withOpacity(0.3),
|
color: _getStatusColor(_shift.status ?? 'open').withOpacity(0.1),
|
||||||
borderRadius: BorderRadius.circular(20),
|
borderRadius: BorderRadius.circular(20),
|
||||||
),
|
),
|
||||||
child: const Text(
|
child: Text(
|
||||||
'Pending 6h ago',
|
(_shift.status ?? 'open').toUpperCase(),
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
fontWeight: FontWeight.w500,
|
fontWeight: FontWeight.w500,
|
||||||
color: AppColors.krowCharcoal,
|
color: _getStatusColor(_shift.status ?? 'open'),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -248,25 +251,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
|
|
||||||
// Tags
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
_buildTag(
|
|
||||||
LucideIcons.zap,
|
|
||||||
'Immediate start',
|
|
||||||
AppColors.krowBlue.withOpacity(0.1),
|
|
||||||
AppColors.krowBlue,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
_buildTag(
|
|
||||||
LucideIcons.star,
|
|
||||||
'No experience',
|
|
||||||
AppColors.krowYellow.withOpacity(0.3),
|
|
||||||
AppColors.krowCharcoal,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Additional Details Collapsible
|
// Additional Details Collapsible
|
||||||
Container(
|
Container(
|
||||||
@@ -310,11 +295,11 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
_buildDetailRow('Tips', 'Yes', true),
|
_buildDetailRow('Tips', _shift.tipsAvailable == true ? 'Yes' : 'No', _shift.tipsAvailable == true),
|
||||||
_buildDetailRow('Travel Time', 'Yes', true),
|
_buildDetailRow('Travel Time', _shift.travelTime == true ? 'Yes' : 'No', _shift.travelTime == true),
|
||||||
_buildDetailRow('Meal Provided', 'No', false),
|
_buildDetailRow('Meal Provided', _shift.mealProvided == true ? 'Yes' : 'No', _shift.mealProvided == true),
|
||||||
_buildDetailRow('Parking Available', 'Yes', true),
|
_buildDetailRow('Parking Available', _shift.parkingAvailable == true ? 'Yes' : 'No', _shift.parkingAvailable == true),
|
||||||
_buildDetailRow('Gas Compensation', 'No', false),
|
_buildDetailRow('Gas Compensation', _shift.gasCompensation == true ? 'Yes' : 'No', _shift.gasCompensation == true),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -550,7 +535,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
..._managers
|
...(_shift.managers ?? [])
|
||||||
.map(
|
.map(
|
||||||
(manager) => Padding(
|
(manager) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
@@ -574,13 +559,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
8,
|
8,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: const Center(
|
child: _buildAvatar(manager),
|
||||||
child: Icon(
|
|
||||||
LucideIcons.user,
|
|
||||||
color: Colors.white,
|
|
||||||
size: 20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Column(
|
Column(
|
||||||
@@ -588,14 +567,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
CrossAxisAlignment.start,
|
CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Text(
|
||||||
manager['name']!,
|
manager.name,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontWeight: FontWeight.w600,
|
fontWeight: FontWeight.w600,
|
||||||
color: AppColors.krowCharcoal,
|
color: AppColors.krowCharcoal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
manager['phone']!,
|
manager.phone,
|
||||||
style: const TextStyle(
|
style: const TextStyle(
|
||||||
fontSize: 12,
|
fontSize: 12,
|
||||||
color: AppColors.krowMuted,
|
color: AppColors.krowMuted,
|
||||||
@@ -611,7 +590,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
context,
|
context,
|
||||||
).showSnackBar(
|
).showSnackBar(
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(manager['phone']!),
|
content: Text(manager.phone),
|
||||||
duration: const Duration(seconds: 3),
|
duration: const Duration(seconds: 3),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -702,16 +681,32 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
child: ElevatedButton(
|
child: ElevatedButton(
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
setState(() => _isApplying = true);
|
setState(() => _isApplying = true);
|
||||||
await Future.delayed(const Duration(seconds: 1));
|
try {
|
||||||
if (mounted) {
|
final acceptUseCase = Modular.get<AcceptShiftUseCase>();
|
||||||
setState(() => _isApplying = false);
|
await acceptUseCase(_shift.id);
|
||||||
Modular.to.pop();
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
if (mounted) {
|
||||||
const SnackBar(
|
setState(() => _isApplying = false);
|
||||||
content: Text('Shift Accepted!'),
|
Modular.to.pop();
|
||||||
backgroundColor: Color(0xFF10B981),
|
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(
|
style: ElevatedButton.styleFrom(
|
||||||
@@ -744,7 +739,33 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
height: 48,
|
height: 48,
|
||||||
child: TextButton(
|
child: TextButton(
|
||||||
onPressed: () => Modular.to.pop(),
|
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(
|
child: const Text(
|
||||||
'Decline shift',
|
'Decline shift',
|
||||||
style: TextStyle(
|
style: TextStyle(
|
||||||
@@ -789,6 +810,39 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
Widget _buildDetailRow(String label, String value, bool isPositive) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||||
|
|||||||
@@ -75,16 +75,22 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _confirmShift(String id) {
|
void _confirmShift(String id) {
|
||||||
// TODO: Implement Bloc event
|
_bloc.add(AcceptShiftEvent(id));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Shift confirmed! (Placeholder)')),
|
const SnackBar(
|
||||||
|
content: Text('Shift confirmed!'),
|
||||||
|
backgroundColor: Color(0xFF10B981),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
void _declineShift(String id) {
|
void _declineShift(String id) {
|
||||||
// TODO: Implement Bloc event
|
_bloc.add(DeclineShiftEvent(id));
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(content: Text('Shift declined. (Placeholder)')),
|
const SnackBar(
|
||||||
|
content: Text('Shift declined.'),
|
||||||
|
backgroundColor: Color(0xFFEF4444),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -97,9 +103,10 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
final List<Shift> myShifts = (state is ShiftsLoaded) ? state.myShifts : [];
|
final List<Shift> myShifts = (state is ShiftsLoaded) ? state.myShifts : [];
|
||||||
final List<Shift> availableJobs = (state is ShiftsLoaded) ? state.availableShifts : [];
|
final List<Shift> availableJobs = (state is ShiftsLoaded) ? state.availableShifts : [];
|
||||||
final List<Shift> pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : [];
|
final List<Shift> pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : [];
|
||||||
final List<Shift> historyShifts = []; // Not in state yet, placeholder
|
final List<Shift> cancelledShifts = (state is ShiftsLoaded) ? state.cancelledShifts : [];
|
||||||
|
final List<Shift> historyShifts = (state is ShiftsLoaded) ? state.historyShifts : [];
|
||||||
|
|
||||||
// Filter logic from POC
|
// Filter logic
|
||||||
final filteredJobs = availableJobs.where((s) {
|
final filteredJobs = availableJobs.where((s) {
|
||||||
final matchesSearch =
|
final matchesSearch =
|
||||||
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||||
@@ -110,10 +117,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
|
|
||||||
if (_jobType == 'all') return true;
|
if (_jobType == 'all') return true;
|
||||||
if (_jobType == 'one-day') {
|
if (_jobType == 'one-day') {
|
||||||
return !s.title.contains('Long Term') && !s.title.contains('Multi-Day');
|
return s.durationDays == null || s.durationDays! <= 1;
|
||||||
}
|
}
|
||||||
if (_jobType == 'multi-day') return s.title.contains('Multi-Day');
|
if (_jobType == 'multi-day') return s.durationDays != null && s.durationDays! > 1;
|
||||||
if (_jobType == 'long-term') return s.title.contains('Long Term');
|
|
||||||
return true;
|
return true;
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
@@ -122,13 +128,23 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
final weekEndDate = calendarDays.last;
|
final weekEndDate = calendarDays.last;
|
||||||
|
|
||||||
final visibleMyShifts = myShifts.where((s) {
|
final visibleMyShifts = myShifts.where((s) {
|
||||||
// Primitive check if shift date string compare
|
try {
|
||||||
// In real app use DateTime logic
|
final date = DateTime.parse(s.date);
|
||||||
final sDateStr = s.date;
|
return date.isAfter(weekStartDate.subtract(const Duration(seconds: 1))) &&
|
||||||
final wStartStr = DateFormat('yyyy-MM-dd').format(weekStartDate);
|
date.isBefore(weekEndDate.add(const Duration(days: 1)));
|
||||||
final wEndStr = DateFormat('yyyy-MM-dd').format(weekEndDate);
|
} catch (_) {
|
||||||
return sDateStr.compareTo(wStartStr) >= 0 &&
|
return false;
|
||||||
sDateStr.compareTo(wEndStr) <= 0;
|
}
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
final visibleCancelledShifts = cancelledShifts.where((s) {
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(s.date);
|
||||||
|
return date.isAfter(weekStartDate.subtract(const Duration(seconds: 1))) &&
|
||||||
|
date.isBefore(weekEndDate.add(const Duration(days: 1)));
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}).toList();
|
}).toList();
|
||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@@ -140,9 +156,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
color: AppColors.krowBlue,
|
color: AppColors.krowBlue,
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
20,
|
20,
|
||||||
MediaQuery.of(context).padding.top + 20,
|
MediaQuery.of(context).padding.top + 10,
|
||||||
|
20,
|
||||||
20,
|
20,
|
||||||
24,
|
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
@@ -157,17 +173,28 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Container(
|
||||||
|
width: 36,
|
||||||
|
height: 36,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white.withOpacity(0.2),
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
child: const Center(
|
||||||
|
child: Icon(UiIcons.user, size: 20, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
// Tabs
|
// Tabs
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTab("myshifts", "My Shifts", LucideIcons.calendar, myShifts.length),
|
_buildTab("myshifts", "My Shifts", UiIcons.calendar, myShifts.length),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildTab("find", "Find Shifts", LucideIcons.search, filteredJobs.length),
|
_buildTab("find", "Find Shifts", UiIcons.search, filteredJobs.length),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildTab("history", "History", LucideIcons.clock, historyShifts.length),
|
_buildTab("history", "History", UiIcons.clock, historyShifts.length),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -178,21 +205,19 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
if (_activeTab == 'myshifts')
|
if (_activeTab == 'myshifts')
|
||||||
Container(
|
Container(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
InkWell(
|
IconButton(
|
||||||
onTap: () => setState(() => _weekOffset--),
|
icon: const Icon(UiIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal),
|
||||||
borderRadius: BorderRadius.circular(20),
|
onPressed: () => setState(() => _weekOffset--),
|
||||||
child: const Padding(
|
constraints: const BoxConstraints(),
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.zero,
|
||||||
child: Icon(LucideIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
DateFormat('MMMM yyyy').format(weekStartDate),
|
DateFormat('MMMM yyyy').format(weekStartDate),
|
||||||
@@ -202,13 +227,11 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
color: AppColors.krowCharcoal,
|
color: AppColors.krowCharcoal,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
InkWell(
|
IconButton(
|
||||||
onTap: () => setState(() => _weekOffset++),
|
icon: const Icon(UiIcons.chevronRight, size: 20, color: AppColors.krowCharcoal),
|
||||||
borderRadius: BorderRadius.circular(20),
|
onPressed: () => setState(() => _weekOffset++),
|
||||||
child: const Padding(
|
constraints: const BoxConstraints(),
|
||||||
padding: EdgeInsets.all(8.0),
|
padding: EdgeInsets.zero,
|
||||||
child: Icon(LucideIcons.chevronRight, size: 20, color: AppColors.krowCharcoal),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -219,52 +242,60 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
children: calendarDays.map((date) {
|
children: calendarDays.map((date) {
|
||||||
final isSelected = _isSameDay(date, _selectedDate);
|
final isSelected = _isSameDay(date, _selectedDate);
|
||||||
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
||||||
final hasShifts = myShifts.any((s) => s.date == dateStr);
|
final hasShifts = myShifts.any((s) {
|
||||||
|
try {
|
||||||
|
return _isSameDay(DateTime.parse(s.date), date);
|
||||||
|
} catch (_) { return false; }
|
||||||
|
});
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => setState(() => _selectedDate = date),
|
onTap: () => setState(() => _selectedDate = date),
|
||||||
child: Container(
|
child: Column(
|
||||||
width: 44,
|
children: [
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
Container(
|
||||||
decoration: BoxDecoration(
|
width: 44,
|
||||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
height: 60,
|
||||||
borderRadius: BorderRadius.circular(999),
|
decoration: BoxDecoration(
|
||||||
border: Border.all(
|
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||||
color: isSelected ? AppColors.krowBlue : AppColors.krowBorder,
|
borderRadius: BorderRadius.circular(12),
|
||||||
width: 1,
|
border: Border.all(
|
||||||
),
|
color: isSelected ? AppColors.krowBlue : AppColors.krowBorder,
|
||||||
),
|
width: 1,
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
date.day.toString().padLeft(2, '0'),
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 20,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: isSelected ? Colors.white : AppColors.krowCharcoal,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 2),
|
child: Column(
|
||||||
Text(
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
DateFormat('E').format(date),
|
children: [
|
||||||
style: TextStyle(
|
Text(
|
||||||
fontSize: 10,
|
date.day.toString().padLeft(2, '0'),
|
||||||
fontWeight: FontWeight.w500,
|
style: TextStyle(
|
||||||
color: isSelected ? Colors.white.withOpacity(0.8) : AppColors.krowMuted,
|
fontSize: 18,
|
||||||
),
|
fontWeight: FontWeight.bold,
|
||||||
),
|
color: isSelected ? Colors.white : AppColors.krowCharcoal,
|
||||||
if (hasShifts)
|
),
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(top: 4),
|
|
||||||
width: 6,
|
|
||||||
height: 6,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: isSelected ? Colors.white : AppColors.krowBlue,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
),
|
||||||
),
|
Text(
|
||||||
],
|
DateFormat('E').format(date),
|
||||||
),
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isSelected ? Colors.white.withOpacity(0.8) : AppColors.krowMuted,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
if (hasShifts && !isSelected)
|
||||||
|
Container(
|
||||||
|
margin: const EdgeInsets.only(top: 4),
|
||||||
|
width: 4,
|
||||||
|
height: 4,
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
color: AppColors.krowBlue,
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}).toList(),
|
}).toList(),
|
||||||
@@ -276,141 +307,177 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
if (_activeTab == 'myshifts')
|
if (_activeTab == 'myshifts')
|
||||||
const Divider(height: 1, color: AppColors.krowBorder),
|
const Divider(height: 1, color: AppColors.krowBorder),
|
||||||
|
|
||||||
|
// Search and Filters for Find Tab (Fixed at top)
|
||||||
|
if (_activeTab == 'find')
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.fromLTRB(20, 16, 20, 16),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
// Search Bar
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Container(
|
||||||
|
height: 48,
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 12),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: const Color(0xFFF8FAFC),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
const Icon(UiIcons.search, size: 20, color: Color(0xFF94A3B8)),
|
||||||
|
const SizedBox(width: 10),
|
||||||
|
Expanded(
|
||||||
|
child: TextField(
|
||||||
|
onChanged: (v) => setState(() => _searchQuery = v),
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
border: InputBorder.none,
|
||||||
|
hintText: "Search jobs, location...",
|
||||||
|
hintStyle: TextStyle(
|
||||||
|
color: Color(0xFF94A3B8),
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Container(
|
||||||
|
height: 48,
|
||||||
|
width: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: Colors.white,
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(color: const Color(0xFFE2E8F0)),
|
||||||
|
),
|
||||||
|
child: const Icon(UiIcons.filter, size: 18, color: Color(0xFF64748B)),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
// Filter Tabs
|
||||||
|
SingleChildScrollView(
|
||||||
|
scrollDirection: Axis.horizontal,
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
_buildFilterTab('all', 'All Jobs'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFilterTab('one-day', 'One Day'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFilterTab('multi-day', 'Multi-Day'),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
_buildFilterTab('long-term', 'Long Term'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
// Body Content
|
// Body Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: state is ShiftsLoading
|
||||||
padding: const EdgeInsets.all(20),
|
? const Center(child: CircularProgressIndicator())
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
if (_activeTab == 'find') ...[
|
const SizedBox(height: 20),
|
||||||
// Search & Filter
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Container(
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: AppColors.krowBorder),
|
|
||||||
boxShadow: [
|
|
||||||
BoxShadow(
|
|
||||||
color: Colors.black.withOpacity(0.05),
|
|
||||||
blurRadius: 4,
|
|
||||||
offset: const Offset(0, 2),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
child: TextField(
|
|
||||||
onChanged: (val) => setState(() => _searchQuery = val), // Local filter for now
|
|
||||||
decoration: const InputDecoration(
|
|
||||||
prefixIcon: Icon(LucideIcons.search, size: 20, color: AppColors.krowMuted),
|
|
||||||
border: InputBorder.none,
|
|
||||||
hintText: "Search jobs...",
|
|
||||||
hintStyle: TextStyle(color: AppColors.krowMuted, fontSize: 14),
|
|
||||||
contentPadding: EdgeInsets.symmetric(vertical: 12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
margin: const EdgeInsets.only(bottom: 16),
|
|
||||||
padding: const EdgeInsets.all(4),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFF1F3F5),
|
|
||||||
borderRadius: BorderRadius.circular(999),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
_buildFilterTab('all', 'All Jobs'),
|
|
||||||
_buildFilterTab('one-day', 'One Day'),
|
|
||||||
_buildFilterTab('multi-day', 'Multi-Day'),
|
|
||||||
_buildFilterTab('long-term', 'Long Term'),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
|
|
||||||
if (_activeTab == 'myshifts') ...[
|
if (_activeTab == 'myshifts') ...[
|
||||||
if (pendingAssignments.isNotEmpty) ...[
|
if (pendingAssignments.isNotEmpty) ...[
|
||||||
Align(
|
_buildSectionHeader("Awaiting Confirmation", const Color(0xFFF59E0B)),
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Row(
|
|
||||||
children: [
|
|
||||||
Container(width: 8, height: 8, decoration: const BoxDecoration(color: Color(0xFFF59E0B), shape: BoxShape.circle)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text("Awaiting Confirmation", style: TextStyle(
|
|
||||||
fontSize: 14, fontWeight: FontWeight.w600, color: Color(0xFFD97706)
|
|
||||||
)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
...pendingAssignments.map((shift) => Padding(
|
...pendingAssignments.map((shift) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 16),
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
child: ShiftAssignmentCard(
|
child: ShiftAssignmentCard(
|
||||||
shift: shift,
|
shift: shift,
|
||||||
onConfirm: () => _confirmShift(shift.id),
|
onConfirm: () => _confirmShift(shift.id),
|
||||||
onDecline: () => _declineShift(shift.id),
|
onDecline: () => _declineShift(shift.id),
|
||||||
|
isConfirming: true,
|
||||||
),
|
),
|
||||||
)),
|
)),
|
||||||
|
const SizedBox(height: 12),
|
||||||
],
|
],
|
||||||
|
|
||||||
// Cancelled Shifts Demo (Visual only as per POC)
|
if (visibleCancelledShifts.isNotEmpty) ...[
|
||||||
Align(
|
_buildSectionHeader("Cancelled Shifts", AppColors.krowMuted),
|
||||||
alignment: Alignment.centerLeft,
|
...visibleCancelledShifts.map((shift) => Padding(
|
||||||
child: Padding(
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
child: _buildCancelledCard(
|
||||||
child: const Text("Cancelled Shifts", style: TextStyle(
|
title: shift.title,
|
||||||
fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowMuted
|
client: shift.clientName,
|
||||||
)),
|
pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}",
|
||||||
),
|
rate: "\$${shift.hourlyRate}/hr · 8h",
|
||||||
),
|
date: _formatDateStr(shift.date),
|
||||||
_buildCancelledCard(
|
time: "${shift.startTime} - ${shift.endTime}",
|
||||||
title: "Annual Tech Conference", client: "TechCorp Inc.", pay: "\$200", rate: "\$25/hr · 8h",
|
address: shift.locationAddress,
|
||||||
date: "Today", time: "10:00 AM - 6:00 PM", address: "123 Convention Center Dr", isLastMinute: true,
|
isLastMinute: true,
|
||||||
onTap: () => setState(() => _cancelledShiftDemo = 'lastMinute')
|
onTap: () {}
|
||||||
),
|
),
|
||||||
const SizedBox(height: 12),
|
)),
|
||||||
_buildCancelledCard(
|
const SizedBox(height: 12),
|
||||||
title: "Morning Catering Setup", client: "EventPro Services", pay: "\$120", rate: "\$20/hr · 6h",
|
],
|
||||||
date: "Tomorrow", time: "8:00 AM - 2:00 PM", address: "456 Grand Ballroom Ave", isLastMinute: false,
|
|
||||||
onTap: () => setState(() => _cancelledShiftDemo = 'advance')
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Confirmed Shifts
|
// Confirmed Shifts
|
||||||
if (visibleMyShifts.isNotEmpty) ...[
|
if (visibleMyShifts.isNotEmpty) ...[
|
||||||
Align(
|
_buildSectionHeader("Confirmed Shifts", AppColors.krowMuted),
|
||||||
alignment: Alignment.centerLeft,
|
|
||||||
child: Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: const Text("Confirmed Shifts", style: TextStyle(
|
|
||||||
fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowMuted
|
|
||||||
)),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
...visibleMyShifts.map((shift) => Padding(
|
...visibleMyShifts.map((shift) => Padding(
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
child: MyShiftCard(shift: shift),
|
child: MyShiftCard(shift: shift),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
|
|
||||||
|
if (visibleMyShifts.isEmpty && pendingAssignments.isEmpty && cancelledShifts.isEmpty)
|
||||||
|
_buildEmptyState(UiIcons.calendar, "No shifts this week", "Try finding new jobs in the Find tab", null, null),
|
||||||
],
|
],
|
||||||
|
|
||||||
if (_activeTab == 'find') ...[
|
if (_activeTab == 'find') ...[
|
||||||
if (filteredJobs.isEmpty)
|
if (filteredJobs.isEmpty)
|
||||||
_buildEmptyState(LucideIcons.search, "No jobs available", "Check back later", null, null)
|
_buildEmptyState(UiIcons.search, "No jobs available", "Check back later", null, null)
|
||||||
else
|
else
|
||||||
...filteredJobs.map((shift) => MyShiftCard(
|
...filteredJobs.map((shift) => Padding(
|
||||||
shift: shift,
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
onAccept: () {},
|
child: MyShiftCard(
|
||||||
onDecline: () {},
|
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),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
)),
|
)),
|
||||||
],
|
],
|
||||||
|
|
||||||
if (_activeTab == 'history')
|
if (_activeTab == 'history') ...[
|
||||||
_buildEmptyState(LucideIcons.clock, "No shift history", "Completed shifts appear here", null, null),
|
if (historyShifts.isEmpty)
|
||||||
|
_buildEmptyState(UiIcons.clock, "No shift history", "Completed shifts appear here", null, null)
|
||||||
|
else
|
||||||
|
...historyShifts.map((shift) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: MyShiftCard(
|
||||||
|
shift: shift,
|
||||||
|
historyMode: true,
|
||||||
|
),
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -423,21 +490,57 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _formatDateStr(String dateStr) {
|
||||||
|
try {
|
||||||
|
final date = DateTime.parse(dateStr);
|
||||||
|
final now = DateTime.now();
|
||||||
|
if (_isSameDay(date, now)) return "Today";
|
||||||
|
final tomorrow = now.add(const Duration(days: 1));
|
||||||
|
if (_isSameDay(date, tomorrow)) return "Tomorrow";
|
||||||
|
return DateFormat('EEE, MMM d').format(date);
|
||||||
|
} catch (_) {
|
||||||
|
return dateStr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Widget _buildSectionHeader(String title, Color dotColor) {
|
||||||
|
return Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 16),
|
||||||
|
child: Row(
|
||||||
|
children: [
|
||||||
|
Container(width: 8, height: 8, decoration: BoxDecoration(color: dotColor, shape: BoxShape.circle)),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Text(title, style: TextStyle(
|
||||||
|
fontSize: 14,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: dotColor == AppColors.krowMuted ? AppColors.krowMuted : dotColor
|
||||||
|
)),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
Widget _buildFilterTab(String id, String label) {
|
Widget _buildFilterTab(String id, String label) {
|
||||||
final isSelected = _jobType == id;
|
final isSelected = _jobType == id;
|
||||||
return Expanded(
|
return GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: () => setState(() => _jobType = id),
|
||||||
onTap: () => setState(() => _jobType = id),
|
child: Container(
|
||||||
child: Container(
|
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
decoration: BoxDecoration(
|
||||||
decoration: BoxDecoration(
|
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||||
color: isSelected ? AppColors.krowBlue : Colors.transparent,
|
borderRadius: BorderRadius.circular(999),
|
||||||
borderRadius: BorderRadius.circular(999),
|
border: Border.all(
|
||||||
boxShadow: isSelected ? [BoxShadow(color: AppColors.krowBlue.withOpacity(0.2), blurRadius: 4, offset: const Offset(0, 2))] : null,
|
color: isSelected ? AppColors.krowBlue : const Color(0xFFE2E8F0),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 12,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: isSelected ? Colors.white : const Color(0xFF64748B),
|
||||||
),
|
),
|
||||||
child: Text(label, textAlign: TextAlign.center, style: TextStyle(
|
|
||||||
fontSize: 11, fontWeight: FontWeight.w600, color: isSelected ? Colors.white : AppColors.krowMuted
|
|
||||||
)),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
@@ -478,17 +581,6 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDemoButton(String label, Color color, VoidCallback onTap) {
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: onTap,
|
|
||||||
child: Container(
|
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
|
|
||||||
decoration: BoxDecoration(color: color, borderRadius: BorderRadius.circular(4)),
|
|
||||||
child: Text(label, style: const TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Colors.white)),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildEmptyState(IconData icon, String title, String subtitle, String? actionLabel, VoidCallback? onAction) {
|
Widget _buildEmptyState(IconData icon, String title, String subtitle, String? actionLabel, VoidCallback? onAction) {
|
||||||
return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 64), child: Column(children: [
|
return Center(child: Padding(padding: const EdgeInsets.symmetric(vertical: 64), child: Column(children: [
|
||||||
Container(width: 64, height: 64, decoration: BoxDecoration(color: const Color(0xFFF1F3F5), borderRadius: BorderRadius.circular(12)), child: Icon(icon, size: 32, color: AppColors.krowMuted)),
|
Container(width: 64, height: 64, decoration: BoxDecoration(color: const Color(0xFFF1F3F5), borderRadius: BorderRadius.circular(12)), child: Icon(icon, size: 32, color: AppColors.krowMuted)),
|
||||||
@@ -506,98 +598,68 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
Widget _buildCancelledCard({required String title, required String client, required String pay, required String rate, required String date, required String time, required String address, required bool isLastMinute, required VoidCallback onTap}) {
|
Widget _buildCancelledCard({required String title, required String client, required String pay, required String rate, required String date, required String time, required String address, required bool isLastMinute, required VoidCallback onTap}) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: onTap,
|
onTap: onTap,
|
||||||
child: Container(padding: const EdgeInsets.all(16), decoration: BoxDecoration(color: Colors.white, borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.krowBorder)), child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
child: Container(
|
||||||
Row(children: [Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFEF4444), shape: BoxShape.circle)), const SizedBox(width: 6), const Text("CANCELLED", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Color(0xFFEF4444))), if (isLastMinute) ...[const SizedBox(width: 4), const Text("• 4hr compensation", style: TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF10B981)))]]),
|
padding: const EdgeInsets.all(16),
|
||||||
const SizedBox(height: 12),
|
decoration: BoxDecoration(
|
||||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
color: Colors.white,
|
||||||
Container(width: 44, height: 44, decoration: BoxDecoration(gradient: LinearGradient(begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [AppColors.krowBlue.withAlpha((0.15 * 255).round()), AppColors.krowBlue.withAlpha((0.08 * 255).round())]), borderRadius: BorderRadius.circular(12), border: Border.all(color: AppColors.krowBlue.withAlpha((0.15 * 255).round()))), child: const Center(child: Icon(LucideIcons.briefcase, color: AppColors.krowBlue, size: 20))),
|
borderRadius: BorderRadius.circular(16),
|
||||||
const SizedBox(width: 12),
|
border: Border.all(color: AppColors.krowBorder)
|
||||||
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
),
|
||||||
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowCharcoal)), Text(client, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))])), Column(crossAxisAlignment: CrossAxisAlignment.end, children: [Text(pay, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.krowCharcoal)), Text(rate, style: const TextStyle(fontSize: 10, color: AppColors.krowMuted))])]),
|
child: Column(
|
||||||
const SizedBox(height: 8),
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
Row(children: [const Icon(LucideIcons.calendar, size: 12, color: AppColors.krowMuted), const SizedBox(width: 4), Text(date, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted)), const SizedBox(width: 12), const Icon(LucideIcons.clock, size: 12, color: AppColors.krowMuted), const SizedBox(width: 4), Text(time, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))]),
|
children: [
|
||||||
const SizedBox(height: 4),
|
Row(children: [
|
||||||
Row(children: [const Icon(LucideIcons.mapPin, size: 12, color: AppColors.krowMuted), const SizedBox(width: 4), Expanded(child: Text(address, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted), overflow: TextOverflow.ellipsis))]),
|
Container(width: 6, height: 6, decoration: const BoxDecoration(color: Color(0xFFEF4444), shape: BoxShape.circle)),
|
||||||
])),
|
const SizedBox(width: 6),
|
||||||
]),
|
const Text("CANCELLED", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: Color(0xFFEF4444))),
|
||||||
])),
|
if (isLastMinute) ...[
|
||||||
);
|
const SizedBox(width: 4),
|
||||||
}
|
const Text("• 4hr compensation", style: TextStyle(fontSize: 10, fontWeight: FontWeight.w500, color: Color(0xFF10B981)))
|
||||||
|
]
|
||||||
void _showCancelledModal(String type) {
|
]),
|
||||||
final isLastMinute = type == 'lastMinute';
|
|
||||||
showDialog(
|
|
||||||
context: context,
|
|
||||||
builder: (context) => AlertDialog(
|
|
||||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
|
|
||||||
title: Row(
|
|
||||||
children: [
|
|
||||||
const Icon(LucideIcons.xCircle, color: Color(0xFFEF4444)),
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
const Text("Shift Cancelled"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
content: Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"We're sorry, but the following shift has been cancelled by the client:",
|
|
||||||
style: TextStyle(fontSize: 14),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
const SizedBox(height: 12),
|
||||||
Container(
|
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
padding: const EdgeInsets.all(12),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade50,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: Colors.grey.shade200),
|
|
||||||
),
|
|
||||||
child: const Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text("Annual Tech Conference", style: TextStyle(fontWeight: FontWeight.bold)),
|
|
||||||
Text("Today, 10:00 AM - 6:00 PM"),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
if (isLastMinute)
|
|
||||||
Container(
|
Container(
|
||||||
padding: const EdgeInsets.all(12),
|
width: 44,
|
||||||
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color(0xFFECFDF5),
|
color: AppColors.krowBlue.withOpacity(0.05),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(12),
|
||||||
border: Border.all(color: const Color(0xFF10B981)),
|
),
|
||||||
),
|
child: const Center(child: Icon(LucideIcons.briefcase, color: AppColors.krowBlue, size: 20))
|
||||||
child: const Row(
|
|
||||||
children: [
|
|
||||||
Icon(LucideIcons.checkCircle, color: Color(0xFF10B981), size: 16),
|
|
||||||
SizedBox(width: 8),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
"You are eligible for 4hr cancellation compensation.",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 12, color: Color(0xFF065F46), fontWeight: FontWeight.w500),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
const Text(
|
|
||||||
"Reduced schedule at the venue. No compensation is due as this was cancelled more than 4 hours in advance.",
|
|
||||||
style: TextStyle(fontSize: 12, color: AppColors.krowMuted),
|
|
||||||
),
|
),
|
||||||
],
|
const SizedBox(width: 12),
|
||||||
),
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
actions: [
|
Row(mainAxisAlignment: MainAxisAlignment.spaceBetween, children: [
|
||||||
TextButton(
|
Expanded(child: Column(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||||
onPressed: () => Navigator.pop(context),
|
Text(title, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowCharcoal)),
|
||||||
child: const Text("Close"),
|
Text(client, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))
|
||||||
),
|
])),
|
||||||
],
|
Column(crossAxisAlignment: CrossAxisAlignment.end, children: [
|
||||||
),
|
Text(pay, style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: AppColors.krowCharcoal)),
|
||||||
|
Text(rate, style: const TextStyle(fontSize: 10, color: AppColors.krowMuted))
|
||||||
|
])
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
Row(children: [
|
||||||
|
const Icon(LucideIcons.calendar, size: 12, color: AppColors.krowMuted),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(date, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted)),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Icon(LucideIcons.clock, size: 12, color: AppColors.krowMuted),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(time, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted))
|
||||||
|
]),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Row(children: [
|
||||||
|
const Icon(LucideIcons.mapPin, size: 12, color: AppColors.krowMuted),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Expanded(child: Text(address, style: const TextStyle(fontSize: 12, color: AppColors.krowMuted), overflow: TextOverflow.ellipsis))
|
||||||
|
]),
|
||||||
|
])),
|
||||||
|
]),
|
||||||
|
]),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -75,16 +75,19 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String _getShiftType() {
|
String _getShiftType() {
|
||||||
// Check title for type indicators (for mock data)
|
if (widget.shift.durationDays != null && widget.shift.durationDays! > 30) {
|
||||||
if (widget.shift.title.contains('Long Term')) return t.staff_shifts.filter.long_term;
|
return t.staff_shifts.filter.long_term;
|
||||||
if (widget.shift.title.contains('Multi-Day')) return t.staff_shifts.filter.multi_day;
|
}
|
||||||
|
if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) {
|
||||||
|
return t.staff_shifts.filter.multi_day;
|
||||||
|
}
|
||||||
return t.staff_shifts.filter.one_day;
|
return t.staff_shifts.filter.one_day;
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
// ignore: unused_local_variable
|
|
||||||
final duration = _calculateDuration();
|
final duration = _calculateDuration();
|
||||||
|
final estimatedTotal = (widget.shift.hourlyRate) * duration;
|
||||||
|
|
||||||
// Status Logic
|
// Status Logic
|
||||||
String? status = widget.shift.status;
|
String? status = widget.shift.status;
|
||||||
@@ -168,7 +171,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
),
|
),
|
||||||
Text(
|
Text(
|
||||||
statusText,
|
statusText,
|
||||||
style: UiTypography.display3r.copyWith(
|
style: UiTypography.footnote2b.copyWith(
|
||||||
color: statusColor,
|
color: statusColor,
|
||||||
letterSpacing: 0.5,
|
letterSpacing: 0.5,
|
||||||
),
|
),
|
||||||
@@ -187,9 +190,8 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
_getShiftType(),
|
_getShiftType(),
|
||||||
style: UiTypography.display3r.copyWith(
|
style: UiTypography.footnote2m.copyWith(
|
||||||
color: UiColors.primary,
|
color: UiColors.primary,
|
||||||
fontWeight: FontWeight.w500,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -197,59 +199,167 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Main Content
|
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Date/Time Column
|
// Logo
|
||||||
|
Container(
|
||||||
|
width: 44,
|
||||||
|
height: 44,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
gradient: LinearGradient(
|
||||||
|
colors: [
|
||||||
|
UiColors.primary.withOpacity(0.09),
|
||||||
|
UiColors.primary.withOpacity(0.03),
|
||||||
|
],
|
||||||
|
begin: Alignment.topLeft,
|
||||||
|
end: Alignment.bottomRight,
|
||||||
|
),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.primary.withOpacity(0.09),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: widget.shift.logoUrl != null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
widget.shift.logoUrl!,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.briefcase,
|
||||||
|
color: UiColors.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
|
||||||
|
// Details
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
Text(
|
Expanded(
|
||||||
_formatDate(widget.shift.date),
|
child: Column(
|
||||||
style: UiTypography.display2m.copyWith(
|
crossAxisAlignment:
|
||||||
color: UiColors.textPrimary,
|
CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
widget.shift.title,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
widget.shift.clientName,
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
if (widget.shift.durationDays != null) ...[
|
const SizedBox(width: 8),
|
||||||
const SizedBox(width: 8),
|
Column(
|
||||||
Container(
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
children: [
|
||||||
decoration: BoxDecoration(
|
Text(
|
||||||
color: UiColors.primary.withOpacity(0.1),
|
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||||
borderRadius: BorderRadius.circular(4),
|
style: UiTypography.title1m.copyWith(
|
||||||
),
|
color: UiColors.textPrimary,
|
||||||
child: Text(
|
|
||||||
t.staff_shifts.details.days(days: widget.shift.durationDays!),
|
|
||||||
style: UiTypography.display3r.copyWith(
|
|
||||||
color: UiColors.primary,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
Text(
|
||||||
|
"\$${widget.shift.hourlyRate.toInt()}/hr · ${duration.toInt()}h",
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 8),
|
||||||
|
|
||||||
|
// Date & Time - Multi-Day or Single Day
|
||||||
|
if (widget.shift.durationDays != null &&
|
||||||
|
widget.shift.durationDays! > 1) ...[
|
||||||
|
// Multi-Day Schedule Display
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
t.staff_shifts.details.days(
|
||||||
|
days: widget.shift.durationDays!,
|
||||||
|
),
|
||||||
|
style: UiTypography.footnote2m.copyWith(
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
"Showing first schedule...",
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.primary,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
] else ...[
|
||||||
|
// Single Day Display
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.calendar,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_formatDate(widget.shift.date),
|
||||||
|
style: UiTypography.footnote1r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
"${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}",
|
||||||
|
style: UiTypography.footnote1r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
const SizedBox(height: 4),
|
const SizedBox(height: 4),
|
||||||
Text(
|
|
||||||
'${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}',
|
// Location
|
||||||
style: UiTypography.body2r.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Text(
|
|
||||||
widget.shift.title,
|
|
||||||
style: UiTypography.body2m.copyWith(
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(
|
const Icon(
|
||||||
@@ -258,10 +368,15 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
color: UiColors.iconSecondary,
|
color: UiColors.iconSecondary,
|
||||||
),
|
),
|
||||||
const SizedBox(width: 4),
|
const SizedBox(width: 4),
|
||||||
Text(
|
Expanded(
|
||||||
widget.shift.clientName,
|
child: Text(
|
||||||
style: UiTypography.display3r.copyWith(
|
widget.shift.locationAddress.isNotEmpty
|
||||||
color: UiColors.textSecondary,
|
? widget.shift.locationAddress
|
||||||
|
: widget.shift.location,
|
||||||
|
style: UiTypography.footnote1r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -269,144 +384,389 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Logo Box
|
|
||||||
Container(
|
|
||||||
width: 48,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: UiColors.background,
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
border: Border.all(color: UiColors.border),
|
|
||||||
),
|
|
||||||
child: widget.shift.logoUrl != null
|
|
||||||
? ClipRRect(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
child: Image.network(
|
|
||||||
widget.shift.logoUrl!,
|
|
||||||
fit: BoxFit.cover,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
: Center(
|
|
||||||
child: Text(
|
|
||||||
widget.shift.clientName.isNotEmpty
|
|
||||||
? widget.shift.clientName[0]
|
|
||||||
: 'K',
|
|
||||||
style: UiTypography.title1m.textLink,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Expanded Actions
|
// Expanded Content
|
||||||
AnimatedCrossFade(
|
AnimatedSize(
|
||||||
firstChild: const SizedBox(height: 0),
|
duration: const Duration(milliseconds: 300),
|
||||||
secondChild: Container(
|
child: _isExpanded
|
||||||
decoration: const BoxDecoration(
|
? Column(
|
||||||
border: Border(
|
children: [
|
||||||
top: BorderSide(color: UiColors.border),
|
const Divider(height: 1, color: UiColors.border),
|
||||||
),
|
Padding(
|
||||||
),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Column(
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
// Warning for Pending
|
// Stats Row
|
||||||
if (status == 'pending' || status == 'open')
|
Row(
|
||||||
Container(
|
children: [
|
||||||
width: double.infinity,
|
Expanded(
|
||||||
padding: const EdgeInsets.symmetric(
|
child: _buildStatCard(
|
||||||
vertical: 8,
|
UiIcons.dollar,
|
||||||
horizontal: 16,
|
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||||
),
|
"Total",
|
||||||
color: UiColors.accent.withOpacity(0.1),
|
),
|
||||||
child: Row(
|
),
|
||||||
children: [
|
const SizedBox(width: 12),
|
||||||
const Icon(
|
Expanded(
|
||||||
UiIcons.warning,
|
child: _buildStatCard(
|
||||||
size: 14,
|
UiIcons.dollar,
|
||||||
color: UiColors.textWarning,
|
"\$${widget.shift.hourlyRate.toInt()}",
|
||||||
),
|
"Hourly Rate",
|
||||||
const SizedBox(width: 8),
|
),
|
||||||
Text(
|
),
|
||||||
t.staff_shifts.status.pending_warning,
|
const SizedBox(width: 12),
|
||||||
style: UiTypography.display3r.copyWith(
|
Expanded(
|
||||||
color: UiColors.textWarning,
|
child: _buildStatCard(
|
||||||
fontWeight: FontWeight.w500,
|
UiIcons.clock,
|
||||||
|
"${duration.toInt()}",
|
||||||
|
"Hours",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 24),
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
Padding(
|
// In/Out Time
|
||||||
padding: const EdgeInsets.all(12),
|
Row(
|
||||||
child: Row(
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
if (status == 'pending' || status == 'open') ...[
|
child: _buildTimeBox(
|
||||||
Expanded(
|
"CLOCK IN TIME",
|
||||||
child: OutlinedButton(
|
widget.shift.startTime,
|
||||||
onPressed: widget.onDecline,
|
),
|
||||||
style: OutlinedButton.styleFrom(
|
),
|
||||||
foregroundColor: UiColors.destructive,
|
const SizedBox(width: 12),
|
||||||
side: const BorderSide(color: UiColors.border),
|
Expanded(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
child: _buildTimeBox(
|
||||||
shape: RoundedRectangleBorder(
|
"CLOCK OUT TIME",
|
||||||
borderRadius: BorderRadius.circular(8),
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
child: Text(t.staff_shifts.action.decline),
|
const SizedBox(height: 24),
|
||||||
),
|
],
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
// Actions
|
||||||
Expanded(
|
if (!widget.historyMode)
|
||||||
child: ElevatedButton(
|
if (status == 'confirmed')
|
||||||
onPressed: widget.onAccept,
|
SizedBox(
|
||||||
style: ElevatedButton.styleFrom(
|
width: double.infinity,
|
||||||
backgroundColor: UiColors.primary,
|
height: 48,
|
||||||
foregroundColor: Colors.white,
|
child: OutlinedButton.icon(
|
||||||
elevation: 0,
|
onPressed: widget.onRequestSwap,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
icon: const Icon(
|
||||||
shape: RoundedRectangleBorder(
|
UiIcons.swap,
|
||||||
borderRadius: BorderRadius.circular(8),
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
child: Text(t.staff_shifts.action.confirm),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
] else if (status == 'confirmed') ...[
|
)
|
||||||
Expanded(
|
: const SizedBox.shrink(),
|
||||||
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.textPrimary,
|
|
||||||
side: const BorderSide(color: UiColors.border),
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(8),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
crossFadeState: _isExpanded
|
|
||||||
? CrossFadeState.showSecond
|
|
||||||
: CrossFadeState.showFirst,
|
|
||||||
duration: const Duration(milliseconds: 200),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ class ShiftAssignmentCard extends StatelessWidget {
|
|||||||
return Container(
|
return Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
borderRadius: BorderRadius.circular(12),
|
borderRadius: UiConstants.radiusLg,
|
||||||
border: Border.all(color: UiColors.border),
|
border: Border.all(color: UiColors.border),
|
||||||
boxShadow: [
|
boxShadow: [
|
||||||
BoxShadow(
|
BoxShadow(
|
||||||
@@ -81,64 +81,157 @@ class ShiftAssignmentCard extends StatelessWidget {
|
|||||||
children: [
|
children: [
|
||||||
// Header
|
// Header
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
padding: const EdgeInsets.all(16),
|
||||||
child: Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Card content starts directly as per prototype
|
||||||
|
|
||||||
Row(
|
Row(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
|
// Logo
|
||||||
Container(
|
Container(
|
||||||
width: 36,
|
width: 44,
|
||||||
height: 36,
|
height: 44,
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: UiColors.secondary,
|
gradient: LinearGradient(
|
||||||
borderRadius: BorderRadius.circular(8),
|
colors: [
|
||||||
),
|
UiColors.primary.withOpacity(0.09),
|
||||||
child: Center(
|
UiColors.primary.withOpacity(0.03),
|
||||||
child: Text(
|
],
|
||||||
shift.clientName.isNotEmpty
|
begin: Alignment.topLeft,
|
||||||
? shift.clientName[0]
|
end: Alignment.bottomRight,
|
||||||
: 'K',
|
),
|
||||||
style: UiTypography.body2b.copyWith(
|
borderRadius: BorderRadius.circular(12),
|
||||||
color: UiColors.textSecondary,
|
border: Border.all(
|
||||||
),
|
color: UiColors.primary.withOpacity(0.09),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
child: shift.logoUrl != null
|
||||||
|
? ClipRRect(
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
child: Image.network(
|
||||||
|
shift.logoUrl!,
|
||||||
|
fit: BoxFit.contain,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
: const Center(
|
||||||
|
child: Icon(
|
||||||
|
UiIcons.briefcase,
|
||||||
|
color: UiColors.primary,
|
||||||
|
size: 20,
|
||||||
|
),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(width: 12),
|
const SizedBox(width: 12),
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
// Details
|
||||||
children: [
|
Expanded(
|
||||||
Text(
|
child: Column(
|
||||||
shift.title,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
style: UiTypography.body2b.copyWith(
|
children: [
|
||||||
color: UiColors.textPrimary,
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
shift.title,
|
||||||
|
style: UiTypography.body2m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
shift.clientName,
|
||||||
|
style: UiTypography.body3r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 8),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.end,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
"\$${totalPay.toStringAsFixed(0)}",
|
||||||
|
style: UiTypography.title1m.copyWith(
|
||||||
|
color: UiColors.textPrimary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
"\$${shift.hourlyRate.toInt()}/hr · ${hours.toInt()}h",
|
||||||
|
style: UiTypography.footnote2r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 12),
|
||||||
Text(
|
|
||||||
shift.clientName,
|
// Date & Time
|
||||||
style: UiTypography.display3r.copyWith(
|
Row(
|
||||||
color: UiColors.textSecondary,
|
children: [
|
||||||
|
const Icon(
|
||||||
|
UiIcons.calendar,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
_formatDate(shift.date),
|
||||||
|
style: UiTypography.footnote1r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 12),
|
||||||
|
const Icon(
|
||||||
|
UiIcons.clock,
|
||||||
|
size: 12,
|
||||||
|
color: UiColors.iconSecondary,
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Text(
|
||||||
|
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
|
||||||
|
style: UiTypography.footnote1r.copyWith(
|
||||||
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
],
|
|
||||||
),
|
// Location
|
||||||
],
|
Row(
|
||||||
),
|
children: [
|
||||||
Column(
|
const Icon(
|
||||||
crossAxisAlignment: CrossAxisAlignment.end,
|
UiIcons.mapPin,
|
||||||
children: [
|
size: 12,
|
||||||
Text(
|
color: UiColors.iconSecondary,
|
||||||
"\$${totalPay.toStringAsFixed(0)}",
|
),
|
||||||
style: UiTypography.display2m.copyWith(
|
const SizedBox(width: 4),
|
||||||
color: UiColors.textPrimary,
|
Expanded(
|
||||||
),
|
child: Text(
|
||||||
),
|
shift.locationAddress.isNotEmpty
|
||||||
Text(
|
? shift.locationAddress
|
||||||
"\$${shift.hourlyRate}/hr · ${hours}h",
|
: shift.location,
|
||||||
style: UiTypography.display3r.copyWith(
|
style: UiTypography.footnote1r.copyWith(
|
||||||
color: UiColors.textSecondary,
|
color: UiColors.textSecondary,
|
||||||
|
),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@@ -147,94 +240,43 @@ class ShiftAssignmentCard extends StatelessWidget {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Details
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||||
child: Column(
|
child: Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
UiIcons.calendar,
|
|
||||||
size: 14,
|
|
||||||
color: UiColors.iconSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
_formatDate(shift.date),
|
|
||||||
style: UiTypography.display3r.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 16),
|
|
||||||
const Icon(
|
|
||||||
UiIcons.clock,
|
|
||||||
size: 14,
|
|
||||||
color: UiColors.iconSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Text(
|
|
||||||
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
|
|
||||||
style: UiTypography.display3r.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
UiIcons.mapPin,
|
|
||||||
size: 14,
|
|
||||||
color: UiColors.iconSecondary,
|
|
||||||
),
|
|
||||||
const SizedBox(width: 6),
|
|
||||||
Expanded(
|
|
||||||
child: Text(
|
|
||||||
shift.location,
|
|
||||||
style: UiTypography.display3r.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
maxLines: 1,
|
|
||||||
overflow: TextOverflow.ellipsis,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
if (isConfirming) ...[
|
|
||||||
const Divider(height: 1),
|
|
||||||
Row(
|
|
||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: OutlinedButton(
|
||||||
onPressed: onDecline,
|
onPressed: onDecline,
|
||||||
style: TextButton.styleFrom(
|
style: OutlinedButton.styleFrom(
|
||||||
foregroundColor: UiColors.destructive,
|
foregroundColor: UiColors.iconSecondary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
side: const BorderSide(color: UiColors.border),
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(t.staff_shifts.action.decline),
|
child: Text(t.staff_shifts.action.decline),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Container(width: 1, height: 48, color: UiColors.border),
|
const SizedBox(width: 12),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: TextButton(
|
child: ElevatedButton(
|
||||||
onPressed: onConfirm,
|
onPressed: onConfirm,
|
||||||
style: TextButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
foregroundColor: UiColors.primary,
|
backgroundColor: UiColors.primary,
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
foregroundColor: Colors.white,
|
||||||
|
elevation: 0,
|
||||||
|
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
child: Text(t.staff_shifts.action.confirm),
|
child: Text(t.staff_shifts.action.confirm),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,6 +4,11 @@ import 'data/repositories_impl/shifts_repository_impl.dart';
|
|||||||
import 'domain/usecases/get_my_shifts_usecase.dart';
|
import 'domain/usecases/get_my_shifts_usecase.dart';
|
||||||
import 'domain/usecases/get_available_shifts_usecase.dart';
|
import 'domain/usecases/get_available_shifts_usecase.dart';
|
||||||
import 'domain/usecases/get_pending_assignments_usecase.dart';
|
import 'domain/usecases/get_pending_assignments_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/get_shift_details_usecase.dart';
|
||||||
import 'presentation/blocs/shifts/shifts_bloc.dart';
|
import 'presentation/blocs/shifts/shifts_bloc.dart';
|
||||||
import 'presentation/pages/shifts_page.dart';
|
import 'presentation/pages/shifts_page.dart';
|
||||||
import 'presentation/pages/shift_details_page.dart';
|
import 'presentation/pages/shift_details_page.dart';
|
||||||
@@ -18,6 +23,11 @@ class StaffShiftsModule extends Module {
|
|||||||
i.add(GetMyShiftsUseCase.new);
|
i.add(GetMyShiftsUseCase.new);
|
||||||
i.add(GetAvailableShiftsUseCase.new);
|
i.add(GetAvailableShiftsUseCase.new);
|
||||||
i.add(GetPendingAssignmentsUseCase.new);
|
i.add(GetPendingAssignmentsUseCase.new);
|
||||||
|
i.add(GetCancelledShiftsUseCase.new);
|
||||||
|
i.add(GetHistoryShiftsUseCase.new);
|
||||||
|
i.add(AcceptShiftUseCase.new);
|
||||||
|
i.add(DeclineShiftUseCase.new);
|
||||||
|
i.add(GetShiftDetailsUseCase.new);
|
||||||
|
|
||||||
// Bloc
|
// Bloc
|
||||||
i.add(ShiftsBloc.new);
|
i.add(ShiftsBloc.new);
|
||||||
|
|||||||
Reference in New Issue
Block a user