Merge pull request #348 from Oloodi/fix_staff_app_bugs

Fix staff app bugs
This commit is contained in:
José Salazar
2026-02-02 09:25:09 -05:00
committed by GitHub
20 changed files with 20305 additions and 20089 deletions

View File

@@ -1,16 +1,16 @@
# Basic Usage # Basic Usage
```dart ```dart
ExampleConnector.instance.CreateStaff(createStaffVariables).execute(); ExampleConnector.instance.createTaskComment(createTaskCommentVariables).execute();
ExampleConnector.instance.UpdateStaff(updateStaffVariables).execute(); ExampleConnector.instance.updateTaskComment(updateTaskCommentVariables).execute();
ExampleConnector.instance.DeleteStaff(deleteStaffVariables).execute(); ExampleConnector.instance.deleteTaskComment(deleteTaskCommentVariables).execute();
ExampleConnector.instance.listStaffAvailabilities(listStaffAvailabilitiesVariables).execute(); ExampleConnector.instance.createTaxForm(createTaxFormVariables).execute();
ExampleConnector.instance.listStaffAvailabilitiesByStaffId(listStaffAvailabilitiesByStaffIdVariables).execute(); ExampleConnector.instance.updateTaxForm(updateTaxFormVariables).execute();
ExampleConnector.instance.getStaffAvailabilityByKey(getStaffAvailabilityByKeyVariables).execute(); ExampleConnector.instance.deleteTaxForm(deleteTaxFormVariables).execute();
ExampleConnector.instance.listStaffAvailabilitiesByDay(listStaffAvailabilitiesByDayVariables).execute(); ExampleConnector.instance.createUserConversation(createUserConversationVariables).execute();
ExampleConnector.instance.createStaffAvailabilityStats(createStaffAvailabilityStatsVariables).execute(); ExampleConnector.instance.updateUserConversation(updateUserConversationVariables).execute();
ExampleConnector.instance.updateStaffAvailabilityStats(updateStaffAvailabilityStatsVariables).execute(); ExampleConnector.instance.markConversationAsRead(markConversationAsReadVariables).execute();
ExampleConnector.instance.deleteStaffAvailabilityStats(deleteStaffAvailabilityStatsVariables).execute(); ExampleConnector.instance.incrementUnreadForUser(incrementUnreadForUserVariables).execute();
``` ```
@@ -23,7 +23,7 @@ Optional fields can be discovered based on classes that have `Optional` object t
This is an example of a mutation with an optional field: This is an example of a mutation with an optional field:
```dart ```dart
await ExampleConnector.instance.searchInvoiceTemplatesByOwnerAndName({ ... }) await ExampleConnector.instance.listStaffAvailabilitiesByDay({ ... })
.offset(...) .offset(...)
.execute(); .execute();
``` ```

View File

