fix: resolve payments compilation error and remove redundant datasource layer
This commit is contained in:
@@ -1,17 +1,10 @@
|
||||
import 'package:design_system/design_system.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 {
|
||||
/// Void callback for when the Sign Up button is pressed.
|
||||
final VoidCallback onSignUpPressed;
|
||||
|
||||
/// Void callback for when the Log In button is pressed.
|
||||
final VoidCallback onLoginPressed;
|
||||
|
||||
/// Creates a [GetStartedActions].
|
||||
const GetStartedActions({
|
||||
super.key,
|
||||
required this.onSignUpPressed,
|
||||
@@ -22,19 +15,37 @@ class GetStartedActions extends StatelessWidget {
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
// Sign Up Button
|
||||
UiButton.primary(
|
||||
text: t.staff_authentication.get_started_page.sign_up_button,
|
||||
children: [
|
||||
ElevatedButton(
|
||||
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: 12),
|
||||
|
||||
// Log In Button
|
||||
UiButton.secondary(
|
||||
text: t.staff_authentication.get_started_page.log_in_button,
|
||||
const SizedBox(height: 16),
|
||||
OutlinedButton(
|
||||
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:design_system/design_system.dart';
|
||||
|
||||
/// A widget that displays the background for the Get Started page.
|
||||
class GetStartedBackground extends StatelessWidget {
|
||||
/// Creates a [GetStartedBackground].
|
||||
const GetStartedBackground({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 24.0),
|
||||
return Container(
|
||||
color: Colors.white,
|
||||
child: Column(
|
||||
children: <Widget>[
|
||||
children: [
|
||||
const SizedBox(height: 32),
|
||||
// Logo
|
||||
Image.asset(UiImageAssets.logoBlue, height: 40),
|
||||
|
||||
Image.asset(
|
||||
UiImageAssets.logoBlue,
|
||||
height: 40,
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: <Widget>[
|
||||
// Hero Image
|
||||
Container(
|
||||
width: 288,
|
||||
height: 288,
|
||||
margin: const EdgeInsets.only(bottom: 24),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: UiColors.secondaryForeground.withAlpha(
|
||||
64,
|
||||
), // 0.5 opacity
|
||||
),
|
||||
child: Padding(
|
||||
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,
|
||||
),
|
||||
),
|
||||
child: Center(
|
||||
child: Container(
|
||||
width: 288,
|
||||
height: 288,
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: const Color(0xFF3A4A5A).withOpacity(0.05),
|
||||
),
|
||||
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,
|
||||
errorBuilder: (context, error, stackTrace) {
|
||||
return Image.asset(UiImageAssets.logoBlue);
|
||||
},
|
||||
),
|
||||
),
|
||||
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: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 {
|
||||
/// Creates a [GetStartedHeader].
|
||||
const GetStartedHeader({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: <Widget>[
|
||||
RichText(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
'Krow Workforce',
|
||||
style: UiTypography.display1b.copyWith(color: UiColors.textPrimary),
|
||||
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),
|
||||
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,
|
||||
style: UiTypography.body1r.textSecondary,
|
||||
),
|
||||
],
|
||||
);
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import '../../domain/usecases/get_todays_shift_usecase.dart';
|
||||
import '../../domain/usecases/get_attendance_status_usecase.dart';
|
||||
import '../../domain/usecases/clock_in_usecase.dart';
|
||||
@@ -16,6 +17,11 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
final ClockOutUseCase _clockOut;
|
||||
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({
|
||||
required GetTodaysShiftUseCase getTodaysShift,
|
||||
required GetAttendanceStatusUseCase getAttendanceStatus,
|
||||
@@ -33,6 +39,9 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
on<CheckInRequested>(_onCheckIn);
|
||||
on<CheckOutRequested>(_onCheckOut);
|
||||
on<CheckInModeChanged>(_onModeChanged);
|
||||
on<RequestLocationPermission>(_onRequestLocationPermission);
|
||||
on<CommuteModeToggled>(_onCommuteModeToggled);
|
||||
on<LocationUpdated>(_onLocationUpdated);
|
||||
|
||||
add(ClockInPageLoaded());
|
||||
}
|
||||
@@ -47,12 +56,20 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
final status = await _getAttendanceStatus();
|
||||
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(
|
||||
status: ClockInStatus.success,
|
||||
todayShift: shift,
|
||||
attendance: status,
|
||||
activityLog: activity,
|
||||
));
|
||||
|
||||
if (shift != null && !status.isCheckedIn) {
|
||||
add(RequestLocationPermission());
|
||||
}
|
||||
|
||||
} catch (e) {
|
||||
emit(state.copyWith(
|
||||
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(
|
||||
DateSelected event,
|
||||
Emitter<ClockInState> emit,
|
||||
@@ -79,6 +159,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
CheckInRequested event,
|
||||
Emitter<ClockInState> emit,
|
||||
) 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));
|
||||
try {
|
||||
final newStatus = await _clockIn(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
abstract class ClockInEvent extends Equatable {
|
||||
const ClockInEvent();
|
||||
@@ -46,3 +47,25 @@ class CheckInModeChanged extends ClockInEvent {
|
||||
@override
|
||||
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:krow_domain/krow_domain.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
|
||||
|
||||
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
||||
|
||||
@@ -12,6 +14,13 @@ class ClockInState extends Equatable {
|
||||
final String checkInMode;
|
||||
final String? errorMessage;
|
||||
|
||||
final Position? currentLocation;
|
||||
final double? distanceFromVenue;
|
||||
final bool isLocationVerified;
|
||||
final bool isCommuteModeOn;
|
||||
final bool hasLocationConsent;
|
||||
final int? etaMinutes;
|
||||
|
||||
const ClockInState({
|
||||
this.status = ClockInStatus.initial,
|
||||
this.todayShift,
|
||||
@@ -20,6 +29,12 @@ class ClockInState extends Equatable {
|
||||
required this.selectedDate,
|
||||
this.checkInMode = 'swipe',
|
||||
this.errorMessage,
|
||||
this.currentLocation,
|
||||
this.distanceFromVenue,
|
||||
this.isLocationVerified = false,
|
||||
this.isCommuteModeOn = false,
|
||||
this.hasLocationConsent = false,
|
||||
this.etaMinutes,
|
||||
});
|
||||
|
||||
ClockInState copyWith({
|
||||
@@ -30,6 +45,12 @@ class ClockInState extends Equatable {
|
||||
DateTime? selectedDate,
|
||||
String? checkInMode,
|
||||
String? errorMessage,
|
||||
Position? currentLocation,
|
||||
double? distanceFromVenue,
|
||||
bool? isLocationVerified,
|
||||
bool? isCommuteModeOn,
|
||||
bool? hasLocationConsent,
|
||||
int? etaMinutes,
|
||||
}) {
|
||||
return ClockInState(
|
||||
status: status ?? this.status,
|
||||
@@ -39,6 +60,12 @@ class ClockInState extends Equatable {
|
||||
selectedDate: selectedDate ?? this.selectedDate,
|
||||
checkInMode: checkInMode ?? this.checkInMode,
|
||||
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,
|
||||
checkInMode,
|
||||
errorMessage,
|
||||
currentLocation,
|
||||
distanceFromVenue,
|
||||
isLocationVerified,
|
||||
isCommuteModeOn,
|
||||
hasLocationConsent,
|
||||
etaMinutes,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -92,10 +92,13 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
if (todayShift != null)
|
||||
CommuteTracker(
|
||||
shift: todayShift,
|
||||
hasLocationConsent: false, // Mock value
|
||||
isCommuteModeOn: false, // Mock value
|
||||
distanceMeters: 500, // Mock value for demo
|
||||
etaMinutes: 8, // Mock value for demo
|
||||
hasLocationConsent: state.hasLocationConsent,
|
||||
isCommuteModeOn: state.isCommuteModeOn,
|
||||
distanceMeters: state.distanceFromVenue,
|
||||
etaMinutes: state.etaMinutes,
|
||||
onCommuteToggled: (value) {
|
||||
_bloc.add(CommuteModeToggled(value));
|
||||
},
|
||||
),
|
||||
// Date Selector
|
||||
DateSelector(
|
||||
@@ -183,9 +186,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
const Text(
|
||||
"9:00 AM - 5:00 PM",
|
||||
style: TextStyle(
|
||||
Text(
|
||||
"${_formatTime(todayShift.startTime)} - ${_formatTime(todayShift.endTime)}",
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: Color(0xFF475569), // slate-600
|
||||
@@ -207,36 +210,73 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
|
||||
// Swipe To Check In / Checked Out State / No Shift State
|
||||
if (todayShift != null && checkOutTime == null) ...[
|
||||
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),
|
||||
if (!isCheckedIn && !_isCheckInAllowed(todayShift))
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(24),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF1F5F9), // slate-100
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
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 &&
|
||||
checkOutTime != null) ...[
|
||||
// Shift Completed State
|
||||
@@ -695,4 +735,43 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
_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 {
|
||||
final Shift? shift;
|
||||
final Function(CommuteMode)? onModeChange;
|
||||
final ValueChanged<bool>? onCommuteToggled;
|
||||
final bool hasLocationConsent;
|
||||
final bool isCommuteModeOn;
|
||||
final double? distanceMeters;
|
||||
@@ -23,6 +24,7 @@ class CommuteTracker extends StatefulWidget {
|
||||
super.key,
|
||||
this.shift,
|
||||
this.onModeChange,
|
||||
this.onCommuteToggled,
|
||||
this.hasLocationConsent = false,
|
||||
this.isCommuteModeOn = false,
|
||||
this.distanceMeters,
|
||||
@@ -44,6 +46,21 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
_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() {
|
||||
if (widget.shift == null) return CommuteMode.lockedNoShift;
|
||||
|
||||
@@ -299,6 +316,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
value: _localIsCommuteOn,
|
||||
onChanged: (value) {
|
||||
setState(() => _localIsCommuteOn = value);
|
||||
widget.onCommuteToggled?.call(value);
|
||||
},
|
||||
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/src/session/staff_session_store.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import '../../domain/repositories/payments_repository.dart';
|
||||
|
||||
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final FirebaseAuth _auth = FirebaseAuth.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
|
||||
DateTime? _toDateTime(dynamic t) {
|
||||
if (t == null) return null;
|
||||
if (t is DateTime) return t;
|
||||
if (t is String) return DateTime.tryParse(t);
|
||||
|
||||
try {
|
||||
// Attempt to deserialize via standard methods
|
||||
return DateTime.tryParse(t.toJson() as String);
|
||||
} catch (_) {
|
||||
try {
|
||||
return DateTime.tryParse(t.toString());
|
||||
} catch (e) {
|
||||
return null;
|
||||
if (t is Timestamp) {
|
||||
return t.toDateTime();
|
||||
}
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
if (t.runtimeType.toString().contains('Timestamp')) {
|
||||
return (t as dynamic).toDate();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
return DateTime.tryParse(t.toString());
|
||||
} catch (_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<PaymentSummary> getPaymentSummary() async {
|
||||
final StaffSession? session = StaffSessionStore.instance.session;
|
||||
if (session?.staff?.id == null) {
|
||||
return const PaymentSummary(
|
||||
weeklyEarnings: 0,
|
||||
monthlyEarnings: 0,
|
||||
pendingEarnings: 0,
|
||||
totalEarnings: 0,
|
||||
);
|
||||
}
|
||||
|
||||
final String currentStaffId = session!.staff!.id;
|
||||
final String currentStaffId = await _getStaffId();
|
||||
|
||||
// Fetch recent payments with a limit
|
||||
// Note: limit is chained on the query builder
|
||||
@@ -82,10 +116,7 @@ class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
|
||||
@override
|
||||
Future<List<StaffPayment>> getPaymentHistory(String period) async {
|
||||
final StaffSession? session = StaffSessionStore.instance.session;
|
||||
if (session?.staff?.id == null) return <StaffPayment>[];
|
||||
|
||||
final String currentStaffId = session!.staff!.id;
|
||||
final String currentStaffId = await _getStaffId();
|
||||
|
||||
try {
|
||||
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:intl/intl.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import '../../domain/repositories/shifts_repository_interface.dart';
|
||||
|
||||
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
@@ -16,31 +17,62 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
// Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation)
|
||||
final Map<String, String> _appToRoleIdMap = {};
|
||||
|
||||
String get _currentStaffId {
|
||||
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;
|
||||
return session!.staff!.id;
|
||||
}
|
||||
// Fallback? Or throw.
|
||||
// If not logged in, we shouldn't be here.
|
||||
return _auth.currentUser?.uid ?? 'STAFF_123';
|
||||
|
||||
// 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 (should ideally not happen if DB is seeded)
|
||||
return user.uid;
|
||||
}
|
||||
|
||||
/// Helper to convert Data Connect Timestamp to DateTime
|
||||
DateTime? _toDateTime(dynamic t) {
|
||||
if (t == null) return null;
|
||||
if (t is DateTime) return t;
|
||||
if (t is String) return DateTime.tryParse(t);
|
||||
|
||||
// Data Connect Timestamp handling
|
||||
try {
|
||||
if (t is String) return DateTime.tryParse(t);
|
||||
// If it accepts toJson
|
||||
try {
|
||||
return DateTime.tryParse(t.toJson() as String);
|
||||
} catch (_) {}
|
||||
// If it's a Timestamp object (depends on SDK), usually .toDate() exists but 'dynamic' hides it.
|
||||
// Assuming toString or toJson covers it, or using helper.
|
||||
return DateTime.now(); // Placeholder if type unknown, but ideally fetch correct value
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
if (t is Timestamp) {
|
||||
return t.toDateTime();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
// Fallback for any object that might have a toDate or similar
|
||||
if (t.runtimeType.toString().contains('Timestamp')) {
|
||||
return (t as dynamic).toDate();
|
||||
}
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
return DateTime.tryParse(t.toString());
|
||||
} catch (_) {}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -53,22 +85,49 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
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 {
|
||||
try {
|
||||
final staffId = await _getStaffId();
|
||||
final response = await _dataConnect
|
||||
.getApplicationsByStaffId(staffId: _currentStaffId)
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.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 = [];
|
||||
|
||||
for (final app in apps) {
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||
|
||||
final shiftTuple = await _getShiftDetails(app.shift.id);
|
||||
if (shiftTuple != null) {
|
||||
shifts.add(shiftTuple);
|
||||
final shift = await _getShiftDetails(app.shift.id);
|
||||
if (shift != null) {
|
||||
// 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;
|
||||
@@ -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
|
||||
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
||||
try {
|
||||
@@ -104,8 +179,9 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: s.status?.stringValue ?? 'OPEN',
|
||||
status: s.status?.stringValue.toLowerCase() ?? 'open',
|
||||
description: s.description,
|
||||
durationDays: s.durationDays,
|
||||
));
|
||||
}
|
||||
|
||||
@@ -152,6 +228,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: s.status?.stringValue ?? 'OPEN',
|
||||
description: s.description,
|
||||
durationDays: s.durationDays,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
@@ -165,9 +242,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
|
||||
final role = rolesResult.data.shiftRoles.first;
|
||||
|
||||
final staffId = await _getStaffId();
|
||||
await _dataConnect.createApplication(
|
||||
shiftId: shiftId,
|
||||
staffId: _currentStaffId,
|
||||
staffId: staffId,
|
||||
roleId: role.id,
|
||||
status: dc.ApplicationStatus.PENDING,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
@@ -198,7 +276,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
roleId = _appToRoleIdMap[appId];
|
||||
} else {
|
||||
// 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;
|
||||
if (app != null) {
|
||||
appId = app.id;
|
||||
|
||||
@@ -25,4 +25,10 @@ abstract interface class ShiftsRepositoryInterface {
|
||||
|
||||
/// Declines a pending shift assignment.
|
||||
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/usecases/get_my_shifts_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_state.dart';
|
||||
@@ -15,14 +19,24 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
||||
final GetMyShiftsUseCase getMyShifts;
|
||||
final GetAvailableShiftsUseCase getAvailableShifts;
|
||||
final GetPendingAssignmentsUseCase getPendingAssignments;
|
||||
final GetCancelledShiftsUseCase getCancelledShifts;
|
||||
final GetHistoryShiftsUseCase getHistoryShifts;
|
||||
final AcceptShiftUseCase acceptShift;
|
||||
final DeclineShiftUseCase declineShift;
|
||||
|
||||
ShiftsBloc({
|
||||
required this.getMyShifts,
|
||||
required this.getAvailableShifts,
|
||||
required this.getPendingAssignments,
|
||||
required this.getCancelledShifts,
|
||||
required this.getHistoryShifts,
|
||||
required this.acceptShift,
|
||||
required this.declineShift,
|
||||
}) : super(ShiftsInitial()) {
|
||||
on<LoadShiftsEvent>(_onLoadShifts);
|
||||
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
|
||||
on<AcceptShiftEvent>(_onAcceptShift);
|
||||
on<DeclineShiftEvent>(_onDeclineShift);
|
||||
}
|
||||
|
||||
Future<void> _onLoadShifts(
|
||||
@@ -39,6 +53,8 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
||||
try {
|
||||
final myShiftsResult = await getMyShifts();
|
||||
final pendingResult = await getPendingAssignments();
|
||||
final cancelledResult = await getCancelledShifts();
|
||||
final historyResult = await getHistoryShifts();
|
||||
|
||||
// Initial available with defaults
|
||||
final availableResult = await getAvailableShifts(const GetAvailableShiftsArguments());
|
||||
@@ -46,7 +62,9 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
||||
emit(ShiftsLoaded(
|
||||
myShifts: myShiftsResult,
|
||||
pendingShifts: pendingResult,
|
||||
cancelledShifts: cancelledResult,
|
||||
availableShifts: availableResult,
|
||||
historyShifts: historyResult,
|
||||
searchQuery: '',
|
||||
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
|
||||
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 {
|
||||
final List<Shift> myShifts;
|
||||
final List<Shift> pendingShifts;
|
||||
final List<Shift> cancelledShifts;
|
||||
final List<Shift> availableShifts;
|
||||
final List<Shift> historyShifts;
|
||||
final String searchQuery;
|
||||
final String jobType;
|
||||
|
||||
const ShiftsLoaded({
|
||||
required this.myShifts,
|
||||
required this.pendingShifts,
|
||||
required this.cancelledShifts,
|
||||
required this.availableShifts,
|
||||
required this.historyShifts,
|
||||
required this.searchQuery,
|
||||
required this.jobType,
|
||||
});
|
||||
@@ -30,21 +34,33 @@ class ShiftsLoaded extends ShiftsState {
|
||||
ShiftsLoaded copyWith({
|
||||
List<Shift>? myShifts,
|
||||
List<Shift>? pendingShifts,
|
||||
List<Shift>? cancelledShifts,
|
||||
List<Shift>? availableShifts,
|
||||
List<Shift>? historyShifts,
|
||||
String? searchQuery,
|
||||
String? jobType,
|
||||
}) {
|
||||
return ShiftsLoaded(
|
||||
myShifts: myShifts ?? this.myShifts,
|
||||
pendingShifts: pendingShifts ?? this.pendingShifts,
|
||||
cancelledShifts: cancelledShifts ?? this.cancelledShifts,
|
||||
availableShifts: availableShifts ?? this.availableShifts,
|
||||
historyShifts: historyShifts ?? this.historyShifts,
|
||||
searchQuery: searchQuery ?? this.searchQuery,
|
||||
jobType: jobType ?? this.jobType,
|
||||
);
|
||||
}
|
||||
|
||||
@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 {
|
||||
|
||||
@@ -4,6 +4,9 @@ import 'package:lucide_icons/lucide_icons.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:design_system/design_system.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/usecases/get_shift_details_usecase.dart';
|
||||
import '../../domain/usecases/accept_shift_usecase.dart';
|
||||
import '../../domain/usecases/decline_shift_usecase.dart';
|
||||
|
||||
// Shim to match POC styles locally
|
||||
class AppColors {
|
||||
@@ -32,11 +35,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
bool _showDetails = true;
|
||||
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
|
||||
void initState() {
|
||||
@@ -49,27 +48,30 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
_shift = widget.shift!;
|
||||
setState(() => _isLoading = false);
|
||||
} else {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
if (mounted) {
|
||||
// Fallback mock shift
|
||||
setState(() {
|
||||
_shift = Shift(
|
||||
id: widget.shiftId,
|
||||
title: 'Event Server',
|
||||
clientName: 'Grand Hotel',
|
||||
logoUrl: null,
|
||||
hourlyRate: 25.0,
|
||||
date: DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
startTime: '16:00',
|
||||
endTime: '22:00',
|
||||
location: 'Downtown',
|
||||
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.',
|
||||
);
|
||||
_isLoading = false;
|
||||
});
|
||||
try {
|
||||
final useCase = Modular.get<GetShiftDetailsUseCase>();
|
||||
final shift = await useCase(widget.shiftId);
|
||||
if (mounted) {
|
||||
if (shift != null) {
|
||||
setState(() {
|
||||
_shift = shift;
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
// Handle case where shift is not found
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Shift not found')),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isLoading = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Error loading shift: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -143,6 +145,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Pending Badge
|
||||
// Status Badge
|
||||
Align(
|
||||
alignment: Alignment.centerRight,
|
||||
child: Container(
|
||||
@@ -151,15 +154,15 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: AppColors.krowYellow.withOpacity(0.3),
|
||||
color: _getStatusColor(_shift.status ?? 'open').withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Text(
|
||||
'Pending 6h ago',
|
||||
child: Text(
|
||||
(_shift.status ?? 'open').toUpperCase(),
|
||||
style: TextStyle(
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: AppColors.krowCharcoal,
|
||||
color: _getStatusColor(_shift.status ?? 'open'),
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -248,25 +251,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
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
|
||||
Container(
|
||||
@@ -310,11 +295,11 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 16),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildDetailRow('Tips', 'Yes', true),
|
||||
_buildDetailRow('Travel Time', 'Yes', true),
|
||||
_buildDetailRow('Meal Provided', 'No', false),
|
||||
_buildDetailRow('Parking Available', 'Yes', true),
|
||||
_buildDetailRow('Gas Compensation', 'No', false),
|
||||
_buildDetailRow('Tips', _shift.tipsAvailable == true ? 'Yes' : 'No', _shift.tipsAvailable == true),
|
||||
_buildDetailRow('Travel Time', _shift.travelTime == true ? 'Yes' : 'No', _shift.travelTime == true),
|
||||
_buildDetailRow('Meal Provided', _shift.mealProvided == true ? 'Yes' : 'No', _shift.mealProvided == true),
|
||||
_buildDetailRow('Parking Available', _shift.parkingAvailable == true ? 'Yes' : 'No', _shift.parkingAvailable == true),
|
||||
_buildDetailRow('Gas Compensation', _shift.gasCompensation == true ? 'Yes' : 'No', _shift.gasCompensation == true),
|
||||
],
|
||||
),
|
||||
),
|
||||
@@ -550,7 +535,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._managers
|
||||
...(_shift.managers ?? [])
|
||||
.map(
|
||||
(manager) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
@@ -574,13 +559,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
8,
|
||||
),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
LucideIcons.user,
|
||||
color: Colors.white,
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
child: _buildAvatar(manager),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Column(
|
||||
@@ -588,14 +567,14 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
manager['name']!,
|
||||
manager.name,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
manager['phone']!,
|
||||
manager.phone,
|
||||
style: const TextStyle(
|
||||
fontSize: 12,
|
||||
color: AppColors.krowMuted,
|
||||
@@ -611,7 +590,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(manager['phone']!),
|
||||
content: Text(manager.phone),
|
||||
duration: const Duration(seconds: 3),
|
||||
),
|
||||
);
|
||||
@@ -702,16 +681,32 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
child: ElevatedButton(
|
||||
onPressed: () async {
|
||||
setState(() => _isApplying = true);
|
||||
await Future.delayed(const Duration(seconds: 1));
|
||||
if (mounted) {
|
||||
setState(() => _isApplying = false);
|
||||
Modular.to.pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Accepted!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
try {
|
||||
final acceptUseCase = Modular.get<AcceptShiftUseCase>();
|
||||
await acceptUseCase(_shift.id);
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isApplying = false);
|
||||
Modular.to.pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Accepted!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
// Ideally, trigger a refresh on the previous screen
|
||||
Modular.get<ShiftsBloc>().add(LoadShiftsEvent());
|
||||
}
|
||||
} catch (e) {
|
||||
if (mounted) {
|
||||
setState(() => _isApplying = false);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Failed to accept shift: $e'),
|
||||
backgroundColor: const Color(0xFFEF4444),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
style: ElevatedButton.styleFrom(
|
||||
@@ -744,7 +739,33 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
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(
|
||||
'Decline shift',
|
||||
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) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 6),
|
||||
|
||||
@@ -75,16 +75,22 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
}
|
||||
|
||||
void _confirmShift(String id) {
|
||||
// TODO: Implement Bloc event
|
||||
_bloc.add(AcceptShiftEvent(id));
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('Shift confirmed! (Placeholder)')),
|
||||
const SnackBar(
|
||||
content: Text('Shift confirmed!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _declineShift(String id) {
|
||||
// TODO: Implement Bloc event
|
||||
_bloc.add(DeclineShiftEvent(id));
|
||||
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> availableJobs = (state is ShiftsLoaded) ? state.availableShifts : [];
|
||||
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 matchesSearch =
|
||||
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||
@@ -110,10 +117,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
|
||||
if (_jobType == 'all') return true;
|
||||
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 == 'long-term') return s.title.contains('Long Term');
|
||||
if (_jobType == 'multi-day') return s.durationDays != null && s.durationDays! > 1;
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
@@ -122,13 +128,23 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
final weekEndDate = calendarDays.last;
|
||||
|
||||
final visibleMyShifts = myShifts.where((s) {
|
||||
// Primitive check if shift date string compare
|
||||
// In real app use DateTime logic
|
||||
final sDateStr = s.date;
|
||||
final wStartStr = DateFormat('yyyy-MM-dd').format(weekStartDate);
|
||||
final wEndStr = DateFormat('yyyy-MM-dd').format(weekEndDate);
|
||||
return sDateStr.compareTo(wStartStr) >= 0 &&
|
||||
sDateStr.compareTo(wEndStr) <= 0;
|
||||
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();
|
||||
|
||||
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();
|
||||
|
||||
return Scaffold(
|
||||
@@ -140,9 +156,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
color: AppColors.krowBlue,
|
||||
padding: EdgeInsets.fromLTRB(
|
||||
20,
|
||||
MediaQuery.of(context).padding.top + 20,
|
||||
MediaQuery.of(context).padding.top + 10,
|
||||
20,
|
||||
20,
|
||||
24,
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
@@ -157,17 +173,28 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
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),
|
||||
// Tabs
|
||||
Row(
|
||||
children: [
|
||||
_buildTab("myshifts", "My Shifts", LucideIcons.calendar, myShifts.length),
|
||||
_buildTab("myshifts", "My Shifts", UiIcons.calendar, myShifts.length),
|
||||
const SizedBox(width: 8),
|
||||
_buildTab("find", "Find Shifts", LucideIcons.search, filteredJobs.length),
|
||||
_buildTab("find", "Find Shifts", UiIcons.search, filteredJobs.length),
|
||||
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')
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 20),
|
||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
||||
child: Column(
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
InkWell(
|
||||
onTap: () => setState(() => _weekOffset--),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(LucideIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.chevronLeft, size: 20, color: AppColors.krowCharcoal),
|
||||
onPressed: () => setState(() => _weekOffset--),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
Text(
|
||||
DateFormat('MMMM yyyy').format(weekStartDate),
|
||||
@@ -202,13 +227,11 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
color: AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
InkWell(
|
||||
onTap: () => setState(() => _weekOffset++),
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(LucideIcons.chevronRight, size: 20, color: AppColors.krowCharcoal),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(UiIcons.chevronRight, size: 20, color: AppColors.krowCharcoal),
|
||||
onPressed: () => setState(() => _weekOffset++),
|
||||
constraints: const BoxConstraints(),
|
||||
padding: EdgeInsets.zero,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -219,52 +242,60 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
children: calendarDays.map((date) {
|
||||
final isSelected = _isSameDay(date, _selectedDate);
|
||||
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(
|
||||
onTap: () => setState(() => _selectedDate = date),
|
||||
child: Container(
|
||||
width: 44,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
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,
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 44,
|
||||
height: 60,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
border: Border.all(
|
||||
color: isSelected ? AppColors.krowBlue : AppColors.krowBorder,
|
||||
width: 1,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
DateFormat('E').format(date),
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.w500,
|
||||
color: isSelected ? Colors.white.withOpacity(0.8) : AppColors.krowMuted,
|
||||
),
|
||||
),
|
||||
if (hasShifts)
|
||||
Container(
|
||||
margin: const EdgeInsets.only(top: 4),
|
||||
width: 6,
|
||||
height: 6,
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? Colors.white : AppColors.krowBlue,
|
||||
shape: BoxShape.circle,
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Text(
|
||||
date.day.toString().padLeft(2, '0'),
|
||||
style: TextStyle(
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: isSelected ? Colors.white : AppColors.krowCharcoal,
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
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(),
|
||||
@@ -276,141 +307,177 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
||||
if (_activeTab == 'myshifts')
|
||||
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
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: state is ShiftsLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
children: [
|
||||
if (_activeTab == 'find') ...[
|
||||
// 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'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
|
||||
const SizedBox(height: 20),
|
||||
if (_activeTab == 'myshifts') ...[
|
||||
if (pendingAssignments.isNotEmpty) ...[
|
||||
Align(
|
||||
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)
|
||||
)),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
_buildSectionHeader("Awaiting Confirmation", const Color(0xFFF59E0B)),
|
||||
...pendingAssignments.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: ShiftAssignmentCard(
|
||||
shift: shift,
|
||||
onConfirm: () => _confirmShift(shift.id),
|
||||
onDecline: () => _declineShift(shift.id),
|
||||
isConfirming: true,
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Cancelled Shifts Demo (Visual only as per POC)
|
||||
Align(
|
||||
alignment: Alignment.centerLeft,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: const Text("Cancelled Shifts", style: TextStyle(
|
||||
fontSize: 14, fontWeight: FontWeight.w600, color: AppColors.krowMuted
|
||||
)),
|
||||
),
|
||||
),
|
||||
_buildCancelledCard(
|
||||
title: "Annual Tech Conference", client: "TechCorp Inc.", pay: "\$200", rate: "\$25/hr · 8h",
|
||||
date: "Today", time: "10:00 AM - 6:00 PM", address: "123 Convention Center Dr", isLastMinute: true,
|
||||
onTap: () => setState(() => _cancelledShiftDemo = 'lastMinute')
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_buildCancelledCard(
|
||||
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),
|
||||
if (visibleCancelledShifts.isNotEmpty) ...[
|
||||
_buildSectionHeader("Cancelled Shifts", AppColors.krowMuted),
|
||||
...visibleCancelledShifts.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: _buildCancelledCard(
|
||||
title: shift.title,
|
||||
client: shift.clientName,
|
||||
pay: "\$${(shift.hourlyRate * 8).toStringAsFixed(0)}",
|
||||
rate: "\$${shift.hourlyRate}/hr · 8h",
|
||||
date: _formatDateStr(shift.date),
|
||||
time: "${shift.startTime} - ${shift.endTime}",
|
||||
address: shift.locationAddress,
|
||||
isLastMinute: true,
|
||||
onTap: () {}
|
||||
),
|
||||
)),
|
||||
const SizedBox(height: 12),
|
||||
],
|
||||
|
||||
// Confirmed Shifts
|
||||
if (visibleMyShifts.isNotEmpty) ...[
|
||||
Align(
|
||||
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
|
||||
)),
|
||||
),
|
||||
),
|
||||
_buildSectionHeader("Confirmed Shifts", AppColors.krowMuted),
|
||||
...visibleMyShifts.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
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 (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
|
||||
...filteredJobs.map((shift) => MyShiftCard(
|
||||
shift: shift,
|
||||
onAccept: () {},
|
||||
onDecline: () {},
|
||||
...filteredJobs.map((shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
onAccept: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Booked!'),
|
||||
backgroundColor: Color(0xFF10B981),
|
||||
),
|
||||
);
|
||||
},
|
||||
onDecline: () {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
||||
content: Text('Shift Declined'),
|
||||
backgroundColor: Color(0xFFEF4444),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
)),
|
||||
],
|
||||
|
||||
if (_activeTab == 'history')
|
||||
_buildEmptyState(LucideIcons.clock, "No shift history", "Completed shifts appear here", null, null),
|
||||
if (_activeTab == 'history') ...[
|
||||
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) {
|
||||
final isSelected = _jobType == id;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => setState(() => _jobType = id),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
boxShadow: isSelected ? [BoxShadow(color: AppColors.krowBlue.withOpacity(0.2), blurRadius: 4, offset: const Offset(0, 2))] : null,
|
||||
return GestureDetector(
|
||||
onTap: () => setState(() => _jobType = id),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
decoration: BoxDecoration(
|
||||
color: isSelected ? AppColors.krowBlue : Colors.white,
|
||||
borderRadius: BorderRadius.circular(999),
|
||||
border: Border.all(
|
||||
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) {
|
||||
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)),
|
||||
@@ -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}) {
|
||||
return GestureDetector(
|
||||
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: [
|
||||
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)))]]),
|
||||
const SizedBox(height: 12),
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
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))),
|
||||
const SizedBox(width: 12),
|
||||
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))])]),
|
||||
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))]),
|
||||
])),
|
||||
]),
|
||||
])),
|
||||
);
|
||||
}
|
||||
|
||||
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),
|
||||
),
|
||||
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: [
|
||||
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)))
|
||||
]
|
||||
]),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
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)
|
||||
Row(crossAxisAlignment: CrossAxisAlignment.start, children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFECFDF5),
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
border: Border.all(color: const Color(0xFF10B981)),
|
||||
),
|
||||
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),
|
||||
color: AppColors.krowBlue.withOpacity(0.05),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(child: Icon(LucideIcons.briefcase, color: AppColors.krowBlue, size: 20))
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text("Close"),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
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))
|
||||
])
|
||||
]),
|
||||
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() {
|
||||
// Check title for type indicators (for mock data)
|
||||
if (widget.shift.title.contains('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! > 30) {
|
||||
return t.staff_shifts.filter.long_term;
|
||||
}
|
||||
if (widget.shift.durationDays != null && widget.shift.durationDays! > 1) {
|
||||
return t.staff_shifts.filter.multi_day;
|
||||
}
|
||||
return t.staff_shifts.filter.one_day;
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// ignore: unused_local_variable
|
||||
final duration = _calculateDuration();
|
||||
final estimatedTotal = (widget.shift.hourlyRate) * duration;
|
||||
|
||||
// Status Logic
|
||||
String? status = widget.shift.status;
|
||||
@@ -168,7 +171,7 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
),
|
||||
Text(
|
||||
statusText,
|
||||
style: UiTypography.display3r.copyWith(
|
||||
style: UiTypography.footnote2b.copyWith(
|
||||
color: statusColor,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
@@ -187,9 +190,8 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
),
|
||||
child: Text(
|
||||
_getShiftType(),
|
||||
style: UiTypography.display3r.copyWith(
|
||||
style: UiTypography.footnote2m.copyWith(
|
||||
color: UiColors.primary,
|
||||
fontWeight: FontWeight.w500,
|
||||
),
|
||||
),
|
||||
),
|
||||
@@ -197,59 +199,167 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Main Content
|
||||
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
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(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
_formatDate(widget.shift.date),
|
||||
style: UiTypography.display2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
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),
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.primary.withOpacity(0.1),
|
||||
borderRadius: BorderRadius.circular(4),
|
||||
),
|
||||
child: Text(
|
||||
t.staff_shifts.details.days(days: widget.shift.durationDays!),
|
||||
style: UiTypography.display3r.copyWith(
|
||||
color: UiColors.primary,
|
||||
fontWeight: FontWeight.w600,
|
||||
const SizedBox(width: 8),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||
style: UiTypography.title1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
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),
|
||||
Text(
|
||||
'${_formatTime(widget.shift.startTime)} - ${_formatTime(widget.shift.endTime)}',
|
||||
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,
|
||||
),
|
||||
|
||||
// Location
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
@@ -258,10 +368,15 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
widget.shift.clientName,
|
||||
style: UiTypography.display3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
Expanded(
|
||||
child: Text(
|
||||
widget.shift.locationAddress.isNotEmpty
|
||||
? 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
|
||||
AnimatedCrossFade(
|
||||
firstChild: const SizedBox(height: 0),
|
||||
secondChild: Container(
|
||||
decoration: const BoxDecoration(
|
||||
border: Border(
|
||||
top: BorderSide(color: UiColors.border),
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Warning for Pending
|
||||
if (status == 'pending' || status == 'open')
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8,
|
||||
horizontal: 16,
|
||||
),
|
||||
color: UiColors.accent.withOpacity(0.1),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.warning,
|
||||
size: 14,
|
||||
color: UiColors.textWarning,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_shifts.status.pending_warning,
|
||||
style: UiTypography.display3r.copyWith(
|
||||
color: UiColors.textWarning,
|
||||
fontWeight: FontWeight.w500,
|
||||
// Expanded Content
|
||||
AnimatedSize(
|
||||
duration: const Duration(milliseconds: 300),
|
||||
child: _isExpanded
|
||||
? Column(
|
||||
children: [
|
||||
const Divider(height: 1, color: UiColors.border),
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
children: [
|
||||
// Stats Row
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
UiIcons.dollar,
|
||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
||||
"Total",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
UiIcons.dollar,
|
||||
"\$${widget.shift.hourlyRate.toInt()}",
|
||||
"Hourly Rate",
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildStatCard(
|
||||
UiIcons.clock,
|
||||
"${duration.toInt()}",
|
||||
"Hours",
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(12),
|
||||
child: Row(
|
||||
children: [
|
||||
if (status == 'pending' || status == 'open') ...[
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: widget.onDecline,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
side: const BorderSide(color: UiColors.border),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
// In/Out Time
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _buildTimeBox(
|
||||
"CLOCK IN TIME",
|
||||
widget.shift.startTime,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: _buildTimeBox(
|
||||
"CLOCK OUT TIME",
|
||||
widget.shift.endTime,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Location
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"LOCATION",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Text(
|
||||
widget.shift.location.isEmpty
|
||||
? "TBD"
|
||||
: widget.shift.location,
|
||||
style: UiTypography.title1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
OutlinedButton.icon(
|
||||
onPressed: () {
|
||||
// Show snackbar with the address
|
||||
ScaffoldMessenger.of(
|
||||
context,
|
||||
).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
widget.shift.locationAddress,
|
||||
),
|
||||
duration: const Duration(
|
||||
seconds: 3,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
icon: const Icon(
|
||||
UiIcons.navigation,
|
||||
size: 14,
|
||||
),
|
||||
label: const Text(
|
||||
"Get direction",
|
||||
style: TextStyle(fontSize: 12),
|
||||
),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.textPrimary,
|
||||
side: const BorderSide(
|
||||
color: UiColors.border,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
20,
|
||||
),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
vertical: 0,
|
||||
),
|
||||
minimumSize: const Size(0, 32),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
height: 128,
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade100,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: const Center(
|
||||
child: Icon(
|
||||
UiIcons.mapPin,
|
||||
color: UiColors.iconSecondary,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
// Placeholder for Map
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
|
||||
// Additional Info
|
||||
if (widget.shift.description != null) ...[
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
"ADDITIONAL INFO",
|
||||
style: TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
widget.shift.description!.split('.')[0],
|
||||
style: UiTypography.body2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.shift.description!,
|
||||
style: UiTypography.body3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Text(t.staff_shifts.action.decline),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: ElevatedButton(
|
||||
onPressed: widget.onAccept,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 12),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
const SizedBox(height: 24),
|
||||
],
|
||||
|
||||
// Actions
|
||||
if (!widget.historyMode)
|
||||
if (status == 'confirmed')
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: OutlinedButton.icon(
|
||||
onPressed: widget.onRequestSwap,
|
||||
icon: const Icon(
|
||||
UiIcons.swap,
|
||||
size: 16,
|
||||
),
|
||||
label: Text(
|
||||
t.staff_shifts.action.request_swap),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.primary,
|
||||
side: const BorderSide(
|
||||
color: UiColors.primary,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(
|
||||
12,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
else if (status == 'swap')
|
||||
Container(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(
|
||||
0xFFFFFBEB,
|
||||
), // amber-50
|
||||
border: Border.all(
|
||||
color: const Color(0xFFFDE68A),
|
||||
), // amber-200
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.center,
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.swap,
|
||||
size: 16,
|
||||
color: Color(0xFFB45309),
|
||||
), // amber-700
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
t.staff_shifts.status.swap_requested,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
color: Color(0xFFB45309),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
)
|
||||
else
|
||||
Column(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: ElevatedButton(
|
||||
onPressed: widget.onAccept,
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
status == 'pending'
|
||||
? t.staff_shifts.action.confirm
|
||||
: "Book Shift",
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.w600,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (status == 'pending' ||
|
||||
status == 'open') ...[
|
||||
const SizedBox(height: 12),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
height: 48,
|
||||
child: OutlinedButton(
|
||||
onPressed: widget.onDecline,
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor:
|
||||
UiColors.destructive,
|
||||
side: const BorderSide(
|
||||
color: UiColors.border,
|
||||
),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius:
|
||||
BorderRadius.circular(12),
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
t.staff_shifts.action.decline),
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Text(t.staff_shifts.action.confirm),
|
||||
),
|
||||
),
|
||||
] else if (status == 'confirmed') ...[
|
||||
Expanded(
|
||||
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),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
)
|
||||
: const SizedBox.shrink(),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildStatCard(IconData icon, String value, String label) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: UiColors.border),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(icon, size: 20, color: UiColors.iconSecondary),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
value,
|
||||
style: UiTypography.title1m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
label,
|
||||
style: UiTypography.footnote2r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTimeBox(String label, String time) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: const Color(0xFFF8FAFC),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: UiColors.textSecondary,
|
||||
letterSpacing: 0.5,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_formatTime(time),
|
||||
style: UiTypography.display2m.copyWith(
|
||||
fontSize: 20,
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,7 +67,7 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
return Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
borderRadius: UiConstants.radiusLg,
|
||||
border: Border.all(color: UiColors.border),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
@@ -81,64 +81,157 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
children: [
|
||||
// Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 16, 16, 12),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
padding: const EdgeInsets.all(16),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Card content starts directly as per prototype
|
||||
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Logo
|
||||
Container(
|
||||
width: 36,
|
||||
height: 36,
|
||||
width: 44,
|
||||
height: 44,
|
||||
decoration: BoxDecoration(
|
||||
color: UiColors.secondary,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Center(
|
||||
child: Text(
|
||||
shift.clientName.isNotEmpty
|
||||
? shift.clientName[0]
|
||||
: 'K',
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
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: 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),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
shift.title,
|
||||
style: UiTypography.body2b.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
|
||||
// Details
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Text(
|
||||
shift.clientName,
|
||||
style: UiTypography.display3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
const SizedBox(height: 12),
|
||||
|
||||
// Date & Time
|
||||
Row(
|
||||
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,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
Text(
|
||||
"\$${totalPay.toStringAsFixed(0)}",
|
||||
style: UiTypography.display2m.copyWith(
|
||||
color: UiColors.textPrimary,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
"\$${shift.hourlyRate}/hr · ${hours}h",
|
||||
style: UiTypography.display3r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
const SizedBox(height: 4),
|
||||
|
||||
// Location
|
||||
Row(
|
||||
children: [
|
||||
const Icon(
|
||||
UiIcons.mapPin,
|
||||
size: 12,
|
||||
color: UiColors.iconSecondary,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Expanded(
|
||||
child: Text(
|
||||
shift.locationAddress.isNotEmpty
|
||||
? shift.locationAddress
|
||||
: shift.location,
|
||||
style: UiTypography.footnote1r.copyWith(
|
||||
color: UiColors.textSecondary,
|
||||
),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
@@ -147,94 +240,43 @@ class ShiftAssignmentCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
|
||||
// Details
|
||||
Padding(
|
||||
padding: const EdgeInsets.fromLTRB(16, 0, 16, 12),
|
||||
child: Column(
|
||||
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(
|
||||
padding: const EdgeInsets.fromLTRB(16, 8, 16, 16),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: OutlinedButton(
|
||||
onPressed: onDecline,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.destructive,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
style: OutlinedButton.styleFrom(
|
||||
foregroundColor: UiColors.iconSecondary,
|
||||
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),
|
||||
),
|
||||
),
|
||||
Container(width: 1, height: 48, color: UiColors.border),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: TextButton(
|
||||
child: ElevatedButton(
|
||||
onPressed: onConfirm,
|
||||
style: TextButton.styleFrom(
|
||||
foregroundColor: UiColors.primary,
|
||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: UiColors.primary,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
padding: const EdgeInsets.symmetric(vertical: 14),
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
),
|
||||
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_available_shifts_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/pages/shifts_page.dart';
|
||||
import 'presentation/pages/shift_details_page.dart';
|
||||
@@ -18,6 +23,11 @@ class StaffShiftsModule extends Module {
|
||||
i.add(GetMyShiftsUseCase.new);
|
||||
i.add(GetAvailableShiftsUseCase.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
|
||||
i.add(ShiftsBloc.new);
|
||||
|
||||
Reference in New Issue
Block a user