feat: implement staff availability, clock-in, payments and fix UI navigation
This commit is contained in:
@@ -1,70 +1,216 @@
|
||||
import 'package:krow_data_connect/krow_data_connect.dart';
|
||||
import 'package:firebase_data_connect/firebase_data_connect.dart';
|
||||
import 'package:krow_data_connect/src/session/staff_session_store.dart';
|
||||
import 'package:krow_domain/krow_domain.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import '../../domain/repositories/shifts_repository_interface.dart';
|
||||
|
||||
extension TimestampExt on Timestamp {
|
||||
DateTime toDate() {
|
||||
return DateTime.fromMillisecondsSinceEpoch(seconds.toInt() * 1000 + nanoseconds ~/ 1000000);
|
||||
}
|
||||
}
|
||||
|
||||
/// Implementation of [ShiftsRepositoryInterface] that delegates to [ShiftsRepositoryMock].
|
||||
///
|
||||
/// This class resides in the data layer and handles the communication with
|
||||
/// the external data sources (currently mocks).
|
||||
class ShiftsRepositoryImpl implements ShiftsRepositoryInterface {
|
||||
final ShiftsRepositoryMock _mock;
|
||||
ShiftsRepositoryImpl();
|
||||
|
||||
ShiftsRepositoryImpl({ShiftsRepositoryMock? mock}) : _mock = mock ?? ShiftsRepositoryMock();
|
||||
// Cache: ShiftID -> ApplicationID (For Accept/Decline)
|
||||
final Map<String, String> _shiftToAppIdMap = {};
|
||||
// Cache: ApplicationID -> RoleID (For Accept/Decline w/ Update mutation)
|
||||
final Map<String, String> _appToRoleIdMap = {};
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getMyShifts() async {
|
||||
return _mock.getMyShifts();
|
||||
String get _currentStaffId {
|
||||
final session = StaffSessionStore.instance.session;
|
||||
if (session?.staff?.id == null) throw Exception('User not logged in');
|
||||
return session!.staff!.id;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
||||
// Delegates to mock. Logic kept here temporarily as per architecture constraints
|
||||
// on data_connect modifications, mimicking a query capable datasource.
|
||||
var shifts = await _mock.getAvailableShifts();
|
||||
|
||||
// Simple in-memory filtering for mock adapter
|
||||
if (query.isNotEmpty) {
|
||||
shifts = shifts.where((s) =>
|
||||
s.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(query.toLowerCase())
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (type != 'all') {
|
||||
if (type == 'one-day') {
|
||||
shifts = shifts.where((s) => !s.title.contains('Multi-Day') && !s.title.contains('Long Term')).toList();
|
||||
} else if (type == 'multi-day') {
|
||||
shifts = shifts.where((s) => s.title.contains('Multi-Day')).toList();
|
||||
} else if (type == 'long-term') {
|
||||
shifts = shifts.where((s) => s.title.contains('Long Term')).toList();
|
||||
}
|
||||
}
|
||||
|
||||
return shifts;
|
||||
Future<List<Shift>> getMyShifts() async {
|
||||
return _fetchApplications(ApplicationStatus.ACCEPTED);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getPendingAssignments() async {
|
||||
return _mock.getPendingAssignments();
|
||||
// Fetch both PENDING (User applied) and OFFERED (Business offered) if schema supports
|
||||
// For now assuming PENDING covers invitations/offers.
|
||||
return _fetchApplications(ApplicationStatus.PENDING);
|
||||
}
|
||||
|
||||
Future<List<Shift>> _fetchApplications(ApplicationStatus status) async {
|
||||
try {
|
||||
final response = await ExampleConnector.instance
|
||||
.getApplicationsByStaffId(staffId: _currentStaffId)
|
||||
.execute();
|
||||
|
||||
return response.data.applications
|
||||
.where((app) => app.status is Known && (app.status as Known).value == status)
|
||||
.map((app) {
|
||||
// Cache IDs for actions
|
||||
_shiftToAppIdMap[app.shift.id] = app.id;
|
||||
_appToRoleIdMap[app.id] = app.shiftRole.roleId;
|
||||
|
||||
return _mapApplicationToShift(app);
|
||||
})
|
||||
.toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<List<Shift>> getAvailableShifts(String query, String type) async {
|
||||
try {
|
||||
final response = await ExampleConnector.instance.listShifts().execute();
|
||||
|
||||
var shifts = response.data.shifts
|
||||
.where((s) => s.status is Known && (s.status as Known).value == ShiftStatus.OPEN)
|
||||
.map((s) => _mapConnectorShiftToDomain(s))
|
||||
.toList();
|
||||
|
||||
// Client-side filtering
|
||||
if (query.isNotEmpty) {
|
||||
shifts = shifts.where((s) =>
|
||||
s.title.toLowerCase().contains(query.toLowerCase()) ||
|
||||
s.clientName.toLowerCase().contains(query.toLowerCase())
|
||||
).toList();
|
||||
}
|
||||
|
||||
if (type != 'all') {
|
||||
if (type == 'one-day') {
|
||||
shifts = shifts.where((s) => !s.title.contains('Multi-Day')).toList();
|
||||
} else if (type == 'multi-day') {
|
||||
shifts = shifts.where((s) => s.title.contains('Multi-Day')).toList();
|
||||
}
|
||||
}
|
||||
return shifts;
|
||||
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<Shift?> getShiftDetails(String shiftId) async {
|
||||
return _mock.getShiftDetails(shiftId);
|
||||
try {
|
||||
final response = await ExampleConnector.instance.getShiftById(id: shiftId).execute();
|
||||
final s = response.data.shift;
|
||||
if (s == null) return null;
|
||||
|
||||
// Map to domain Shift
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
clientName: s.order.business.businessName,
|
||||
hourlyRate: s.cost ?? 0.0,
|
||||
location: s.location ?? 'Unknown',
|
||||
locationAddress: s.locationAddress ?? '',
|
||||
date: s.date?.toDate().toIso8601String() ?? '',
|
||||
startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()),
|
||||
endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()),
|
||||
createdDate: s.createdAt?.toDate().toIso8601String() ?? '',
|
||||
tipsAvailable: false,
|
||||
mealProvided: false,
|
||||
managers: [],
|
||||
description: s.description,
|
||||
);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> applyForShift(String shiftId) async {
|
||||
// API LIMITATION: 'createApplication' requires roleId.
|
||||
// 'listShifts' / 'getShiftById' does not currently return the Shift's available Roles.
|
||||
// We cannot reliably apply for a shift without knowing the Role ID.
|
||||
// Falling back to Mock delay for now.
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
|
||||
// In future:
|
||||
// 1. Fetch Shift Roles
|
||||
// 2. Select Role
|
||||
// 3. createApplication(shiftId, roleId, staffId, status: PENDING, origin: MOBILE)
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> acceptShift(String shiftId) async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await _updateApplicationStatus(shiftId, ApplicationStatus.ACCEPTED);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> declineShift(String shiftId) async {
|
||||
await Future.delayed(const Duration(milliseconds: 500));
|
||||
await _updateApplicationStatus(shiftId, ApplicationStatus.REJECTED);
|
||||
}
|
||||
|
||||
Future<void> _updateApplicationStatus(String shiftId, ApplicationStatus newStatus) async {
|
||||
String? appId = _shiftToAppIdMap[shiftId];
|
||||
String? roleId;
|
||||
|
||||
// Refresh if missing from cache
|
||||
if (appId == null) {
|
||||
await getPendingAssignments();
|
||||
appId = _shiftToAppIdMap[shiftId];
|
||||
}
|
||||
roleId = _appToRoleIdMap[appId];
|
||||
|
||||
if (appId == null || roleId == null) {
|
||||
throw Exception("Application not found for shift $shiftId");
|
||||
}
|
||||
|
||||
await ExampleConnector.instance.updateApplicationStatus(
|
||||
id: appId,
|
||||
roleId: roleId,
|
||||
)
|
||||
.status(newStatus)
|
||||
.execute();
|
||||
}
|
||||
|
||||
// Mappers
|
||||
|
||||
Shift _mapApplicationToShift(GetApplicationsByStaffIdApplications app) {
|
||||
final s = app.shift;
|
||||
final r = app.shiftRole;
|
||||
final statusVal = app.status is Known
|
||||
? (app.status as Known).value.name.toLowerCase() : 'pending';
|
||||
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: r.role.name,
|
||||
clientName: s.order.business.businessName,
|
||||
hourlyRate: r.role.costPerHour,
|
||||
location: s.location ?? 'Unknown',
|
||||
locationAddress: s.location ?? '',
|
||||
date: s.date?.toDate().toIso8601String() ?? '',
|
||||
startTime: DateFormat('HH:mm').format(r.startTime?.toDate() ?? DateTime.now()),
|
||||
endTime: DateFormat('HH:mm').format(r.endTime?.toDate() ?? DateTime.now()),
|
||||
createdDate: app.createdAt?.toDate().toIso8601String() ?? '',
|
||||
status: statusVal,
|
||||
description: null,
|
||||
managers: [],
|
||||
);
|
||||
}
|
||||
|
||||
Shift _mapConnectorShiftToDomain(ListShiftsShifts s) {
|
||||
return Shift(
|
||||
id: s.id,
|
||||
title: s.title,
|
||||
clientName: s.order.business.businessName,
|
||||
hourlyRate: s.cost ?? 0.0,
|
||||
location: s.location ?? 'Unknown',
|
||||
locationAddress: s.locationAddress ?? '',
|
||||
date: s.date?.toDate().toIso8601String() ?? '',
|
||||
startTime: DateFormat('HH:mm').format(s.startTime?.toDate() ?? DateTime.now()),
|
||||
endTime: DateFormat('HH:mm').format(s.endTime?.toDate() ?? DateTime.now()),
|
||||
createdDate: s.createdAt?.toDate().toIso8601String() ?? '',
|
||||
description: s.description,
|
||||
managers: [],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user