@@ -7,7 +7,7 @@ class UpdateApplicationStatusVariablesBuilder {
Optional<ApplicationStatus> _status = Optional.optional((data) => ApplicationStatus.values.byName(data), enumSerializer); Optional<ApplicationStatus> _status = Optional.optional((data) => ApplicationStatus.values.byName(data), enumSerializer);
Optional<Timestamp> _checkInTime = Optional.optional((json) => json['checkInTime'] = Timestamp.fromJson(json['checkInTime']), defaultSerializer); Optional<Timestamp> _checkInTime = Optional.optional((json) => json['checkInTime'] = Timestamp.fromJson(json['checkInTime']), defaultSerializer);
Optional<Timestamp> _checkOutTime = Optional.optional((json) => json['checkOutTime'] = Timestamp.fromJson(json['checkOutTime']), defaultSerializer); Optional<Timestamp> _checkOutTime = Optional.optional((json) => json['checkOutTime'] = Timestamp.fromJson(json['checkOutTime']), defaultSerializer);
String roleId; Optional<String> _roleId = Optional.optional(nativeFromJson, nativeToJson);
final FirebaseDataConnect _dataConnect; UpdateApplicationStatusVariablesBuilder shiftId(String? t) { final FirebaseDataConnect _dataConnect; UpdateApplicationStatusVariablesBuilder shiftId(String? t) {
_shiftId.value = t; _shiftId.value = t;
@@ -29,8 +29,12 @@ class UpdateApplicationStatusVariablesBuilder {
_checkOutTime.value = t; _checkOutTime.value = t;
return this; return this;
} }
UpdateApplicationStatusVariablesBuilder roleId(String? t) {
_roleId.value = t;
return this;
}
UpdateApplicationStatusVariablesBuilder(this._dataConnect, {required this.id,required this.roleId,}); UpdateApplicationStatusVariablesBuilder(this._dataConnect, {required this.id,});
Deserializer<UpdateApplicationStatusData> dataDeserializer = (dynamic json) => UpdateApplicationStatusData.fromJson(jsonDecode(json)); Deserializer<UpdateApplicationStatusData> dataDeserializer = (dynamic json) => UpdateApplicationStatusData.fromJson(jsonDecode(json));
Serializer<UpdateApplicationStatusVariables> varsSerializer = (UpdateApplicationStatusVariables vars) => jsonEncode(vars.toJson()); Serializer<UpdateApplicationStatusVariables> varsSerializer = (UpdateApplicationStatusVariables vars) => jsonEncode(vars.toJson());
Future<OperationResult<UpdateApplicationStatusData, UpdateApplicationStatusVariables>> execute() { Future<OperationResult<UpdateApplicationStatusData, UpdateApplicationStatusVariables>> execute() {
@@ -38,7 +42,7 @@ class UpdateApplicationStatusVariablesBuilder {
} }
MutationRef<UpdateApplicationStatusData, UpdateApplicationStatusVariables> ref() { MutationRef<UpdateApplicationStatusData, UpdateApplicationStatusVariables> ref() {
UpdateApplicationStatusVariables vars= UpdateApplicationStatusVariables(id: id,shiftId: _shiftId,staffId: _staffId,status: _status,checkInTime: _checkInTime,checkOutTime: _checkOutTime,roleId: roleId,); UpdateApplicationStatusVariables vars= UpdateApplicationStatusVariables(id: id,shiftId: _shiftId,staffId: _staffId,status: _status,checkInTime: _checkInTime,checkOutTime: _checkOutTime,roleId: _roleId,);
return _dataConnect.mutation("updateApplicationStatus", dataDeserializer, varsSerializer, vars); return _dataConnect.mutation("updateApplicationStatus", dataDeserializer, varsSerializer, vars);
} }
} }
@@ -121,12 +125,11 @@ class UpdateApplicationStatusVariables {
late final Optional<ApplicationStatus>status; late final Optional<ApplicationStatus>status;
late final Optional<Timestamp>checkInTime; late final Optional<Timestamp>checkInTime;
late final Optional<Timestamp>checkOutTime; late final Optional<Timestamp>checkOutTime;
final String roleId; late final Optional<String>roleId;
@Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.') @Deprecated('fromJson is deprecated for Variable classes as they are no longer required for deserialization.')
UpdateApplicationStatusVariables.fromJson(Map<String, dynamic> json): UpdateApplicationStatusVariables.fromJson(Map<String, dynamic> json):
id = nativeFromJson<String>(json['id']), id = nativeFromJson<String>(json['id']) {
roleId = nativeFromJson<String>(json['roleId']) {
@@ -150,6 +153,9 @@ class UpdateApplicationStatusVariables {
checkOutTime.value = json['checkOutTime'] == null ? null : Timestamp.fromJson(json['checkOutTime']); checkOutTime.value = json['checkOutTime'] == null ? null : Timestamp.fromJson(json['checkOutTime']);
roleId = Optional.optional(nativeFromJson, nativeToJson);
roleId.value = json['roleId'] == null ? null : nativeFromJson<String>(json['roleId']);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
@@ -192,7 +198,9 @@ class UpdateApplicationStatusVariables {
if(checkOutTime.state == OptionalState.set) { if(checkOutTime.state == OptionalState.set) {
json['checkOutTime'] = checkOutTime.toJson(); json['checkOutTime'] = checkOutTime.toJson();
} }
json['roleId'] = nativeToJson<String>(roleId); if(roleId.state == OptionalState.set) {
json['roleId'] = roleId.toJson();
}
return json; return json;
} }

View File

@@ -10,6 +10,7 @@ class ClockInAdapter {
DateTime? checkInTime, DateTime? checkInTime,
DateTime? checkOutTime, DateTime? checkOutTime,
String? activeShiftId, String? activeShiftId,
String? activeApplicationId,
}) { }) {
final bool isCheckedIn = status == 'CHECKED_IN' || status == 'LATE'; // Assuming LATE is also checked in? final bool isCheckedIn = status == 'CHECKED_IN' || status == 'LATE'; // Assuming LATE is also checked in?
@@ -21,6 +22,7 @@ class ClockInAdapter {
checkInTime: checkInTime, checkInTime: checkInTime,
checkOutTime: checkOutTime, checkOutTime: checkOutTime,
activeShiftId: activeShiftId, activeShiftId: activeShiftId,
activeApplicationId: activeApplicationId,
); );
} }
} }

View File

@@ -6,14 +6,22 @@ class AttendanceStatus extends Equatable {
final DateTime? checkInTime; final DateTime? checkInTime;
final DateTime? checkOutTime; final DateTime? checkOutTime;
final String? activeShiftId; final String? activeShiftId;
final String? activeApplicationId;
const AttendanceStatus({ const AttendanceStatus({
this.isCheckedIn = false, this.isCheckedIn = false,
this.checkInTime, this.checkInTime,
this.checkOutTime, this.checkOutTime,
this.activeShiftId, this.activeShiftId,
this.activeApplicationId,
}); });
@override @override
List<Object?> get props => [isCheckedIn, checkInTime, checkOutTime, activeShiftId]; List<Object?> get props => [
isCheckedIn,
checkInTime,
checkOutTime,
activeShiftId,
activeApplicationId,
];
} }

