Merge pull request #344 from Oloodi/312-feature-integrate-google-maps-places-autocomplete-for-hub-address-validation
Continuation of the development of the mobile apps
This commit is contained in:
@@ -7,7 +7,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.krow_client"
|
namespace = "com.krowwithus.client"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.krow_client
|
package com.krowwithus.client
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
|
|||||||
set(BINARY_NAME "krow_client")
|
set(BINARY_NAME "krow_client")
|
||||||
# The unique GTK application identifier for this application. See:
|
# The unique GTK application identifier for this application. See:
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
set(APPLICATION_ID "com.example.krow_client")
|
set(APPLICATION_ID "com.krowwithus.client")
|
||||||
|
|
||||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# versions of CMake.
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ plugins {
|
|||||||
}
|
}
|
||||||
|
|
||||||
android {
|
android {
|
||||||
namespace = "com.example.krow_staff"
|
namespace = "com.krowwithus.staff"
|
||||||
compileSdk = flutter.compileSdkVersion
|
compileSdk = flutter.compileSdkVersion
|
||||||
ndkVersion = flutter.ndkVersion
|
ndkVersion = flutter.ndkVersion
|
||||||
|
|
||||||
|
|||||||
@@ -193,6 +193,14 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"oauth_client": [
|
"oauth_client": [
|
||||||
|
{
|
||||||
|
"client_id": "933560802882-ikdfv3o5f47g36qqgvfq55o4m19n7gk4.apps.googleusercontent.com",
|
||||||
|
"client_type": 1,
|
||||||
|
"android_info": {
|
||||||
|
"package_name": "com.krowwithus.staff",
|
||||||
|
"certificate_hash": "ac917ae8470ab29f1107c773c6017ff5ea5d102d"
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
|
"client_id": "933560802882-grp98a1v7amflnnup68vh01tj06eaem1.apps.googleusercontent.com",
|
||||||
"client_type": 3
|
"client_type": 3
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.example.krow_staff
|
package com.krowwithus.staff
|
||||||
|
|
||||||
import io.flutter.embedding.android.FlutterActivity
|
import io.flutter.embedding.android.FlutterActivity
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
|
|||||||
set(BINARY_NAME "krow_staff")
|
set(BINARY_NAME "krow_staff")
|
||||||
# The unique GTK application identifier for this application. See:
|
# The unique GTK application identifier for this application. See:
|
||||||
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
# https://wiki.gnome.org/HowDoI/ChooseApplicationID
|
||||||
set(APPLICATION_ID "com.example.krow_staff")
|
set(APPLICATION_ID "com.krowwithus.staff")
|
||||||
|
|
||||||
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent
|
||||||
# versions of CMake.
|
# versions of CMake.
|
||||||
|
|||||||
@@ -2,3 +2,4 @@ library core;
|
|||||||
|
|
||||||
export 'src/domain/arguments/usecase_argument.dart';
|
export 'src/domain/arguments/usecase_argument.dart';
|
||||||
export 'src/domain/usecases/usecase.dart';
|
export 'src/domain/usecases/usecase.dart';
|
||||||
|
export 'src/utils/date_time_utils.dart';
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
class DateTimeUtils {
|
||||||
|
/// Converts a [DateTime] (assumed UTC if not specified) to the device's local time.
|
||||||
|
static DateTime toDeviceTime(DateTime date) {
|
||||||
|
return date.toLocal();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,6 +24,8 @@ class Shift extends Equatable {
|
|||||||
final double? longitude;
|
final double? longitude;
|
||||||
final String? status;
|
final String? status;
|
||||||
final int? durationDays; // For multi-day shifts
|
final int? durationDays; // For multi-day shifts
|
||||||
|
final int? requiredSlots;
|
||||||
|
final int? filledSlots;
|
||||||
|
|
||||||
const Shift({
|
const Shift({
|
||||||
required this.id,
|
required this.id,
|
||||||
@@ -49,6 +51,8 @@ class Shift extends Equatable {
|
|||||||
this.longitude,
|
this.longitude,
|
||||||
this.status,
|
this.status,
|
||||||
this.durationDays,
|
this.durationDays,
|
||||||
|
this.requiredSlots,
|
||||||
|
this.filledSlots,
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -76,6 +80,8 @@ class Shift extends Equatable {
|
|||||||
longitude,
|
longitude,
|
||||||
status,
|
status,
|
||||||
durationDays,
|
durationDays,
|
||||||
|
requiredSlots,
|
||||||
|
filledSlots,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import 'dart:developer' as developer;
|
|||||||
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
import 'package:firebase_auth/firebase_auth.dart' as firebase;
|
||||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||||
import 'package:krow_domain/krow_domain.dart' as domain;
|
|
||||||
import 'package:krow_domain/krow_domain.dart'
|
import 'package:krow_domain/krow_domain.dart'
|
||||||
show
|
show
|
||||||
InvalidCredentialsException,
|
InvalidCredentialsException,
|
||||||
@@ -15,6 +14,7 @@ import 'package:krow_domain/krow_domain.dart'
|
|||||||
UnauthorizedAppException,
|
UnauthorizedAppException,
|
||||||
PasswordMismatchException,
|
PasswordMismatchException,
|
||||||
GoogleOnlyAccountException;
|
GoogleOnlyAccountException;
|
||||||
|
import 'package:krow_domain/krow_domain.dart' as domain;
|
||||||
|
|
||||||
import '../../domain/repositories/auth_repository_interface.dart';
|
import '../../domain/repositories/auth_repository_interface.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import 'package:firebase_auth/firebase_auth.dart' as auth;
|
||||||
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
|
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../../domain/repositories/profile_setup_repository.dart';
|
||||||
|
|
||||||
|
class ProfileSetupRepositoryImpl implements ProfileSetupRepository {
|
||||||
|
final auth.FirebaseAuth _firebaseAuth;
|
||||||
|
final ExampleConnector _dataConnect;
|
||||||
|
|
||||||
|
ProfileSetupRepositoryImpl({
|
||||||
|
required auth.FirebaseAuth firebaseAuth,
|
||||||
|
required ExampleConnector dataConnect,
|
||||||
|
}) : _firebaseAuth = firebaseAuth,
|
||||||
|
_dataConnect = dataConnect;
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<void> submitProfile({
|
||||||
|
required String fullName,
|
||||||
|
String? bio,
|
||||||
|
required List<String> preferredLocations,
|
||||||
|
required double maxDistanceMiles,
|
||||||
|
required List<String> industries,
|
||||||
|
required List<String> skills,
|
||||||
|
}) async {
|
||||||
|
final auth.User? firebaseUser = _firebaseAuth.currentUser;
|
||||||
|
if (firebaseUser == null) {
|
||||||
|
throw Exception('User not authenticated.');
|
||||||
|
}
|
||||||
|
|
||||||
|
final StaffSession? session = StaffSessionStore.instance.session;
|
||||||
|
final String email = session?.user.email ?? '';
|
||||||
|
final String? phone = firebaseUser.phoneNumber;
|
||||||
|
|
||||||
|
final fdc.OperationResult<CreateStaffData, CreateStaffVariables>
|
||||||
|
result = await _dataConnect
|
||||||
|
.createStaff(
|
||||||
|
userId: firebaseUser.uid,
|
||||||
|
fullName: fullName,
|
||||||
|
)
|
||||||
|
.bio(bio)
|
||||||
|
.preferredLocations(preferredLocations)
|
||||||
|
.maxDistanceMiles(maxDistanceMiles.toInt())
|
||||||
|
.industries(industries)
|
||||||
|
.skills(skills)
|
||||||
|
.email(email.isEmpty ? null : email)
|
||||||
|
.phone(phone)
|
||||||
|
.execute();
|
||||||
|
|
||||||
|
final String staffId = result.data.staff_insert.id;
|
||||||
|
|
||||||
|
final Staff staff = Staff(
|
||||||
|
id: staffId,
|
||||||
|
authProviderId: firebaseUser.uid,
|
||||||
|
name: fullName,
|
||||||
|
email: email,
|
||||||
|
phone: phone,
|
||||||
|
status: StaffStatus.completedProfile,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (session != null) {
|
||||||
|
StaffSessionStore.instance.setSession(
|
||||||
|
StaffSession(user: session.user, staff: staff),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
abstract class ProfileSetupRepository {
|
||||||
|
Future<void> submitProfile({
|
||||||
|
required String fullName,
|
||||||
|
String? bio,
|
||||||
|
required List<String> preferredLocations,
|
||||||
|
required double maxDistanceMiles,
|
||||||
|
required List<String> industries,
|
||||||
|
required List<String> skills,
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import '../repositories/profile_setup_repository.dart';
|
||||||
|
|
||||||
|
class SubmitProfileSetup {
|
||||||
|
final ProfileSetupRepository repository;
|
||||||
|
|
||||||
|
SubmitProfileSetup(this.repository);
|
||||||
|
|
||||||
|
Future<void> call({
|
||||||
|
required String fullName,
|
||||||
|
String? bio,
|
||||||
|
required List<String> preferredLocations,
|
||||||
|
required double maxDistanceMiles,
|
||||||
|
required List<String> industries,
|
||||||
|
required List<String> skills,
|
||||||
|
}) {
|
||||||
|
return repository.submitProfile(
|
||||||
|
fullName: fullName,
|
||||||
|
bio: bio,
|
||||||
|
preferredLocations: preferredLocations,
|
||||||
|
maxDistanceMiles: maxDistanceMiles,
|
||||||
|
industries: industries,
|
||||||
|
skills: skills,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,5 @@
|
|||||||
import 'package:firebase_auth/firebase_auth.dart' as auth;
|
|
||||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
import '../../../domain/usecases/submit_profile_setup_usecase.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
|
||||||
|
|
||||||
import 'profile_setup_event.dart';
|
import 'profile_setup_event.dart';
|
||||||
import 'profile_setup_state.dart';
|
import 'profile_setup_state.dart';
|
||||||
@@ -13,10 +10,8 @@ export 'profile_setup_state.dart';
|
|||||||
/// BLoC responsible for managing the profile setup state and logic.
|
/// BLoC responsible for managing the profile setup state and logic.
|
||||||
class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
||||||
ProfileSetupBloc({
|
ProfileSetupBloc({
|
||||||
required auth.FirebaseAuth firebaseAuth,
|
required SubmitProfileSetup submitProfileSetup,
|
||||||
required dc.ExampleConnector dataConnect,
|
}) : _submitProfileSetup = submitProfileSetup,
|
||||||
}) : _firebaseAuth = firebaseAuth,
|
|
||||||
_dataConnect = dataConnect,
|
|
||||||
super(const ProfileSetupState()) {
|
super(const ProfileSetupState()) {
|
||||||
on<ProfileSetupFullNameChanged>(_onFullNameChanged);
|
on<ProfileSetupFullNameChanged>(_onFullNameChanged);
|
||||||
on<ProfileSetupBioChanged>(_onBioChanged);
|
on<ProfileSetupBioChanged>(_onBioChanged);
|
||||||
@@ -27,8 +22,7 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
|||||||
on<ProfileSetupSubmitted>(_onSubmitted);
|
on<ProfileSetupSubmitted>(_onSubmitted);
|
||||||
}
|
}
|
||||||
|
|
||||||
final auth.FirebaseAuth _firebaseAuth;
|
final SubmitProfileSetup _submitProfileSetup;
|
||||||
final dc.ExampleConnector _dataConnect;
|
|
||||||
|
|
||||||
/// Handles the [ProfileSetupFullNameChanged] event.
|
/// Handles the [ProfileSetupFullNameChanged] event.
|
||||||
void _onFullNameChanged(
|
void _onFullNameChanged(
|
||||||
@@ -86,44 +80,14 @@ class ProfileSetupBloc extends Bloc<ProfileSetupEvent, ProfileSetupState> {
|
|||||||
emit(state.copyWith(status: ProfileSetupStatus.loading));
|
emit(state.copyWith(status: ProfileSetupStatus.loading));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
final auth.User? firebaseUser = _firebaseAuth.currentUser;
|
await _submitProfileSetup(
|
||||||
if (firebaseUser == null) {
|
fullName: state.fullName,
|
||||||
throw Exception('User not authenticated.');
|
bio: state.bio.isEmpty ? null : state.bio,
|
||||||
}
|
preferredLocations: state.preferredLocations,
|
||||||
|
maxDistanceMiles: state.maxDistanceMiles,
|
||||||
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
|
industries: state.industries,
|
||||||
final String email = session?.user.email ?? '';
|
skills: state.skills,
|
||||||
final String? phone = firebaseUser.phoneNumber;
|
|
||||||
|
|
||||||
final fdc.OperationResult<dc.CreateStaffData, dc.CreateStaffVariables>
|
|
||||||
result = await _dataConnect
|
|
||||||
.createStaff(
|
|
||||||
userId: firebaseUser.uid,
|
|
||||||
fullName: state.fullName,
|
|
||||||
)
|
|
||||||
.bio(state.bio.isEmpty ? null : state.bio)
|
|
||||||
.preferredLocations(state.preferredLocations)
|
|
||||||
.maxDistanceMiles(state.maxDistanceMiles.toInt())
|
|
||||||
.industries(state.industries)
|
|
||||||
.skills(state.skills)
|
|
||||||
.email(email.isEmpty ? null : email)
|
|
||||||
.phone(phone)
|
|
||||||
.execute();
|
|
||||||
|
|
||||||
final String staffId = result.data.staff_insert.id ;
|
|
||||||
final Staff staff = Staff(
|
|
||||||
id: staffId,
|
|
||||||
authProviderId: firebaseUser.uid,
|
|
||||||
name: state.fullName,
|
|
||||||
email: email,
|
|
||||||
phone: phone,
|
|
||||||
status: StaffStatus.completedProfile,
|
|
||||||
);
|
);
|
||||||
if (session != null) {
|
|
||||||
dc.StaffSessionStore.instance.setSession(
|
|
||||||
dc.StaffSession(user: session.user, staff: staff),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
emit(state.copyWith(status: ProfileSetupStatus.success));
|
emit(state.copyWith(status: ProfileSetupStatus.success));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
|
||||||
class GetStartedActions extends StatelessWidget {
|
class GetStartedActions extends StatelessWidget {
|
||||||
final VoidCallback onSignUpPressed;
|
final VoidCallback onSignUpPressed;
|
||||||
@@ -13,40 +14,15 @@ class GetStartedActions extends StatelessWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsStaffAuthenticationGetStartedPageEn i18n =
|
||||||
|
t.staff_authentication.get_started_page;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
spacing: UiConstants.space4,
|
||||||
ElevatedButton(
|
children: <Widget>[
|
||||||
onPressed: onSignUpPressed,
|
UiButton.primary(onPressed: onSignUpPressed, text: i18n.sign_up_button),
|
||||||
style: ElevatedButton.styleFrom(
|
UiButton.secondary(onPressed: onLoginPressed, text: i18n.log_in_button),
|
||||||
backgroundColor: UiColors.primary,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
'Create Account',
|
|
||||||
style: UiTypography.buttonL.copyWith(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
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,26 +1,42 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:staff_authentication/staff_authentication.dart';
|
||||||
|
|
||||||
|
/// A widget that displays the welcome text and description on the Get Started page.
|
||||||
class GetStartedHeader extends StatelessWidget {
|
class GetStartedHeader extends StatelessWidget {
|
||||||
|
/// Creates a [GetStartedHeader].
|
||||||
const GetStartedHeader({super.key});
|
const GetStartedHeader({super.key});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
final TranslationsStaffAuthenticationGetStartedPageEn i18n =
|
||||||
|
t.staff_authentication.get_started_page;
|
||||||
|
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: <Widget>[
|
||||||
Text(
|
RichText(
|
||||||
'Krow Workforce',
|
|
||||||
style: UiTypography.display1b.copyWith(color: UiColors.textPrimary),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
text: TextSpan(
|
||||||
|
style: UiTypography.displayM,
|
||||||
|
children: <InlineSpan>[
|
||||||
|
TextSpan(
|
||||||
|
text: i18n.title_part1,
|
||||||
|
),
|
||||||
|
TextSpan(
|
||||||
|
text: i18n.title_part2,
|
||||||
|
style: UiTypography.displayMb.textLink,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Text(
|
Text(
|
||||||
'Find flexible shifts that fit your schedule.',
|
i18n.subtitle,
|
||||||
style: UiTypography.body1r.copyWith(color: UiColors.textSecondary),
|
|
||||||
textAlign: TextAlign.center,
|
textAlign: TextAlign.center,
|
||||||
|
style: UiTypography.body1r.textSecondary,
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,9 @@ import 'package:staff_authentication/src/data/repositories_impl/auth_repository_
|
|||||||
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
|
import 'package:staff_authentication/src/domain/repositories/auth_repository_interface.dart';
|
||||||
import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart';
|
import 'package:staff_authentication/src/domain/usecases/sign_in_with_phone_usecase.dart';
|
||||||
import 'package:staff_authentication/src/domain/usecases/verify_otp_usecase.dart';
|
import 'package:staff_authentication/src/domain/usecases/verify_otp_usecase.dart';
|
||||||
|
import 'package:staff_authentication/src/domain/repositories/profile_setup_repository.dart';
|
||||||
|
import 'package:staff_authentication/src/data/repositories_impl/profile_setup_repository_impl.dart';
|
||||||
|
import 'package:staff_authentication/src/domain/usecases/submit_profile_setup_usecase.dart';
|
||||||
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
|
import 'package:staff_authentication/src/presentation/blocs/auth_bloc.dart';
|
||||||
import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
|
import 'package:staff_authentication/src/presentation/blocs/profile_setup/profile_setup_bloc.dart';
|
||||||
import 'package:staff_authentication/src/presentation/pages/get_started_page.dart';
|
import 'package:staff_authentication/src/presentation/pages/get_started_page.dart';
|
||||||
@@ -35,10 +38,17 @@ class StaffAuthenticationModule extends Module {
|
|||||||
dataConnect: ExampleConnector.instance,
|
dataConnect: ExampleConnector.instance,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
i.addLazySingleton<ProfileSetupRepository>(
|
||||||
|
() => ProfileSetupRepositoryImpl(
|
||||||
|
firebaseAuth: firebase.FirebaseAuth.instance,
|
||||||
|
dataConnect: ExampleConnector.instance,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// UseCases
|
// UseCases
|
||||||
i.addLazySingleton(SignInWithPhoneUseCase.new);
|
i.addLazySingleton(SignInWithPhoneUseCase.new);
|
||||||
i.addLazySingleton(VerifyOtpUseCase.new);
|
i.addLazySingleton(VerifyOtpUseCase.new);
|
||||||
|
i.addLazySingleton(SubmitProfileSetup.new);
|
||||||
|
|
||||||
// BLoCs
|
// BLoCs
|
||||||
i.addLazySingleton<AuthBloc>(
|
i.addLazySingleton<AuthBloc>(
|
||||||
@@ -49,8 +59,7 @@ class StaffAuthenticationModule extends Module {
|
|||||||
);
|
);
|
||||||
i.add<ProfileSetupBloc>(
|
i.add<ProfileSetupBloc>(
|
||||||
() => ProfileSetupBloc(
|
() => ProfileSetupBloc(
|
||||||
firebaseAuth: firebase.FirebaseAuth.instance,
|
submitProfileSetup: i.get<SubmitProfileSetup>(),
|
||||||
dataConnect: ExampleConnector.instance,
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||||
import 'package:staff_home/src/domain/entities/shift.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||||
|
|
||||||
extension TimestampExt on Timestamp {
|
extension TimestampExt on Timestamp {
|
||||||
DateTime toDate() {
|
DateTime toDate() {
|
||||||
return DateTime.fromMillisecondsSinceEpoch(seconds.toInt() * 1000 + nanoseconds ~/ 1000000);
|
return DateTimeUtils.toDeviceTime(toDateTime());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class HomeRepositoryImpl implements HomeRepository {
|
class HomeRepositoryImpl implements HomeRepository {
|
||||||
HomeRepositoryImpl();
|
HomeRepositoryImpl();
|
||||||
|
|
||||||
@@ -63,7 +63,15 @@ class HomeRepositoryImpl implements HomeRepository {
|
|||||||
final response = await ExampleConnector.instance.listShifts().execute();
|
final response = await ExampleConnector.instance.listShifts().execute();
|
||||||
|
|
||||||
return response.data.shifts
|
return response.data.shifts
|
||||||
.where((s) => s.status is Known && (s.status as Known).value == ShiftStatus.OPEN)
|
.where((s) {
|
||||||
|
final isOpen = s.status is Known && (s.status as Known).value == ShiftStatus.OPEN;
|
||||||
|
if (!isOpen) return false;
|
||||||
|
|
||||||
|
final start = s.startTime?.toDate();
|
||||||
|
if (start == null) return false;
|
||||||
|
|
||||||
|
return start.isAfter(DateTime.now());
|
||||||
|
})
|
||||||
.take(10)
|
.take(10)
|
||||||
.map((s) => _mapConnectorShiftToDomain(s))
|
.map((s) => _mapConnectorShiftToDomain(s))
|
||||||
.toList();
|
.toList();
|
||||||
@@ -72,6 +80,12 @@ class HomeRepositoryImpl implements HomeRepository {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Future<String?> getStaffName() async {
|
||||||
|
final session = StaffSessionStore.instance.session;
|
||||||
|
return session?.staff?.name;
|
||||||
|
}
|
||||||
|
|
||||||
// Mappers specific to Home's Domain Entity 'Shift'
|
// Mappers specific to Home's Domain Entity 'Shift'
|
||||||
// Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift.
|
// Note: Home's 'Shift' entity might differ slightly from 'StaffPayment' Shift.
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:staff_home/src/domain/entities/shift.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Repository interface for home screen data operations.
|
/// Repository interface for home screen data operations.
|
||||||
///
|
///
|
||||||
@@ -14,4 +14,7 @@ abstract class HomeRepository {
|
|||||||
|
|
||||||
/// Retrieves shifts recommended for the worker based on their profile.
|
/// Retrieves shifts recommended for the worker based on their profile.
|
||||||
Future<List<Shift>> getRecommendedShifts();
|
Future<List<Shift>> getRecommendedShifts();
|
||||||
|
|
||||||
|
/// Retrieves the current staff member's name.
|
||||||
|
Future<String?> getStaffName();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import 'package:staff_home/src/domain/entities/shift.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||||
|
|
||||||
/// Use case for fetching all shifts displayed on the home screen.
|
/// Use case for fetching all shifts displayed on the home screen.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import 'package:bloc/bloc.dart';
|
import 'package:bloc/bloc.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
import 'package:staff_home/src/domain/entities/shift.dart';
|
|
||||||
import 'package:staff_home/src/domain/usecases/get_home_shifts.dart';
|
import 'package:staff_home/src/domain/usecases/get_home_shifts.dart';
|
||||||
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
import 'package:staff_home/src/domain/repositories/home_repository.dart';
|
||||||
|
|
||||||
@@ -10,9 +10,11 @@ part 'home_state.dart';
|
|||||||
/// Simple Cubit to manage home page state (shifts + loading/error).
|
/// Simple Cubit to manage home page state (shifts + loading/error).
|
||||||
class HomeCubit extends Cubit<HomeState> {
|
class HomeCubit extends Cubit<HomeState> {
|
||||||
final GetHomeShifts _getHomeShifts;
|
final GetHomeShifts _getHomeShifts;
|
||||||
|
final HomeRepository _repository;
|
||||||
|
|
||||||
HomeCubit(HomeRepository repository)
|
HomeCubit(HomeRepository repository)
|
||||||
: _getHomeShifts = GetHomeShifts(repository),
|
: _getHomeShifts = GetHomeShifts(repository),
|
||||||
|
_repository = repository,
|
||||||
super(const HomeState.initial());
|
super(const HomeState.initial());
|
||||||
|
|
||||||
Future<void> loadShifts() async {
|
Future<void> loadShifts() async {
|
||||||
@@ -20,6 +22,7 @@ class HomeCubit extends Cubit<HomeState> {
|
|||||||
emit(state.copyWith(status: HomeStatus.loading));
|
emit(state.copyWith(status: HomeStatus.loading));
|
||||||
try {
|
try {
|
||||||
final result = await _getHomeShifts.call();
|
final result = await _getHomeShifts.call();
|
||||||
|
final name = await _repository.getStaffName();
|
||||||
if (isClosed) return;
|
if (isClosed) return;
|
||||||
emit(
|
emit(
|
||||||
state.copyWith(
|
state.copyWith(
|
||||||
@@ -27,6 +30,7 @@ class HomeCubit extends Cubit<HomeState> {
|
|||||||
todayShifts: result.today,
|
todayShifts: result.today,
|
||||||
tomorrowShifts: result.tomorrow,
|
tomorrowShifts: result.tomorrow,
|
||||||
recommendedShifts: result.recommended,
|
recommendedShifts: result.recommended,
|
||||||
|
staffName: name,
|
||||||
// Mock profile status for now, ideally fetched from a user repository
|
// Mock profile status for now, ideally fetched from a user repository
|
||||||
isProfileComplete: false,
|
isProfileComplete: false,
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class HomeState extends Equatable {
|
|||||||
final List<Shift> recommendedShifts;
|
final List<Shift> recommendedShifts;
|
||||||
final bool autoMatchEnabled;
|
final bool autoMatchEnabled;
|
||||||
final bool isProfileComplete;
|
final bool isProfileComplete;
|
||||||
|
final String? staffName;
|
||||||
final String? errorMessage;
|
final String? errorMessage;
|
||||||
|
|
||||||
const HomeState({
|
const HomeState({
|
||||||
@@ -18,6 +19,7 @@ class HomeState extends Equatable {
|
|||||||
this.recommendedShifts = const [],
|
this.recommendedShifts = const [],
|
||||||
this.autoMatchEnabled = false,
|
this.autoMatchEnabled = false,
|
||||||
this.isProfileComplete = false,
|
this.isProfileComplete = false,
|
||||||
|
this.staffName,
|
||||||
this.errorMessage,
|
this.errorMessage,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -30,6 +32,7 @@ class HomeState extends Equatable {
|
|||||||
List<Shift>? recommendedShifts,
|
List<Shift>? recommendedShifts,
|
||||||
bool? autoMatchEnabled,
|
bool? autoMatchEnabled,
|
||||||
bool? isProfileComplete,
|
bool? isProfileComplete,
|
||||||
|
String? staffName,
|
||||||
String? errorMessage,
|
String? errorMessage,
|
||||||
}) {
|
}) {
|
||||||
return HomeState(
|
return HomeState(
|
||||||
@@ -39,18 +42,20 @@ class HomeState extends Equatable {
|
|||||||
recommendedShifts: recommendedShifts ?? this.recommendedShifts,
|
recommendedShifts: recommendedShifts ?? this.recommendedShifts,
|
||||||
autoMatchEnabled: autoMatchEnabled ?? this.autoMatchEnabled,
|
autoMatchEnabled: autoMatchEnabled ?? this.autoMatchEnabled,
|
||||||
isProfileComplete: isProfileComplete ?? this.isProfileComplete,
|
isProfileComplete: isProfileComplete ?? this.isProfileComplete,
|
||||||
|
staffName: staffName ?? this.staffName,
|
||||||
errorMessage: errorMessage ?? this.errorMessage,
|
errorMessage: errorMessage ?? this.errorMessage,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
List<Object?> get props => [
|
List<Object?> get props => [
|
||||||
status,
|
status,
|
||||||
todayShifts,
|
todayShifts,
|
||||||
tomorrowShifts,
|
tomorrowShifts,
|
||||||
recommendedShifts,
|
recommendedShifts,
|
||||||
autoMatchEnabled,
|
autoMatchEnabled,
|
||||||
isProfileComplete,
|
isProfileComplete,
|
||||||
errorMessage,
|
staffName,
|
||||||
];
|
errorMessage,
|
||||||
}
|
];
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
/// Extension on [IModularNavigator] providing typed navigation helpers
|
/// Extension on [IModularNavigator] providing typed navigation helpers
|
||||||
/// for the Staff Home feature (worker home screen).
|
/// for the Staff Home feature (worker home screen).
|
||||||
@@ -40,4 +41,9 @@ extension HomeNavigator on IModularNavigator {
|
|||||||
void pushSettings() {
|
void pushSettings() {
|
||||||
pushNamed('/settings');
|
pushNamed('/settings');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Navigates to the shift details page for the given [shift].
|
||||||
|
void pushShiftDetails(Shift shift) {
|
||||||
|
pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,12 @@ class WorkerHomePage extends StatelessWidget {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
const HomeHeader(),
|
BlocBuilder<HomeCubit, HomeState>(
|
||||||
|
buildWhen: (previous, current) => previous.staffName != current.staffName,
|
||||||
|
builder: (context, state) {
|
||||||
|
return HomeHeader(userName: state.staffName);
|
||||||
|
},
|
||||||
|
),
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
|
padding: const EdgeInsets.symmetric(horizontal: UiConstants.space4),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
|||||||
@@ -2,15 +2,21 @@ import 'package:core_localization/core_localization.dart';
|
|||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
|
||||||
/// Header widget for the staff home page, using design system tokens.
|
/// Header widget for the staff home page, using design system tokens.
|
||||||
class HomeHeader extends StatelessWidget {
|
class HomeHeader extends StatelessWidget {
|
||||||
|
final String? userName;
|
||||||
|
|
||||||
/// Creates a [HomeHeader].
|
/// Creates a [HomeHeader].
|
||||||
const HomeHeader({super.key});
|
const HomeHeader({super.key, this.userName});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final headerI18n = t.staff.home.header;
|
final headerI18n = t.staff.home.header;
|
||||||
|
final nameToDisplay = userName ?? headerI18n.user_name_placeholder;
|
||||||
|
final initial = nameToDisplay.isNotEmpty
|
||||||
|
? nameToDisplay[0].toUpperCase()
|
||||||
|
: 'K';
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: EdgeInsets.fromLTRB(
|
padding: EdgeInsets.fromLTRB(
|
||||||
UiConstants.space4,
|
UiConstants.space4,
|
||||||
@@ -18,45 +24,42 @@ class HomeHeader extends StatelessWidget {
|
|||||||
UiConstants.space4,
|
UiConstants.space4,
|
||||||
UiConstants.space3,
|
UiConstants.space3,
|
||||||
),
|
),
|
||||||
child:Row(
|
child: Row(
|
||||||
|
spacing: UiConstants.space3,
|
||||||
|
children: [
|
||||||
|
Container(
|
||||||
|
width: 48,
|
||||||
|
height: 48,
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
border: Border.all(
|
||||||
|
color: UiColors.primary.withOpacity(0.2),
|
||||||
|
width: 2,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: CircleAvatar(
|
||||||
|
backgroundColor: UiColors.primary.withOpacity(0.1),
|
||||||
|
child: Text(
|
||||||
|
initial,
|
||||||
|
style: const TextStyle(
|
||||||
|
color: UiColors.primary,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
Text(
|
||||||
width: 48,
|
headerI18n.welcome_back,
|
||||||
height: 48,
|
style: UiTypography.body3r.textSecondary,
|
||||||
decoration: BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
border: Border.all(
|
|
||||||
color: UiColors.primary.withOpacity(0.2),
|
|
||||||
width: 2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: CircleAvatar(
|
|
||||||
backgroundColor: UiColors.primary.withOpacity(0.1),
|
|
||||||
child: const Text(
|
|
||||||
'K',
|
|
||||||
style: TextStyle(
|
|
||||||
color: UiColors.primary,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: UiConstants.space3),
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
headerI18n.welcome_back,
|
|
||||||
style: UiTypography.body3r.copyWith(color: UiColors.mutedForeground),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
headerI18n.user_name_placeholder,
|
|
||||||
style: UiTypography.headline4m,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
|
Text(nameToDisplay, style: UiTypography.headline4m),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import 'package:flutter/material.dart';
|
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
|
||||||
|
|
||||||
import 'package:design_system/design_system.dart';
|
|
||||||
import 'package:staff_home/src/domain/entities/shift.dart';
|
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
import 'package:staff_home/src/presentation/navigation/home_navigator.dart';
|
||||||
|
|
||||||
class RecommendedShiftCard extends StatelessWidget {
|
class RecommendedShiftCard extends StatelessWidget {
|
||||||
final Shift shift;
|
final Shift shift;
|
||||||
@@ -18,13 +19,7 @@ class RecommendedShiftCard extends StatelessWidget {
|
|||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
Modular.to.pushShiftDetails(shift);
|
||||||
SnackBar(
|
|
||||||
content: Text(recI18n.applied_for(title: shift.title)),
|
|
||||||
backgroundColor: Colors.green,
|
|
||||||
duration: const Duration(seconds: 2),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
width: 300,
|
width: 300,
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import 'package:lucide_icons/lucide_icons.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:staff_home/src/domain/entities/shift.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
class ShiftCard extends StatefulWidget {
|
class ShiftCard extends StatefulWidget {
|
||||||
final Shift shift;
|
final Shift shift;
|
||||||
|
|||||||
@@ -28,6 +28,8 @@ dependencies:
|
|||||||
path: ../../../core
|
path: ../../../core
|
||||||
krow_domain:
|
krow_domain:
|
||||||
path: ../../../domain
|
path: ../../../domain
|
||||||
|
staff_shifts:
|
||||||
|
path: ../shifts
|
||||||
krow_data_connect:
|
krow_data_connect:
|
||||||
path: ../../../data_connect
|
path: ../../../data_connect
|
||||||
firebase_data_connect:
|
firebase_data_connect:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:firebase_auth/firebase_auth.dart';
|
import 'package:firebase_auth/firebase_auth.dart';
|
||||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||||
|
import 'package:krow_core/core.dart';
|
||||||
import '../../domain/repositories/shifts_repository_interface.dart';
|
import '../../domain/repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||||
@@ -51,27 +52,26 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
|
|
||||||
DateTime? _toDateTime(dynamic t) {
|
DateTime? _toDateTime(dynamic t) {
|
||||||
if (t == null) return null;
|
if (t == null) return null;
|
||||||
if (t is DateTime) return t;
|
DateTime? dt;
|
||||||
if (t is String) return DateTime.tryParse(t);
|
if (t is Timestamp) {
|
||||||
|
dt = t.toDateTime();
|
||||||
// Data Connect Timestamp handling
|
} else if (t is String) {
|
||||||
try {
|
dt = DateTime.tryParse(t);
|
||||||
if (t is Timestamp) {
|
} else {
|
||||||
return t.toDateTime();
|
try {
|
||||||
|
dt = DateTime.tryParse(t.toJson() as String);
|
||||||
|
} catch (_) {
|
||||||
|
try {
|
||||||
|
dt = DateTime.tryParse(t.toString());
|
||||||
|
} catch (e) {
|
||||||
|
dt = null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} 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 (_) {}
|
|
||||||
|
|
||||||
|
if (dt != null) {
|
||||||
|
return DateTimeUtils.toDeviceTime(dt);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,6 +127,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
status: _mapStatus(status),
|
status: _mapStatus(status),
|
||||||
description: shift.description,
|
description: shift.description,
|
||||||
durationDays: shift.durationDays,
|
durationDays: shift.durationDays,
|
||||||
|
requiredSlots: shift.requiredSlots,
|
||||||
|
filledSlots: shift.filledSlots,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -182,6 +184,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
status: s.status?.stringValue.toLowerCase() ?? 'open',
|
status: s.status?.stringValue.toLowerCase() ?? 'open',
|
||||||
description: s.description,
|
description: s.description,
|
||||||
durationDays: s.durationDays,
|
durationDays: s.durationDays,
|
||||||
|
requiredSlots: null, // Basic list doesn't fetch detailed role stats yet
|
||||||
|
filledSlots: null,
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -210,6 +214,20 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
final s = result.data.shift;
|
final s = result.data.shift;
|
||||||
if (s == null) return null;
|
if (s == null) return null;
|
||||||
|
|
||||||
|
int? required;
|
||||||
|
int? filled;
|
||||||
|
try {
|
||||||
|
final rolesRes = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
|
||||||
|
if (rolesRes.data.shiftRoles.isNotEmpty) {
|
||||||
|
required = 0;
|
||||||
|
filled = 0;
|
||||||
|
for(var r in rolesRes.data.shiftRoles) {
|
||||||
|
required = (required ?? 0) + r.count;
|
||||||
|
filled = (filled ?? 0) + (r.assigned ?? 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_) {}
|
||||||
|
|
||||||
final startDt = _toDateTime(s.startTime);
|
final startDt = _toDateTime(s.startTime);
|
||||||
final endDt = _toDateTime(s.endTime);
|
final endDt = _toDateTime(s.endTime);
|
||||||
final createdDt = _toDateTime(s.createdAt);
|
final createdDt = _toDateTime(s.createdAt);
|
||||||
@@ -229,6 +247,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
status: s.status?.stringValue ?? 'OPEN',
|
status: s.status?.stringValue ?? 'OPEN',
|
||||||
description: s.description,
|
description: s.description,
|
||||||
durationDays: s.durationDays,
|
durationDays: s.durationDays,
|
||||||
|
requiredSlots: required,
|
||||||
|
filledSlots: filled,
|
||||||
);
|
);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
@@ -236,7 +256,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Future<void> applyForShift(String shiftId) async {
|
Future<void> applyForShift(String shiftId, {bool isInstantBook = false}) async {
|
||||||
final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
|
final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
|
||||||
if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift');
|
if (rolesResult.data.shiftRoles.isEmpty) throw Exception('No open roles for this shift');
|
||||||
|
|
||||||
@@ -246,8 +266,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
await _dataConnect.createApplication(
|
await _dataConnect.createApplication(
|
||||||
shiftId: shiftId,
|
shiftId: shiftId,
|
||||||
staffId: staffId,
|
staffId: staffId,
|
||||||
roleId: role.id,
|
roleId: role.roleId,
|
||||||
status: dc.ApplicationStatus.PENDING,
|
status: isInstantBook ? dc.ApplicationStatus.ACCEPTED : dc.ApplicationStatus.PENDING,
|
||||||
origin: dc.ApplicationOrigin.STAFF,
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
).execute();
|
).execute();
|
||||||
}
|
}
|
||||||
@@ -286,6 +306,22 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (appId == null || roleId == null) {
|
if (appId == null || roleId == null) {
|
||||||
|
// If we are rejecting and can't find an application, create one as rejected (declining an available shift)
|
||||||
|
if (newStatus == dc.ApplicationStatus.REJECTED) {
|
||||||
|
final rolesResult = await _dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute();
|
||||||
|
if (rolesResult.data.shiftRoles.isNotEmpty) {
|
||||||
|
final role = rolesResult.data.shiftRoles.first;
|
||||||
|
final staffId = await _getStaffId();
|
||||||
|
await _dataConnect.createApplication(
|
||||||
|
shiftId: shiftId,
|
||||||
|
staffId: staffId,
|
||||||
|
roleId: role.id,
|
||||||
|
status: dc.ApplicationStatus.REJECTED,
|
||||||
|
origin: dc.ApplicationOrigin.STAFF,
|
||||||
|
).execute();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
throw Exception("Application not found for shift $shiftId");
|
throw Exception("Application not found for shift $shiftId");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,9 @@ abstract interface class ShiftsRepositoryInterface {
|
|||||||
Future<Shift?> getShiftDetails(String shiftId);
|
Future<Shift?> getShiftDetails(String shiftId);
|
||||||
|
|
||||||
/// Applies for a specific open shift.
|
/// Applies for a specific open shift.
|
||||||
Future<void> applyForShift(String shiftId);
|
///
|
||||||
|
/// [isInstantBook] determines if the application should be immediately accepted.
|
||||||
|
Future<void> applyForShift(String shiftId, {bool isInstantBook = false});
|
||||||
|
|
||||||
/// Accepts a pending shift assignment.
|
/// Accepts a pending shift assignment.
|
||||||
Future<void> acceptShift(String shiftId);
|
Future<void> acceptShift(String shiftId);
|
||||||
|
|||||||
@@ -0,0 +1,11 @@
|
|||||||
|
import '../repositories/shifts_repository_interface.dart';
|
||||||
|
|
||||||
|
class ApplyForShiftUseCase {
|
||||||
|
final ShiftsRepositoryInterface repository;
|
||||||
|
|
||||||
|
ApplyForShiftUseCase(this.repository);
|
||||||
|
|
||||||
|
Future<void> call(String shiftId, {bool isInstantBook = false}) async {
|
||||||
|
return repository.applyForShift(shiftId, isInstantBook: isInstantBook);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
import 'package:bloc/bloc.dart';
|
||||||
|
import '../../../domain/usecases/apply_for_shift_usecase.dart';
|
||||||
|
import '../../../domain/usecases/decline_shift_usecase.dart';
|
||||||
|
import '../../../domain/usecases/get_shift_details_usecase.dart';
|
||||||
|
import 'shift_details_event.dart';
|
||||||
|
import 'shift_details_state.dart';
|
||||||
|
|
||||||
|
class ShiftDetailsBloc extends Bloc<ShiftDetailsEvent, ShiftDetailsState> {
|
||||||
|
final GetShiftDetailsUseCase getShiftDetails;
|
||||||
|
final ApplyForShiftUseCase applyForShift;
|
||||||
|
final DeclineShiftUseCase declineShift;
|
||||||
|
|
||||||
|
ShiftDetailsBloc({
|
||||||
|
required this.getShiftDetails,
|
||||||
|
required this.applyForShift,
|
||||||
|
required this.declineShift,
|
||||||
|
}) : super(ShiftDetailsInitial()) {
|
||||||
|
on<LoadShiftDetailsEvent>(_onLoadDetails);
|
||||||
|
on<BookShiftDetailsEvent>(_onBookShift);
|
||||||
|
on<DeclineShiftDetailsEvent>(_onDeclineShift);
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onLoadDetails(
|
||||||
|
LoadShiftDetailsEvent event,
|
||||||
|
Emitter<ShiftDetailsState> emit,
|
||||||
|
) async {
|
||||||
|
emit(ShiftDetailsLoading());
|
||||||
|
try {
|
||||||
|
final shift = await getShiftDetails(event.shiftId);
|
||||||
|
if (shift != null) {
|
||||||
|
emit(ShiftDetailsLoaded(shift));
|
||||||
|
} else {
|
||||||
|
emit(const ShiftDetailsError("Shift not found"));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
emit(ShiftDetailsError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onBookShift(
|
||||||
|
BookShiftDetailsEvent event,
|
||||||
|
Emitter<ShiftDetailsState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await applyForShift(event.shiftId, isInstantBook: true);
|
||||||
|
emit(const ShiftActionSuccess("Shift successfully booked!"));
|
||||||
|
} catch (e) {
|
||||||
|
emit(ShiftDetailsError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _onDeclineShift(
|
||||||
|
DeclineShiftDetailsEvent event,
|
||||||
|
Emitter<ShiftDetailsState> emit,
|
||||||
|
) async {
|
||||||
|
try {
|
||||||
|
await declineShift(event.shiftId);
|
||||||
|
emit(const ShiftActionSuccess("Shift declined"));
|
||||||
|
} catch (e) {
|
||||||
|
emit(ShiftDetailsError(e.toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
|
||||||
|
abstract class ShiftDetailsEvent extends Equatable {
|
||||||
|
const ShiftDetailsEvent();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class LoadShiftDetailsEvent extends ShiftDetailsEvent {
|
||||||
|
final String shiftId;
|
||||||
|
const LoadShiftDetailsEvent(this.shiftId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [shiftId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class BookShiftDetailsEvent extends ShiftDetailsEvent {
|
||||||
|
final String shiftId;
|
||||||
|
const BookShiftDetailsEvent(this.shiftId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [shiftId];
|
||||||
|
}
|
||||||
|
|
||||||
|
class DeclineShiftDetailsEvent extends ShiftDetailsEvent {
|
||||||
|
final String shiftId;
|
||||||
|
const DeclineShiftDetailsEvent(this.shiftId);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [shiftId];
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
abstract class ShiftDetailsState extends Equatable {
|
||||||
|
const ShiftDetailsState();
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShiftDetailsInitial extends ShiftDetailsState {}
|
||||||
|
|
||||||
|
class ShiftDetailsLoading extends ShiftDetailsState {}
|
||||||
|
|
||||||
|
class ShiftDetailsLoaded extends ShiftDetailsState {
|
||||||
|
final Shift shift;
|
||||||
|
const ShiftDetailsLoaded(this.shift);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [shift];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShiftDetailsError extends ShiftDetailsState {
|
||||||
|
final String message;
|
||||||
|
const ShiftDetailsError(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
|
|
||||||
|
class ShiftActionSuccess extends ShiftDetailsState {
|
||||||
|
final String message;
|
||||||
|
const ShiftActionSuccess(this.message);
|
||||||
|
|
||||||
|
@override
|
||||||
|
List<Object?> get props => [message];
|
||||||
|
}
|
||||||
@@ -3,14 +3,12 @@ import 'package:equatable/equatable.dart';
|
|||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:meta/meta.dart';
|
import 'package:meta/meta.dart';
|
||||||
|
|
||||||
import '../../../domain/usecases/get_available_shifts_usecase.dart';
|
|
||||||
import '../../../domain/arguments/get_available_shifts_arguments.dart';
|
import '../../../domain/arguments/get_available_shifts_arguments.dart';
|
||||||
import '../../../domain/usecases/get_my_shifts_usecase.dart';
|
import '../../../domain/usecases/get_available_shifts_usecase.dart';
|
||||||
import '../../../domain/usecases/get_pending_assignments_usecase.dart';
|
|
||||||
import '../../../domain/usecases/get_cancelled_shifts_usecase.dart';
|
import '../../../domain/usecases/get_cancelled_shifts_usecase.dart';
|
||||||
import '../../../domain/usecases/get_history_shifts_usecase.dart';
|
import '../../../domain/usecases/get_history_shifts_usecase.dart';
|
||||||
import '../../../domain/usecases/accept_shift_usecase.dart';
|
import '../../../domain/usecases/get_my_shifts_usecase.dart';
|
||||||
import '../../../domain/usecases/decline_shift_usecase.dart';
|
import '../../../domain/usecases/get_pending_assignments_usecase.dart';
|
||||||
|
|
||||||
part 'shifts_event.dart';
|
part 'shifts_event.dart';
|
||||||
part 'shifts_state.dart';
|
part 'shifts_state.dart';
|
||||||
@@ -21,8 +19,6 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
final GetPendingAssignmentsUseCase getPendingAssignments;
|
final GetPendingAssignmentsUseCase getPendingAssignments;
|
||||||
final GetCancelledShiftsUseCase getCancelledShifts;
|
final GetCancelledShiftsUseCase getCancelledShifts;
|
||||||
final GetHistoryShiftsUseCase getHistoryShifts;
|
final GetHistoryShiftsUseCase getHistoryShifts;
|
||||||
final AcceptShiftUseCase acceptShift;
|
|
||||||
final DeclineShiftUseCase declineShift;
|
|
||||||
|
|
||||||
ShiftsBloc({
|
ShiftsBloc({
|
||||||
required this.getMyShifts,
|
required this.getMyShifts,
|
||||||
@@ -30,13 +26,9 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
required this.getPendingAssignments,
|
required this.getPendingAssignments,
|
||||||
required this.getCancelledShifts,
|
required this.getCancelledShifts,
|
||||||
required this.getHistoryShifts,
|
required this.getHistoryShifts,
|
||||||
required this.acceptShift,
|
|
||||||
required this.declineShift,
|
|
||||||
}) : super(ShiftsInitial()) {
|
}) : super(ShiftsInitial()) {
|
||||||
on<LoadShiftsEvent>(_onLoadShifts);
|
on<LoadShiftsEvent>(_onLoadShifts);
|
||||||
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
|
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
|
||||||
on<AcceptShiftEvent>(_onAcceptShift);
|
|
||||||
on<DeclineShiftEvent>(_onDeclineShift);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onLoadShifts(
|
Future<void> _onLoadShifts(
|
||||||
@@ -63,7 +55,7 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
myShifts: myShiftsResult,
|
myShifts: myShiftsResult,
|
||||||
pendingShifts: pendingResult,
|
pendingShifts: pendingResult,
|
||||||
cancelledShifts: cancelledResult,
|
cancelledShifts: cancelledResult,
|
||||||
availableShifts: availableResult,
|
availableShifts: _filterPastShifts(availableResult),
|
||||||
historyShifts: historyResult,
|
historyShifts: historyResult,
|
||||||
searchQuery: '',
|
searchQuery: '',
|
||||||
jobType: 'all',
|
jobType: 'all',
|
||||||
@@ -89,7 +81,7 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
));
|
));
|
||||||
|
|
||||||
emit(currentState.copyWith(
|
emit(currentState.copyWith(
|
||||||
availableShifts: result,
|
availableShifts: _filterPastShifts(result),
|
||||||
searchQuery: event.query ?? currentState.searchQuery,
|
searchQuery: event.query ?? currentState.searchQuery,
|
||||||
jobType: event.jobType ?? currentState.jobType,
|
jobType: event.jobType ?? currentState.jobType,
|
||||||
));
|
));
|
||||||
@@ -99,27 +91,16 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> _onAcceptShift(
|
List<Shift> _filterPastShifts(List<Shift> shifts) {
|
||||||
AcceptShiftEvent event,
|
final now = DateTime.now();
|
||||||
Emitter<ShiftsState> emit,
|
return shifts.where((shift) {
|
||||||
) async {
|
if (shift.date.isEmpty) return false;
|
||||||
try {
|
try {
|
||||||
await acceptShift(event.shiftId);
|
final shiftDate = DateTime.parse(shift.date);
|
||||||
add(LoadShiftsEvent()); // Reload lists
|
return shiftDate.isAfter(now);
|
||||||
} catch (_) {
|
} catch (_) {
|
||||||
// Handle error
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}).toList();
|
||||||
|
|
||||||
Future<void> _onDeclineShift(
|
|
||||||
DeclineShiftEvent event,
|
|
||||||
Emitter<ShiftsState> emit,
|
|
||||||
) async {
|
|
||||||
try {
|
|
||||||
await declineShift(event.shiftId);
|
|
||||||
add(LoadShiftsEvent()); // Reload lists
|
|
||||||
} catch (_) {
|
|
||||||
// Handle error
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ import 'package:krow_domain/krow_domain.dart';
|
|||||||
|
|
||||||
extension ShiftsNavigator on IModularNavigator {
|
extension ShiftsNavigator on IModularNavigator {
|
||||||
void pushShiftDetails(Shift shift) {
|
void pushShiftDetails(Shift shift) {
|
||||||
pushNamed('/shifts/details/${shift.id}', arguments: shift);
|
pushNamed('/worker-main/shift-details/${shift.id}', arguments: shift);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Example for going back or internal navigation if needed
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,13 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
import 'package:flutter_modular/flutter_modular.dart';
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:lucide_icons/lucide_icons.dart';
|
|
||||||
import 'package:intl/intl.dart';
|
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import '../blocs/shifts/shifts_bloc.dart';
|
import '../blocs/shifts/shifts_bloc.dart';
|
||||||
import '../widgets/my_shift_card.dart';
|
import '../widgets/tabs/my_shifts_tab.dart';
|
||||||
import '../widgets/shift_assignment_card.dart';
|
import '../widgets/tabs/find_shifts_tab.dart';
|
||||||
|
import '../widgets/tabs/history_shifts_tab.dart';
|
||||||
// Shim to match POC styles locally
|
import '../styles/shifts_styles.dart';
|
||||||
class AppColors {
|
|
||||||
static const Color krowBlue = UiColors.primary;
|
|
||||||
static const Color krowYellow = Color(0xFFFFED4A);
|
|
||||||
static const Color krowCharcoal = UiColors.textPrimary;
|
|
||||||
static const Color krowMuted = UiColors.textSecondary;
|
|
||||||
static const Color krowBorder = UiColors.border;
|
|
||||||
static const Color krowBackground = UiColors.background;
|
|
||||||
static const Color white = Colors.white;
|
|
||||||
static const Color black = Colors.black;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ShiftsPage extends StatefulWidget {
|
class ShiftsPage extends StatefulWidget {
|
||||||
final String? initialTab;
|
final String? initialTab;
|
||||||
@@ -31,15 +19,6 @@ class ShiftsPage extends StatefulWidget {
|
|||||||
|
|
||||||
class _ShiftsPageState extends State<ShiftsPage> {
|
class _ShiftsPageState extends State<ShiftsPage> {
|
||||||
late String _activeTab;
|
late String _activeTab;
|
||||||
String _searchQuery = '';
|
|
||||||
// ignore: unused_field
|
|
||||||
String? _cancelledShiftDemo; // 'lastMinute' or 'advance'
|
|
||||||
String _jobType = 'all'; // all, one-day, multi-day, long-term
|
|
||||||
|
|
||||||
// Calendar State
|
|
||||||
DateTime _selectedDate = DateTime.now();
|
|
||||||
int _weekOffset = 0;
|
|
||||||
|
|
||||||
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
|
final ShiftsBloc _bloc = Modular.get<ShiftsBloc>();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -59,93 +38,30 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
List<DateTime> _getCalendarDays() {
|
|
||||||
final now = DateTime.now();
|
|
||||||
int reactDayIndex = now.weekday == 7 ? 0 : now.weekday;
|
|
||||||
int daysSinceFriday = (reactDayIndex + 2) % 7;
|
|
||||||
final start = now
|
|
||||||
.subtract(Duration(days: daysSinceFriday))
|
|
||||||
.add(Duration(days: _weekOffset * 7));
|
|
||||||
final startDate = DateTime(start.year, start.month, start.day);
|
|
||||||
return List.generate(7, (index) => startDate.add(Duration(days: index)));
|
|
||||||
}
|
|
||||||
|
|
||||||
bool _isSameDay(DateTime a, DateTime b) {
|
|
||||||
return a.year == b.year && a.month == b.month && a.day == b.day;
|
|
||||||
}
|
|
||||||
|
|
||||||
void _confirmShift(String id) {
|
|
||||||
_bloc.add(AcceptShiftEvent(id));
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Shift confirmed!'),
|
|
||||||
backgroundColor: Color(0xFF10B981),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
void _declineShift(String id) {
|
|
||||||
_bloc.add(DeclineShiftEvent(id));
|
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
|
||||||
const SnackBar(
|
|
||||||
content: Text('Shift declined.'),
|
|
||||||
backgroundColor: Color(0xFFEF4444),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return BlocProvider.value(
|
return BlocProvider.value(
|
||||||
value: _bloc,
|
value: _bloc,
|
||||||
child: BlocBuilder<ShiftsBloc, ShiftsState>(
|
child: BlocBuilder<ShiftsBloc, ShiftsState>(
|
||||||
builder: (context, state) {
|
builder: (context, state) {
|
||||||
final List<Shift> myShifts = (state is ShiftsLoaded) ? state.myShifts : [];
|
final List<Shift> myShifts = (state is ShiftsLoaded)
|
||||||
final List<Shift> availableJobs = (state is ShiftsLoaded) ? state.availableShifts : [];
|
? state.myShifts
|
||||||
final List<Shift> pendingAssignments = (state is ShiftsLoaded) ? state.pendingShifts : [];
|
: [];
|
||||||
final List<Shift> cancelledShifts = (state is ShiftsLoaded) ? state.cancelledShifts : [];
|
final List<Shift> availableJobs = (state is ShiftsLoaded)
|
||||||
final List<Shift> historyShifts = (state is ShiftsLoaded) ? state.historyShifts : [];
|
? state.availableShifts
|
||||||
|
: [];
|
||||||
|
final List<Shift> pendingAssignments = (state is ShiftsLoaded)
|
||||||
|
? state.pendingShifts
|
||||||
|
: [];
|
||||||
|
final List<Shift> cancelledShifts = (state is ShiftsLoaded)
|
||||||
|
? state.cancelledShifts
|
||||||
|
: [];
|
||||||
|
final List<Shift> historyShifts = (state is ShiftsLoaded)
|
||||||
|
? state.historyShifts
|
||||||
|
: [];
|
||||||
|
|
||||||
// Filter logic
|
// Note: "filteredJobs" logic moved to FindShiftsTab
|
||||||
final filteredJobs = availableJobs.where((s) {
|
// Note: Calendar logic moved to MyShiftsTab
|
||||||
final matchesSearch =
|
|
||||||
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
|
||||||
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
|
||||||
s.clientName.toLowerCase().contains(_searchQuery.toLowerCase());
|
|
||||||
|
|
||||||
if (!matchesSearch) return false;
|
|
||||||
|
|
||||||
if (_jobType == 'all') return true;
|
|
||||||
if (_jobType == 'one-day') {
|
|
||||||
return s.durationDays == null || s.durationDays! <= 1;
|
|
||||||
}
|
|
||||||
if (_jobType == 'multi-day') return s.durationDays != null && s.durationDays! > 1;
|
|
||||||
return true;
|
|
||||||
}).toList();
|
|
||||||
|
|
||||||
final calendarDays = _getCalendarDays();
|
|
||||||
final weekStartDate = calendarDays.first;
|
|
||||||
final weekEndDate = calendarDays.last;
|
|
||||||
|
|
||||||
final visibleMyShifts = myShifts.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();
|
|
||||||
|
|
||||||
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(
|
return Scaffold(
|
||||||
backgroundColor: AppColors.krowBackground,
|
backgroundColor: AppColors.krowBackground,
|
||||||
@@ -161,326 +77,58 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
20,
|
20,
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
spacing: 16,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
const Text(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
"Shifts",
|
||||||
children: [
|
style: TextStyle(
|
||||||
const Text(
|
fontSize: 24,
|
||||||
"Shifts",
|
fontWeight: FontWeight.bold,
|
||||||
style: TextStyle(
|
color: Colors.white,
|
||||||
fontSize: 24,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
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
|
// Tabs
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
_buildTab("myshifts", "My Shifts", UiIcons.calendar, myShifts.length),
|
_buildTab(
|
||||||
|
"myshifts",
|
||||||
|
"My Shifts",
|
||||||
|
UiIcons.calendar,
|
||||||
|
myShifts.length,
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildTab("find", "Find Shifts", UiIcons.search, filteredJobs.length),
|
_buildTab(
|
||||||
|
"find",
|
||||||
|
"Find Shifts",
|
||||||
|
UiIcons.search,
|
||||||
|
availableJobs.length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs.
|
||||||
|
),
|
||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
_buildTab("history", "History", UiIcons.clock, historyShifts.length),
|
_buildTab(
|
||||||
|
"history",
|
||||||
|
"History",
|
||||||
|
UiIcons.clock,
|
||||||
|
historyShifts.length,
|
||||||
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Calendar Selector
|
|
||||||
if (_activeTab == 'myshifts')
|
|
||||||
Container(
|
|
||||||
color: Colors.white,
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.only(bottom: 12),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
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),
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 16,
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: AppColors.krowCharcoal,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
icon: const Icon(UiIcons.chevronRight, size: 20, color: AppColors.krowCharcoal),
|
|
||||||
onPressed: () => setState(() => _weekOffset++),
|
|
||||||
constraints: const BoxConstraints(),
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Days Grid
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
|
||||||
children: calendarDays.map((date) {
|
|
||||||
final isSelected = _isSameDay(date, _selectedDate);
|
|
||||||
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
|
||||||
final hasShifts = myShifts.any((s) {
|
|
||||||
try {
|
|
||||||
return _isSameDay(DateTime.parse(s.date), date);
|
|
||||||
} catch (_) { return false; }
|
|
||||||
});
|
|
||||||
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => setState(() => _selectedDate = date),
|
|
||||||
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,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
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(),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
|
|
||||||
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
|
// Body Content
|
||||||
Expanded(
|
Expanded(
|
||||||
child: state is ShiftsLoading
|
child: state is ShiftsLoading
|
||||||
? const Center(child: CircularProgressIndicator())
|
? const Center(child: CircularProgressIndicator())
|
||||||
: SingleChildScrollView(
|
: _buildTabContent(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
myShifts,
|
||||||
child: Column(
|
pendingAssignments,
|
||||||
children: [
|
cancelledShifts,
|
||||||
const SizedBox(height: 20),
|
availableJobs,
|
||||||
if (_activeTab == 'myshifts') ...[
|
historyShifts,
|
||||||
if (pendingAssignments.isNotEmpty) ...[
|
),
|
||||||
_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),
|
|
||||||
],
|
|
||||||
|
|
||||||
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) ...[
|
|
||||||
_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(UiIcons.search, "No jobs available", "Check back later", null, null)
|
|
||||||
else
|
|
||||||
...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') ...[
|
|
||||||
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),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -490,62 +138,33 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
String _formatDateStr(String dateStr) {
|
Widget _buildTabContent(
|
||||||
try {
|
List<Shift> myShifts,
|
||||||
final date = DateTime.parse(dateStr);
|
List<Shift> pendingAssignments,
|
||||||
final now = DateTime.now();
|
List<Shift> cancelledShifts,
|
||||||
if (_isSameDay(date, now)) return "Today";
|
List<Shift> availableJobs,
|
||||||
final tomorrow = now.add(const Duration(days: 1));
|
List<Shift> historyShifts,
|
||||||
if (_isSameDay(date, tomorrow)) return "Tomorrow";
|
) {
|
||||||
return DateFormat('EEE, MMM d').format(date);
|
switch (_activeTab) {
|
||||||
} catch (_) {
|
case 'myshifts':
|
||||||
return dateStr;
|
return MyShiftsTab(
|
||||||
|
myShifts: myShifts,
|
||||||
|
pendingAssignments: pendingAssignments,
|
||||||
|
cancelledShifts: cancelledShifts,
|
||||||
|
);
|
||||||
|
case 'find':
|
||||||
|
return FindShiftsTab(
|
||||||
|
availableJobs: availableJobs,
|
||||||
|
);
|
||||||
|
case 'history':
|
||||||
|
return HistoryShiftsTab(
|
||||||
|
historyShifts: historyShifts,
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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 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),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTab(String id, String label, IconData icon, int count) {
|
Widget _buildTab(String id, String label, IconData icon, int count) {
|
||||||
final isActive = _activeTab == id;
|
final isActive = _activeTab == id;
|
||||||
return Expanded(
|
return Expanded(
|
||||||
@@ -554,112 +173,57 @@ class _ShiftsPageState extends State<ShiftsPage> {
|
|||||||
child: Container(
|
child: Container(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: isActive ? Colors.white : Colors.white.withAlpha((0.2 * 255).round()),
|
color: isActive
|
||||||
borderRadius: BorderRadius.circular(8),
|
? Colors.white
|
||||||
|
: Colors.white.withAlpha((0.2 * 255).round()),
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
child: Row(
|
child: Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, size: 14, color: isActive ? AppColors.krowBlue : Colors.white),
|
Icon(
|
||||||
const SizedBox(width: 6),
|
icon,
|
||||||
Flexible(child: Text(label, style: TextStyle(fontSize: 13, fontWeight: FontWeight.w500, color: isActive ? AppColors.krowBlue : Colors.white), overflow: TextOverflow.ellipsis)),
|
size: 14,
|
||||||
const SizedBox(width: 4),
|
color: isActive ? AppColors.krowBlue : Colors.white,
|
||||||
Container(
|
),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
const SizedBox(width: 6),
|
||||||
constraints: const BoxConstraints(minWidth: 18),
|
Flexible(
|
||||||
decoration: BoxDecoration(
|
child: Text(
|
||||||
color: isActive ? AppColors.krowBlue.withAlpha((0.1 * 255).round()) : Colors.white.withAlpha((0.2 * 255).round()),
|
label,
|
||||||
borderRadius: BorderRadius.circular(999),
|
style: TextStyle(
|
||||||
|
fontSize: 13,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: isActive ? AppColors.krowBlue : Colors.white,
|
||||||
),
|
),
|
||||||
child: Center(child: Text("$count", style: TextStyle(fontSize: 10, fontWeight: FontWeight.bold, color: isActive ? AppColors.krowBlue : Colors.white))),
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(width: 4),
|
||||||
|
Container(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
|
||||||
|
constraints: const BoxConstraints(minWidth: 18),
|
||||||
|
decoration: BoxDecoration(
|
||||||
|
color: isActive
|
||||||
|
? AppColors.krowBlue.withAlpha((0.1 * 255).round())
|
||||||
|
: Colors.white.withAlpha((0.2 * 255).round()),
|
||||||
|
borderRadius: BorderRadius.circular(999),
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
"$count",
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 10,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: isActive ? AppColors.krowBlue : 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)),
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
Text(title, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.w500, color: AppColors.krowCharcoal)),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(subtitle, style: const TextStyle(fontSize: 14, color: AppColors.krowMuted)),
|
|
||||||
if (actionLabel != null && onAction != null) ...[
|
|
||||||
const SizedBox(height: 16),
|
|
||||||
ElevatedButton(onPressed: onAction, style: ElevatedButton.styleFrom(backgroundColor: AppColors.krowBlue, foregroundColor: Colors.white, shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8))), child: Text(actionLabel)),
|
|
||||||
]
|
|
||||||
])));
|
|
||||||
}
|
|
||||||
|
|
||||||
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(
|
|
||||||
color: AppColors.krowBlue.withOpacity(0.05),
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
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))
|
|
||||||
]),
|
|
||||||
])),
|
|
||||||
]),
|
|
||||||
]),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,13 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
|
||||||
|
class AppColors {
|
||||||
|
static const Color krowBlue = UiColors.primary;
|
||||||
|
static const Color krowYellow = Color(0xFFFFED4A);
|
||||||
|
static const Color krowCharcoal = UiColors.textPrimary;
|
||||||
|
static const Color krowMuted = UiColors.textSecondary;
|
||||||
|
static const Color krowBorder = UiColors.border;
|
||||||
|
static const Color krowBackground = UiColors.background;
|
||||||
|
static const Color white = Colors.white;
|
||||||
|
static const Color black = Colors.black;
|
||||||
|
}
|
||||||
@@ -1,25 +1,17 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
import 'package:krow_domain/krow_domain.dart';
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
import 'package:design_system/design_system.dart';
|
import 'package:design_system/design_system.dart';
|
||||||
import 'package:core_localization/core_localization.dart';
|
import 'package:core_localization/core_localization.dart';
|
||||||
|
import 'package:staff_shifts/src/presentation/navigation/shifts_navigator.dart';
|
||||||
|
|
||||||
class MyShiftCard extends StatefulWidget {
|
class MyShiftCard extends StatefulWidget {
|
||||||
final Shift shift;
|
final Shift shift;
|
||||||
final bool historyMode;
|
|
||||||
final VoidCallback? onAccept;
|
|
||||||
final VoidCallback? onDecline;
|
|
||||||
final VoidCallback? onRequestSwap;
|
|
||||||
final int index;
|
|
||||||
|
|
||||||
const MyShiftCard({
|
const MyShiftCard({
|
||||||
super.key,
|
super.key,
|
||||||
required this.shift,
|
required this.shift,
|
||||||
this.historyMode = false,
|
|
||||||
this.onAccept,
|
|
||||||
this.onDecline,
|
|
||||||
this.onRequestSwap,
|
|
||||||
this.index = 0,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@@ -27,8 +19,6 @@ class MyShiftCard extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _MyShiftCardState extends State<MyShiftCard> {
|
class _MyShiftCardState extends State<MyShiftCard> {
|
||||||
bool _isExpanded = false;
|
|
||||||
|
|
||||||
String _formatTime(String time) {
|
String _formatTime(String time) {
|
||||||
if (time.isEmpty) return '';
|
if (time.isEmpty) return '';
|
||||||
try {
|
try {
|
||||||
@@ -120,9 +110,10 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () => setState(() => _isExpanded = !_isExpanded),
|
onTap: () {
|
||||||
child: AnimatedContainer(
|
Modular.to.pushShiftDetails(widget.shift);
|
||||||
duration: const Duration(milliseconds: 300),
|
},
|
||||||
|
child: Container(
|
||||||
margin: const EdgeInsets.only(bottom: 12),
|
margin: const EdgeInsets.only(bottom: 12),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
@@ -389,384 +380,9 @@ class _MyShiftCardState extends State<MyShiftCard> {
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Expanded Content
|
|
||||||
AnimatedSize(
|
|
||||||
duration: const Duration(milliseconds: 300),
|
|
||||||
child: _isExpanded
|
|
||||||
? Column(
|
|
||||||
children: [
|
|
||||||
const Divider(height: 1, color: UiColors.border),
|
|
||||||
Padding(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
// Stats Row
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.dollar,
|
|
||||||
"\$${estimatedTotal.toStringAsFixed(0)}",
|
|
||||||
"Total",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.dollar,
|
|
||||||
"\$${widget.shift.hourlyRate.toInt()}",
|
|
||||||
"Hourly Rate",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: _buildStatCard(
|
|
||||||
UiIcons.clock,
|
|
||||||
"${duration.toInt()}",
|
|
||||||
"Hours",
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// In/Out Time
|
|
||||||
Row(
|
|
||||||
children: [
|
|
||||||
Expanded(
|
|
||||||
child: _buildTimeBox(
|
|
||||||
"CLOCK IN TIME",
|
|
||||||
widget.shift.startTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(width: 12),
|
|
||||||
Expanded(
|
|
||||||
child: _buildTimeBox(
|
|
||||||
"CLOCK OUT TIME",
|
|
||||||
widget.shift.endTime,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Location
|
|
||||||
Column(
|
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"LOCATION",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.spaceBetween,
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
widget.shift.location.isEmpty
|
|
||||||
? "TBD"
|
|
||||||
: widget.shift.location,
|
|
||||||
style: UiTypography.title1m.copyWith(
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
OutlinedButton.icon(
|
|
||||||
onPressed: () {
|
|
||||||
// Show snackbar with the address
|
|
||||||
ScaffoldMessenger.of(
|
|
||||||
context,
|
|
||||||
).showSnackBar(
|
|
||||||
SnackBar(
|
|
||||||
content: Text(
|
|
||||||
widget.shift.locationAddress,
|
|
||||||
),
|
|
||||||
duration: const Duration(
|
|
||||||
seconds: 3,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
icon: const Icon(
|
|
||||||
UiIcons.navigation,
|
|
||||||
size: 14,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
"Get direction",
|
|
||||||
style: TextStyle(fontSize: 12),
|
|
||||||
),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.textPrimary,
|
|
||||||
side: const BorderSide(
|
|
||||||
color: UiColors.border,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
20,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
padding: const EdgeInsets.symmetric(
|
|
||||||
horizontal: 12,
|
|
||||||
vertical: 0,
|
|
||||||
),
|
|
||||||
minimumSize: const Size(0, 32),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
Container(
|
|
||||||
height: 128,
|
|
||||||
width: double.infinity,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: Colors.grey.shade100,
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: const Center(
|
|
||||||
child: Icon(
|
|
||||||
UiIcons.mapPin,
|
|
||||||
color: UiColors.iconSecondary,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
// Placeholder for Map
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
|
|
||||||
// Additional Info
|
|
||||||
if (widget.shift.description != null) ...[
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
child: Column(
|
|
||||||
crossAxisAlignment:
|
|
||||||
CrossAxisAlignment.start,
|
|
||||||
children: [
|
|
||||||
const Text(
|
|
||||||
"ADDITIONAL INFO",
|
|
||||||
style: TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
widget.shift.description!.split('.')[0],
|
|
||||||
style: UiTypography.body2m.copyWith(
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
widget.shift.description!,
|
|
||||||
style: UiTypography.body3r.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 24),
|
|
||||||
],
|
|
||||||
|
|
||||||
// Actions
|
|
||||||
if (!widget.historyMode)
|
|
||||||
if (status == 'confirmed')
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 48,
|
|
||||||
child: OutlinedButton.icon(
|
|
||||||
onPressed: widget.onRequestSwap,
|
|
||||||
icon: const Icon(
|
|
||||||
UiIcons.swap,
|
|
||||||
size: 16,
|
|
||||||
),
|
|
||||||
label: Text(
|
|
||||||
t.staff_shifts.action.request_swap),
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor: UiColors.primary,
|
|
||||||
side: const BorderSide(
|
|
||||||
color: UiColors.primary,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius: BorderRadius.circular(
|
|
||||||
12,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else if (status == 'swap')
|
|
||||||
Container(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 48,
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(
|
|
||||||
0xFFFFFBEB,
|
|
||||||
), // amber-50
|
|
||||||
border: Border.all(
|
|
||||||
color: const Color(0xFFFDE68A),
|
|
||||||
), // amber-200
|
|
||||||
borderRadius: BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
child: Row(
|
|
||||||
mainAxisAlignment:
|
|
||||||
MainAxisAlignment.center,
|
|
||||||
children: [
|
|
||||||
const Icon(
|
|
||||||
UiIcons.swap,
|
|
||||||
size: 16,
|
|
||||||
color: Color(0xFFB45309),
|
|
||||||
), // amber-700
|
|
||||||
const SizedBox(width: 8),
|
|
||||||
Text(
|
|
||||||
t.staff_shifts.status.swap_requested,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
color: Color(0xFFB45309),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else
|
|
||||||
Column(
|
|
||||||
children: [
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 48,
|
|
||||||
child: ElevatedButton(
|
|
||||||
onPressed: widget.onAccept,
|
|
||||||
style: ElevatedButton.styleFrom(
|
|
||||||
backgroundColor: UiColors.primary,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
status == 'pending'
|
|
||||||
? t.staff_shifts.action.confirm
|
|
||||||
: "Book Shift",
|
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.w600,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
if (status == 'pending' ||
|
|
||||||
status == 'open') ...[
|
|
||||||
const SizedBox(height: 12),
|
|
||||||
SizedBox(
|
|
||||||
width: double.infinity,
|
|
||||||
height: 48,
|
|
||||||
child: OutlinedButton(
|
|
||||||
onPressed: widget.onDecline,
|
|
||||||
style: OutlinedButton.styleFrom(
|
|
||||||
foregroundColor:
|
|
||||||
UiColors.destructive,
|
|
||||||
side: const BorderSide(
|
|
||||||
color: UiColors.border,
|
|
||||||
),
|
|
||||||
shape: RoundedRectangleBorder(
|
|
||||||
borderRadius:
|
|
||||||
BorderRadius.circular(12),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
child: Text(
|
|
||||||
t.staff_shifts.action.decline),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
)
|
|
||||||
: const SizedBox.shrink(),
|
|
||||||
),
|
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildStatCard(IconData icon, String value, String label) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.symmetric(vertical: 16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFF8FAFC),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
border: Border.all(color: UiColors.border),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Container(
|
|
||||||
width: 40,
|
|
||||||
height: 40,
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
color: Colors.white,
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
),
|
|
||||||
child: Icon(icon, size: 20, color: UiColors.iconSecondary),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 8),
|
|
||||||
Text(
|
|
||||||
value,
|
|
||||||
style: UiTypography.title1m.copyWith(
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: UiTypography.footnote2r.copyWith(
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
Widget _buildTimeBox(String label, String time) {
|
|
||||||
return Container(
|
|
||||||
padding: const EdgeInsets.all(16),
|
|
||||||
decoration: BoxDecoration(
|
|
||||||
color: const Color(0xFFF8FAFC),
|
|
||||||
borderRadius: BorderRadius.circular(16),
|
|
||||||
),
|
|
||||||
child: Column(
|
|
||||||
children: [
|
|
||||||
Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(
|
|
||||||
fontSize: 10,
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
color: UiColors.textSecondary,
|
|
||||||
letterSpacing: 0.5,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const SizedBox(height: 4),
|
|
||||||
Text(
|
|
||||||
_formatTime(time),
|
|
||||||
style: UiTypography.display2m.copyWith(
|
|
||||||
fontSize: 20,
|
|
||||||
color: UiColors.textPrimary,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import '../../styles/shifts_styles.dart';
|
||||||
|
|
||||||
|
class EmptyStateView extends StatelessWidget {
|
||||||
|
final IconData icon;
|
||||||
|
final String title;
|
||||||
|
final String subtitle;
|
||||||
|
final String? actionLabel;
|
||||||
|
final VoidCallback? onAction;
|
||||||
|
|
||||||
|
const EmptyStateView({
|
||||||
|
super.key,
|
||||||
|
required this.icon,
|
||||||
|
required this.title,
|
||||||
|
required this.subtitle,
|
||||||
|
this.actionLabel,
|
||||||
|
this.onAction,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
title,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w500,
|
||||||
|
color: AppColors.krowCharcoal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 4),
|
||||||
|
Text(
|
||||||
|
subtitle,
|
||||||
|
style: const TextStyle(fontSize: 14, color: AppColors.krowMuted),
|
||||||
|
),
|
||||||
|
if (actionLabel != null && onAction != null) ...[
|
||||||
|
const SizedBox(height: 16),
|
||||||
|
ElevatedButton(
|
||||||
|
onPressed: onAction,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: AppColors.krowBlue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
shape: RoundedRectangleBorder(
|
||||||
|
borderRadius: BorderRadius.circular(8),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
child: Text(actionLabel!),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
|
||||||
|
import '../../styles/shifts_styles.dart';
|
||||||
|
import '../my_shift_card.dart';
|
||||||
|
import '../shared/empty_state_view.dart';
|
||||||
|
|
||||||
|
class FindShiftsTab extends StatefulWidget {
|
||||||
|
final List<Shift> availableJobs;
|
||||||
|
|
||||||
|
const FindShiftsTab({
|
||||||
|
super.key,
|
||||||
|
required this.availableJobs,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<FindShiftsTab> createState() => _FindShiftsTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||||
|
String _searchQuery = '';
|
||||||
|
String _jobType = 'all';
|
||||||
|
|
||||||
|
Widget _buildFilterTab(String id, String label) {
|
||||||
|
final isSelected = _jobType == id;
|
||||||
|
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),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
// Filter logic
|
||||||
|
final filteredJobs = widget.availableJobs.where((s) {
|
||||||
|
final matchesSearch =
|
||||||
|
s.title.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||||
|
s.location.toLowerCase().contains(_searchQuery.toLowerCase()) ||
|
||||||
|
s.clientName.toLowerCase().contains(_searchQuery.toLowerCase());
|
||||||
|
|
||||||
|
if (!matchesSearch) return false;
|
||||||
|
|
||||||
|
if (_jobType == 'all') return true;
|
||||||
|
if (_jobType == 'one-day') {
|
||||||
|
return s.durationDays == null || s.durationDays! <= 1;
|
||||||
|
}
|
||||||
|
if (_jobType == 'multi-day')
|
||||||
|
return s.durationDays != null && s.durationDays! > 1;
|
||||||
|
return true;
|
||||||
|
}).toList();
|
||||||
|
|
||||||
|
return Column(
|
||||||
|
children: [
|
||||||
|
// Search and Filters
|
||||||
|
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'),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: filteredJobs.isEmpty
|
||||||
|
? EmptyStateView(
|
||||||
|
icon: UiIcons.search,
|
||||||
|
title: "No jobs available",
|
||||||
|
subtitle: "Check back later",
|
||||||
|
)
|
||||||
|
: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
...filteredJobs.map(
|
||||||
|
(shift) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: MyShiftCard(
|
||||||
|
shift: shift,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import '../../navigation/shifts_navigator.dart';
|
||||||
|
import '../my_shift_card.dart';
|
||||||
|
import '../shared/empty_state_view.dart';
|
||||||
|
|
||||||
|
class HistoryShiftsTab extends StatelessWidget {
|
||||||
|
final List<Shift> historyShifts;
|
||||||
|
|
||||||
|
const HistoryShiftsTab({
|
||||||
|
super.key,
|
||||||
|
required this.historyShifts,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
if (historyShifts.isEmpty) {
|
||||||
|
return EmptyStateView(
|
||||||
|
icon: UiIcons.clock,
|
||||||
|
title: "No shift history",
|
||||||
|
subtitle: "Completed shifts appear here",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
...historyShifts.map(
|
||||||
|
(shift) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: GestureDetector(
|
||||||
|
onTap: () => Modular.to.pushShiftDetails(shift),
|
||||||
|
child: MyShiftCard(
|
||||||
|
shift: shift,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,582 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||||
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:lucide_icons/lucide_icons.dart';
|
||||||
|
import 'package:design_system/design_system.dart';
|
||||||
|
import 'package:krow_domain/krow_domain.dart';
|
||||||
|
import '../../blocs/shifts/shifts_bloc.dart';
|
||||||
|
import '../my_shift_card.dart';
|
||||||
|
import '../shift_assignment_card.dart';
|
||||||
|
import '../shared/empty_state_view.dart';
|
||||||
|
import '../../styles/shifts_styles.dart';
|
||||||
|
|
||||||
|
class MyShiftsTab extends StatefulWidget {
|
||||||
|
final List<Shift> myShifts;
|
||||||
|
final List<Shift> pendingAssignments;
|
||||||
|
final List<Shift> cancelledShifts;
|
||||||
|
|
||||||
|
const MyShiftsTab({
|
||||||
|
super.key,
|
||||||
|
required this.myShifts,
|
||||||
|
required this.pendingAssignments,
|
||||||
|
required this.cancelledShifts,
|
||||||
|
});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyShiftsTab> createState() => _MyShiftsTabState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyShiftsTabState extends State<MyShiftsTab> {
|
||||||
|
DateTime _selectedDate = DateTime.now();
|
||||||
|
int _weekOffset = 0;
|
||||||
|
|
||||||
|
List<DateTime> _getCalendarDays() {
|
||||||
|
final now = DateTime.now();
|
||||||
|
int reactDayIndex = now.weekday == 7 ? 0 : now.weekday;
|
||||||
|
int daysSinceFriday = (reactDayIndex + 2) % 7;
|
||||||
|
final start = now
|
||||||
|
.subtract(Duration(days: daysSinceFriday))
|
||||||
|
.add(Duration(days: _weekOffset * 7));
|
||||||
|
final startDate = DateTime(start.year, start.month, start.day);
|
||||||
|
return List.generate(7, (index) => startDate.add(Duration(days: index)));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool _isSameDay(DateTime a, DateTime b) {
|
||||||
|
return a.year == b.year && a.month == b.month && a.day == b.day;
|
||||||
|
}
|
||||||
|
|
||||||
|
void _confirmShift(String id) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Accept Shift'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to accept this shift?',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.read<ShiftsBloc>().add(AcceptShiftEvent(id));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Shift confirmed!'),
|
||||||
|
backgroundColor: Color(0xFF10B981),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFF10B981),
|
||||||
|
),
|
||||||
|
child: const Text('Accept'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
void _declineShift(String id) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (context) => AlertDialog(
|
||||||
|
title: const Text('Decline Shift'),
|
||||||
|
content: const Text(
|
||||||
|
'Are you sure you want to decline this shift? This action cannot be undone.',
|
||||||
|
),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => Navigator.of(context).pop(),
|
||||||
|
child: const Text('Cancel'),
|
||||||
|
),
|
||||||
|
TextButton(
|
||||||
|
onPressed: () {
|
||||||
|
Navigator.of(context).pop();
|
||||||
|
context.read<ShiftsBloc>().add(DeclineShiftEvent(id));
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
const SnackBar(
|
||||||
|
content: Text('Shift declined.'),
|
||||||
|
backgroundColor: Color(0xFFEF4444),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
style: TextButton.styleFrom(
|
||||||
|
foregroundColor: const Color(0xFFEF4444),
|
||||||
|
),
|
||||||
|
child: const Text('Decline'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
final calendarDays = _getCalendarDays();
|
||||||
|
final weekStartDate = calendarDays.first;
|
||||||
|
final weekEndDate = calendarDays.last;
|
||||||
|
|
||||||
|
final visibleMyShifts = widget.myShifts.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();
|
||||||
|
|
||||||
|
final visibleCancelledShifts = widget.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 Column(
|
||||||
|
children: [
|
||||||
|
// Calendar Selector
|
||||||
|
Container(
|
||||||
|
color: Colors.white,
|
||||||
|
padding: const EdgeInsets.symmetric(
|
||||||
|
vertical: 16,
|
||||||
|
horizontal: 16,
|
||||||
|
),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
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),
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
fontWeight: FontWeight.w600,
|
||||||
|
color: AppColors.krowCharcoal,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: const Icon(
|
||||||
|
UiIcons.chevronRight,
|
||||||
|
size: 20,
|
||||||
|
color: AppColors.krowCharcoal,
|
||||||
|
),
|
||||||
|
onPressed: () => setState(() => _weekOffset++),
|
||||||
|
constraints: const BoxConstraints(),
|
||||||
|
padding: EdgeInsets.zero,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
// Days Grid
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: calendarDays.map((date) {
|
||||||
|
final isSelected = _isSameDay(date, _selectedDate);
|
||||||
|
// ignore: unused_local_variable
|
||||||
|
final dateStr = DateFormat('yyyy-MM-dd').format(date);
|
||||||
|
final hasShifts = widget.myShifts.any((s) {
|
||||||
|
try {
|
||||||
|
return _isSameDay(DateTime.parse(s.date), date);
|
||||||
|
} catch (_) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => setState(() => _selectedDate = date),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
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(),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Divider(height: 1, color: AppColors.krowBorder),
|
||||||
|
|
||||||
|
Expanded(
|
||||||
|
child: SingleChildScrollView(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
|
child: Column(
|
||||||
|
children: [
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
if (widget.pendingAssignments.isNotEmpty) ...[
|
||||||
|
_buildSectionHeader(
|
||||||
|
"Awaiting Confirmation",
|
||||||
|
const Color(0xFFF59E0B),
|
||||||
|
),
|
||||||
|
...widget.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),
|
||||||
|
],
|
||||||
|
|
||||||
|
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) ...[
|
||||||
|
_buildSectionHeader(
|
||||||
|
"Confirmed Shifts",
|
||||||
|
AppColors.krowMuted,
|
||||||
|
),
|
||||||
|
...visibleMyShifts.map(
|
||||||
|
(shift) => Padding(
|
||||||
|
padding: const EdgeInsets.only(bottom: 12),
|
||||||
|
child: MyShiftCard(shift: shift),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
|
||||||
|
if (visibleMyShifts.isEmpty &&
|
||||||
|
widget.pendingAssignments.isEmpty &&
|
||||||
|
widget.cancelledShifts.isEmpty)
|
||||||
|
const EmptyStateView(
|
||||||
|
icon: UiIcons.calendar,
|
||||||
|
title: "No shifts this week",
|
||||||
|
subtitle: "Try finding new jobs in the Find tab",
|
||||||
|
),
|
||||||
|
|
||||||
|
const SizedBox(height: 40),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 _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(
|
||||||
|
color: AppColors.krowBlue.withOpacity(0.05),
|
||||||
|
borderRadius: BorderRadius.circular(12),
|
||||||
|
),
|
||||||
|
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,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
import 'package:flutter_modular/flutter_modular.dart';
|
||||||
|
import 'domain/repositories/shifts_repository_interface.dart';
|
||||||
|
import 'data/repositories_impl/shifts_repository_impl.dart';
|
||||||
|
import 'domain/usecases/get_shift_details_usecase.dart';
|
||||||
|
import 'domain/usecases/accept_shift_usecase.dart';
|
||||||
|
import 'domain/usecases/decline_shift_usecase.dart';
|
||||||
|
import 'domain/usecases/apply_for_shift_usecase.dart';
|
||||||
|
import 'presentation/blocs/shift_details/shift_details_bloc.dart';
|
||||||
|
import 'presentation/pages/shift_details_page.dart';
|
||||||
|
|
||||||
|
class ShiftDetailsModule extends Module {
|
||||||
|
@override
|
||||||
|
void binds(Injector i) {
|
||||||
|
// Repository
|
||||||
|
i.add<ShiftsRepositoryInterface>(ShiftsRepositoryImpl.new);
|
||||||
|
|
||||||
|
// UseCases
|
||||||
|
i.add(GetShiftDetailsUseCase.new);
|
||||||
|
i.add(AcceptShiftUseCase.new);
|
||||||
|
i.add(DeclineShiftUseCase.new);
|
||||||
|
i.add(ApplyForShiftUseCase.new);
|
||||||
|
|
||||||
|
// Bloc
|
||||||
|
i.add(ShiftDetailsBloc.new);
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void routes(RouteManager r) {
|
||||||
|
r.child('/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,8 +8,10 @@ import 'domain/usecases/get_cancelled_shifts_usecase.dart';
|
|||||||
import 'domain/usecases/get_history_shifts_usecase.dart';
|
import 'domain/usecases/get_history_shifts_usecase.dart';
|
||||||
import 'domain/usecases/accept_shift_usecase.dart';
|
import 'domain/usecases/accept_shift_usecase.dart';
|
||||||
import 'domain/usecases/decline_shift_usecase.dart';
|
import 'domain/usecases/decline_shift_usecase.dart';
|
||||||
|
import 'domain/usecases/apply_for_shift_usecase.dart';
|
||||||
import 'domain/usecases/get_shift_details_usecase.dart';
|
import 'domain/usecases/get_shift_details_usecase.dart';
|
||||||
import 'presentation/blocs/shifts/shifts_bloc.dart';
|
import 'presentation/blocs/shifts/shifts_bloc.dart';
|
||||||
|
import 'presentation/blocs/shift_details/shift_details_bloc.dart';
|
||||||
import 'presentation/pages/shifts_page.dart';
|
import 'presentation/pages/shifts_page.dart';
|
||||||
import 'presentation/pages/shift_details_page.dart';
|
import 'presentation/pages/shift_details_page.dart';
|
||||||
|
|
||||||
@@ -27,15 +29,16 @@ class StaffShiftsModule extends Module {
|
|||||||
i.add(GetHistoryShiftsUseCase.new);
|
i.add(GetHistoryShiftsUseCase.new);
|
||||||
i.add(AcceptShiftUseCase.new);
|
i.add(AcceptShiftUseCase.new);
|
||||||
i.add(DeclineShiftUseCase.new);
|
i.add(DeclineShiftUseCase.new);
|
||||||
|
i.add(ApplyForShiftUseCase.new);
|
||||||
i.add(GetShiftDetailsUseCase.new);
|
i.add(GetShiftDetailsUseCase.new);
|
||||||
|
|
||||||
// Bloc
|
// Bloc
|
||||||
i.add(ShiftsBloc.new);
|
i.add(ShiftsBloc.new);
|
||||||
|
i.add(ShiftDetailsBloc.new);
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void routes(RouteManager r) {
|
void routes(RouteManager r) {
|
||||||
r.child('/', child: (_) => const ShiftsPage());
|
r.child('/', child: (_) => const ShiftsPage());
|
||||||
r.child('/details/:id', child: (_) => ShiftDetailsPage(shiftId: r.args.params['id'], shift: r.args.data));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
library staff_shifts;
|
library staff_shifts;
|
||||||
|
|
||||||
export 'src/staff_shifts_module.dart';
|
export 'src/staff_shifts_module.dart';
|
||||||
|
export 'src/shift_details_module.dart';
|
||||||
|
export 'src/presentation/navigation/shifts_navigator.dart';
|
||||||
|
|
||||||
|
|||||||
@@ -77,6 +77,10 @@ class StaffMainModule extends Module {
|
|||||||
'/availability',
|
'/availability',
|
||||||
module: StaffAvailabilityModule(),
|
module: StaffAvailabilityModule(),
|
||||||
);
|
);
|
||||||
|
r.module(
|
||||||
|
'/shift-details',
|
||||||
|
module: ShiftDetailsModule(),
|
||||||
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user