refactor: centralize data connect error handling and resolve build issues across applications
This commit addresses several critical issues across the mobile monorepo:
1. Centralized Error Handling: Integrated DataErrorHandler mixin into all repository implementations, ensuring consistent mapping of Data Connect exceptions to domain AppExceptions.
2. Build Stabilization: Fixed numerous type mismatches, parameter signature errors in widgets (e.g., google_places_flutter itemBuilder), and naming conflicts (StaffSession, FirebaseAuth).
3. Code Quality: Applied 'dart fix' across all modified packages and manually cleared debug print statements and UI clutter.
4. Mono-repo alignment: Standardized Data Connect usage and aliasing ('dc.') for better maintainability.
Signed-off-by: Suriya <suriya@tenext.in>
This commit is contained in:
@@ -10,7 +10,9 @@ import '../../domain/ui_entities/auth_mode.dart';
|
||||
import '../../domain/repositories/auth_repository_interface.dart';
|
||||
|
||||
/// Implementation of [AuthRepositoryInterface].
|
||||
class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
class AuthRepositoryImpl
|
||||
with DataErrorHandler
|
||||
implements AuthRepositoryInterface {
|
||||
AuthRepositoryImpl({
|
||||
required this.firebaseAuth,
|
||||
required this.dataConnect,
|
||||
@@ -112,31 +114,35 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
}
|
||||
|
||||
final QueryResult<GetUserByIdData, GetUserByIdVariables> response =
|
||||
await dataConnect.getUserById(
|
||||
id: firebaseUser.uid,
|
||||
).execute();
|
||||
await executeProtected(() => dataConnect
|
||||
.getUserById(
|
||||
id: firebaseUser.uid,
|
||||
)
|
||||
.execute());
|
||||
final GetUserByIdUser? user = response.data.user;
|
||||
|
||||
GetStaffByUserIdStaffs? staffRecord;
|
||||
|
||||
if (mode == AuthMode.signup) {
|
||||
if (user == null) {
|
||||
await dataConnect
|
||||
await executeProtected(() => dataConnect
|
||||
.createUser(
|
||||
id: firebaseUser.uid,
|
||||
role: UserBaseRole.USER,
|
||||
)
|
||||
.userRole('STAFF')
|
||||
.execute();
|
||||
.execute());
|
||||
} else {
|
||||
if (user.userRole != 'STAFF') {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception('User is not authorized for this app.');
|
||||
}
|
||||
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
|
||||
staffResponse = await dataConnect.getStaffByUserId(
|
||||
userId: firebaseUser.uid,
|
||||
).execute();
|
||||
staffResponse = await executeProtected(() => dataConnect
|
||||
.getStaffByUserId(
|
||||
userId: firebaseUser.uid,
|
||||
)
|
||||
.execute());
|
||||
if (staffResponse.data.staffs.isNotEmpty) {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception(
|
||||
@@ -155,9 +161,11 @@ class AuthRepositoryImpl implements AuthRepositoryInterface {
|
||||
}
|
||||
|
||||
final QueryResult<GetStaffByUserIdData, GetStaffByUserIdVariables>
|
||||
staffResponse = await dataConnect.getStaffByUserId(
|
||||
userId: firebaseUser.uid,
|
||||
).execute();
|
||||
staffResponse = await executeProtected(() => dataConnect
|
||||
.getStaffByUserId(
|
||||
userId: firebaseUser.uid,
|
||||
)
|
||||
.execute());
|
||||
if (staffResponse.data.staffs.isEmpty) {
|
||||
await firebaseAuth.signOut();
|
||||
throw Exception(
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||
|
||||
import '../../domain/repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Implementation of [ClockInRepositoryInterface] using Firebase Data Connect.
|
||||
class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final Map<String, String> _shiftToApplicationId = {};
|
||||
String? _activeApplicationId;
|
||||
class ClockInRepositoryImpl
|
||||
with dc.DataErrorHandler
|
||||
implements ClockInRepositoryInterface {
|
||||
|
||||
ClockInRepositoryImpl({
|
||||
required dc.ExampleConnector dataConnect,
|
||||
}) : _dataConnect = dataConnect;
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final Map<String, String> _shiftToApplicationId = <String, String>{};
|
||||
String? _activeApplicationId;
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
final StaffSession? session = StaffSessionStore.instance.session;
|
||||
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
|
||||
final String? staffId = session?.staff?.id;
|
||||
if (staffId != null && staffId.isNotEmpty) {
|
||||
return staffId;
|
||||
@@ -24,7 +26,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
throw Exception('Staff session not found');
|
||||
}
|
||||
|
||||
/// Helper to convert Data Connect Timestamp to DateTime
|
||||
/// Helper to convert Data Connect fdc.Timestamp to DateTime
|
||||
DateTime? _toDateTime(dynamic t) {
|
||||
if (t == null) return null;
|
||||
DateTime? dt;
|
||||
@@ -34,7 +36,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
dt = DateTime.tryParse(t);
|
||||
} else {
|
||||
try {
|
||||
if (t is Timestamp) {
|
||||
if (t is fdc.Timestamp) {
|
||||
dt = t.toDateTime();
|
||||
}
|
||||
} catch (_) {}
|
||||
@@ -46,9 +48,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
if (dt == null) {
|
||||
dt = DateTime.tryParse(t.toString());
|
||||
}
|
||||
dt ??= DateTime.tryParse(t.toString());
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
@@ -58,13 +58,13 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Helper to create Timestamp from DateTime
|
||||
Timestamp _fromDateTime(DateTime d) {
|
||||
// Assuming Timestamp.fromJson takes an ISO string
|
||||
return Timestamp.fromJson(d.toUtc().toIso8601String());
|
||||
/// Helper to create fdc.Timestamp from DateTime
|
||||
fdc.Timestamp _fromDateTime(DateTime d) {
|
||||
// Assuming fdc.Timestamp.fromJson takes an ISO string
|
||||
return fdc.Timestamp.fromJson(d.toUtc().toIso8601String());
|
||||
}
|
||||
|
||||
({Timestamp start, Timestamp end}) _utcDayRange(DateTime localDay) {
|
||||
({fdc.Timestamp start, fdc.Timestamp end}) _utcDayRange(DateTime localDay) {
|
||||
final DateTime dayStartUtc = DateTime.utc(
|
||||
localDay.year,
|
||||
localDay.month,
|
||||
@@ -91,22 +91,24 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
String staffId,
|
||||
) async {
|
||||
final DateTime now = DateTime.now();
|
||||
final range = _utcDayRange(now);
|
||||
final QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables>
|
||||
result = await _dataConnect
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(range.start)
|
||||
.dayEnd(range.end)
|
||||
.execute();
|
||||
final ({fdc.Timestamp start, fdc.Timestamp end}) range = _utcDayRange(now);
|
||||
final fdc.QueryResult<dc.GetApplicationsByStaffIdData,
|
||||
dc.GetApplicationsByStaffIdVariables> result = await executeProtected(
|
||||
() => _dataConnect
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(range.start)
|
||||
.dayEnd(range.end)
|
||||
.execute(),
|
||||
);
|
||||
|
||||
final apps = result.data.applications;
|
||||
if (apps.isEmpty) return const [];
|
||||
final List<dc.GetApplicationsByStaffIdApplications> apps = result.data.applications;
|
||||
if (apps.isEmpty) return const <dc.GetApplicationsByStaffIdApplications>[];
|
||||
|
||||
_shiftToApplicationId
|
||||
..clear()
|
||||
..addEntries(apps.map((app) => MapEntry(app.shiftId, app.id)));
|
||||
..addEntries(apps.map((dc.GetApplicationsByStaffIdApplications app) => MapEntry(app.shiftId, app.id)));
|
||||
|
||||
apps.sort((a, b) {
|
||||
apps.sort((dc.GetApplicationsByStaffIdApplications a, dc.GetApplicationsByStaffIdApplications b) {
|
||||
final DateTime? aTime =
|
||||
_toDateTime(a.shift.startTime) ?? _toDateTime(a.shift.date);
|
||||
final DateTime? bTime =
|
||||
@@ -122,28 +124,17 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
return apps;
|
||||
}
|
||||
|
||||
dc.GetApplicationsByStaffIdApplications? _getActiveApplication(
|
||||
List<dc.GetApplicationsByStaffIdApplications> apps,
|
||||
) {
|
||||
try {
|
||||
return apps.firstWhere((app) {
|
||||
final status = app.status.stringValue;
|
||||
return status == 'CHECKED_IN' || status == 'LATE';
|
||||
});
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getTodaysShifts() async {
|
||||
final String staffId = await _getStaffId();
|
||||
final List<dc.GetApplicationsByStaffIdApplications> apps =
|
||||
await _getTodaysApplications(staffId);
|
||||
if (apps.isEmpty) return const [];
|
||||
if (apps.isEmpty) return const <Shift>[];
|
||||
|
||||
final List<Shift> shifts = [];
|
||||
for (final app in apps) {
|
||||
final List<Shift> shifts = <Shift>[];
|
||||
for (final dc.GetApplicationsByStaffIdApplications app in apps) {
|
||||
final dc.GetApplicationsByStaffIdApplicationsShift shift = app.shift;
|
||||
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
|
||||
@@ -189,7 +180,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
}
|
||||
|
||||
dc.GetApplicationsByStaffIdApplications? activeApp;
|
||||
for (final app in apps) {
|
||||
for (final dc.GetApplicationsByStaffIdApplications app in apps) {
|
||||
if (app.checkInTime != null && app.checkOutTime == null) {
|
||||
if (activeApp == null) {
|
||||
activeApp = app;
|
||||
@@ -209,7 +200,7 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
}
|
||||
|
||||
_activeApplicationId = activeApp.id;
|
||||
print('Active check-in appId=$_activeApplicationId');
|
||||
|
||||
return AttendanceStatus(
|
||||
isCheckedIn: true,
|
||||
checkInTime: _toDateTime(activeApp.checkInTime),
|
||||
@@ -227,39 +218,22 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
dc.GetApplicationsByStaffIdApplications? app;
|
||||
if (cachedAppId != null) {
|
||||
try {
|
||||
final apps = await _getTodaysApplications(staffId);
|
||||
app = apps.firstWhere((a) => a.id == cachedAppId);
|
||||
final List<dc.GetApplicationsByStaffIdApplications> apps = await _getTodaysApplications(staffId);
|
||||
app = apps.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.id == cachedAppId);
|
||||
} catch (_) {}
|
||||
}
|
||||
app ??= (await _getTodaysApplications(staffId))
|
||||
.firstWhere((a) => a.shiftId == shiftId);
|
||||
.firstWhere((dc.GetApplicationsByStaffIdApplications a) => a.shiftId == shiftId);
|
||||
|
||||
final Timestamp checkInTs = _fromDateTime(DateTime.now());
|
||||
print(
|
||||
'ClockIn request: appId=${app.id} shiftId=$shiftId '
|
||||
'checkInTime=${checkInTs.toJson()}',
|
||||
);
|
||||
try {
|
||||
await _dataConnect
|
||||
.updateApplicationStatus(
|
||||
id: app.id,
|
||||
)
|
||||
.checkInTime(checkInTs)
|
||||
.execute();
|
||||
_activeApplicationId = app.id;
|
||||
} catch (e) {
|
||||
print('ClockIn updateApplicationStatus error: $e');
|
||||
print('ClockIn error type: ${e.runtimeType}');
|
||||
try {
|
||||
final dynamic err = e;
|
||||
final dynamic details =
|
||||
err.details ?? err.response ?? err.data ?? err.message;
|
||||
if (details != null) {
|
||||
print('ClockIn error details: $details');
|
||||
}
|
||||
} catch (_) {}
|
||||
rethrow;
|
||||
}
|
||||
final fdc.Timestamp checkInTs = _fromDateTime(DateTime.now());
|
||||
|
||||
await executeProtected(() => _dataConnect
|
||||
.updateApplicationStatus(
|
||||
id: app!.id,
|
||||
)
|
||||
.checkInTime(checkInTs)
|
||||
.execute());
|
||||
_activeApplicationId = app.id;
|
||||
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
@@ -270,25 +244,18 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
int? breakTimeMinutes,
|
||||
String? applicationId,
|
||||
}) async {
|
||||
final String staffId = await _getStaffId();
|
||||
await _getStaffId(); // Validate session
|
||||
|
||||
print(
|
||||
'ClockOut request: applicationId=$applicationId '
|
||||
'activeApplicationId=$_activeApplicationId',
|
||||
);
|
||||
|
||||
final String? targetAppId = applicationId ?? _activeApplicationId;
|
||||
if (targetAppId == null || targetAppId.isEmpty) {
|
||||
throw Exception('No active application id for checkout');
|
||||
}
|
||||
final appResult = await _dataConnect
|
||||
final fdc.QueryResult<dc.GetApplicationByIdData, dc.GetApplicationByIdVariables> appResult = await executeProtected(() => _dataConnect
|
||||
.getApplicationById(id: targetAppId)
|
||||
.execute();
|
||||
final app = appResult.data.application;
|
||||
print(
|
||||
'ClockOut getApplicationById: id=${app?.id} '
|
||||
'checkIn=${app?.checkInTime?.toJson()} '
|
||||
'checkOut=${app?.checkOutTime?.toJson()}',
|
||||
);
|
||||
.execute());
|
||||
final dc.GetApplicationByIdApplication? app = appResult.data.application;
|
||||
|
||||
if (app == null) {
|
||||
throw Exception('Application not found for checkout');
|
||||
}
|
||||
@@ -296,12 +263,12 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
|
||||
throw Exception('No active shift found to clock out');
|
||||
}
|
||||
|
||||
await _dataConnect
|
||||
await executeProtected(() => _dataConnect
|
||||
.updateApplicationStatus(
|
||||
id: targetAppId,
|
||||
)
|
||||
.checkOutTime(_fromDateTime(DateTime.now()))
|
||||
.execute();
|
||||
.execute());
|
||||
|
||||
return getAttendanceStatus();
|
||||
}
|
||||
|
||||
@@ -2,18 +2,18 @@ import 'package:krow_core/core.dart';
|
||||
|
||||
/// Represents the arguments required for the [ClockInUseCase].
|
||||
class ClockInArguments extends UseCaseArgument {
|
||||
/// The ID of the shift to clock in to.
|
||||
final String shiftId;
|
||||
|
||||
/// Optional notes provided by the user during clock-in.
|
||||
final String? notes;
|
||||
|
||||
/// Creates a [ClockInArguments] instance.
|
||||
const ClockInArguments({
|
||||
required this.shiftId,
|
||||
this.notes,
|
||||
});
|
||||
/// The ID of the shift to clock in to.
|
||||
final String shiftId;
|
||||
|
||||
/// Optional notes provided by the user during clock-in.
|
||||
final String? notes;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shiftId, notes];
|
||||
List<Object?> get props => <Object?>[shiftId, notes];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,13 @@ import 'package:krow_core/core.dart';
|
||||
|
||||
/// Represents the arguments required for the [ClockOutUseCase].
|
||||
class ClockOutArguments extends UseCaseArgument {
|
||||
|
||||
/// Creates a [ClockOutArguments] instance.
|
||||
const ClockOutArguments({
|
||||
this.notes,
|
||||
this.breakTimeMinutes,
|
||||
this.applicationId,
|
||||
});
|
||||
/// Optional notes provided by the user during clock-out.
|
||||
final String? notes;
|
||||
|
||||
@@ -11,13 +18,6 @@ class ClockOutArguments extends UseCaseArgument {
|
||||
/// Optional application id for checkout.
|
||||
final String? applicationId;
|
||||
|
||||
/// Creates a [ClockOutArguments] instance.
|
||||
const ClockOutArguments({
|
||||
this.notes,
|
||||
this.breakTimeMinutes,
|
||||
this.applicationId,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [notes, breakTimeMinutes, applicationId];
|
||||
List<Object?> get props => <Object?>[notes, breakTimeMinutes, applicationId];
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ import '../arguments/clock_in_arguments.dart';
|
||||
|
||||
/// Use case for clocking in a user.
|
||||
class ClockInUseCase implements UseCase<ClockInArguments, AttendanceStatus> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
ClockInUseCase(this._repository);
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<AttendanceStatus> call(ClockInArguments arguments) {
|
||||
|
||||
@@ -5,9 +5,9 @@ import '../arguments/clock_out_arguments.dart';
|
||||
|
||||
/// Use case for clocking out a user.
|
||||
class ClockOutUseCase implements UseCase<ClockOutArguments, AttendanceStatus> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
ClockOutUseCase(this._repository);
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<AttendanceStatus> call(ClockOutArguments arguments) {
|
||||
|
||||
@@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Use case for getting the current attendance status (check-in/out times).
|
||||
class GetAttendanceStatusUseCase implements NoInputUseCase<AttendanceStatus> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
GetAttendanceStatusUseCase(this._repository);
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<AttendanceStatus> call() {
|
||||
|
||||
@@ -4,9 +4,9 @@ import '../repositories/clock_in_repository_interface.dart';
|
||||
|
||||
/// Use case for retrieving the user's scheduled shifts for today.
|
||||
class GetTodaysShiftUseCase implements NoInputUseCase<List<Shift>> {
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
GetTodaysShiftUseCase(this._repository);
|
||||
final ClockInRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<List<Shift>> call() {
|
||||
|
||||
@@ -11,13 +11,6 @@ import 'clock_in_event.dart';
|
||||
import 'clock_in_state.dart';
|
||||
|
||||
class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
final GetTodaysShiftUseCase _getTodaysShift;
|
||||
final GetAttendanceStatusUseCase _getAttendanceStatus;
|
||||
final ClockInUseCase _clockIn;
|
||||
final ClockOutUseCase _clockOut;
|
||||
|
||||
// Mock Venue Location (e.g., Grand Hotel, NYC)
|
||||
static const double allowedRadiusMeters = 500;
|
||||
|
||||
ClockInBloc({
|
||||
required GetTodaysShiftUseCase getTodaysShift,
|
||||
@@ -41,6 +34,13 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
|
||||
add(ClockInPageLoaded());
|
||||
}
|
||||
final GetTodaysShiftUseCase _getTodaysShift;
|
||||
final GetAttendanceStatusUseCase _getAttendanceStatus;
|
||||
final ClockInUseCase _clockIn;
|
||||
final ClockOutUseCase _clockOut;
|
||||
|
||||
// Mock Venue Location (e.g., Grand Hotel, NYC)
|
||||
static const double allowedRadiusMeters = 500;
|
||||
|
||||
Future<void> _onLoaded(
|
||||
ClockInPageLoaded event,
|
||||
@@ -48,8 +48,8 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.loading));
|
||||
try {
|
||||
final shifts = await _getTodaysShift();
|
||||
final status = await _getAttendanceStatus();
|
||||
final List<Shift> shifts = await _getTodaysShift();
|
||||
final AttendanceStatus status = await _getAttendanceStatus();
|
||||
|
||||
// Check permissions silently on load? Maybe better to wait for user interaction or specific event
|
||||
// However, if shift exists, we might want to check permission state
|
||||
@@ -58,7 +58,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
if (status.activeShiftId != null) {
|
||||
try {
|
||||
selectedShift =
|
||||
shifts.firstWhere((s) => s.id == status.activeShiftId);
|
||||
shifts.firstWhere((Shift s) => s.id == status.activeShiftId);
|
||||
} catch (_) {}
|
||||
}
|
||||
selectedShift ??= shifts.last;
|
||||
@@ -93,7 +93,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
permission = await Geolocator.requestPermission();
|
||||
}
|
||||
|
||||
final hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
|
||||
final bool hasConsent = permission == LocationPermission.always || permission == LocationPermission.whileInUse;
|
||||
|
||||
emit(state.copyWith(hasLocationConsent: hasConsent));
|
||||
|
||||
@@ -105,9 +105,9 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
}
|
||||
}
|
||||
|
||||
void _startLocationUpdates() async {
|
||||
Future<void> _startLocationUpdates() async {
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||
final Position position = await Geolocator.getCurrentPosition(desiredAccuracy: LocationAccuracy.high);
|
||||
|
||||
double distance = 0;
|
||||
bool isVerified = false; // Require location match by default if shift has location
|
||||
@@ -195,7 +195,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
|
||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||
try {
|
||||
final newStatus = await _clockIn(
|
||||
final AttendanceStatus newStatus = await _clockIn(
|
||||
ClockInArguments(shiftId: event.shiftId, notes: event.notes),
|
||||
);
|
||||
emit(state.copyWith(
|
||||
@@ -216,7 +216,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
|
||||
) async {
|
||||
emit(state.copyWith(status: ClockInStatus.actionInProgress));
|
||||
try {
|
||||
final newStatus = await _clockOut(
|
||||
final AttendanceStatus newStatus = await _clockOut(
|
||||
ClockOutArguments(
|
||||
notes: event.notes,
|
||||
breakTimeMinutes: 0, // Should be passed from event if supported
|
||||
|
||||
@@ -6,75 +6,75 @@ abstract class ClockInEvent extends Equatable {
|
||||
const ClockInEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class ClockInPageLoaded extends ClockInEvent {}
|
||||
|
||||
class ShiftSelected extends ClockInEvent {
|
||||
final Shift shift;
|
||||
const ShiftSelected(this.shift);
|
||||
final Shift shift;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shift];
|
||||
List<Object?> get props => <Object?>[shift];
|
||||
}
|
||||
|
||||
class DateSelected extends ClockInEvent {
|
||||
final DateTime date;
|
||||
|
||||
const DateSelected(this.date);
|
||||
final DateTime date;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [date];
|
||||
List<Object?> get props => <Object?>[date];
|
||||
}
|
||||
|
||||
class CheckInRequested extends ClockInEvent {
|
||||
|
||||
const CheckInRequested({required this.shiftId, this.notes});
|
||||
final String shiftId;
|
||||
final String? notes;
|
||||
|
||||
const CheckInRequested({required this.shiftId, this.notes});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [shiftId, notes];
|
||||
List<Object?> get props => <Object?>[shiftId, notes];
|
||||
}
|
||||
|
||||
class CheckOutRequested extends ClockInEvent {
|
||||
|
||||
const CheckOutRequested({this.notes, this.breakTimeMinutes});
|
||||
final String? notes;
|
||||
final int? breakTimeMinutes;
|
||||
|
||||
const CheckOutRequested({this.notes, this.breakTimeMinutes});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [notes, breakTimeMinutes];
|
||||
List<Object?> get props => <Object?>[notes, breakTimeMinutes];
|
||||
}
|
||||
|
||||
class CheckInModeChanged extends ClockInEvent {
|
||||
final String mode;
|
||||
|
||||
const CheckInModeChanged(this.mode);
|
||||
final String mode;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [mode];
|
||||
List<Object?> get props => <Object?>[mode];
|
||||
}
|
||||
|
||||
class CommuteModeToggled extends ClockInEvent {
|
||||
final bool isEnabled;
|
||||
|
||||
const CommuteModeToggled(this.isEnabled);
|
||||
final bool isEnabled;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [isEnabled];
|
||||
List<Object?> get props => <Object?>[isEnabled];
|
||||
}
|
||||
|
||||
class RequestLocationPermission extends ClockInEvent {}
|
||||
|
||||
class LocationUpdated extends ClockInEvent {
|
||||
|
||||
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
|
||||
final Position position;
|
||||
final double distance;
|
||||
final bool isVerified;
|
||||
|
||||
const LocationUpdated({required this.position, required this.distance, required this.isVerified});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [position, distance, isVerified];
|
||||
List<Object?> get props => <Object?>[position, distance, isVerified];
|
||||
}
|
||||
|
||||
@@ -6,6 +6,22 @@ import 'package:geolocator/geolocator.dart';
|
||||
enum ClockInStatus { initial, loading, success, failure, actionInProgress }
|
||||
|
||||
class ClockInState extends Equatable {
|
||||
|
||||
const ClockInState({
|
||||
this.status = ClockInStatus.initial,
|
||||
this.todayShifts = const <Shift>[],
|
||||
this.selectedShift,
|
||||
this.attendance = const AttendanceStatus(),
|
||||
required this.selectedDate,
|
||||
this.checkInMode = 'swipe',
|
||||
this.errorMessage,
|
||||
this.currentLocation,
|
||||
this.distanceFromVenue,
|
||||
this.isLocationVerified = false,
|
||||
this.isCommuteModeOn = false,
|
||||
this.hasLocationConsent = false,
|
||||
this.etaMinutes,
|
||||
});
|
||||
final ClockInStatus status;
|
||||
final List<Shift> todayShifts;
|
||||
final Shift? selectedShift;
|
||||
@@ -21,22 +37,6 @@ class ClockInState extends Equatable {
|
||||
final bool hasLocationConsent;
|
||||
final int? etaMinutes;
|
||||
|
||||
const ClockInState({
|
||||
this.status = ClockInStatus.initial,
|
||||
this.todayShifts = const [],
|
||||
this.selectedShift,
|
||||
this.attendance = const AttendanceStatus(),
|
||||
required this.selectedDate,
|
||||
this.checkInMode = 'swipe',
|
||||
this.errorMessage,
|
||||
this.currentLocation,
|
||||
this.distanceFromVenue,
|
||||
this.isLocationVerified = false,
|
||||
this.isCommuteModeOn = false,
|
||||
this.hasLocationConsent = false,
|
||||
this.etaMinutes,
|
||||
});
|
||||
|
||||
ClockInState copyWith({
|
||||
ClockInStatus? status,
|
||||
List<Shift>? todayShifts,
|
||||
@@ -70,7 +70,7 @@ class ClockInState extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
List<Object?> get props => <Object?>[
|
||||
status,
|
||||
todayShifts,
|
||||
selectedShift,
|
||||
|
||||
@@ -1,17 +1,9 @@
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:geolocator/geolocator.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
|
||||
// --- State ---
|
||||
class ClockInState extends Equatable {
|
||||
final bool isLoading;
|
||||
final bool isLocationVerified;
|
||||
final String? error;
|
||||
final Position? currentLocation;
|
||||
final double? distanceFromVenue;
|
||||
final bool isClockedIn;
|
||||
final DateTime? clockInTime;
|
||||
|
||||
const ClockInState({
|
||||
this.isLoading = false,
|
||||
@@ -22,6 +14,13 @@ class ClockInState extends Equatable {
|
||||
this.isClockedIn = false,
|
||||
this.clockInTime,
|
||||
});
|
||||
final bool isLoading;
|
||||
final bool isLocationVerified;
|
||||
final String? error;
|
||||
final Position? currentLocation;
|
||||
final double? distanceFromVenue;
|
||||
final bool isClockedIn;
|
||||
final DateTime? clockInTime;
|
||||
|
||||
ClockInState copyWith({
|
||||
bool? isLoading,
|
||||
@@ -44,7 +43,7 @@ class ClockInState extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
List<Object?> get props => <Object?>[
|
||||
isLoading,
|
||||
isLocationVerified,
|
||||
error,
|
||||
@@ -56,13 +55,13 @@ class ClockInState extends Equatable {
|
||||
}
|
||||
|
||||
// --- Cubit ---
|
||||
class ClockInCubit extends Cubit<ClockInState> {
|
||||
class ClockInCubit extends Cubit<ClockInState> { // 500m radius
|
||||
|
||||
ClockInCubit() : super(const ClockInState());
|
||||
// Mock Venue Location (e.g., Grand Hotel, NYC)
|
||||
static const double venueLat = 40.7128;
|
||||
static const double venueLng = -74.0060;
|
||||
static const double allowedRadiusMeters = 500; // 500m radius
|
||||
|
||||
ClockInCubit() : super(const ClockInState());
|
||||
static const double allowedRadiusMeters = 500;
|
||||
|
||||
Future<void> checkLocationPermission() async {
|
||||
emit(state.copyWith(isLoading: true, error: null));
|
||||
@@ -95,18 +94,18 @@ class ClockInCubit extends Cubit<ClockInState> {
|
||||
|
||||
Future<void> _getCurrentLocation() async {
|
||||
try {
|
||||
final position = await Geolocator.getCurrentPosition(
|
||||
final Position position = await Geolocator.getCurrentPosition(
|
||||
desiredAccuracy: LocationAccuracy.high,
|
||||
);
|
||||
|
||||
final distance = Geolocator.distanceBetween(
|
||||
final double distance = Geolocator.distanceBetween(
|
||||
position.latitude,
|
||||
position.longitude,
|
||||
venueLat,
|
||||
venueLng,
|
||||
);
|
||||
|
||||
final isWithinRadius = distance <= allowedRadiusMeters;
|
||||
final bool isWithinRadius = distance <= allowedRadiusMeters;
|
||||
|
||||
emit(state.copyWith(
|
||||
isLoading: false,
|
||||
|
||||
@@ -36,7 +36,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
return BlocProvider<ClockInBloc>.value(
|
||||
value: _bloc,
|
||||
child: BlocConsumer<ClockInBloc, ClockInState>(
|
||||
listener: (context, state) {
|
||||
listener: (BuildContext context, ClockInState state) {
|
||||
if (state.status == ClockInStatus.failure &&
|
||||
state.errorMessage != null) {
|
||||
ScaffoldMessenger.of(
|
||||
@@ -44,7 +44,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
).showSnackBar(SnackBar(content: Text(state.errorMessage!)));
|
||||
}
|
||||
},
|
||||
builder: (context, state) {
|
||||
builder: (BuildContext context, ClockInState state) {
|
||||
if (state.status == ClockInStatus.loading &&
|
||||
state.todayShifts.isEmpty) {
|
||||
return const Scaffold(
|
||||
@@ -52,23 +52,23 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
);
|
||||
}
|
||||
|
||||
final todayShifts = state.todayShifts;
|
||||
final selectedShift = state.selectedShift;
|
||||
final activeShiftId = state.attendance.activeShiftId;
|
||||
final List<Shift> todayShifts = state.todayShifts;
|
||||
final Shift? selectedShift = state.selectedShift;
|
||||
final String? activeShiftId = state.attendance.activeShiftId;
|
||||
final bool isActiveSelected =
|
||||
selectedShift != null && selectedShift.id == activeShiftId;
|
||||
final checkInTime =
|
||||
final DateTime? checkInTime =
|
||||
isActiveSelected ? state.attendance.checkInTime : null;
|
||||
final checkOutTime =
|
||||
final DateTime? checkOutTime =
|
||||
isActiveSelected ? state.attendance.checkOutTime : null;
|
||||
final isCheckedIn =
|
||||
final bool isCheckedIn =
|
||||
state.attendance.isCheckedIn && isActiveSelected;
|
||||
|
||||
// Format times for display
|
||||
final checkInStr = checkInTime != null
|
||||
final String checkInStr = checkInTime != null
|
||||
? DateFormat('h:mm a').format(checkInTime)
|
||||
: '--:-- --';
|
||||
final checkOutStr = checkOutTime != null
|
||||
final String checkOutStr = checkOutTime != null
|
||||
? DateFormat('h:mm a').format(checkOutTime)
|
||||
: '--:-- --';
|
||||
|
||||
@@ -94,7 +94,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
// Commute Tracker (shows before date selector when applicable)
|
||||
if (selectedShift != null)
|
||||
CommuteTracker(
|
||||
@@ -103,15 +103,15 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
isCommuteModeOn: state.isCommuteModeOn,
|
||||
distanceMeters: state.distanceFromVenue,
|
||||
etaMinutes: state.etaMinutes,
|
||||
onCommuteToggled: (value) {
|
||||
onCommuteToggled: (bool value) {
|
||||
_bloc.add(CommuteModeToggled(value));
|
||||
},
|
||||
),
|
||||
// Date Selector
|
||||
DateSelector(
|
||||
selectedDate: state.selectedDate,
|
||||
onSelect: (date) => _bloc.add(DateSelected(date)),
|
||||
shiftDates: [
|
||||
onSelect: (DateTime date) => _bloc.add(DateSelected(date)),
|
||||
shiftDates: <String>[
|
||||
DateFormat('yyyy-MM-dd').format(DateTime.now()),
|
||||
],
|
||||
),
|
||||
@@ -136,7 +136,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
Column(
|
||||
children: todayShifts
|
||||
.map(
|
||||
(shift) => GestureDetector(
|
||||
(Shift shift) => GestureDetector(
|
||||
onTap: () =>
|
||||
_bloc.add(ShiftSelected(shift)),
|
||||
child: Container(
|
||||
@@ -162,12 +162,12 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
child: Row(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
shift.id ==
|
||||
selectedShift?.id
|
||||
@@ -208,7 +208,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.end,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"${_formatTime(shift.startTime)} - ${_formatTime(shift.endTime)}",
|
||||
style: const TextStyle(
|
||||
@@ -236,7 +236,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
|
||||
// Swipe To Check In / Checked Out State / No Shift State
|
||||
if (selectedShift != null && checkOutTime == null) ...[
|
||||
if (selectedShift != null && checkOutTime == null) ...<Widget>[
|
||||
if (!isCheckedIn &&
|
||||
!_isCheckInAllowed(selectedShift))
|
||||
Container(
|
||||
@@ -247,7 +247,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 48,
|
||||
@@ -296,7 +296,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
onCheckOut: () {
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => LunchBreakDialog(
|
||||
builder: (BuildContext context) => LunchBreakDialog(
|
||||
onComplete: () {
|
||||
Navigator.of(
|
||||
context,
|
||||
@@ -308,7 +308,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
},
|
||||
),
|
||||
] else if (selectedShift != null &&
|
||||
checkOutTime != null) ...[
|
||||
checkOutTime != null) ...<Widget>[
|
||||
// Shift Completed State
|
||||
Container(
|
||||
padding: const EdgeInsets.all(24),
|
||||
@@ -320,7 +320,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
), // emerald-200
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 48,
|
||||
height: 48,
|
||||
@@ -354,7 +354,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
],
|
||||
),
|
||||
),
|
||||
] else ...[
|
||||
] else ...<Widget>[
|
||||
// No Shift State
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@@ -364,8 +364,8 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
),
|
||||
child: const Column(
|
||||
children: [
|
||||
const Text(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"No confirmed shifts for today",
|
||||
style: TextStyle(
|
||||
fontSize: 16,
|
||||
@@ -374,8 +374,8 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
const Text(
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
"Accept a shift to clock in",
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
@@ -389,7 +389,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
],
|
||||
|
||||
// Checked In Banner
|
||||
if (isCheckedIn && checkInTime != null) ...[
|
||||
if (isCheckedIn && checkInTime != null) ...<Widget>[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -403,11 +403,11 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
child: Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Checked in at",
|
||||
style: TextStyle(
|
||||
@@ -468,7 +468,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
String value,
|
||||
String currentMode,
|
||||
) {
|
||||
final isSelected = currentMode == value;
|
||||
final bool isSelected = currentMode == value;
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () => _bloc.add(CheckInModeChanged(value)),
|
||||
@@ -478,18 +478,18 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
color: isSelected ? Colors.white : Colors.transparent,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
? <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
offset: const Offset(0, 1),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
: <BoxShadow>[],
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
icon,
|
||||
size: 16,
|
||||
@@ -520,12 +520,12 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
barrierDismissible: false,
|
||||
builder: (BuildContext dialogContext) {
|
||||
return StatefulBuilder(
|
||||
builder: (context, setState) {
|
||||
builder: (BuildContext context, setState) {
|
||||
return AlertDialog(
|
||||
title: Text(scanned ? 'Tag Scanned!' : 'Scan NFC Tag'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 96,
|
||||
height: 96,
|
||||
@@ -559,7 +559,7 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 14, color: Colors.grey.shade600),
|
||||
),
|
||||
if (!scanned) ...[
|
||||
if (!scanned) ...<Widget>[
|
||||
const SizedBox(height: 24),
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
@@ -620,14 +620,14 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
if (timeStr.isEmpty) return '';
|
||||
try {
|
||||
// Try parsing as ISO string first (which contains date)
|
||||
final dt = DateTime.parse(timeStr);
|
||||
final DateTime dt = DateTime.parse(timeStr);
|
||||
return DateFormat('h:mm a').format(dt);
|
||||
} catch (_) {
|
||||
// Fallback for strict "HH:mm" or "HH:mm:ss" strings
|
||||
try {
|
||||
final parts = timeStr.split(':');
|
||||
final List<String> parts = timeStr.split(':');
|
||||
if (parts.length >= 2) {
|
||||
final dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
|
||||
final DateTime dt = DateTime(2022, 1, 1, int.parse(parts[0]), int.parse(parts[1]));
|
||||
return DateFormat('h:mm a').format(dt);
|
||||
}
|
||||
return timeStr;
|
||||
@@ -638,12 +638,11 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
}
|
||||
|
||||
bool _isCheckInAllowed(Shift shift) {
|
||||
if (shift == null) return false;
|
||||
try {
|
||||
// Parse shift date (e.g. 2024-01-31T09:00:00)
|
||||
// The Shift entity has 'date' which is the start DateTime string
|
||||
final shiftStart = DateTime.parse(shift.startTime);
|
||||
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
final DateTime shiftStart = DateTime.parse(shift.startTime);
|
||||
final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
return DateTime.now().isAfter(windowStart);
|
||||
} catch (e) {
|
||||
// Fallback: If parsing fails, allow check in to avoid blocking.
|
||||
@@ -652,10 +651,9 @@ class _ClockInPageState extends State<ClockInPage> {
|
||||
}
|
||||
|
||||
String _getCheckInAvailabilityTime(Shift shift) {
|
||||
if (shift == null) return '';
|
||||
try {
|
||||
final shiftStart = DateTime.parse(shift.startTime.trim());
|
||||
final windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
final DateTime shiftStart = DateTime.parse(shift.startTime.trim());
|
||||
final DateTime windowStart = shiftStart.subtract(const Duration(minutes: 15));
|
||||
return DateFormat('h:mm a').format(windowStart);
|
||||
} catch (e) {
|
||||
return 'soon';
|
||||
|
||||
@@ -4,11 +4,6 @@ import 'package:lucide_icons/lucide_icons.dart';
|
||||
enum AttendanceType { checkin, checkout, breaks, days }
|
||||
|
||||
class AttendanceCard extends StatelessWidget {
|
||||
final AttendanceType type;
|
||||
final String title;
|
||||
final String value;
|
||||
final String subtitle;
|
||||
final String? scheduledTime;
|
||||
|
||||
const AttendanceCard({
|
||||
super.key,
|
||||
@@ -18,10 +13,15 @@ class AttendanceCard extends StatelessWidget {
|
||||
required this.subtitle,
|
||||
this.scheduledTime,
|
||||
});
|
||||
final AttendanceType type;
|
||||
final String title;
|
||||
final String value;
|
||||
final String subtitle;
|
||||
final String? scheduledTime;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final styles = _getStyles(type);
|
||||
final _AttendanceStyle styles = _getStyles(type);
|
||||
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
@@ -29,7 +29,7 @@ class AttendanceCard extends StatelessWidget {
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(color: Colors.grey.shade100),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
@@ -40,7 +40,7 @@ class AttendanceCard extends StatelessWidget {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -72,7 +72,7 @@ class AttendanceCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
),
|
||||
if (scheduledTime != null) ...[
|
||||
if (scheduledTime != null) ...<Widget>[
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
"Scheduled: $scheduledTime",
|
||||
@@ -123,13 +123,13 @@ class AttendanceCard extends StatelessWidget {
|
||||
}
|
||||
|
||||
class _AttendanceStyle {
|
||||
final IconData icon;
|
||||
final Color bgColor;
|
||||
final Color iconColor;
|
||||
|
||||
_AttendanceStyle({
|
||||
required this.icon,
|
||||
required this.bgColor,
|
||||
required this.iconColor,
|
||||
});
|
||||
final IconData icon;
|
||||
final Color bgColor;
|
||||
final Color iconColor;
|
||||
}
|
||||
|
||||
@@ -12,13 +12,6 @@ enum CommuteMode {
|
||||
}
|
||||
|
||||
class CommuteTracker extends StatefulWidget {
|
||||
final Shift? shift;
|
||||
final Function(CommuteMode)? onModeChange;
|
||||
final ValueChanged<bool>? onCommuteToggled;
|
||||
final bool hasLocationConsent;
|
||||
final bool isCommuteModeOn;
|
||||
final double? distanceMeters;
|
||||
final int? etaMinutes;
|
||||
|
||||
const CommuteTracker({
|
||||
super.key,
|
||||
@@ -30,6 +23,13 @@ class CommuteTracker extends StatefulWidget {
|
||||
this.distanceMeters,
|
||||
this.etaMinutes,
|
||||
});
|
||||
final Shift? shift;
|
||||
final Function(CommuteMode)? onModeChange;
|
||||
final ValueChanged<bool>? onCommuteToggled;
|
||||
final bool hasLocationConsent;
|
||||
final bool isCommuteModeOn;
|
||||
final double? distanceMeters;
|
||||
final int? etaMinutes;
|
||||
|
||||
@override
|
||||
State<CommuteTracker> createState() => _CommuteTrackerState();
|
||||
@@ -65,7 +65,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
if (widget.shift == null) return CommuteMode.lockedNoShift;
|
||||
|
||||
// For demo purposes, check if we're within 24 hours of shift
|
||||
final now = DateTime.now();
|
||||
final DateTime now = DateTime.now();
|
||||
DateTime shiftStart;
|
||||
try {
|
||||
// Try parsing startTime as full datetime first
|
||||
@@ -81,8 +81,8 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
);
|
||||
}
|
||||
}
|
||||
final hoursUntilShift = shiftStart.difference(now).inHours;
|
||||
final inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
|
||||
final int hoursUntilShift = shiftStart.difference(now).inHours;
|
||||
final bool inCommuteWindow = hoursUntilShift <= 24 && hoursUntilShift >= 0;
|
||||
|
||||
if (_localIsCommuteOn) {
|
||||
// Check if arrived (mock: if distance < 200m)
|
||||
@@ -102,7 +102,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
}
|
||||
|
||||
String _formatDistance(double meters) {
|
||||
final miles = meters / 1609.34;
|
||||
final double miles = meters / 1609.34;
|
||||
return miles < 0.1
|
||||
? '${meters.round()} m'
|
||||
: '${miles.toStringAsFixed(1)} mi';
|
||||
@@ -110,7 +110,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
|
||||
int _getMinutesUntilShift() {
|
||||
if (widget.shift == null) return 0;
|
||||
final now = DateTime.now();
|
||||
final DateTime now = DateTime.now();
|
||||
DateTime shiftStart;
|
||||
try {
|
||||
// Try parsing startTime as full datetime first
|
||||
@@ -131,7 +131,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final mode = _getAppMode();
|
||||
final CommuteMode mode = _getAppMode();
|
||||
|
||||
// Notify parent of mode change
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
@@ -164,13 +164,13 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colors: <Color>[
|
||||
Color(0xFFEFF6FF), // blue-50
|
||||
Color(0xFFECFEFF), // cyan-50
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
@@ -180,10 +180,10 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -198,11 +198,11 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
const Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Enable Commute Tracking?',
|
||||
style: TextStyle(
|
||||
fontSize: 14,
|
||||
@@ -210,7 +210,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
color: Color(0xFF0F172A), // slate-900
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
SizedBox(height: 4),
|
||||
Text(
|
||||
'Share location 1hr before shift so your manager can see you\'re on the way.',
|
||||
style: TextStyle(
|
||||
@@ -225,7 +225,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: OutlinedButton(
|
||||
onPressed: () {
|
||||
@@ -268,7 +268,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
@@ -277,7 +277,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 32,
|
||||
height: 32,
|
||||
@@ -295,9 +295,9 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
'On My Way',
|
||||
style: TextStyle(
|
||||
@@ -308,7 +308,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.clock,
|
||||
size: 12,
|
||||
@@ -338,11 +338,11 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
Switch(
|
||||
value: _localIsCommuteOn,
|
||||
onChanged: (value) {
|
||||
onChanged: (bool value) {
|
||||
setState(() => _localIsCommuteOn = value);
|
||||
widget.onCommuteToggled?.call(value);
|
||||
},
|
||||
activeColor: AppColors.krowBlue,
|
||||
activeThumbColor: AppColors.krowBlue,
|
||||
),
|
||||
],
|
||||
),
|
||||
@@ -356,7 +356,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
gradient: LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colors: <Color>[
|
||||
Color(0xFF2563EB), // blue-600
|
||||
Color(0xFF0891B2), // cyan-600
|
||||
],
|
||||
@@ -364,19 +364,19 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(20),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
TweenAnimationBuilder(
|
||||
tween: Tween<double>(begin: 1.0, end: 1.1),
|
||||
duration: const Duration(seconds: 1),
|
||||
curve: Curves.easeInOut,
|
||||
builder: (context, double scale, child) {
|
||||
builder: (BuildContext context, double scale, Widget? child) {
|
||||
return Transform.scale(
|
||||
scale: scale,
|
||||
child: Container(
|
||||
@@ -418,7 +418,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
if (widget.distanceMeters != null) ...[
|
||||
if (widget.distanceMeters != null) ...<Widget>[
|
||||
Container(
|
||||
width: double.infinity,
|
||||
constraints: const BoxConstraints(maxWidth: 300),
|
||||
@@ -431,7 +431,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Distance to Site',
|
||||
style: TextStyle(
|
||||
@@ -451,7 +451,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
],
|
||||
),
|
||||
),
|
||||
if (widget.etaMinutes != null) ...[
|
||||
if (widget.etaMinutes != null) ...<Widget>[
|
||||
const SizedBox(height: 12),
|
||||
Container(
|
||||
width: double.infinity,
|
||||
@@ -465,7 +465,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
'Estimated Arrival',
|
||||
style: TextStyle(
|
||||
@@ -530,13 +530,13 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [
|
||||
colors: <Color>[
|
||||
Color(0xFFECFDF5), // emerald-50
|
||||
Color(0xFFD1FAE5), // green-50
|
||||
],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
@@ -545,7 +545,7 @@ class _CommuteTrackerState extends State<CommuteTracker> {
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 64,
|
||||
height: 64,
|
||||
|
||||
@@ -2,21 +2,21 @@ import 'package:flutter/material.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
|
||||
class DateSelector extends StatelessWidget {
|
||||
final DateTime selectedDate;
|
||||
final ValueChanged<DateTime> onSelect;
|
||||
final List<String> shiftDates;
|
||||
|
||||
const DateSelector({
|
||||
super.key,
|
||||
required this.selectedDate,
|
||||
required this.onSelect,
|
||||
this.shiftDates = const [],
|
||||
this.shiftDates = const <String>[],
|
||||
});
|
||||
final DateTime selectedDate;
|
||||
final ValueChanged<DateTime> onSelect;
|
||||
final List<String> shiftDates;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final today = DateTime.now();
|
||||
final dates = List.generate(7, (index) {
|
||||
final DateTime today = DateTime.now();
|
||||
final List<DateTime> dates = List.generate(7, (int index) {
|
||||
return today.add(Duration(days: index - 3));
|
||||
});
|
||||
|
||||
@@ -24,10 +24,10 @@ class DateSelector extends StatelessWidget {
|
||||
height: 80,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: dates.map((date) {
|
||||
final isSelected = _isSameDay(date, selectedDate);
|
||||
final isToday = _isSameDay(date, today);
|
||||
final hasShift = shiftDates.contains(_formatDateIso(date));
|
||||
children: dates.map((DateTime date) {
|
||||
final bool isSelected = _isSameDay(date, selectedDate);
|
||||
final bool isToday = _isSameDay(date, today);
|
||||
final bool hasShift = shiftDates.contains(_formatDateIso(date));
|
||||
|
||||
return Expanded(
|
||||
child: GestureDetector(
|
||||
@@ -39,18 +39,18 @@ class DateSelector extends StatelessWidget {
|
||||
color: isSelected ? const Color(0xFF0032A0) : Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: isSelected
|
||||
? [
|
||||
? <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: const Color(0xFF0032A0).withOpacity(0.3),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
]
|
||||
: [],
|
||||
: <BoxShadow>[],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
DateFormat('d').format(date),
|
||||
style: TextStyle(
|
||||
|
||||
@@ -3,14 +3,14 @@ import 'package:design_system/design_system.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class LocationMapPlaceholder extends StatelessWidget {
|
||||
final bool isVerified;
|
||||
final double? distance;
|
||||
|
||||
const LocationMapPlaceholder({
|
||||
super.key,
|
||||
required this.isVerified,
|
||||
this.distance,
|
||||
});
|
||||
final bool isVerified;
|
||||
final double? distance;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -31,12 +31,12 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
// Fallback UI if image fails (which it will without key)
|
||||
const Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Icon(LucideIcons.mapPin, size: 48, color: UiColors.iconSecondary),
|
||||
SizedBox(height: 8),
|
||||
Text('Map View (GPS)', style: TextStyle(color: UiColors.textSecondary)),
|
||||
@@ -54,7 +54,7 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 8,
|
||||
@@ -63,7 +63,7 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Icon(
|
||||
isVerified ? LucideIcons.checkCircle : LucideIcons.alertCircle,
|
||||
color: isVerified ? UiColors.textSuccess : UiColors.destructive,
|
||||
@@ -73,7 +73,7 @@ class LocationMapPlaceholder extends StatelessWidget {
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
isVerified ? 'Location Verified' : 'Location Check',
|
||||
style: UiTypography.body1b.copyWith(color: UiColors.textPrimary),
|
||||
|
||||
@@ -2,9 +2,9 @@ import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class LunchBreakDialog extends StatefulWidget {
|
||||
final VoidCallback onComplete;
|
||||
|
||||
const LunchBreakDialog({super.key, required this.onComplete});
|
||||
final VoidCallback onComplete;
|
||||
|
||||
@override
|
||||
State<LunchBreakDialog> createState() => _LunchBreakDialogState();
|
||||
@@ -23,7 +23,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
String _additionalNotes = '';
|
||||
|
||||
final List<String> _timeOptions = _generateTimeOptions();
|
||||
final List<String> _noLunchReasons = [
|
||||
final List<String> _noLunchReasons = <String>[
|
||||
'Unpredictable Workflows',
|
||||
'Poor Time Management',
|
||||
'Lack of coverage or short Staff',
|
||||
@@ -32,12 +32,12 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
];
|
||||
|
||||
static List<String> _generateTimeOptions() {
|
||||
List<String> options = [];
|
||||
final List<String> options = <String>[];
|
||||
for (int h = 0; h < 24; h++) {
|
||||
for (int m = 0; m < 60; m += 15) {
|
||||
final hour = h % 12 == 0 ? 12 : h % 12;
|
||||
final ampm = h < 12 ? 'am' : 'pm';
|
||||
final timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm';
|
||||
final int hour = h % 12 == 0 ? 12 : h % 12;
|
||||
final String ampm = h < 12 ? 'am' : 'pm';
|
||||
final String timeStr = '$hour:${m.toString().padLeft(2, '0')}$ampm';
|
||||
options.add(timeStr);
|
||||
}
|
||||
}
|
||||
@@ -78,7 +78,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 80,
|
||||
height: 80,
|
||||
@@ -104,7 +104,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: GestureDetector(
|
||||
onTap: () {
|
||||
@@ -171,7 +171,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"When did you take lunch?",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
@@ -179,13 +179,13 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
const SizedBox(height: 24),
|
||||
// Mock Inputs
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
value: _breakStart,
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (v) => setState(() => _breakStart = v),
|
||||
initialValue: _breakStart,
|
||||
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (String? v) => setState(() => _breakStart = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'Start',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
@@ -196,9 +196,9 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
Expanded(
|
||||
child: DropdownButtonFormField<String>(
|
||||
isExpanded: true,
|
||||
value: _breakEnd,
|
||||
items: _timeOptions.map((t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (v) => setState(() => _breakEnd = v),
|
||||
initialValue: _breakEnd,
|
||||
items: _timeOptions.map((String t) => DropdownMenuItem(value: t, child: Text(t, style: const TextStyle(fontSize: 13)))).toList(),
|
||||
onChanged: (String? v) => setState(() => _breakEnd = v),
|
||||
decoration: const InputDecoration(
|
||||
labelText: 'End',
|
||||
contentPadding: EdgeInsets.symmetric(horizontal: 10, vertical: 8),
|
||||
@@ -230,17 +230,17 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Why didn't you take lunch?",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
..._noLunchReasons.map((reason) => RadioListTile<String>(
|
||||
..._noLunchReasons.map((String reason) => RadioListTile<String>(
|
||||
title: Text(reason),
|
||||
value: reason,
|
||||
groupValue: _noLunchReason,
|
||||
onChanged: (val) => setState(() => _noLunchReason = val),
|
||||
onChanged: (String? val) => setState(() => _noLunchReason = val),
|
||||
)),
|
||||
|
||||
const SizedBox(height: 24),
|
||||
@@ -264,14 +264,14 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Additional Notes",
|
||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
onChanged: (v) => _additionalNotes = v,
|
||||
onChanged: (String v) => _additionalNotes = v,
|
||||
decoration: const InputDecoration(
|
||||
hintText: 'Add any details...',
|
||||
border: OutlineInputBorder(),
|
||||
@@ -300,7 +300,7 @@ class _LunchBreakDialogState extends State<LunchBreakDialog> {
|
||||
padding: const EdgeInsets.all(24),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(LucideIcons.checkCircle, size: 64, color: Colors.green),
|
||||
const SizedBox(height: 24),
|
||||
const Text(
|
||||
|
||||
@@ -2,11 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class SwipeToCheckIn extends StatefulWidget {
|
||||
final VoidCallback? onCheckIn;
|
||||
final VoidCallback? onCheckOut;
|
||||
final bool isLoading;
|
||||
final String mode; // 'swipe' or 'nfc'
|
||||
final bool isCheckedIn;
|
||||
|
||||
const SwipeToCheckIn({
|
||||
super.key,
|
||||
@@ -16,6 +11,11 @@ class SwipeToCheckIn extends StatefulWidget {
|
||||
this.mode = 'swipe',
|
||||
this.isCheckedIn = false,
|
||||
});
|
||||
final VoidCallback? onCheckIn;
|
||||
final VoidCallback? onCheckOut;
|
||||
final bool isLoading;
|
||||
final String mode; // 'swipe' or 'nfc'
|
||||
final bool isCheckedIn;
|
||||
|
||||
@override
|
||||
State<SwipeToCheckIn> createState() => _SwipeToCheckInState();
|
||||
@@ -50,7 +50,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
|
||||
void _onDragEnd(DragEndDetails details, double maxWidth) {
|
||||
if (_isComplete || widget.isLoading) return;
|
||||
final threshold = (maxWidth - _handleSize - 8) * 0.8;
|
||||
final double threshold = (maxWidth - _handleSize - 8) * 0.8;
|
||||
if (_dragValue > threshold) {
|
||||
setState(() {
|
||||
_dragValue = maxWidth - _handleSize - 8;
|
||||
@@ -72,7 +72,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final baseColor = widget.isCheckedIn
|
||||
final Color baseColor = widget.isCheckedIn
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF0032A0);
|
||||
|
||||
@@ -94,7 +94,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
decoration: BoxDecoration(
|
||||
color: baseColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: baseColor.withOpacity(0.4),
|
||||
blurRadius: 25,
|
||||
@@ -105,7 +105,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(LucideIcons.wifi, color: Colors.white),
|
||||
const SizedBox(width: 12),
|
||||
Text(
|
||||
@@ -127,19 +127,19 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
}
|
||||
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final maxWidth = constraints.maxWidth;
|
||||
final maxDrag = maxWidth - _handleSize - 8;
|
||||
builder: (BuildContext context, BoxConstraints constraints) {
|
||||
final double maxWidth = constraints.maxWidth;
|
||||
final double maxDrag = maxWidth - _handleSize - 8;
|
||||
|
||||
// Calculate background color based on drag
|
||||
final progress = _dragValue / maxDrag;
|
||||
final startColor = widget.isCheckedIn
|
||||
final double progress = _dragValue / maxDrag;
|
||||
final Color startColor = widget.isCheckedIn
|
||||
? const Color(0xFF10B981)
|
||||
: const Color(0xFF0032A0);
|
||||
final endColor = widget.isCheckedIn
|
||||
final Color endColor = widget.isCheckedIn
|
||||
? const Color(0xFF0032A0)
|
||||
: const Color(0xFF10B981);
|
||||
final currentColor =
|
||||
final Color currentColor =
|
||||
Color.lerp(startColor, endColor, progress) ?? startColor;
|
||||
|
||||
return Container(
|
||||
@@ -147,7 +147,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
decoration: BoxDecoration(
|
||||
color: currentColor,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 4,
|
||||
@@ -156,7 +156,7 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
],
|
||||
),
|
||||
child: Stack(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Center(
|
||||
child: Opacity(
|
||||
opacity: 1.0 - progress,
|
||||
@@ -187,15 +187,15 @@ class _SwipeToCheckInState extends State<SwipeToCheckIn>
|
||||
left: 4 + _dragValue,
|
||||
top: 4,
|
||||
child: GestureDetector(
|
||||
onHorizontalDragUpdate: (d) => _onDragUpdate(d, maxWidth),
|
||||
onHorizontalDragEnd: (d) => _onDragEnd(d, maxWidth),
|
||||
onHorizontalDragUpdate: (DragUpdateDetails d) => _onDragUpdate(d, maxWidth),
|
||||
onHorizontalDragEnd: (DragEndDetails d) => _onDragEnd(d, maxWidth),
|
||||
child: Container(
|
||||
width: _handleSize,
|
||||
height: _handleSize,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.1),
|
||||
blurRadius: 2,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library staff_clock_in;
|
||||
library;
|
||||
|
||||
export 'src/staff_clock_in_module.dart';
|
||||
export 'src/presentation/pages/clock_in_page.dart';
|
||||
|
||||
@@ -11,7 +11,9 @@ extension TimestampExt on Timestamp {
|
||||
}
|
||||
}
|
||||
|
||||
class HomeRepositoryImpl implements HomeRepository {
|
||||
class HomeRepositoryImpl
|
||||
with DataErrorHandler
|
||||
implements HomeRepository {
|
||||
HomeRepositoryImpl();
|
||||
|
||||
String get _currentStaffId {
|
||||
@@ -31,33 +33,32 @@ class HomeRepositoryImpl implements HomeRepository {
|
||||
}
|
||||
|
||||
Future<List<Shift>> _getShiftsForDate(DateTime date) async {
|
||||
try {
|
||||
final staffId = _currentStaffId;
|
||||
|
||||
// Create start and end timestamps for the target date
|
||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||
final DateTime end = DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
||||
|
||||
final response = await ExampleConnector.instance
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(_toTimestamp(start))
|
||||
.dayEnd(_toTimestamp(end))
|
||||
.execute();
|
||||
|
||||
// Filter for ACCEPTED applications (same logic as shifts_repository_impl)
|
||||
final apps = response.data.applications.where(
|
||||
(app) => (app.status is Known && (app.status as Known).value == ApplicationStatus.ACCEPTED) || (app.status is Known && (app.status as Known).value == ApplicationStatus.CONFIRMED)
|
||||
);
|
||||
|
||||
final List<Shift> shifts = [];
|
||||
for (final app in apps) {
|
||||
shifts.add(_mapApplicationToShift(app));
|
||||
}
|
||||
|
||||
return shifts;
|
||||
} catch (e) {
|
||||
return [];
|
||||
final staffId = _currentStaffId;
|
||||
|
||||
// Create start and end timestamps for the target date
|
||||
final DateTime start = DateTime(date.year, date.month, date.day);
|
||||
final DateTime end =
|
||||
DateTime(date.year, date.month, date.day, 23, 59, 59, 999);
|
||||
|
||||
final response = await executeProtected(() => ExampleConnector.instance
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.dayStart(_toTimestamp(start))
|
||||
.dayEnd(_toTimestamp(end))
|
||||
.execute());
|
||||
|
||||
// Filter for ACCEPTED applications (same logic as shifts_repository_impl)
|
||||
final apps = response.data.applications.where((app) =>
|
||||
(app.status is Known &&
|
||||
(app.status as Known).value == ApplicationStatus.ACCEPTED) ||
|
||||
(app.status is Known &&
|
||||
(app.status as Known).value == ApplicationStatus.CONFIRMED));
|
||||
|
||||
final List<Shift> shifts = [];
|
||||
for (final app in apps) {
|
||||
shifts.add(_mapApplicationToShift(app));
|
||||
}
|
||||
|
||||
return shifts;
|
||||
}
|
||||
|
||||
Timestamp _toTimestamp(DateTime dateTime) {
|
||||
@@ -69,27 +70,24 @@ class HomeRepositoryImpl implements HomeRepository {
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getRecommendedShifts() async {
|
||||
try {
|
||||
// Logic: List ALL open shifts (simple recommendation engine)
|
||||
// Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED.
|
||||
final response = await ExampleConnector.instance.listShifts().execute();
|
||||
// Logic: List ALL open shifts (simple recommendation engine)
|
||||
// Limitation: listShifts might return ALL shifts. We should ideally filter by status=PUBLISHED.
|
||||
final response = await executeProtected(() => ExampleConnector.instance.listShifts().execute());
|
||||
|
||||
return response.data.shifts
|
||||
.where((s) {
|
||||
final isOpen = s.status is Known && (s.status as Known).value == ShiftStatus.OPEN;
|
||||
if (!isOpen) return false;
|
||||
return response.data.shifts
|
||||
.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;
|
||||
final start = s.startTime?.toDate();
|
||||
if (start == null) return false;
|
||||
|
||||
return start.isAfter(DateTime.now());
|
||||
})
|
||||
.take(10)
|
||||
.map((s) => _mapConnectorShiftToDomain(s))
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
return start.isAfter(DateTime.now());
|
||||
})
|
||||
.take(10)
|
||||
.map((s) => _mapConnectorShiftToDomain(s))
|
||||
.toList();
|
||||
}
|
||||
|
||||
@override
|
||||
|
||||
@@ -1,22 +1,22 @@
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../../domain/repositories/payments_repository.dart';
|
||||
|
||||
class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
|
||||
PaymentsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance;
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance;
|
||||
|
||||
String? _cachedStaffId;
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
// 1. Check Session Store
|
||||
final StaffSession? session = StaffSessionStore.instance.session;
|
||||
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
|
||||
if (session?.staff?.id != null) {
|
||||
return session!.staff!.id;
|
||||
}
|
||||
@@ -25,13 +25,13 @@ class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
if (_cachedStaffId != null) return _cachedStaffId!;
|
||||
|
||||
// 3. Fetch from Data Connect using Firebase UID
|
||||
final user = _auth.currentUser;
|
||||
final firebase_auth.User? user = _auth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User is not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
final QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> response = await _dataConnect.getStaffByUserId(userId: user.uid).execute();
|
||||
if (response.data.staffs.isNotEmpty) {
|
||||
_cachedStaffId = response.data.staffs.first.id;
|
||||
return _cachedStaffId!;
|
||||
@@ -66,9 +66,7 @@ class PaymentsRepositoryImpl implements PaymentsRepository {
|
||||
} catch (_) {}
|
||||
|
||||
try {
|
||||
if (dt == null) {
|
||||
dt = DateTime.tryParse(t.toString());
|
||||
}
|
||||
dt ??= DateTime.tryParse(t.toString());
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,11 +2,11 @@ import 'package:krow_core/core.dart';
|
||||
|
||||
/// Arguments for getting payment history.
|
||||
class GetPaymentHistoryArguments extends UseCaseArgument {
|
||||
|
||||
const GetPaymentHistoryArguments(this.period);
|
||||
/// The period to filter by (e.g., "monthly", "weekly").
|
||||
final String period;
|
||||
|
||||
const GetPaymentHistoryArguments(this.period);
|
||||
|
||||
@override
|
||||
List<Object?> get props => [period];
|
||||
List<Object?> get props => <Object?>[period];
|
||||
}
|
||||
|
||||
@@ -7,10 +7,10 @@ import '../repositories/payments_repository.dart';
|
||||
///
|
||||
/// This use case delegates the data retrieval to [PaymentsRepository].
|
||||
class GetPaymentHistoryUseCase extends UseCase<GetPaymentHistoryArguments, List<StaffPayment>> {
|
||||
final PaymentsRepository repository;
|
||||
|
||||
/// Creates a [GetPaymentHistoryUseCase].
|
||||
GetPaymentHistoryUseCase(this.repository);
|
||||
final PaymentsRepository repository;
|
||||
|
||||
@override
|
||||
Future<List<StaffPayment>> call(GetPaymentHistoryArguments arguments) async {
|
||||
|
||||
@@ -4,10 +4,10 @@ import '../repositories/payments_repository.dart';
|
||||
|
||||
/// Use case to retrieve payment summary information.
|
||||
class GetPaymentSummaryUseCase extends NoInputUseCase<PaymentSummary> {
|
||||
final PaymentsRepository repository;
|
||||
|
||||
/// Creates a [GetPaymentSummaryUseCase].
|
||||
GetPaymentSummaryUseCase(this.repository);
|
||||
final PaymentsRepository repository;
|
||||
|
||||
@override
|
||||
Future<PaymentSummary> call() async {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import 'package:flutter/src/widgets/framework.dart';
|
||||
import 'package:flutter_modular/flutter_modular.dart';
|
||||
import 'package:krow_core/core.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'domain/repositories/payments_repository.dart';
|
||||
import 'domain/usecases/get_payment_summary_usecase.dart';
|
||||
import 'domain/usecases/get_payment_history_usecase.dart';
|
||||
@@ -26,7 +26,7 @@ class StaffPaymentsModule extends Module {
|
||||
void routes(RouteManager r) {
|
||||
r.child(
|
||||
StaffPaths.childRoute(StaffPaths.payments, StaffPaths.payments),
|
||||
child: (context) => const PaymentsPage(),
|
||||
child: (BuildContext context) => const PaymentsPage(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,8 +7,6 @@ import 'payments_event.dart';
|
||||
import 'payments_state.dart';
|
||||
|
||||
class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
||||
final GetPaymentSummaryUseCase getPaymentSummary;
|
||||
final GetPaymentHistoryUseCase getPaymentHistory;
|
||||
|
||||
PaymentsBloc({
|
||||
required this.getPaymentSummary,
|
||||
@@ -17,6 +15,8 @@ class PaymentsBloc extends Bloc<PaymentsEvent, PaymentsState> {
|
||||
on<LoadPaymentsEvent>(_onLoadPayments);
|
||||
on<ChangePeriodEvent>(_onChangePeriod);
|
||||
}
|
||||
final GetPaymentSummaryUseCase getPaymentSummary;
|
||||
final GetPaymentHistoryUseCase getPaymentHistory;
|
||||
|
||||
Future<void> _onLoadPayments(
|
||||
LoadPaymentsEvent event,
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import 'package:bloc/bloc.dart';
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
abstract class PaymentsEvent extends Equatable {
|
||||
const PaymentsEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class LoadPaymentsEvent extends PaymentsEvent {}
|
||||
|
||||
class ChangePeriodEvent extends PaymentsEvent {
|
||||
final String period;
|
||||
|
||||
const ChangePeriodEvent(this.period);
|
||||
final String period;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [period];
|
||||
List<Object?> get props => <Object?>[period];
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ abstract class PaymentsState extends Equatable {
|
||||
const PaymentsState();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
class PaymentsInitial extends PaymentsState {}
|
||||
@@ -13,15 +13,15 @@ class PaymentsInitial extends PaymentsState {}
|
||||
class PaymentsLoading extends PaymentsState {}
|
||||
|
||||
class PaymentsLoaded extends PaymentsState {
|
||||
final PaymentSummary summary;
|
||||
final List<StaffPayment> history;
|
||||
final String activePeriod;
|
||||
|
||||
const PaymentsLoaded({
|
||||
required this.summary,
|
||||
required this.history,
|
||||
this.activePeriod = 'week',
|
||||
});
|
||||
final PaymentSummary summary;
|
||||
final List<StaffPayment> history;
|
||||
final String activePeriod;
|
||||
|
||||
PaymentsLoaded copyWith({
|
||||
PaymentSummary? summary,
|
||||
@@ -36,14 +36,14 @@ class PaymentsLoaded extends PaymentsState {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [summary, history, activePeriod];
|
||||
List<Object?> get props => <Object?>[summary, history, activePeriod];
|
||||
}
|
||||
|
||||
class PaymentsError extends PaymentsState {
|
||||
final String message;
|
||||
|
||||
const PaymentsError(this.message);
|
||||
final String message;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [message];
|
||||
List<Object?> get props => <Object?>[message];
|
||||
}
|
||||
|
||||
@@ -1,10 +1,6 @@
|
||||
import 'package:equatable/equatable.dart';
|
||||
|
||||
class PaymentStats extends Equatable {
|
||||
final double weeklyEarnings;
|
||||
final double monthlyEarnings;
|
||||
final double pendingEarnings;
|
||||
final double totalEarnings;
|
||||
|
||||
const PaymentStats({
|
||||
this.weeklyEarnings = 0.0,
|
||||
@@ -12,9 +8,13 @@ class PaymentStats extends Equatable {
|
||||
this.pendingEarnings = 0.0,
|
||||
this.totalEarnings = 0.0,
|
||||
});
|
||||
final double weeklyEarnings;
|
||||
final double monthlyEarnings;
|
||||
final double pendingEarnings;
|
||||
final double totalEarnings;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [
|
||||
List<Object?> get props => <Object?>[
|
||||
weeklyEarnings,
|
||||
monthlyEarnings,
|
||||
pendingEarnings,
|
||||
|
||||
@@ -177,7 +177,7 @@ class _PaymentsPageState extends State<PaymentsPage> {
|
||||
|
||||
// Recent Payments
|
||||
if (state.history.isNotEmpty) Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Recent Payments",
|
||||
style: TextStyle(
|
||||
|
||||
@@ -4,21 +4,21 @@ import 'package:intl/intl.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
|
||||
class EarningsGraph extends StatelessWidget {
|
||||
final List<StaffPayment> payments;
|
||||
final String period;
|
||||
|
||||
const EarningsGraph({
|
||||
super.key,
|
||||
required this.payments,
|
||||
required this.period,
|
||||
});
|
||||
final List<StaffPayment> payments;
|
||||
final String period;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
// Basic data processing for the graph
|
||||
// We'll aggregate payments by date
|
||||
final validPayments = payments.where((p) => p.paidAt != null).toList()
|
||||
..sort((a, b) => a.paidAt!.compareTo(b.paidAt!));
|
||||
final List<StaffPayment> validPayments = payments.where((StaffPayment p) => p.paidAt != null).toList()
|
||||
..sort((StaffPayment a, StaffPayment b) => a.paidAt!.compareTo(b.paidAt!));
|
||||
|
||||
// If no data, show empty state or simple placeholder
|
||||
if (validPayments.isEmpty) {
|
||||
@@ -32,9 +32,9 @@ class EarningsGraph extends StatelessWidget {
|
||||
);
|
||||
}
|
||||
|
||||
final spots = _generateSpots(validPayments);
|
||||
final maxX = spots.isNotEmpty ? spots.last.x : 0.0;
|
||||
final maxY = spots.isNotEmpty ? spots.map((s) => s.y).reduce((a, b) => a > b ? a : b) : 0.0;
|
||||
final List<FlSpot> spots = _generateSpots(validPayments);
|
||||
final double maxX = spots.isNotEmpty ? spots.last.x : 0.0;
|
||||
final double maxY = spots.isNotEmpty ? spots.map((FlSpot s) => s.y).reduce((double a, double b) => a > b ? a : b) : 0.0;
|
||||
|
||||
return Container(
|
||||
height: 220,
|
||||
@@ -42,7 +42,7 @@ class EarningsGraph extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
offset: const Offset(0, 4),
|
||||
@@ -52,7 +52,7 @@ class EarningsGraph extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Earnings Trend",
|
||||
style: TextStyle(
|
||||
@@ -70,10 +70,10 @@ class EarningsGraph extends StatelessWidget {
|
||||
bottomTitles: AxisTitles(
|
||||
sideTitles: SideTitles(
|
||||
showTitles: true,
|
||||
getTitlesWidget: (value, meta) {
|
||||
getTitlesWidget: (double value, TitleMeta meta) {
|
||||
// Simple logic to show a few dates
|
||||
if (value % 2 != 0) return const SizedBox();
|
||||
final index = value.toInt();
|
||||
final int index = value.toInt();
|
||||
if (index >= 0 && index < validPayments.length) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(top: 8.0),
|
||||
@@ -92,7 +92,7 @@ class EarningsGraph extends StatelessWidget {
|
||||
rightTitles: const AxisTitles(sideTitles: SideTitles(showTitles: false)),
|
||||
),
|
||||
borderData: FlBorderData(show: false),
|
||||
lineBarsData: [
|
||||
lineBarsData: <LineChartBarData>[
|
||||
LineChartBarData(
|
||||
spots: spots,
|
||||
isCurved: true,
|
||||
@@ -121,7 +121,7 @@ class EarningsGraph extends StatelessWidget {
|
||||
List<FlSpot> _generateSpots(List<StaffPayment> data) {
|
||||
// Generate spots based on index in the list for simplicity in this demo
|
||||
// Real implementation would map to actual dates on X-axis
|
||||
return List.generate(data.length, (index) {
|
||||
return List.generate(data.length, (int index) {
|
||||
return FlSpot(index.toDouble(), data[index].amount);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,15 +2,6 @@ import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class PaymentHistoryItem extends StatelessWidget {
|
||||
final double amount;
|
||||
final String title;
|
||||
final String location;
|
||||
final String address;
|
||||
final String date;
|
||||
final String workedTime;
|
||||
final int hours;
|
||||
final double rate;
|
||||
final String status;
|
||||
|
||||
const PaymentHistoryItem({
|
||||
super.key,
|
||||
@@ -24,6 +15,15 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
required this.rate,
|
||||
required this.status,
|
||||
});
|
||||
final double amount;
|
||||
final String title;
|
||||
final String location;
|
||||
final String address;
|
||||
final String date;
|
||||
final String workedTime;
|
||||
final int hours;
|
||||
final double rate;
|
||||
final String status;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -32,7 +32,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
@@ -42,10 +42,10 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
// Status Badge
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 6,
|
||||
height: 6,
|
||||
@@ -70,7 +70,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
// Icon
|
||||
Container(
|
||||
width: 44,
|
||||
@@ -90,15 +90,15 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
// Content
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
@@ -119,7 +119,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.end,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Text(
|
||||
"\$${amount.toStringAsFixed(0)}",
|
||||
style: const TextStyle(
|
||||
@@ -143,7 +143,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
|
||||
// Date and Time
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.calendar,
|
||||
size: 12,
|
||||
@@ -177,7 +177,7 @@ class PaymentHistoryItem extends StatelessWidget {
|
||||
|
||||
// Address
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Icon(
|
||||
LucideIcons.mapPin,
|
||||
size: 12,
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class PaymentStatsCard extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String label;
|
||||
final String amount;
|
||||
|
||||
const PaymentStatsCard({
|
||||
super.key,
|
||||
@@ -14,6 +9,10 @@ class PaymentStatsCard extends StatelessWidget {
|
||||
required this.label,
|
||||
required this.amount,
|
||||
});
|
||||
final IconData icon;
|
||||
final Color iconColor;
|
||||
final String label;
|
||||
final String amount;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -22,7 +21,7 @@ class PaymentStatsCard extends StatelessWidget {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
@@ -32,9 +31,9 @@ class PaymentStatsCard extends StatelessWidget {
|
||||
),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Icon(icon, size: 16, color: iconColor),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
|
||||
@@ -2,14 +2,14 @@ import 'package:flutter/material.dart';
|
||||
import 'package:lucide_icons/lucide_icons.dart';
|
||||
|
||||
class PendingPayCard extends StatelessWidget {
|
||||
final double amount;
|
||||
final VoidCallback onCashOut;
|
||||
|
||||
const PendingPayCard({
|
||||
super.key,
|
||||
required this.amount,
|
||||
required this.onCashOut,
|
||||
});
|
||||
final double amount;
|
||||
final VoidCallback onCashOut;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -17,12 +17,12 @@ class PendingPayCard extends StatelessWidget {
|
||||
padding: const EdgeInsets.all(14),
|
||||
decoration: BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
colors: [Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50
|
||||
colors: <Color>[Color(0xFFEFF6FF), Color(0xFFEFF6FF)], // blue-50 to blue-50
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
boxShadow: <BoxShadow>[
|
||||
BoxShadow(
|
||||
color: Colors.black.withOpacity(0.05),
|
||||
blurRadius: 2,
|
||||
@@ -32,9 +32,9 @@ class PendingPayCard extends StatelessWidget {
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Row(
|
||||
children: [
|
||||
children: <Widget>[
|
||||
Container(
|
||||
width: 40,
|
||||
height: 40,
|
||||
@@ -51,7 +51,7 @@ class PendingPayCard extends StatelessWidget {
|
||||
const SizedBox(width: 10),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
children: <Widget>[
|
||||
const Text(
|
||||
"Pending",
|
||||
style: TextStyle(
|
||||
|
||||
@@ -11,7 +11,8 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
firebase_data_connect:
|
||||
firebase_data_connect: ^0.2.2+2
|
||||
firebase_auth: ^6.1.4
|
||||
flutter_modular: ^6.3.2
|
||||
lucide_icons: ^0.257.0
|
||||
intl: ^0.20.0
|
||||
@@ -29,6 +30,8 @@ dependencies:
|
||||
krow_data_connect:
|
||||
path: ../../../data_connect
|
||||
|
||||
flutter_bloc: any
|
||||
equatable: any
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../../domain/repositories/certificates_repository.dart';
|
||||
|
||||
class CertificatesRepositoryMock implements CertificatesRepository {
|
||||
@override
|
||||
Future<List<StaffDocument>> getCertificates() async {
|
||||
final DateTime now = DateTime.now();
|
||||
|
||||
// Create copies with dynamic dates
|
||||
final List<StaffDocument> dynamicDocuments = <StaffDocument>[
|
||||
StaffDocument(
|
||||
id: '1',
|
||||
documentId: 'background',
|
||||
staffId: 'current_user',
|
||||
name: 'Background Check',
|
||||
description: 'Required for all shifts',
|
||||
status: DocumentStatus.verified,
|
||||
expiryDate: now.add(const Duration(days: 365)),
|
||||
),
|
||||
StaffDocument(
|
||||
id: '2',
|
||||
documentId: 'food_handler',
|
||||
staffId: 'current_user',
|
||||
name: 'Food Handler',
|
||||
description: 'Required for food service',
|
||||
status: DocumentStatus.verified,
|
||||
expiryDate: now.add(const Duration(days: 15)),
|
||||
),
|
||||
const StaffDocument(
|
||||
id: '3',
|
||||
documentId: 'rbs',
|
||||
staffId: 'current_user',
|
||||
name: 'RBS Alcohol',
|
||||
description: 'Required for bar shifts',
|
||||
status: DocumentStatus.missing,
|
||||
expiryDate: null,
|
||||
),
|
||||
];
|
||||
|
||||
await Future<void>.delayed(const Duration(seconds: 1)); // Simulate network delay
|
||||
return dynamicDocuments;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
@@ -14,21 +14,21 @@ import '../../domain/repositories/personal_info_repository_interface.dart';
|
||||
/// - Mapping between data_connect DTOs and domain entities
|
||||
/// - Containing no business logic
|
||||
class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
final ExampleConnector _dataConnect;
|
||||
final FirebaseAuth _firebaseAuth;
|
||||
|
||||
/// Creates a [PersonalInfoRepositoryImpl].
|
||||
///
|
||||
/// Requires the Firebase Data Connect connector instance and Firebase Auth.
|
||||
PersonalInfoRepositoryImpl({
|
||||
required ExampleConnector dataConnect,
|
||||
required FirebaseAuth firebaseAuth,
|
||||
required firebase_auth.FirebaseAuth firebaseAuth,
|
||||
}) : _dataConnect = dataConnect,
|
||||
_firebaseAuth = firebaseAuth;
|
||||
final ExampleConnector _dataConnect;
|
||||
final firebase_auth.FirebaseAuth _firebaseAuth;
|
||||
|
||||
@override
|
||||
Future<Staff> getStaffProfile() async {
|
||||
final user = _firebaseAuth.currentUser;
|
||||
final firebase_auth.User? user = _firebaseAuth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User not authenticated');
|
||||
}
|
||||
@@ -41,7 +41,7 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
throw Exception('Staff profile not found for User ID: ${user.uid}');
|
||||
}
|
||||
|
||||
final rawStaff = result.data.staffs.first;
|
||||
final GetStaffByUserIdStaffs rawStaff = result.data.staffs.first;
|
||||
|
||||
// Map from data_connect DTO to domain entity
|
||||
return _mapToStaffEntity(rawStaff);
|
||||
@@ -50,7 +50,7 @@ class PersonalInfoRepositoryImpl implements PersonalInfoRepositoryInterface {
|
||||
@override
|
||||
Future<Staff> updateStaffProfile({required String staffId, required Map<String, dynamic> data}) async {
|
||||
// Start building the update mutation
|
||||
var updateBuilder = _dataConnect.updateStaff(id: staffId);
|
||||
UpdateStaffVariablesBuilder updateBuilder = _dataConnect.updateStaff(id: staffId);
|
||||
|
||||
// Apply updates from map if present
|
||||
if (data.containsKey('name')) {
|
||||
|
||||
@@ -8,12 +8,12 @@ import '../repositories/personal_info_repository_interface.dart';
|
||||
/// which delegates to the data_connect layer for data access.
|
||||
class GetPersonalInfoUseCase
|
||||
implements NoInputUseCase<Staff> {
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
/// Creates a [GetPersonalInfoUseCase].
|
||||
///
|
||||
/// Requires a [PersonalInfoRepositoryInterface] to fetch data.
|
||||
GetPersonalInfoUseCase(this._repository);
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<Staff> call() {
|
||||
|
||||
@@ -4,19 +4,19 @@ import '../repositories/personal_info_repository_interface.dart';
|
||||
|
||||
/// Arguments for updating staff profile information.
|
||||
class UpdatePersonalInfoParams extends UseCaseArgument {
|
||||
|
||||
const UpdatePersonalInfoParams({
|
||||
required this.staffId,
|
||||
required this.data,
|
||||
});
|
||||
/// The staff member's ID.
|
||||
final String staffId;
|
||||
|
||||
/// The fields to update.
|
||||
final Map<String, dynamic> data;
|
||||
|
||||
const UpdatePersonalInfoParams({
|
||||
required this.staffId,
|
||||
required this.data,
|
||||
});
|
||||
|
||||
@override
|
||||
List<Object?> get props => [staffId, data];
|
||||
List<Object?> get props => <Object?>[staffId, data];
|
||||
}
|
||||
|
||||
/// Use case for updating staff profile information.
|
||||
@@ -25,12 +25,12 @@ class UpdatePersonalInfoParams extends UseCaseArgument {
|
||||
/// through the repository, which delegates to the data_connect layer.
|
||||
class UpdatePersonalInfoUseCase
|
||||
implements UseCase<UpdatePersonalInfoParams, Staff> {
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
/// Creates an [UpdatePersonalInfoUseCase].
|
||||
///
|
||||
/// Requires a [PersonalInfoRepositoryInterface] to update data.
|
||||
UpdatePersonalInfoUseCase(this._repository);
|
||||
final PersonalInfoRepositoryInterface _repository;
|
||||
|
||||
@override
|
||||
Future<Staff> call(UpdatePersonalInfoParams params) {
|
||||
|
||||
@@ -14,8 +14,6 @@ import 'personal_info_state.dart';
|
||||
/// use cases following Clean Architecture principles.
|
||||
class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
implements Disposable {
|
||||
final GetPersonalInfoUseCase _getPersonalInfoUseCase;
|
||||
final UpdatePersonalInfoUseCase _updatePersonalInfoUseCase;
|
||||
|
||||
/// Creates a [PersonalInfoBloc].
|
||||
///
|
||||
@@ -33,6 +31,8 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
|
||||
add(const PersonalInfoLoadRequested());
|
||||
}
|
||||
final GetPersonalInfoUseCase _getPersonalInfoUseCase;
|
||||
final UpdatePersonalInfoUseCase _updatePersonalInfoUseCase;
|
||||
|
||||
/// Handles loading staff profile information.
|
||||
Future<void> _onLoadRequested(
|
||||
@@ -45,13 +45,13 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
|
||||
// Initialize form values from staff entity
|
||||
// Note: Staff entity currently stores address as a string, but we want to map it to 'preferredLocations'
|
||||
final Map<String, dynamic> initialValues = {
|
||||
final Map<String, dynamic> initialValues = <String, dynamic>{
|
||||
'name': staff.name,
|
||||
'email': staff.email,
|
||||
'phone': staff.phone,
|
||||
'preferredLocations': staff.address != null
|
||||
? [staff.address]
|
||||
: [], // TODO: Map correctly when Staff entity supports list
|
||||
? <String?>[staff.address]
|
||||
: <dynamic>[], // TODO: Map correctly when Staff entity supports list
|
||||
'avatar': staff.avatar,
|
||||
};
|
||||
|
||||
@@ -95,13 +95,13 @@ class PersonalInfoBloc extends Bloc<PersonalInfoEvent, PersonalInfoState>
|
||||
);
|
||||
|
||||
// Update local state with the returned staff and keep form values in sync
|
||||
final Map<String, dynamic> newValues = {
|
||||
final Map<String, dynamic> newValues = <String, dynamic>{
|
||||
'name': updatedStaff.name,
|
||||
'email': updatedStaff.email,
|
||||
'phone': updatedStaff.phone,
|
||||
'preferredLocations': updatedStaff.address != null
|
||||
? [updatedStaff.address]
|
||||
: [],
|
||||
? <String?>[updatedStaff.address]
|
||||
: <dynamic>[],
|
||||
'avatar': updatedStaff.avatar,
|
||||
};
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ abstract class PersonalInfoEvent extends Equatable {
|
||||
const PersonalInfoEvent();
|
||||
|
||||
@override
|
||||
List<Object?> get props => [];
|
||||
List<Object?> get props => <Object?>[];
|
||||
}
|
||||
|
||||
/// Event to load personal information.
|
||||
@@ -15,16 +15,16 @@ class PersonalInfoLoadRequested extends PersonalInfoEvent {
|
||||
|
||||
/// Event to update a field value.
|
||||
class PersonalInfoFieldChanged extends PersonalInfoEvent {
|
||||
final String field;
|
||||
final dynamic value;
|
||||
|
||||
const PersonalInfoFieldChanged({
|
||||
required this.field,
|
||||
required this.value,
|
||||
});
|
||||
final String field;
|
||||
final dynamic value;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [field, value];
|
||||
List<Object?> get props => <Object?>[field, value];
|
||||
}
|
||||
|
||||
/// Event to submit the form.
|
||||
@@ -34,9 +34,9 @@ class PersonalInfoFormSubmitted extends PersonalInfoEvent {
|
||||
|
||||
/// Event when an address is selected from autocomplete.
|
||||
class PersonalInfoAddressSelected extends PersonalInfoEvent {
|
||||
final String address;
|
||||
const PersonalInfoAddressSelected(this.address);
|
||||
final String address;
|
||||
|
||||
@override
|
||||
List<Object?> get props => [address];
|
||||
List<Object?> get props => <Object?>[address];
|
||||
}
|
||||
|
||||
@@ -29,6 +29,21 @@ enum PersonalInfoStatus {
|
||||
///
|
||||
/// Uses the shared [Staff] entity from the domain layer.
|
||||
class PersonalInfoState extends Equatable {
|
||||
|
||||
/// Creates a [PersonalInfoState].
|
||||
const PersonalInfoState({
|
||||
this.status = PersonalInfoStatus.initial,
|
||||
this.staff,
|
||||
this.formValues = const <String, dynamic>{},
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// Initial state.
|
||||
const PersonalInfoState.initial()
|
||||
: status = PersonalInfoStatus.initial,
|
||||
staff = null,
|
||||
formValues = const <String, dynamic>{},
|
||||
errorMessage = null;
|
||||
/// The current status of the operation.
|
||||
final PersonalInfoStatus status;
|
||||
|
||||
@@ -41,21 +56,6 @@ class PersonalInfoState extends Equatable {
|
||||
/// Error message if an error occurred.
|
||||
final String? errorMessage;
|
||||
|
||||
/// Creates a [PersonalInfoState].
|
||||
const PersonalInfoState({
|
||||
this.status = PersonalInfoStatus.initial,
|
||||
this.staff,
|
||||
this.formValues = const {},
|
||||
this.errorMessage,
|
||||
});
|
||||
|
||||
/// Initial state.
|
||||
const PersonalInfoState.initial()
|
||||
: status = PersonalInfoStatus.initial,
|
||||
staff = null,
|
||||
formValues = const {},
|
||||
errorMessage = null;
|
||||
|
||||
/// Creates a copy of this state with the given fields replaced.
|
||||
PersonalInfoState copyWith({
|
||||
PersonalInfoStatus? status,
|
||||
@@ -72,5 +72,5 @@ class PersonalInfoState extends Equatable {
|
||||
}
|
||||
|
||||
@override
|
||||
List<Object?> get props => [status, staff, formValues, errorMessage];
|
||||
List<Object?> get props => <Object?>[status, staff, formValues, errorMessage];
|
||||
}
|
||||
|
||||
@@ -18,14 +18,14 @@ import 'save_button.dart';
|
||||
/// following Clean Architecture's separation of concerns principle and the design system guidelines.
|
||||
/// Works with the shared [Staff] entity from the domain layer.
|
||||
class PersonalInfoContent extends StatefulWidget {
|
||||
/// The staff profile to display and edit.
|
||||
final Staff staff;
|
||||
|
||||
/// Creates a [PersonalInfoContent].
|
||||
const PersonalInfoContent({
|
||||
super.key,
|
||||
required this.staff,
|
||||
});
|
||||
/// The staff profile to display and edit.
|
||||
final Staff staff;
|
||||
|
||||
@override
|
||||
State<PersonalInfoContent> createState() => _PersonalInfoContentState();
|
||||
@@ -81,8 +81,8 @@ class _PersonalInfoContentState extends State<PersonalInfoContent> {
|
||||
// The backend expects List<AnyValue> (JSON/List) for preferredLocations
|
||||
final List<String> locations = _locationsController.text
|
||||
.split(',')
|
||||
.map((e) => e.trim())
|
||||
.where((e) => e.isNotEmpty)
|
||||
.map((String e) => e.trim())
|
||||
.where((String e) => e.isNotEmpty)
|
||||
.toList();
|
||||
|
||||
context.read<PersonalInfoBloc>().add(
|
||||
|
||||
@@ -9,6 +9,17 @@ import 'package:design_system/design_system.dart';
|
||||
/// and editable fields for phone and address.
|
||||
/// Uses only design system tokens for colors, typography, and spacing.
|
||||
class PersonalInfoForm extends StatelessWidget {
|
||||
|
||||
/// Creates a [PersonalInfoForm].
|
||||
const PersonalInfoForm({
|
||||
super.key,
|
||||
required this.fullName,
|
||||
required this.email,
|
||||
required this.emailController,
|
||||
required this.phoneController,
|
||||
required this.locationsController,
|
||||
this.enabled = true,
|
||||
});
|
||||
/// The staff member's full name (read-only).
|
||||
final String fullName;
|
||||
|
||||
@@ -27,17 +38,6 @@ class PersonalInfoForm extends StatelessWidget {
|
||||
/// Whether the form fields are enabled for editing.
|
||||
final bool enabled;
|
||||
|
||||
/// Creates a [PersonalInfoForm].
|
||||
const PersonalInfoForm({
|
||||
super.key,
|
||||
required this.fullName,
|
||||
required this.email,
|
||||
required this.emailController,
|
||||
required this.phoneController,
|
||||
required this.locationsController,
|
||||
this.enabled = true,
|
||||
});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final TranslationsStaffOnboardingPersonalInfoEn i18n = t.staff.onboarding.personal_info;
|
||||
@@ -57,7 +57,7 @@ class PersonalInfoForm extends StatelessWidget {
|
||||
hint: i18n.email_label,
|
||||
enabled: enabled,
|
||||
keyboardType: TextInputType.emailAddress,
|
||||
autofillHints: const [AutofillHints.email],
|
||||
autofillHints: const <String>[AutofillHints.email],
|
||||
),
|
||||
const SizedBox(height: UiConstants.space4),
|
||||
|
||||
@@ -85,9 +85,9 @@ class PersonalInfoForm extends StatelessWidget {
|
||||
/// A label widget for form fields.
|
||||
/// A label widget for form fields.
|
||||
class _FieldLabel extends StatelessWidget {
|
||||
final String text;
|
||||
|
||||
const _FieldLabel({required this.text});
|
||||
final String text;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -101,9 +101,9 @@ class _FieldLabel extends StatelessWidget {
|
||||
/// A read-only field widget for displaying non-editable information.
|
||||
/// A read-only field widget for displaying non-editable information.
|
||||
class _ReadOnlyField extends StatelessWidget {
|
||||
final String value;
|
||||
|
||||
const _ReadOnlyField({required this.value});
|
||||
final String value;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
@@ -129,11 +129,6 @@ class _ReadOnlyField extends StatelessWidget {
|
||||
/// An editable text field widget.
|
||||
/// An editable text field widget.
|
||||
class _EditableField extends StatelessWidget {
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final bool enabled;
|
||||
final TextInputType? keyboardType;
|
||||
final Iterable<String>? autofillHints;
|
||||
|
||||
const _EditableField({
|
||||
required this.controller,
|
||||
@@ -142,6 +137,11 @@ class _EditableField extends StatelessWidget {
|
||||
this.keyboardType,
|
||||
this.autofillHints,
|
||||
});
|
||||
final TextEditingController controller;
|
||||
final String hint;
|
||||
final bool enabled;
|
||||
final TextInputType? keyboardType;
|
||||
final Iterable<String>? autofillHints;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -9,14 +9,6 @@ import 'package:design_system/design_system.dart';
|
||||
/// Includes a camera icon button for changing the photo.
|
||||
/// Uses only design system tokens for colors, typography, and spacing.
|
||||
class ProfilePhotoWidget extends StatelessWidget {
|
||||
/// The URL of the staff member's photo.
|
||||
final String? photoUrl;
|
||||
|
||||
/// The staff member's full name (used for initial avatar).
|
||||
final String fullName;
|
||||
|
||||
/// Callback when the photo/camera button is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
/// Creates a [ProfilePhotoWidget].
|
||||
const ProfilePhotoWidget({
|
||||
@@ -25,6 +17,14 @@ class ProfilePhotoWidget extends StatelessWidget {
|
||||
required this.fullName,
|
||||
required this.onTap,
|
||||
});
|
||||
/// The URL of the staff member's photo.
|
||||
final String? photoUrl;
|
||||
|
||||
/// The staff member's full name (used for initial avatar).
|
||||
final String fullName;
|
||||
|
||||
/// Callback when the photo/camera button is tapped.
|
||||
final VoidCallback? onTap;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -7,14 +7,6 @@ import 'package:design_system/design_system.dart';
|
||||
/// Displays a full-width button with a save icon and customizable label.
|
||||
/// Uses only design system tokens for colors, typography, and spacing.
|
||||
class SaveButton extends StatelessWidget {
|
||||
/// Callback when the button is pressed.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The button label text.
|
||||
final String label;
|
||||
|
||||
/// Whether to show a loading indicator.
|
||||
final bool isLoading;
|
||||
|
||||
/// Creates a [SaveButton].
|
||||
const SaveButton({
|
||||
@@ -23,6 +15,14 @@ class SaveButton extends StatelessWidget {
|
||||
required this.label,
|
||||
this.isLoading = false,
|
||||
});
|
||||
/// Callback when the button is pressed.
|
||||
final VoidCallback? onPressed;
|
||||
|
||||
/// The button label text.
|
||||
final String label;
|
||||
|
||||
/// Whether to show a loading indicator.
|
||||
final bool isLoading;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
/// Export the modular feature definition.
|
||||
library;
|
||||
export 'src/staff_profile_info_module.dart';
|
||||
|
||||
@@ -28,6 +28,8 @@ dependencies:
|
||||
krow_data_connect:
|
||||
path: ../../../../../data_connect
|
||||
|
||||
firebase_auth: any
|
||||
firebase_data_connect: any
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart' as dc;
|
||||
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:firebase_auth/firebase_auth.dart' as firebase_auth;
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart' as fdc;
|
||||
import 'package:krow_core/core.dart';
|
||||
import '../../domain/repositories/shifts_repository_interface.dart';
|
||||
|
||||
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
class ShiftsRepositoryImpl
|
||||
with dc.DataErrorHandler
|
||||
implements ShiftsRepositoryInterface {
|
||||
final dc.ExampleConnector _dataConnect;
|
||||
final FirebaseAuth _auth = FirebaseAuth.instance;
|
||||
final firebase_auth.FirebaseAuth _auth = firebase_auth.FirebaseAuth.instance;
|
||||
|
||||
ShiftsRepositoryImpl() : _dataConnect = dc.ExampleConnector.instance;
|
||||
|
||||
@@ -22,7 +23,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
|
||||
Future<String> _getStaffId() async {
|
||||
// 1. Check Session Store
|
||||
final StaffSession? session = StaffSessionStore.instance.session;
|
||||
final dc.StaffSession? session = dc.StaffSessionStore.instance.session;
|
||||
if (session?.staff?.id != null) {
|
||||
return session!.staff!.id;
|
||||
}
|
||||
@@ -31,15 +32,15 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
if (_cachedStaffId != null) return _cachedStaffId!;
|
||||
|
||||
// 3. Fetch from Data Connect using Firebase UID
|
||||
final user = _auth.currentUser;
|
||||
final firebase_auth.User? user = _auth.currentUser;
|
||||
if (user == null) {
|
||||
throw Exception('User is not authenticated');
|
||||
}
|
||||
|
||||
try {
|
||||
final response = await _dataConnect
|
||||
final fdc.QueryResult<dc.GetStaffByUserIdData, dc.GetStaffByUserIdVariables> response = await executeProtected(() => _dataConnect
|
||||
.getStaffByUserId(userId: user.uid)
|
||||
.execute();
|
||||
.execute());
|
||||
if (response.data.staffs.isNotEmpty) {
|
||||
_cachedStaffId = response.data.staffs.first.id;
|
||||
return _cachedStaffId!;
|
||||
@@ -52,10 +53,10 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
return user.uid;
|
||||
}
|
||||
|
||||
DateTime? _toDateTime(dynamic t, {String? debugKey}) {
|
||||
DateTime? _toDateTime(dynamic t) {
|
||||
if (t == null) return null;
|
||||
DateTime? dt;
|
||||
if (t is Timestamp) {
|
||||
if (t is fdc.Timestamp) {
|
||||
dt = t.toDateTime();
|
||||
} else if (t is String) {
|
||||
dt = DateTime.tryParse(t);
|
||||
@@ -73,11 +74,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
|
||||
if (dt != null) {
|
||||
final local = DateTimeUtils.toDeviceTime(dt);
|
||||
if (debugKey != null && debugKey.isNotEmpty) {
|
||||
print(
|
||||
'ShiftDate convert: key=$debugKey raw=$t parsed=${dt.toIso8601String()} local=${local.toIso8601String()}',
|
||||
);
|
||||
}
|
||||
|
||||
return local;
|
||||
}
|
||||
return null;
|
||||
@@ -103,135 +100,125 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getHistoryShifts() async {
|
||||
try {
|
||||
final staffId = await _getStaffId();
|
||||
final response = await _dataConnect
|
||||
.listCompletedApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
final List<Shift> shifts = [];
|
||||
final staffId = await _getStaffId();
|
||||
final fdc.QueryResult<dc.ListCompletedApplicationsByStaffIdData, dc.ListCompletedApplicationsByStaffIdVariables> response = await executeProtected(() => _dataConnect
|
||||
.listCompletedApplicationsByStaffId(staffId: staffId)
|
||||
.execute());
|
||||
final List<Shift> shifts = [];
|
||||
|
||||
for (final app in response.data.applications) {
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||
for (final app in response.data.applications) {
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
final DateTime? shiftDate = _toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _toDateTime(app.createdAt);
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
final DateTime? shiftDate = _toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _toDateTime(app.createdAt);
|
||||
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT),
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return shifts;
|
||||
} catch (e) {
|
||||
return <Shift>[];
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: _mapStatus(dc.ApplicationStatus.CHECKED_OUT),
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return shifts;
|
||||
}
|
||||
|
||||
Future<List<Shift>> _fetchApplications({
|
||||
DateTime? start,
|
||||
DateTime? end,
|
||||
}) async {
|
||||
try {
|
||||
final staffId = await _getStaffId();
|
||||
var query = _dataConnect.getApplicationsByStaffId(staffId: staffId);
|
||||
if (start != null && end != null) {
|
||||
query = query
|
||||
.dayStart(_toTimestamp(start))
|
||||
.dayEnd(_toTimestamp(end));
|
||||
}
|
||||
final response = await query.execute();
|
||||
|
||||
final apps = response.data.applications;
|
||||
final List<Shift> shifts = [];
|
||||
|
||||
for (final app in apps) {
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
final DateTime? shiftDate = _toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _toDateTime(app.createdAt);
|
||||
|
||||
// Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED)
|
||||
final bool hasCheckIn = app.checkInTime != null;
|
||||
final bool hasCheckOut = app.checkOutTime != null;
|
||||
dc.ApplicationStatus? appStatus;
|
||||
if (app.status is dc.Known<dc.ApplicationStatus>) {
|
||||
appStatus = (app.status as dc.Known<dc.ApplicationStatus>).value;
|
||||
}
|
||||
final String mappedStatus = hasCheckOut
|
||||
? 'completed'
|
||||
: hasCheckIn
|
||||
? 'checked_in'
|
||||
: _mapStatus(appStatus ?? dc.ApplicationStatus.ACCEPTED);
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: mappedStatus,
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return shifts;
|
||||
} catch (e) {
|
||||
return <Shift>[];
|
||||
final staffId = await _getStaffId();
|
||||
var query = _dataConnect.getApplicationsByStaffId(staffId: staffId);
|
||||
if (start != null && end != null) {
|
||||
query = query.dayStart(_toTimestamp(start)).dayEnd(_toTimestamp(end));
|
||||
}
|
||||
final fdc.QueryResult<dc.GetApplicationsByStaffIdData, dc.GetApplicationsByStaffIdVariables> response = await executeProtected(() => query.execute());
|
||||
|
||||
final apps = response.data.applications;
|
||||
final List<Shift> shifts = [];
|
||||
|
||||
for (final app in apps) {
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.id;
|
||||
|
||||
final String roleName = app.shiftRole.role.name;
|
||||
final String orderName =
|
||||
(app.shift.order.eventName ?? '').trim().isNotEmpty
|
||||
? app.shift.order.eventName!
|
||||
: app.shift.order.business.businessName;
|
||||
final String title = '$roleName - $orderName';
|
||||
final DateTime? shiftDate = _toDateTime(app.shift.date);
|
||||
final DateTime? startDt = _toDateTime(app.shiftRole.startTime);
|
||||
final DateTime? endDt = _toDateTime(app.shiftRole.endTime);
|
||||
final DateTime? createdDt = _toDateTime(app.createdAt);
|
||||
|
||||
// Override status to reflect the application state (e.g., CHECKED_OUT, ACCEPTED)
|
||||
final bool hasCheckIn = app.checkInTime != null;
|
||||
final bool hasCheckOut = app.checkOutTime != null;
|
||||
dc.ApplicationStatus? appStatus;
|
||||
if (app.status is dc.Known<dc.ApplicationStatus>) {
|
||||
appStatus = (app.status as dc.Known<dc.ApplicationStatus>).value;
|
||||
}
|
||||
final String mappedStatus = hasCheckOut
|
||||
? 'completed'
|
||||
: hasCheckIn
|
||||
? 'checked_in'
|
||||
: _mapStatus(appStatus ?? dc.ApplicationStatus.ACCEPTED);
|
||||
shifts.add(
|
||||
Shift(
|
||||
id: app.shift.id,
|
||||
roleId: app.shiftRole.roleId,
|
||||
title: title,
|
||||
clientName: app.shift.order.business.businessName,
|
||||
logoUrl: app.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: app.shiftRole.role.costPerHour,
|
||||
location: app.shift.location ?? '',
|
||||
locationAddress: app.shift.order.teamHub.hubName,
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: mappedStatus,
|
||||
description: app.shift.description,
|
||||
durationDays: app.shift.durationDays,
|
||||
requiredSlots: app.shiftRole.count,
|
||||
filledSlots: app.shiftRole.assigned ?? 0,
|
||||
hasApplied: true,
|
||||
),
|
||||
);
|
||||
}
|
||||
return shifts;
|
||||
}
|
||||
|
||||
Timestamp _toTimestamp(DateTime dateTime) {
|
||||
fdc.Timestamp _toTimestamp(DateTime dateTime) {
|
||||
final DateTime utc = dateTime.toUtc();
|
||||
final int seconds = utc.millisecondsSinceEpoch ~/ 1000;
|
||||
final int nanoseconds = (utc.microsecondsSinceEpoch % 1000000) * 1000;
|
||||
return Timestamp(nanoseconds, seconds);
|
||||
return fdc.Timestamp(nanoseconds, seconds);
|
||||
}
|
||||
|
||||
String _mapStatus(dc.ApplicationStatus status) {
|
||||
@@ -252,74 +239,60 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
||||
try {
|
||||
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||
if (vendorId == null || vendorId.isEmpty) {
|
||||
return <Shift>[];
|
||||
}
|
||||
|
||||
final result = await _dataConnect
|
||||
.listShiftRolesByVendorId(vendorId: vendorId)
|
||||
.execute();
|
||||
final allShiftRoles = result.data.shiftRoles;
|
||||
|
||||
final List<Shift> mappedShifts = [];
|
||||
for (final sr in allShiftRoles) {
|
||||
print(
|
||||
'FindShifts raw: shiftId=${sr.shiftId} roleId=${sr.roleId} '
|
||||
'start=${sr.startTime?.toJson()} end=${sr.endTime?.toJson()} '
|
||||
'shiftDate=${sr.shift.date?.toJson()}',
|
||||
);
|
||||
final DateTime? shiftDate = _toDateTime(sr.shift.date);
|
||||
final startDt = _toDateTime(sr.startTime);
|
||||
final endDt = _toDateTime(sr.endTime);
|
||||
final createdDt = _toDateTime(sr.createdAt);
|
||||
print(
|
||||
'FindShifts mapped: shiftId=${sr.shiftId} '
|
||||
'origStart=${sr.startTime?.toJson()} '
|
||||
'origEnd=${sr.endTime?.toJson()} '
|
||||
'mappedStart=${startDt != null ? DateFormat('HH:mm').format(startDt) : ''} '
|
||||
'mappedEnd=${endDt != null ? DateFormat('HH:mm').format(endDt) : ''}',
|
||||
);
|
||||
mappedShifts.add(
|
||||
Shift(
|
||||
id: sr.shiftId,
|
||||
roleId: sr.roleId,
|
||||
title: sr.role.name,
|
||||
clientName: sr.shift.order.business.businessName,
|
||||
logoUrl: null,
|
||||
hourlyRate: sr.role.costPerHour,
|
||||
location: sr.shift.location ?? '',
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null
|
||||
? DateFormat('HH:mm').format(startDt)
|
||||
: '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
||||
description: sr.shift.description,
|
||||
durationDays: sr.shift.durationDays,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
return mappedShifts
|
||||
.where(
|
||||
(s) =>
|
||||
s.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return mappedShifts;
|
||||
} catch (e) {
|
||||
final String? vendorId = dc.StaffSessionStore.instance.session?.ownerId;
|
||||
if (vendorId == null || vendorId.isEmpty) {
|
||||
return <Shift>[];
|
||||
}
|
||||
|
||||
final fdc.QueryResult<dc.ListShiftRolesByVendorIdData, dc.ListShiftRolesByVendorIdVariables> result = await executeProtected(() => _dataConnect
|
||||
.listShiftRolesByVendorId(vendorId: vendorId)
|
||||
.execute());
|
||||
final allShiftRoles = result.data.shiftRoles;
|
||||
|
||||
final List<Shift> mappedShifts = [];
|
||||
for (final sr in allShiftRoles) {
|
||||
|
||||
final DateTime? shiftDate = _toDateTime(sr.shift.date);
|
||||
final startDt = _toDateTime(sr.startTime);
|
||||
final endDt = _toDateTime(sr.endTime);
|
||||
final createdDt = _toDateTime(sr.createdAt);
|
||||
|
||||
mappedShifts.add(
|
||||
Shift(
|
||||
id: sr.shiftId,
|
||||
roleId: sr.roleId,
|
||||
title: sr.role.name,
|
||||
clientName: sr.shift.order.business.businessName,
|
||||
logoUrl: null,
|
||||
hourlyRate: sr.role.costPerHour,
|
||||
location: sr.shift.location ?? '',
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: shiftDate?.toIso8601String() ?? '',
|
||||
startTime: startDt != null
|
||||
? DateFormat('HH:mm').format(startDt)
|
||||
: '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: sr.shift.status?.stringValue.toLowerCase() ?? 'open',
|
||||
description: sr.shift.description,
|
||||
durationDays: sr.shift.durationDays,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (query.isNotEmpty) {
|
||||
return mappedShifts
|
||||
.where(
|
||||
(s) =>
|
||||
s.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(query.toLowerCase()),
|
||||
)
|
||||
.toList();
|
||||
}
|
||||
|
||||
return mappedShifts;
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -328,108 +301,101 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
}
|
||||
|
||||
Future<Shift?> _getShiftDetails(String shiftId, {String? roleId}) async {
|
||||
try {
|
||||
if (roleId != null && roleId.isNotEmpty) {
|
||||
final roleResult = await _dataConnect
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
|
||||
.execute();
|
||||
final sr = roleResult.data.shiftRole;
|
||||
if (sr == null) return null;
|
||||
if (roleId != null && roleId.isNotEmpty) {
|
||||
final roleResult = await executeProtected(() => _dataConnect
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: roleId)
|
||||
.execute());
|
||||
final sr = roleResult.data.shiftRole;
|
||||
if (sr == null) return null;
|
||||
|
||||
final DateTime? startDt = _toDateTime(sr.startTime);
|
||||
final DateTime? endDt = _toDateTime(sr.endTime);
|
||||
final DateTime? createdDt = _toDateTime(sr.createdAt);
|
||||
final DateTime? startDt = _toDateTime(sr.startTime);
|
||||
final DateTime? endDt = _toDateTime(sr.endTime);
|
||||
final DateTime? createdDt = _toDateTime(sr.createdAt);
|
||||
|
||||
final String? staffId = await _getStaffId();
|
||||
bool hasApplied = false;
|
||||
String status = 'open';
|
||||
if (staffId != null) {
|
||||
final apps = await _dataConnect
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
final app = apps.data.applications
|
||||
.where(
|
||||
(a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId,
|
||||
)
|
||||
.firstOrNull;
|
||||
if (app != null) {
|
||||
hasApplied = true;
|
||||
if (app.status is dc.Known<dc.ApplicationStatus>) {
|
||||
final dc.ApplicationStatus s =
|
||||
(app.status as dc.Known<dc.ApplicationStatus>).value;
|
||||
status = _mapStatus(s);
|
||||
}
|
||||
}
|
||||
final String staffId = await _getStaffId();
|
||||
bool hasApplied = false;
|
||||
String status = 'open';
|
||||
final apps = await executeProtected(() =>
|
||||
_dataConnect.getApplicationsByStaffId(staffId: staffId).execute());
|
||||
final app = apps.data.applications
|
||||
.where(
|
||||
(a) => a.shiftId == shiftId && a.shiftRole.roleId == roleId,
|
||||
)
|
||||
.firstOrNull;
|
||||
if (app != null) {
|
||||
hasApplied = true;
|
||||
if (app.status is dc.Known<dc.ApplicationStatus>) {
|
||||
final dc.ApplicationStatus s =
|
||||
(app.status as dc.Known<dc.ApplicationStatus>).value;
|
||||
status = _mapStatus(s);
|
||||
}
|
||||
|
||||
return Shift(
|
||||
id: sr.shiftId,
|
||||
roleId: sr.roleId,
|
||||
title: sr.shift.order.business.businessName,
|
||||
clientName: sr.shift.order.business.businessName,
|
||||
logoUrl: sr.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: sr.role.costPerHour,
|
||||
location: sr.shift.location ?? sr.shift.order.teamHub.hubName,
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: startDt?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: status,
|
||||
description: sr.shift.description,
|
||||
durationDays: null,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
hasApplied: hasApplied,
|
||||
totalValue: sr.totalValue,
|
||||
);
|
||||
}
|
||||
|
||||
final result = await _dataConnect.getShiftById(id: shiftId).execute();
|
||||
final s = result.data.shift;
|
||||
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 endDt = _toDateTime(s.endTime);
|
||||
final createdDt = _toDateTime(s.createdAt);
|
||||
|
||||
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
clientName: s.order.business.businessName,
|
||||
logoUrl: null,
|
||||
hourlyRate: s.cost ?? 0.0,
|
||||
location: s.location ?? '',
|
||||
locationAddress: s.locationAddress ?? '',
|
||||
id: sr.shiftId,
|
||||
roleId: sr.roleId,
|
||||
title: sr.shift.order.business.businessName,
|
||||
clientName: sr.shift.order.business.businessName,
|
||||
logoUrl: sr.shift.order.business.companyLogoUrl,
|
||||
hourlyRate: sr.role.costPerHour,
|
||||
location: sr.shift.location ?? sr.shift.order.teamHub.hubName,
|
||||
locationAddress: sr.shift.locationAddress ?? '',
|
||||
date: startDt?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: s.status?.stringValue ?? 'OPEN',
|
||||
description: s.description,
|
||||
durationDays: s.durationDays,
|
||||
requiredSlots: required,
|
||||
filledSlots: filled,
|
||||
status: status,
|
||||
description: sr.shift.description,
|
||||
durationDays: null,
|
||||
requiredSlots: sr.count,
|
||||
filledSlots: sr.assigned ?? 0,
|
||||
hasApplied: hasApplied,
|
||||
totalValue: sr.totalValue,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
|
||||
final fdc.QueryResult<dc.GetShiftByIdData, dc.GetShiftByIdVariables> result =
|
||||
await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute());
|
||||
final s = result.data.shift;
|
||||
if (s == null) return null;
|
||||
|
||||
int? required;
|
||||
int? filled;
|
||||
try {
|
||||
final rolesRes = await executeProtected(() =>
|
||||
_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 endDt = _toDateTime(s.endTime);
|
||||
final createdDt = _toDateTime(s.createdAt);
|
||||
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
clientName: s.order.business.businessName,
|
||||
logoUrl: null,
|
||||
hourlyRate: s.cost ?? 0.0,
|
||||
location: s.location ?? '',
|
||||
locationAddress: s.locationAddress ?? '',
|
||||
date: startDt?.toIso8601String() ?? '',
|
||||
startTime: startDt != null ? DateFormat('HH:mm').format(startDt) : '',
|
||||
endTime: endDt != null ? DateFormat('HH:mm').format(endDt) : '',
|
||||
createdDate: createdDt?.toIso8601String() ?? '',
|
||||
status: s.status?.stringValue ?? 'OPEN',
|
||||
description: s.description,
|
||||
durationDays: s.durationDays,
|
||||
requiredSlots: required,
|
||||
filledSlots: filled,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
@@ -445,14 +411,15 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
throw Exception('Missing role id.');
|
||||
}
|
||||
|
||||
final roleResult = await _dataConnect
|
||||
final roleResult = await executeProtected(() => _dataConnect
|
||||
.getShiftRoleById(shiftId: shiftId, roleId: targetRoleId)
|
||||
.execute();
|
||||
.execute());
|
||||
final role = roleResult.data.shiftRole;
|
||||
if (role == null) {
|
||||
throw Exception('Shift role not found');
|
||||
}
|
||||
final shiftResult = await _dataConnect.getShiftById(id: shiftId).execute();
|
||||
final shiftResult =
|
||||
await executeProtected(() => _dataConnect.getShiftById(id: shiftId).execute());
|
||||
final shift = shiftResult.data.shift;
|
||||
if (shift == null) {
|
||||
throw Exception('Shift not found');
|
||||
@@ -474,26 +441,23 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
999,
|
||||
999,
|
||||
);
|
||||
print(
|
||||
'Staff applyForShift: dayStartUtc=${_toTimestamp(dayStartUtc).toJson()} '
|
||||
'dayEndUtc=${_toTimestamp(dayEndUtc).toJson()}',
|
||||
);
|
||||
final dayApplications = await _dataConnect
|
||||
|
||||
final dayApplications = await executeProtected(() => _dataConnect
|
||||
.vaidateDayStaffApplication(staffId: staffId)
|
||||
.dayStart(_toTimestamp(dayStartUtc))
|
||||
.dayEnd(_toTimestamp(dayEndUtc))
|
||||
.execute();
|
||||
.execute());
|
||||
if (dayApplications.data.applications.isNotEmpty) {
|
||||
throw Exception('The user already has a shift that day.');
|
||||
}
|
||||
}
|
||||
final existingApplicationResult = await _dataConnect
|
||||
final existingApplicationResult = await executeProtected(() => _dataConnect
|
||||
.getApplicationByStaffShiftAndRole(
|
||||
staffId: staffId,
|
||||
shiftId: shiftId,
|
||||
roleId: targetRoleId,
|
||||
)
|
||||
.execute();
|
||||
.execute());
|
||||
if (existingApplicationResult.data.applications.isNotEmpty) {
|
||||
throw Exception('Application already exists.');
|
||||
}
|
||||
@@ -508,7 +472,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
bool updatedRole = false;
|
||||
bool updatedShift = false;
|
||||
try {
|
||||
final appResult = await _dataConnect
|
||||
final appResult = await executeProtected(() => _dataConnect
|
||||
.createApplication(
|
||||
shiftId: shiftId,
|
||||
staffId: staffId,
|
||||
@@ -517,29 +481,36 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
)
|
||||
// TODO: this should be PENDING so a vendor can accept it.
|
||||
.execute();
|
||||
.execute());
|
||||
appId = appResult.data.application_insert.id;
|
||||
|
||||
await _dataConnect
|
||||
await executeProtected(() => _dataConnect
|
||||
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
|
||||
.assigned(assigned + 1)
|
||||
.execute();
|
||||
.execute());
|
||||
updatedRole = true;
|
||||
|
||||
await _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute();
|
||||
await executeProtected(
|
||||
() => _dataConnect.updateShift(id: shiftId).filled(filled + 1).execute());
|
||||
updatedShift = true;
|
||||
} catch (e) {
|
||||
if (updatedShift) {
|
||||
await _dataConnect.updateShift(id: shiftId).filled(filled).execute();
|
||||
try {
|
||||
await _dataConnect.updateShift(id: shiftId).filled(filled).execute();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (updatedRole) {
|
||||
await _dataConnect
|
||||
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
|
||||
.assigned(assigned)
|
||||
.execute();
|
||||
try {
|
||||
await _dataConnect
|
||||
.updateShiftRole(shiftId: shiftId, roleId: targetRoleId)
|
||||
.assigned(assigned)
|
||||
.execute();
|
||||
} catch (_) {}
|
||||
}
|
||||
if (appId != null) {
|
||||
await _dataConnect.deleteApplication(id: appId).execute();
|
||||
try {
|
||||
await _dataConnect.deleteApplication(id: appId).execute();
|
||||
} catch (_) {}
|
||||
}
|
||||
rethrow;
|
||||
}
|
||||
@@ -573,9 +544,8 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
} else {
|
||||
// Fallback fetch
|
||||
final staffId = await _getStaffId();
|
||||
final apps = await _dataConnect
|
||||
.getApplicationsByStaffId(staffId: staffId)
|
||||
.execute();
|
||||
final apps = await executeProtected(() =>
|
||||
_dataConnect.getApplicationsByStaffId(staffId: staffId).execute());
|
||||
final app = apps.data.applications
|
||||
.where((a) => a.shiftId == shiftId)
|
||||
.firstOrNull;
|
||||
@@ -588,13 +558,12 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
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();
|
||||
final rolesResult = await executeProtected(() =>
|
||||
_dataConnect.listShiftRolesByShiftId(shiftId: shiftId).execute());
|
||||
if (rolesResult.data.shiftRoles.isNotEmpty) {
|
||||
final role = rolesResult.data.shiftRoles.first;
|
||||
final staffId = await _getStaffId();
|
||||
await _dataConnect
|
||||
await executeProtected(() => _dataConnect
|
||||
.createApplication(
|
||||
shiftId: shiftId,
|
||||
staffId: staffId,
|
||||
@@ -602,16 +571,16 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
status: dc.ApplicationStatus.REJECTED,
|
||||
origin: dc.ApplicationOrigin.STAFF,
|
||||
)
|
||||
.execute();
|
||||
.execute());
|
||||
return;
|
||||
}
|
||||
}
|
||||
throw Exception("Application not found for shift $shiftId");
|
||||
}
|
||||
|
||||
await _dataConnect
|
||||
.updateApplicationStatus(id: appId)
|
||||
await executeProtected(() => _dataConnect
|
||||
.updateApplicationStatus(id: appId!)
|
||||
.status(newStatus)
|
||||
.execute();
|
||||
.execute());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/shifts_repository_interface.dart';
|
||||
|
||||
class AcceptShiftUseCase {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import '../repositories/shifts_repository_interface.dart';
|
||||
|
||||
class DeclineShiftUseCase {
|
||||
|
||||
@@ -223,7 +223,7 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
const SizedBox(height: 8),
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
SizedBox(
|
||||
width: 24,
|
||||
height: 24,
|
||||
child: displayShift.logoUrl != null
|
||||
@@ -462,12 +462,12 @@ class _ShiftDetailsPageState extends State<ShiftDetailsPage> {
|
||||
),
|
||||
],
|
||||
const SizedBox(height: 20),
|
||||
if (displayShift!.status != 'confirmed' &&
|
||||
displayShift!.hasApplied != true &&
|
||||
(displayShift!.requiredSlots == null ||
|
||||
displayShift!.filledSlots == null ||
|
||||
displayShift!.filledSlots! <
|
||||
displayShift!.requiredSlots!))
|
||||
if (displayShift.status != 'confirmed' &&
|
||||
displayShift.hasApplied != true &&
|
||||
(displayShift.requiredSlots == null ||
|
||||
displayShift.filledSlots == null ||
|
||||
displayShift.filledSlots! <
|
||||
displayShift.requiredSlots!))
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
|
||||
@@ -22,12 +22,6 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
String _searchQuery = '';
|
||||
String _jobType = 'all';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
print('FindShiftsTab init: tab entered, data pending');
|
||||
}
|
||||
|
||||
Widget _buildFilterTab(String id, String label) {
|
||||
final isSelected = _jobType == id;
|
||||
return GestureDetector(
|
||||
@@ -69,8 +63,9 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
if (_jobType == 'one-day') {
|
||||
return s.durationDays == null || s.durationDays! <= 1;
|
||||
}
|
||||
if (_jobType == 'multi-day')
|
||||
if (_jobType == 'multi-day') {
|
||||
return s.durationDays != null && s.durationDays! > 1;
|
||||
}
|
||||
return true;
|
||||
}).toList();
|
||||
|
||||
@@ -176,21 +171,8 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
|
||||
...filteredJobs.map(
|
||||
(shift) => Padding(
|
||||
padding: const EdgeInsets.only(bottom: 12),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Debug shiftId: ${shift.id}',
|
||||
style: const TextStyle(
|
||||
fontSize: 10,
|
||||
color: Color(0xFF94A3B8),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
MyShiftCard(
|
||||
shift: shift,
|
||||
),
|
||||
],
|
||||
child: MyShiftCard(
|
||||
shift: shift,
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
@@ -13,7 +13,6 @@ import 'domain/usecases/get_shift_details_usecase.dart';
|
||||
import 'presentation/blocs/shifts/shifts_bloc.dart';
|
||||
import 'presentation/blocs/shift_details/shift_details_bloc.dart';
|
||||
import 'presentation/pages/shifts_page.dart';
|
||||
import 'presentation/pages/shift_details_page.dart';
|
||||
|
||||
class StaffShiftsModule extends Module {
|
||||
@override
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
library staff_shifts;
|
||||
library;
|
||||
|
||||
export 'src/staff_shifts_module.dart';
|
||||
export 'src/shift_details_module.dart';
|
||||
|
||||
Reference in New Issue
Block a user