View File

@@ -45,7 +45,7 @@ class ViewOrdersRepositoryImpl implements IViewOrdersRepository {
'Your Company'; 'Your Company';
return result.data.shiftRoles.map((dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole) { return result.data.shiftRoles.map((dc.ListShiftRolesByBusinessAndDateRangeShiftRoles shiftRole) {
final DateTime? shiftDate = shiftRole.shift.date?.toDateTime(); final DateTime? shiftDate = shiftRole.shift.date?.toDateTime().toLocal();
final String dateStr = shiftDate == null final String dateStr = shiftDate == null
? '' ? ''
: DateFormat('yyyy-MM-dd').format(shiftDate); : DateFormat('yyyy-MM-dd').format(shiftDate);

View File

@@ -9,6 +9,7 @@ import '../../domain/repositories/clock_in_repository_interface.dart';
class ClockInRepositoryImpl implements ClockInRepositoryInterface { class ClockInRepositoryImpl implements ClockInRepositoryInterface {
final dc.ExampleConnector _dataConnect; final dc.ExampleConnector _dataConnect;
final Map<String, String> _shiftToApplicationId = {}; final Map<String, String> _shiftToApplicationId = {};
String? _activeApplicationId;
ClockInRepositoryImpl({ ClockInRepositoryImpl({
required dc.ExampleConnector dataConnect, required dc.ExampleConnector dataConnect,
@@ -187,16 +188,34 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
return const AttendanceStatus(isCheckedIn: false); return const AttendanceStatus(isCheckedIn: false);
} }
final dc.GetApplicationsByStaffIdApplications? activeApp = dc.GetApplicationsByStaffIdApplications? activeApp;
_getActiveApplication(apps); for (final app in apps) {
final dc.GetApplicationsByStaffIdApplications app = if (app.checkInTime != null && app.checkOutTime == null) {
activeApp ?? apps.last; if (activeApp == null) {
activeApp = app;
} else {
final DateTime? current = _toDateTime(activeApp.checkInTime);
final DateTime? next = _toDateTime(app.checkInTime);
if (current == null || (next != null && next.isAfter(current))) {
activeApp = app;
}
}
}
}
return ClockInAdapter.toAttendanceStatus( if (activeApp == null) {
status: app.status.stringValue, _activeApplicationId = null;
checkInTime: _toDateTime(app.checkInTime), return const AttendanceStatus(isCheckedIn: false);
checkOutTime: _toDateTime(app.checkOutTime), }
activeShiftId: app.shiftId,
_activeApplicationId = activeApp.id;
print('Active check-in appId=$_activeApplicationId');
return AttendanceStatus(
isCheckedIn: true,
checkInTime: _toDateTime(activeApp.checkInTime),
checkOutTime: _toDateTime(activeApp.checkOutTime),
activeShiftId: activeApp.shiftId,
activeApplicationId: activeApp.id,
); );
} }
@@ -215,34 +234,74 @@ class ClockInRepositoryImpl implements ClockInRepositoryInterface {
app ??= (await _getTodaysApplications(staffId)) app ??= (await _getTodaysApplications(staffId))
.firstWhere((a) => a.shiftId == shiftId); .firstWhere((a) => a.shiftId == shiftId);
await _dataConnect final Timestamp checkInTs = _fromDateTime(DateTime.now());
.updateApplicationStatus( print(
id: app.id, 'ClockIn request: appId=${app.id} shiftId=$shiftId '
roleId: app.shiftRole.id, 'checkInTime=${checkInTs.toJson()}',
) );
.checkInTime(_fromDateTime(DateTime.now())) try {
.execute(); 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;
}
return getAttendanceStatus(); return getAttendanceStatus();
} }
@override @override
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes}) async { Future<AttendanceStatus> clockOut({
String? notes,
int? breakTimeMinutes,
String? applicationId,
}) async {
final String staffId = await _getStaffId(); final String staffId = await _getStaffId();
final List<dc.GetApplicationsByStaffIdApplications> apps = print(
await _getTodaysApplications(staffId); 'ClockOut request: applicationId=$applicationId '
final dc.GetApplicationsByStaffIdApplications? app = 'activeApplicationId=$_activeApplicationId',
_getActiveApplication(apps); );
if (app == null) throw Exception('No active shift found to clock out'); final String? targetAppId = applicationId ?? _activeApplicationId;
if (targetAppId == null || targetAppId.isEmpty) {
throw Exception('No active application id for checkout');
}
final appResult = await _dataConnect
.getApplicationById(id: targetAppId)
.execute();
final app = appResult.data.application;
print(
'ClockOut getApplicationById: id=${app?.id} '
'checkIn=${app?.checkInTime?.toJson()} '
'checkOut=${app?.checkOutTime?.toJson()}',
);
if (app == null) {
throw Exception('Application not found for checkout');
}
if (app.checkInTime == null || app.checkOutTime != null) {
throw Exception('No active shift found to clock out');
}
await _dataConnect.updateApplicationStatus( await _dataConnect
id: app.id, .updateApplicationStatus(
roleId: app.shiftRole.id, id: targetAppId,
) )
.status(dc.ApplicationStatus.CHECKED_OUT) .checkOutTime(_fromDateTime(DateTime.now()))
.checkOutTime(_fromDateTime(DateTime.now())) .execute();
.execute();
return getAttendanceStatus(); return getAttendanceStatus();
} }

View File

@@ -8,12 +8,16 @@ class ClockOutArguments extends UseCaseArgument {
/// Optional break time in minutes. /// Optional break time in minutes.
final int? breakTimeMinutes; final int? breakTimeMinutes;
/// Optional application id for checkout.
final String? applicationId;
/// Creates a [ClockOutArguments] instance. /// Creates a [ClockOutArguments] instance.
const ClockOutArguments({ const ClockOutArguments({
this.notes, this.notes,
this.breakTimeMinutes, this.breakTimeMinutes,
this.applicationId,
}); });
@override @override
List<Object?> get props => [notes, breakTimeMinutes]; List<Object?> get props => [notes, breakTimeMinutes, applicationId];
} }

View File

@@ -17,5 +17,9 @@ abstract class ClockInRepositoryInterface {
/// Checks the user out for the currently active shift. /// Checks the user out for the currently active shift.
/// Optionally accepts [breakTimeMinutes] if tracked. /// Optionally accepts [breakTimeMinutes] if tracked.
Future<AttendanceStatus> clockOut({String? notes, int? breakTimeMinutes}); Future<AttendanceStatus> clockOut({
String? notes,
int? breakTimeMinutes,
String? applicationId,
});
} }

View File

@@ -14,6 +14,7 @@ class ClockOutUseCase implements UseCase<ClockOutArguments, AttendanceStatus> {
return _repository.clockOut( return _repository.clockOut(
notes: arguments.notes, notes: arguments.notes,
breakTimeMinutes: arguments.breakTimeMinutes, breakTimeMinutes: arguments.breakTimeMinutes,
applicationId: arguments.applicationId,
); );
} }
} }

View File

@@ -220,6 +220,7 @@ class ClockInBloc extends Bloc<ClockInEvent, ClockInState> {
ClockOutArguments( ClockOutArguments(
notes: event.notes, notes: event.notes,
breakTimeMinutes: 0, // Should be passed from event if supported breakTimeMinutes: 0, // Should be passed from event if supported
applicationId: state.attendance.activeApplicationId,
), ),
); );
emit(state.copyWith( emit(state.copyWith(

View File

@@ -52,7 +52,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
return user.uid; return user.uid;
} }
DateTime? _toDateTime(dynamic t) { DateTime? _toDateTime(dynamic t, {String? debugKey}) {
if (t == null) return null; if (t == null) return null;
DateTime? dt; DateTime? dt;
if (t is Timestamp) { if (t is Timestamp) {
@@ -72,60 +72,33 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
} }
if (dt != null) { if (dt != null) {
return DateTimeUtils.toDeviceTime(dt); 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; return null;
} }
/// Helper method to map Data Connect application to domain Shift using ShiftAdapter.
Shift _mapApplicationToShift(
dynamic app,
String status, {
bool hasApplied = true,
}) {
return ShiftAdapter.fromApplicationData(
shiftId: app.shift.id,
roleId: app.shiftRole.roleId,
roleName: app.shiftRole.role.name,
businessName: app.shift.order.business.businessName,
companyLogoUrl: app.shift.order.business.companyLogoUrl,
costPerHour: app.shiftRole.role.costPerHour,
shiftLocation: app.shift.location,
teamHubName: app.shift.order.teamHub.hubName,
shiftDate: _toDateTime(app.shift.date),
startTime: _toDateTime(app.shiftRole.startTime),
endTime: _toDateTime(app.shiftRole.endTime),
createdAt: _toDateTime(app.createdAt),
status: status,
description: app.shift.description,
durationDays: app.shift.durationDays,
count: app.shiftRole.count,
assigned: app.shiftRole.assigned,
eventName: app.shift.order.eventName,
hasApplied: hasApplied,
);
}
@override @override
Future<List<Shift>> getMyShifts({ Future<List<Shift>> getMyShifts({
required DateTime start, required DateTime start,
required DateTime end, required DateTime end,
}) async { }) async {
return _fetchApplications( return _fetchApplications(start: start, end: end);
[dc.ApplicationStatus.ACCEPTED, dc.ApplicationStatus.CONFIRMED],
start: start,
end: end,
);
} }
@override @override
Future<List<Shift>> getPendingAssignments() async { Future<List<Shift>> getPendingAssignments() async {
return _fetchApplications([dc.ApplicationStatus.PENDING]); return <Shift>[];
} }
@override @override
Future<List<Shift>> getCancelledShifts() async { Future<List<Shift>> getCancelledShifts() async {
return _fetchApplications([dc.ApplicationStatus.REJECTED]); return <Shift>[];
} }
@override @override
@@ -141,10 +114,37 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
_shiftToAppIdMap[app.shift.id] = app.id; _shiftToAppIdMap[app.shift.id] = app.id;
_appToRoleIdMap[app.id] = app.shiftRole.id; _appToRoleIdMap[app.id] = app.shiftRole.id;
final 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( shifts.add(
_mapApplicationToShift( Shift(
app, id: app.shift.id,
_mapStatus(dc.ApplicationStatus.CHECKED_OUT), 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,
), ),
); );
} }
@@ -154,8 +154,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
} }
} }
Future<List<Shift>> _fetchApplications( Future<List<Shift>> _fetchApplications({
List<dc.ApplicationStatus> statuses, {
DateTime? start, DateTime? start,
DateTime? end, DateTime? end,
}) async { }) async {
@@ -169,22 +168,58 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
} }
final response = await query.execute(); final response = await query.execute();
final statusNames = statuses.map((s) => s.name).toSet(); final apps = response.data.applications;
final apps = response.data.applications.where(
(app) => statusNames.contains(app.status.stringValue),
);
final List<Shift> shifts = []; final List<Shift> shifts = [];
for (final app in apps) { for (final app in apps) {
_shiftToAppIdMap[app.shift.id] = app.id; _shiftToAppIdMap[app.shift.id] = app.id;
_appToRoleIdMap[app.id] = app.shiftRole.id; _appToRoleIdMap[app.id] = app.shiftRole.id;
// Use the first matching status for mapping final String roleName = app.shiftRole.role.name;
final matchingStatus = statuses.firstWhere( final String orderName =
(s) => s.name == app.status.stringValue, (app.shift.order.eventName ?? '').trim().isNotEmpty
orElse: () => statuses.first, ? 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,
),
); );
shifts.add(_mapApplicationToShift(app, _mapStatus(matchingStatus)));
} }
return shifts; return shifts;
} catch (e) { } catch (e) {
@@ -230,10 +265,22 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
final List<Shift> mappedShifts = []; final List<Shift> mappedShifts = [];
for (final sr in allShiftRoles) { 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 DateTime? shiftDate = _toDateTime(sr.shift.date);
final startDt = _toDateTime(sr.startTime); final startDt = _toDateTime(sr.startTime);
final endDt = _toDateTime(sr.endTime); final endDt = _toDateTime(sr.endTime);
final createdDt = _toDateTime(sr.createdAt); 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( mappedShifts.add(
Shift( Shift(
id: sr.shiftId, id: sr.shiftId,
@@ -293,7 +340,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
final DateTime? endDt = _toDateTime(sr.endTime); final DateTime? endDt = _toDateTime(sr.endTime);
final DateTime? createdDt = _toDateTime(sr.createdAt); final DateTime? createdDt = _toDateTime(sr.createdAt);
final String? staffId = _auth.currentUser?.uid; final String? staffId = await _getStaffId();
bool hasApplied = false; bool hasApplied = false;
String status = 'open'; String status = 'open';
if (staffId != null) { if (staffId != null) {
@@ -466,7 +513,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
shiftId: shiftId, shiftId: shiftId,
staffId: staffId, staffId: staffId,
roleId: targetRoleId, roleId: targetRoleId,
status: dc.ApplicationStatus.CONFIRMED, status: dc.ApplicationStatus.ACCEPTED,
origin: dc.ApplicationOrigin.STAFF, origin: dc.ApplicationOrigin.STAFF,
) )
// TODO: this should be PENDING so a vendor can accept it. // TODO: this should be PENDING so a vendor can accept it.
@@ -500,7 +547,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
@override @override
Future<void> acceptShift(String shiftId) async { Future<void> acceptShift(String shiftId) async {
await _updateApplicationStatus(shiftId, dc.ApplicationStatus.CONFIRMED); await _updateApplicationStatus(shiftId, dc.ApplicationStatus.ACCEPTED);
} }
@override @override
@@ -563,7 +610,7 @@ class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
} }
await _dataConnect await _dataConnect
.updateApplicationStatus(id: appId, roleId: roleId) .updateApplicationStatus(id: appId)
.status(newStatus) .status(newStatus)
.execute(); .execute();
} }

View File

@@ -30,6 +30,7 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
}) : super(ShiftsInitial()) { }) : super(ShiftsInitial()) {
on<LoadShiftsEvent>(_onLoadShifts); on<LoadShiftsEvent>(_onLoadShifts);
on<LoadHistoryShiftsEvent>(_onLoadHistoryShifts); on<LoadHistoryShiftsEvent>(_onLoadHistoryShifts);
on<LoadAvailableShiftsEvent>(_onLoadAvailableShifts);
on<LoadShiftsForRangeEvent>(_onLoadShiftsForRange); on<LoadShiftsForRangeEvent>(_onLoadShiftsForRange);
on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts); on<FilterAvailableShiftsEvent>(_onFilterAvailableShifts);
} }
@@ -50,18 +51,15 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
final myShiftsResult = await getMyShifts( final myShiftsResult = await getMyShifts(
GetMyShiftsArguments(start: days.first, end: days.last), GetMyShiftsArguments(start: days.first, end: days.last),
); );
final pendingResult = await getPendingAssignments();
final cancelledResult = await getCancelledShifts();
// Initial available with defaults
final availableResult = await getAvailableShifts(const GetAvailableShiftsArguments());
emit(ShiftsLoaded( emit(ShiftsLoaded(
myShifts: myShiftsResult, myShifts: myShiftsResult,
pendingShifts: pendingResult, pendingShifts: const [],
cancelledShifts: cancelledResult, cancelledShifts: const [],
availableShifts: _filterPastShifts(availableResult), availableShifts: const [],
historyShifts: const [], historyShifts: const [],
availableLoading: false,
availableLoaded: false,
historyLoading: false, historyLoading: false,
historyLoaded: false, historyLoaded: false,
searchQuery: '', searchQuery: '',
@@ -93,6 +91,28 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
} }
} }
Future<void> _onLoadAvailableShifts(
LoadAvailableShiftsEvent event,
Emitter<ShiftsState> emit,
) async {
final currentState = state;
if (currentState is! ShiftsLoaded) return;
if (currentState.availableLoading || currentState.availableLoaded) return;
emit(currentState.copyWith(availableLoading: true));
try {
final availableResult =
await getAvailableShifts(const GetAvailableShiftsArguments());
emit(currentState.copyWith(
availableShifts: _filterPastShifts(availableResult),
availableLoading: false,
availableLoaded: true,
));
} catch (_) {
emit(currentState.copyWith(availableLoading: false));
}
}
Future<void> _onLoadShiftsForRange( Future<void> _onLoadShiftsForRange(
LoadShiftsForRangeEvent event, LoadShiftsForRangeEvent event,
Emitter<ShiftsState> emit, Emitter<ShiftsState> emit,
@@ -108,17 +128,14 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
return; return;
} }
final pendingResult = await getPendingAssignments();
final cancelledResult = await getCancelledShifts();
final availableResult =
await getAvailableShifts(const GetAvailableShiftsArguments());
emit(ShiftsLoaded( emit(ShiftsLoaded(
myShifts: myShiftsResult, myShifts: myShiftsResult,
pendingShifts: pendingResult, pendingShifts: const [],
cancelledShifts: cancelledResult, cancelledShifts: const [],
availableShifts: _filterPastShifts(availableResult), availableShifts: const [],
historyShifts: const [], historyShifts: const [],
availableLoading: false,
availableLoaded: false,
historyLoading: false, historyLoading: false,
historyLoaded: false, historyLoaded: false,
searchQuery: '', searchQuery: '',
@@ -135,6 +152,10 @@ class ShiftsBloc extends Bloc<ShiftsEvent, ShiftsState> {
) async { ) async {
final currentState = state; final currentState = state;
if (currentState is ShiftsLoaded) { if (currentState is ShiftsLoaded) {
if (!currentState.availableLoaded && !currentState.availableLoading) {
add(LoadAvailableShiftsEvent());
return;
}
// Optimistic update or loading indicator? // Optimistic update or loading indicator?
// Since it's filtering, we can just reload available. // Since it's filtering, we can just reload available.

View File

@@ -12,6 +12,8 @@ class LoadShiftsEvent extends ShiftsEvent {}
class LoadHistoryShiftsEvent extends ShiftsEvent {} class LoadHistoryShiftsEvent extends ShiftsEvent {}
class LoadAvailableShiftsEvent extends ShiftsEvent {}
class LoadShiftsForRangeEvent extends ShiftsEvent { class LoadShiftsForRangeEvent extends ShiftsEvent {
final DateTime start; final DateTime start;
final DateTime end; final DateTime end;

View File

@@ -18,6 +18,8 @@ class ShiftsLoaded extends ShiftsState {
final List<Shift> cancelledShifts; final List<Shift> cancelledShifts;
final List<Shift> availableShifts; final List<Shift> availableShifts;
final List<Shift> historyShifts; final List<Shift> historyShifts;
final bool availableLoading;
final bool availableLoaded;
final bool historyLoading; final bool historyLoading;
final bool historyLoaded; final bool historyLoaded;
final String searchQuery; final String searchQuery;
@@ -29,6 +31,8 @@ class ShiftsLoaded extends ShiftsState {
required this.cancelledShifts, required this.cancelledShifts,
required this.availableShifts, required this.availableShifts,
required this.historyShifts, required this.historyShifts,
required this.availableLoading,
required this.availableLoaded,
required this.historyLoading, required this.historyLoading,
required this.historyLoaded, required this.historyLoaded,
required this.searchQuery, required this.searchQuery,
@@ -41,6 +45,8 @@ class ShiftsLoaded extends ShiftsState {
List<Shift>? cancelledShifts, List<Shift>? cancelledShifts,
List<Shift>? availableShifts, List<Shift>? availableShifts,
List<Shift>? historyShifts, List<Shift>? historyShifts,
bool? availableLoading,
bool? availableLoaded,
bool? historyLoading, bool? historyLoading,
bool? historyLoaded, bool? historyLoaded,
String? searchQuery, String? searchQuery,
@@ -52,6 +58,8 @@ class ShiftsLoaded extends ShiftsState {
cancelledShifts: cancelledShifts ?? this.cancelledShifts, cancelledShifts: cancelledShifts ?? this.cancelledShifts,
availableShifts: availableShifts ?? this.availableShifts, availableShifts: availableShifts ?? this.availableShifts,
historyShifts: historyShifts ?? this.historyShifts, historyShifts: historyShifts ?? this.historyShifts,
availableLoading: availableLoading ?? this.availableLoading,
availableLoaded: availableLoaded ?? this.availableLoaded,
historyLoading: historyLoading ?? this.historyLoading, historyLoading: historyLoading ?? this.historyLoading,
historyLoaded: historyLoaded ?? this.historyLoaded, historyLoaded: historyLoaded ?? this.historyLoaded,
searchQuery: searchQuery ?? this.searchQuery, searchQuery: searchQuery ?? this.searchQuery,
@@ -66,6 +74,8 @@ class ShiftsLoaded extends ShiftsState {
cancelledShifts, cancelledShifts,
availableShifts, availableShifts,
historyShifts, historyShifts,
availableLoading,
availableLoaded,
historyLoading, historyLoading,
historyLoaded, historyLoaded,
searchQuery, searchQuery,

View File

@@ -32,6 +32,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
if (_activeTab == 'history') { if (_activeTab == 'history') {
_bloc.add(LoadHistoryShiftsEvent()); _bloc.add(LoadHistoryShiftsEvent());
} }
if (_activeTab == 'find') {
_bloc.add(LoadAvailableShiftsEvent());
}
} }
@override @override
@@ -55,12 +58,19 @@ class _ShiftsPageState extends State<ShiftsPage> {
value: _bloc, value: _bloc,
child: BlocBuilder<ShiftsBloc, ShiftsState>( child: BlocBuilder<ShiftsBloc, ShiftsState>(
builder: (context, state) { builder: (context, state) {
final bool baseLoaded = state is ShiftsLoaded;
final List<Shift> myShifts = (state is ShiftsLoaded) final List<Shift> myShifts = (state is ShiftsLoaded)
? state.myShifts ? state.myShifts
: []; : [];
final List<Shift> availableJobs = (state is ShiftsLoaded) final List<Shift> availableJobs = (state is ShiftsLoaded)
? state.availableShifts ? state.availableShifts
: []; : [];
final bool availableLoading = (state is ShiftsLoaded)
? state.availableLoading
: false;
final bool availableLoaded = (state is ShiftsLoaded)
? state.availableLoaded
: false;
final List<Shift> pendingAssignments = (state is ShiftsLoaded) final List<Shift> pendingAssignments = (state is ShiftsLoaded)
? state.pendingShifts ? state.pendingShifts
: []; : [];
@@ -114,6 +124,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
"My Shifts", "My Shifts",
UiIcons.calendar, UiIcons.calendar,
myShifts.length, myShifts.length,
enabled: true,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildTab( _buildTab(
@@ -122,6 +133,8 @@ class _ShiftsPageState extends State<ShiftsPage> {
UiIcons.search, UiIcons.search,
availableJobs availableJobs
.length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs. .length, // Passed unfiltered count as badge? Or logic inside? Pass availableJobs.
showCount: availableLoaded,
enabled: baseLoaded,
), ),
const SizedBox(width: 8), const SizedBox(width: 8),
_buildTab( _buildTab(
@@ -130,6 +143,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
UiIcons.clock, UiIcons.clock,
historyShifts.length, historyShifts.length,
showCount: historyLoaded, showCount: historyLoaded,
enabled: baseLoaded,
), ),
], ],
), ),
@@ -147,6 +161,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
cancelledShifts, cancelledShifts,
availableJobs, availableJobs,
historyShifts, historyShifts,
availableLoading,
historyLoading, historyLoading,
), ),
), ),
@@ -164,6 +179,7 @@ class _ShiftsPageState extends State<ShiftsPage> {
List<Shift> cancelledShifts, List<Shift> cancelledShifts,
List<Shift> availableJobs, List<Shift> availableJobs,
List<Shift> historyShifts, List<Shift> historyShifts,
bool availableLoading,
bool historyLoading, bool historyLoading,
) { ) {
switch (_activeTab) { switch (_activeTab) {
@@ -175,6 +191,9 @@ class _ShiftsPageState extends State<ShiftsPage> {
initialDate: _selectedDate, initialDate: _selectedDate,
); );
case 'find': case 'find':
if (availableLoading) {
return const Center(child: CircularProgressIndicator());
}
return FindShiftsTab(availableJobs: availableJobs); return FindShiftsTab(availableJobs: availableJobs);
case 'history': case 'history':
if (historyLoading) { if (historyLoading) {
@@ -192,15 +211,21 @@ class _ShiftsPageState extends State<ShiftsPage> {
IconData icon, IconData icon,
int count, { int count, {
bool showCount = true, bool showCount = true,
bool enabled = true,
}) { }) {
final isActive = _activeTab == id; final isActive = _activeTab == id;
return Expanded( return Expanded(
child: GestureDetector( child: GestureDetector(
onTap: () { onTap: !enabled
? null
: () {
setState(() => _activeTab = id); setState(() => _activeTab = id);
if (id == 'history') { if (id == 'history') {
_bloc.add(LoadHistoryShiftsEvent()); _bloc.add(LoadHistoryShiftsEvent());
} }
if (id == 'find') {
_bloc.add(LoadAvailableShiftsEvent());
}
}, },
child: Container( child: Container(
padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8), padding: const EdgeInsets.symmetric(vertical: 10, horizontal: 8),
@@ -217,7 +242,11 @@ class _ShiftsPageState extends State<ShiftsPage> {
Icon( Icon(
icon, icon,
size: 14, size: 14,
color: isActive ? AppColors.krowBlue : Colors.white, color: !enabled
? Colors.white.withAlpha((0.5 * 255).round())
: isActive
? AppColors.krowBlue
: Colors.white,
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
Flexible( Flexible(
@@ -226,7 +255,11 @@ class _ShiftsPageState extends State<ShiftsPage> {
style: TextStyle( style: TextStyle(
fontSize: 13, fontSize: 13,
fontWeight: FontWeight.w500, fontWeight: FontWeight.w500,
color: isActive ? AppColors.krowBlue : Colors.white, color: !enabled
? Colors.white.withAlpha((0.5 * 255).round())
: isActive
? AppColors.krowBlue
: Colors.white,
), ),
overflow: TextOverflow.ellipsis, overflow: TextOverflow.ellipsis,
), ),

View File

@@ -90,6 +90,10 @@ class _MyShiftCardState extends State<MyShiftCard> {
statusText = t.staff_shifts.status.confirmed; statusText = t.staff_shifts.status.confirmed;
statusColor = UiColors.textLink; statusColor = UiColors.textLink;
statusBg = UiColors.primary; statusBg = UiColors.primary;
} else if (status == 'checked_in') {
statusText = 'Checked in';
statusColor = UiColors.textSuccess;
statusBg = UiColors.iconSuccess;
} else if (status == 'pending' || status == 'open') { } else if (status == 'pending' || status == 'open') {
statusText = t.staff_shifts.status.act_now; statusText = t.staff_shifts.status.act_now;
statusColor = UiColors.destructive; statusColor = UiColors.destructive;

View File

@@ -170,8 +170,21 @@ class _FindShiftsTabState extends State<FindShiftsTab> {
...filteredJobs.map( ...filteredJobs.map(
(shift) => Padding( (shift) => Padding(
padding: const EdgeInsets.only(bottom: 12), padding: const EdgeInsets.only(bottom: 12),
child: MyShiftCard( child: Column(
shift: shift, crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'Debug shiftId: ${shift.id}',
style: const TextStyle(
fontSize: 10,
color: Color(0xFF94A3B8),
),
),
const SizedBox(height: 4),
MyShiftCard(
shift: shift,
),
],
), ),
), ),
), ),

View File

@@ -27,7 +27,7 @@ mutation updateApplicationStatus(
$status: ApplicationStatus $status: ApplicationStatus
$checkInTime: Timestamp $checkInTime: Timestamp
$checkOutTime: Timestamp $checkOutTime: Timestamp
$roleId: UUID! $roleId: UUID
) @auth(level: USER) { ) @auth(level: USER) {
application_update( application_update(
id: $id id: $id